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
+6 -2
View File
@@ -11,6 +11,8 @@ import { main } from "@/seqta/main";
import { delay } from "./seqta/utils/delay"; import { delay } from "./seqta/utils/delay";
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle"; import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours"; import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
import { installThemeImagePagePatch } from "@/seqta/utils/patchThemeImagesPageContext";
import { initVerboseLogging, verboseInfo } from "@/utils/verboseLog";
function registerFetchSeqtaAppLinkListener() { function registerFetchSeqtaAppLinkListener() {
browser.runtime.onMessage.addListener((request, _sender, sendResponse) => { browser.runtime.onMessage.addListener((request, _sender, sendResponse) => {
@@ -49,6 +51,7 @@ if (document.childNodes[1]) {
) ?? false; ) ?? false;
if (hasSEQTAText) { if (hasSEQTAText) {
installSeqtaMenuColourPatch(); installSeqtaMenuColourPatch();
installThemeImagePagePatch();
} }
init(); init();
} }
@@ -61,7 +64,7 @@ async function init() {
!IsSEQTAPage !IsSEQTAPage
) { ) {
IsSEQTAPage = true; IsSEQTAPage = true;
console.info("[BetterSEQTA+] Verified SEQTA Page"); verboseInfo("[BetterSEQTA+] Verified SEQTA Page");
if (typeof window !== "undefined" && window === window.top) { if (typeof window !== "undefined" && window === window.top) {
void browser.runtime.sendMessage({ type: "cloudSettingsPoll" }).catch(() => {}); void browser.runtime.sendMessage({ type: "cloudSettingsPoll" }).catch(() => {});
@@ -100,6 +103,7 @@ async function init() {
try { try {
await initializeSettingsState(); await initializeSettingsState();
initVerboseLogging();
if (typeof settingsState.onoff === "undefined") { if (typeof settingsState.onoff === "undefined") {
await browser.runtime.sendMessage({ type: "setDefaultStorage" }); await browser.runtime.sendMessage({ type: "setDefaultStorage" });
@@ -118,7 +122,7 @@ async function init() {
initializeHideSensitiveToggle(); initializeHideSensitiveToggle();
} }
console.info( verboseInfo(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
); );
} catch (error) { } catch (error) {
@@ -0,0 +1,38 @@
<script lang="ts">
import { blobToDataUrl } from '@/plugins/built-in/themes/themeImageUrl'
let {
source,
alt = '',
class: className = '',
} = $props<{
source: string | Blob | null | undefined
alt?: string
class?: string
}>()
let src = $state('')
$effect(() => {
const value = source
if (!value) {
src = ''
return
}
if (typeof value === 'string') {
src = value
return
}
let cancelled = false
blobToDataUrl(value).then((url) => {
if (!cancelled) src = url
})
return () => {
cancelled = true
}
})
</script>
{#if src}
<img src={src} alt={alt} class={className} />
{/if}
@@ -9,6 +9,7 @@
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager' import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
import { cloudAuth } from '@/seqta/utils/CloudAuth' import { cloudAuth } from '@/seqta/utils/CloudAuth'
import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte' import SignInToFavoriteModal from '@/interface/components/SignInToFavoriteModal.svelte'
import ThemeBlobImage from '@/interface/components/themes/ThemeBlobImage.svelte'
const themeManager = ThemeManager.getInstance(); const themeManager = ThemeManager.getInstance();
@@ -237,8 +238,8 @@
<div class="relative top-0 z-10 flex justify-center w-full h-full overflow-hidden transition dark:text-white rounded-xl group place-items-center bg-zinc-100 dark:bg-zinc-900 { isEditMode ? 'animate-shake brightness-90' : ''}"> <div class="relative top-0 z-10 flex justify-center w-full h-full overflow-hidden transition dark:text-white rounded-xl group place-items-center bg-zinc-100 dark:bg-zinc-900 { isEditMode ? 'animate-shake brightness-90' : ''}">
{#if theme.coverImage} {#if theme.coverImage}
<img <ThemeBlobImage
src={typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)} source={theme.coverImage}
alt={theme.name} alt={theme.name}
class="object-cover absolute inset-0 z-0 w-full h-full pointer-events-none" class="object-cover absolute inset-0 z-0 w-full h-full pointer-events-none"
/> />
+3 -1
View File
@@ -4,9 +4,10 @@ import IconFamily from "@/resources/fonts/IconFamily.woff";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import renderSvelte from "./main"; import renderSvelte from "./main";
import { initializeSettingsState } from "@/seqta/utils/listeners/SettingsState"; import { initializeSettingsState } from "@/seqta/utils/listeners/SettingsState";
import { initVerboseLogging, verboseInfo } from "@/utils/verboseLog";
function InjectCustomIcons() { function InjectCustomIcons() {
console.info("[BetterSEQTA+] Injecting Icons"); verboseInfo("[BetterSEQTA+] Injecting Icons");
const style = document.createElement("style"); const style = document.createElement("style");
style.setAttribute("type", "text/css"); style.setAttribute("type", "text/css");
@@ -30,5 +31,6 @@ InjectCustomIcons();
(async () => { (async () => {
await initializeSettingsState(); await initializeSettingsState();
initVerboseLogging();
renderSvelte(Settings, mountPoint, { standalone: true }); renderSvelte(Settings, mountPoint, { standalone: true });
})(); })();
@@ -492,6 +492,18 @@
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} /> <Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
</div> </div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3">
<div class="pr-4">
<h2 class="text-sm font-bold">Verbose logging</h2>
<p class="text-xs">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p>
</div>
<div>
<Switch
state={$settingsState.verboseLogging ?? false}
onChange={(isOn: boolean) => settingsState.verboseLogging = isOn}
/>
</div>
</div>
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
<div class="pr-4"> <div class="pr-4">
<h2 class="text-sm font-bold">Sensitive Hider</h2> <h2 class="text-sm font-bold">Sensitive Hider</h2>
+1 -1
View File
@@ -22,7 +22,7 @@
<button <button
onclick={() => editMode = !editMode} onclick={() => editMode = !editMode}
class="absolute top-0 right-0 z-10 px-2 h-8 text-lg rounded-xl bg-zinc-100 dark:bg-zinc-700"> class="absolute top-0 right-0 z-10 px-2 h-8 text-lg rounded-xl bg-zinc-100 dark:bg-zinc-700">
<span class="mr-2">{editMode ? 'Done' : 'Edit'}</span> <span class="mr-2">{editMode ? 'Done' : 'Remove'}</span>
<span class="font-IconFamily">{editMode ? '\ue9e4' : '\uec38'}</span> <span class="font-IconFamily">{editMode ? '\ue9e4' : '\uec38'}</span>
</button> </button>
+3 -2
View File
@@ -27,6 +27,7 @@
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager' import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
import { themeUpdates } from '../hooks/ThemeUpdates' import { themeUpdates } from '../hooks/ThemeUpdates'
import { CloseThemeCreator } from '@/plugins/built-in/themes/ThemeCreator' import { CloseThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
import ThemeBlobImage from '@/interface/components/themes/ThemeBlobImage.svelte'
const { themeID } = $props<{ themeID: string }>() const { themeID } = $props<{ themeID: string }>()
const themeManager = ThemeManager.getInstance(); const themeManager = ThemeManager.getInstance();
@@ -230,7 +231,7 @@
{#each theme.CustomImages as image (image.id)} {#each theme.CustomImages as image (image.id)}
<div class="flex gap-2 items-center px-2 py-2 mb-4 h-16 bg-white rounded-lg shadow-lg dark:bg-zinc-700"> <div class="flex gap-2 items-center px-2 py-2 mb-4 h-16 bg-white rounded-lg shadow-lg dark:bg-zinc-700">
<div class="h-full"> <div class="h-full">
<img src={URL.createObjectURL(image.blob)} alt={image.variableName} class="object-contain h-full rounded" /> <ThemeBlobImage source={image.blob} alt={image.variableName} class="object-contain h-full rounded" />
</div> </div>
<input <input
type="text" type="text"
@@ -330,7 +331,7 @@
{/if} {/if}
{#if theme.coverImage} {#if theme.coverImage}
<div class="absolute z-20 w-full h-full opacity-0 transition-opacity pointer-events-none group-hover:opacity-100 bg-black/20"></div> <div class="absolute z-20 w-full h-full opacity-0 transition-opacity pointer-events-none group-hover:opacity-100 bg-black/20"></div>
<img src="{typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}" alt='Cover' class="object-cover absolute z-0 w-full h-full rounded" /> <ThemeBlobImage source={theme.coverImage} alt="Cover" class="object-cover absolute z-0 w-full h-full rounded" />
{/if} {/if}
</div> </div>
@@ -15,6 +15,7 @@
import HighlightedText from '../utils/HighlightedText.svelte'; import HighlightedText from '../utils/HighlightedText.svelte';
import { matchesHotkey } from '../utils/hotkeyUtils'; import { matchesHotkey } from '../utils/hotkeyUtils';
import browser from 'webextension-polyfill'; import browser from 'webextension-polyfill';
import { verboseDebug } from '@/utils/verboseLog';
const { const {
transparencyEffects, transparencyEffects,
@@ -160,7 +161,7 @@
dynamicItems.forEach(item => dynamicIdToItemMap.set(item.id, item)); dynamicItems.forEach(item => dynamicIdToItemMap.set(item.id, item));
commands.forEach(item => commandIdToItemMap.set(item.id, item)); commands.forEach(item => commandIdToItemMap.set(item.id, item));
console.debug(`[Global Search] Indexed ${commands.length} command items and ${dynamicItems.length} dynamic items.`); verboseDebug(`[Global Search] Indexed ${commands.length} command items and ${dynamicItems.length} dynamic items.`);
} }
const performSearch = async () => { const performSearch = async () => {
@@ -2,6 +2,7 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"; import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
export interface BaseCommandItem { export interface BaseCommandItem {
id: string; id: string;
text: string; text: string;
@@ -105,7 +106,7 @@ async function navigateToSpecificLesson(lesson: any) {
if (lessonDate === todayDateString && lessonPeriod === normalizedLessonPeriod) { if (lessonDate === todayDateString && lessonPeriod === normalizedLessonPeriod) {
// Found the exact matching lesson, click it // Found the exact matching lesson, click it
(lessonElement as HTMLElement).click(); (lessonElement as HTMLElement).click();
console.log(`Navigated to exact lesson: ${lessonDate} ${lessonPeriod}`); verboseLog(`Navigated to exact lesson: ${lessonDate} ${lessonPeriod}`);
return true; return true;
} }
} }
@@ -7,6 +7,7 @@ import {
hotkeySetting, hotkeySetting,
Setting, Setting,
} from "@/plugins/core/settingsHelpers"; } from "@/plugins/core/settingsHelpers";
import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
import styles from "./styles.css?inline"; import styles from "./styles.css?inline";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing } from "../indexing/indexer"; import { runIndexing } from "../indexing/indexer";
@@ -70,7 +71,7 @@ const settings = defineSettings({
try { try {
const workerManager = VectorWorkerManager.getInstance(); const workerManager = VectorWorkerManager.getInstance();
await workerManager.resetWorker(); await workerManager.resetWorker();
console.log("Vector worker reset successfully"); verboseLog("Vector worker reset successfully");
} catch (e) { } catch (e) {
console.warn("Failed to reset vector worker:", e); console.warn("Failed to reset vector worker:", e);
} }
@@ -90,7 +91,7 @@ const settings = defineSettings({
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const req = indexedDB.deleteDatabase(dbName); const req = indexedDB.deleteDatabase(dbName);
req.onsuccess = () => { req.onsuccess = () => {
console.log(`Successfully deleted database: ${dbName}`); verboseLog(`Successfully deleted database: ${dbName}`);
resolve(); resolve();
}; };
req.onerror = () => { req.onerror = () => {
@@ -103,7 +104,7 @@ const settings = defineSettings({
setTimeout(() => { setTimeout(() => {
const retryReq = indexedDB.deleteDatabase(dbName); const retryReq = indexedDB.deleteDatabase(dbName);
retryReq.onsuccess = () => { retryReq.onsuccess = () => {
console.log(`Successfully deleted database on retry: ${dbName}`); verboseLog(`Successfully deleted database on retry: ${dbName}`);
resolve(); resolve();
}; };
retryReq.onerror = () => reject(retryReq.error); retryReq.onerror = () => reject(retryReq.error);
@@ -176,7 +177,7 @@ const globalSearchPlugin: Plugin<typeof settings> = {
try { try {
const wasUpdated = await checkAndHandleUpdate(); const wasUpdated = await checkAndHandleUpdate();
if (wasUpdated) { if (wasUpdated) {
console.log( verboseLog(
"[Global Search] Extension updated — search index reset; the next indexing pass will repopulate.", "[Global Search] Extension updated — search index reset; the next indexing pass will repopulate.",
); );
} }
@@ -188,7 +189,7 @@ const globalSearchPlugin: Plugin<typeof settings> = {
error?.message?.includes("MIME type") || error?.message?.includes("MIME type") ||
error?.message?.includes("NS_ERROR_CORRUPTED_CONTENT") error?.message?.includes("NS_ERROR_CORRUPTED_CONTENT")
) { ) {
console.debug( verboseDebug(
"[Global Search] Version check skipped due to asset loading restrictions:", "[Global Search] Version check skipped due to asset loading restrictions:",
error.message, error.message,
); );
@@ -217,7 +218,7 @@ const globalSearchPlugin: Plugin<typeof settings> = {
if (isVectorSearchSupported()) { if (isVectorSearchSupported()) {
VectorWorkerManager.getInstance(); VectorWorkerManager.getInstance();
} else { } else {
console.debug("[Global Search] Skipping vector worker warm-up (Firefox detected - using text search only)"); verboseDebug("[Global Search] Skipping vector worker warm-up (Firefox detected - using text search only)");
} }
} catch (error) { } catch (error) {
console.warn("[Global Search] Vector worker warm-up failed:", error); console.warn("[Global Search] Vector worker warm-up failed:", error);
@@ -230,15 +231,15 @@ const globalSearchPlugin: Plugin<typeof settings> = {
resetWorker: async () => { resetWorker: async () => {
const workerManager = VectorWorkerManager.getInstance(); const workerManager = VectorWorkerManager.getInstance();
await workerManager.resetWorker(); await workerManager.resetWorker();
console.log("Vector worker reset via debug helper"); verboseLog("Vector worker reset via debug helper");
}, },
checkWorkerStatus: () => { checkWorkerStatus: () => {
const workerManager = VectorWorkerManager.getInstance(); const workerManager = VectorWorkerManager.getInstance();
console.log("Streaming active:", workerManager.isStreamingActive()); verboseLog("Streaming active:", workerManager.isStreamingActive());
}, },
passiveItems: async () => { passiveItems: async () => {
const items = await getStoredPassiveItems(); const items = await getStoredPassiveItems();
console.log(`Captured ${items.length} passive items`); verboseLog(`Captured ${items.length} passive items`);
return items; return items;
}, },
runSelfTests: async () => { runSelfTests: async () => {
@@ -250,7 +251,7 @@ const globalSearchPlugin: Plugin<typeof settings> = {
checkIndexedDBSize: async () => { checkIndexedDBSize: async () => {
try { try {
const estimate = await navigator.storage.estimate(); const estimate = await navigator.storage.estimate();
console.log("Storage estimate:", estimate); verboseLog("Storage estimate:", estimate);
// Check embeddiaDB size // Check embeddiaDB size
const dbRequest = indexedDB.open("embeddiaDB"); const dbRequest = indexedDB.open("embeddiaDB");
@@ -260,7 +261,7 @@ const globalSearchPlugin: Plugin<typeof settings> = {
const store = transaction.objectStore("embeddiaObjectStore"); const store = transaction.objectStore("embeddiaObjectStore");
const countRequest = store.count(); const countRequest = store.count();
countRequest.onsuccess = () => { countRequest.onsuccess = () => {
console.log("embeddiaDB item count:", countRequest.result); verboseLog("embeddiaDB item count:", countRequest.result);
}; };
}; };
} catch (e) { } catch (e) {
@@ -3,6 +3,7 @@ import type { IndexItem } from "./types";
import ReactFiber from "@/seqta/utils/ReactFiber"; import ReactFiber from "@/seqta/utils/ReactFiber";
import { delay } from "@/seqta/utils/delay"; import { delay } from "@/seqta/utils/delay";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
interface MessageMetadata { interface MessageMetadata {
messageId: number; messageId: number;
author: string; author: string;
@@ -171,7 +172,7 @@ export const actionMap: Record<string, ActionHandler<any>> = {
if ((assessmentId === undefined || assessmentId === null) && itemClone.id && itemClone.id.startsWith('assignment-')) { if ((assessmentId === undefined || assessmentId === null) && itemClone.id && itemClone.id.startsWith('assignment-')) {
const extractedId = itemClone.id.replace('assignment-', ''); const extractedId = itemClone.id.replace('assignment-', '');
assessmentId = Number(extractedId) || extractedId; assessmentId = Number(extractedId) || extractedId;
console.log("[Assessment Action] Extracted assessmentId from item ID:", assessmentId); verboseLog("[Assessment Action] Extracted assessmentId from item ID:", assessmentId);
} }
// Convert to numbers, but preserve 0 as valid // Convert to numbers, but preserve 0 as valid
@@ -198,7 +199,7 @@ export const actionMap: Record<string, ActionHandler<any>> = {
if (hasProgrammeId && hasMetaclassId && hasAssessmentId) { if (hasProgrammeId && hasMetaclassId && hasAssessmentId) {
const url = `#?page=/assessments/${programmeId}:${metaclassId}&item=${assessmentId}`; const url = `#?page=/assessments/${programmeId}:${metaclassId}&item=${assessmentId}`;
console.log("[Assessment Action] ✅ Navigating to:", url); verboseLog("[Assessment Action] ✅ Navigating to:", url);
window.location.hash = url; window.location.hash = url;
} else { } else {
// Fallback: try to navigate to assessments page if metadata is incomplete // Fallback: try to navigate to assessments page if metadata is incomplete
@@ -7,6 +7,7 @@ import { loadDynamicItems } from "../utils/dynamicItems";
import { getVectorizedItemIds } from "./utils"; import { getVectorizedItemIds } from "./utils";
import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion"; import { INDEX_SCHEMA_VERSION, SCHEMA_VERSION_KEY } from "./schemaVersion";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const META_STORE = "meta"; const META_STORE = "meta";
const LOCK_KEY = "bsq-indexer-lock"; const LOCK_KEY = "bsq-indexer-lock";
const HEARTBEAT_INTERVAL = 10000; const HEARTBEAT_INTERVAL = 10000;
@@ -101,7 +102,7 @@ async function updateLastRunMeta(jobId: string): Promise<void> {
async function acquireLock(): Promise<boolean> { async function acquireLock(): Promise<boolean> {
if (isIndexingActive) { if (isIndexingActive) {
console.debug("[Indexer] Already indexing in this tab"); verboseDebug("[Indexer] Already indexing in this tab");
return false; return false;
} }
@@ -200,7 +201,7 @@ export async function loadAllStoredItems(): Promise<IndexItem[]> {
console.error(`Error loading items for job store ${jobId}:`, error); console.error(`Error loading items for job store ${jobId}:`, error);
} }
} }
console.debug( verboseDebug(
`[Indexer] Loaded ${all.length} items from all primary stores.`, `[Indexer] Loaded ${all.length} items from all primary stores.`,
); );
return all; return all;
@@ -210,7 +211,7 @@ export async function runIndexing(): Promise<void> {
await ensureSchemaCurrent(); await ensureSchemaCurrent();
if (!(await acquireLock())) { if (!(await acquireLock())) {
console.debug( verboseDebug(
"%c[Indexer] Could not acquire lock - another tab is indexing or this tab is already indexing", "%c[Indexer] Could not acquire lock - another tab is indexing or this tab is already indexing",
"color: gray", "color: gray",
); );
@@ -218,7 +219,7 @@ export async function runIndexing(): Promise<void> {
} }
startHeartbeat(); startHeartbeat();
console.debug("%c[Indexer] Starting indexing...", "color: green"); verboseDebug("%c[Indexer] Starting indexing...", "color: green");
const jobIds = Object.keys(jobs); const jobIds = Object.keys(jobs);
let completedJobs = 0; let completedJobs = 0;
@@ -236,7 +237,7 @@ export async function runIndexing(): Promise<void> {
const lastRun = await getLastRunMeta(jobId); const lastRun = await getLastRunMeta(jobId);
if (!shouldRun(job, lastRun)) { if (!shouldRun(job, lastRun)) {
console.debug( verboseDebug(
`%c[Indexer] Skipping job "${jobId}" (not due)`, `%c[Indexer] Skipping job "${jobId}" (not due)`,
"color: gray", "color: gray",
); );
@@ -288,7 +289,7 @@ export async function runIndexing(): Promise<void> {
setProgress: (p) => saveProgress(jobId, p), setProgress: (p) => saveProgress(jobId, p),
}; };
console.debug(`%c[Indexer] Running job "${jobId}"...`, "color: #4ea1ff"); verboseDebug(`%c[Indexer] Running job "${jobId}"...`, "color: #4ea1ff");
try { try {
const newItemsRaw = await job.run(ctx); const newItemsRaw = await job.run(ctx);
@@ -300,12 +301,12 @@ export async function runIndexing(): Promise<void> {
await setStoredItems(merged); await setStoredItems(merged);
await updateLastRunMeta(jobId); await updateLastRunMeta(jobId);
console.debug( verboseDebug(
`%c[Indexer] ${job.label}: ${newItemsRaw.length} new items reported by run, ${merged.length} total items now in '${jobId}' store.`, `%c[Indexer] ${job.label}: ${newItemsRaw.length} new items reported by run, ${merged.length} total items now in '${jobId}' store.`,
"color: #00c46f", "color: #00c46f",
); );
} catch (err) { } catch (err) {
console.debug(`%c[Indexer] Job ${job.label} failed:`, "color: red"); verboseDebug(`%c[Indexer] Job ${job.label} failed:`, "color: red");
console.error(err); console.error(err);
} }
@@ -321,7 +322,7 @@ export async function runIndexing(): Promise<void> {
let allItemsInPrimaryStores = await loadAllStoredItems(); let allItemsInPrimaryStores = await loadAllStoredItems();
if (allItemsInPrimaryStores.length > 0) { if (allItemsInPrimaryStores.length > 0) {
console.debug( verboseDebug(
`%c[Indexer] Checking ${allItemsInPrimaryStores.length} items for vectorization...`, `%c[Indexer] Checking ${allItemsInPrimaryStores.length} items for vectorization...`,
"color: #4ea1ff", "color: #4ea1ff",
); );
@@ -331,7 +332,7 @@ export async function runIndexing(): Promise<void> {
const newItemsToVectorize = allItemsInPrimaryStores.filter(item => !vectorizedItemIds.has(item.id)); const newItemsToVectorize = allItemsInPrimaryStores.filter(item => !vectorizedItemIds.has(item.id));
if (newItemsToVectorize.length > 0) { if (newItemsToVectorize.length > 0) {
console.debug( verboseDebug(
`%c[Indexer] Sending ${newItemsToVectorize.length} new items to worker for vectorization (${allItemsInPrimaryStores.length - newItemsToVectorize.length} already vectorized)`, `%c[Indexer] Sending ${newItemsToVectorize.length} new items to worker for vectorization (${allItemsInPrimaryStores.length - newItemsToVectorize.length} already vectorized)`,
"color: #4ea1ff", "color: #4ea1ff",
); );
@@ -389,7 +390,7 @@ export async function runIndexing(): Promise<void> {
); );
} }
}); });
console.debug( verboseDebug(
"%c[Indexer] Vectorization task for stored items sent to worker.", "%c[Indexer] Vectorization task for stored items sent to worker.",
"color: green", "color: green",
); );
@@ -408,7 +409,7 @@ export async function runIndexing(): Promise<void> {
); );
} }
} else { } else {
console.debug( verboseDebug(
`%c[Indexer] All ${allItemsInPrimaryStores.length} items are already vectorized, skipping worker initialization.`, `%c[Indexer] All ${allItemsInPrimaryStores.length} items are already vectorized, skipping worker initialization.`,
"color: gray", "color: gray",
); );
@@ -421,7 +422,7 @@ export async function runIndexing(): Promise<void> {
); );
} }
} else { } else {
console.debug( verboseDebug(
"%c[Indexer] No items found in primary stores to send for vectorization.", "%c[Indexer] No items found in primary stores to send for vectorization.",
"color: gray", "color: gray",
); );
@@ -1,5 +1,6 @@
import type { IndexItem, Job } from "../types"; import type { IndexItem, Job } from "../types";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const fetchJSON = async (url: string, body: any) => { const fetchJSON = async (url: string, body: any) => {
const res = await fetch(`${location.origin}${url}`, { const res = await fetch(`${location.origin}${url}`, {
method: "POST", method: "POST",
@@ -128,7 +129,7 @@ export const assignmentsJob: Job = {
const student = 69; // TODO: Get from context if available const student = 69; // TODO: Get from context if available
console.debug("[Assignments job] Starting indexing - fetching all assessments (upcoming and past)..."); verboseDebug("[Assignments job] Starting indexing - fetching all assessments (upcoming and past)...");
// Fetch data in parallel // Fetch data in parallel
const [upcoming, subjects] = await Promise.all([ const [upcoming, subjects] = await Promise.all([
@@ -136,12 +137,12 @@ export const assignmentsJob: Job = {
fetchSubjects(), fetchSubjects(),
]); ]);
console.debug(`[Assignments job] Fetched ${upcoming.length} upcoming assessments and ${subjects.length} subjects`); verboseDebug(`[Assignments job] Fetched ${upcoming.length} upcoming assessments and ${subjects.length} subjects`);
// Fetch past assessments for ALL subjects to ensure we get all historical assignments // Fetch past assessments for ALL subjects to ensure we get all historical assignments
const past = await fetchPastAssessments(student, subjects); const past = await fetchPastAssessments(student, subjects);
console.debug(`[Assignments job] Fetched ${past.length} past assessments`); verboseDebug(`[Assignments job] Fetched ${past.length} past assessments`);
// Create a lookup map from subject code to programme/metaclass // Create a lookup map from subject code to programme/metaclass
const subjectLookup = new Map<string, { programme: number; metaclass: number }>(); const subjectLookup = new Map<string, { programme: number; metaclass: number }>();
@@ -220,7 +221,7 @@ export const assignmentsJob: Job = {
const assessmentArray = Array.from(allAssessments.values()); const assessmentArray = Array.from(allAssessments.values());
const pastCount = assessmentArray.filter(a => !a.isUpcoming).length; const pastCount = assessmentArray.filter(a => !a.isUpcoming).length;
const upcomingCount = assessmentArray.filter(a => a.isUpcoming).length; const upcomingCount = assessmentArray.filter(a => a.isUpcoming).length;
console.debug(`[Assignments job] Processing ${assessmentArray.length} total assessments (${upcomingCount} upcoming, ${pastCount} past)`); verboseDebug(`[Assignments job] Processing ${assessmentArray.length} total assessments (${upcomingCount} upcoming, ${pastCount} past)`);
const batchSize = 15; // Increased batch size for better performance const batchSize = 15; // Increased batch size for better performance
// Skip fetching assessment details - the API endpoint doesn't exist or returns 404 // Skip fetching assessment details - the API endpoint doesn't exist or returns 404
@@ -321,7 +322,7 @@ export const assignmentsJob: Job = {
renderComponentId: "assessment", renderComponentId: "assessment",
}; };
console.debug(`[Assignments job] ✅ Created item for assignment ${assessment.id}:`, { verboseDebug(`[Assignments job] ✅ Created item for assignment ${assessment.id}:`, {
id: item.id, id: item.id,
programmeId: item.metadata.programmeId, programmeId: item.metadata.programmeId,
programmeID: item.metadata.programmeID, programmeID: item.metadata.programmeID,
@@ -350,7 +351,7 @@ export const assignmentsJob: Job = {
const newItemsCount = items.filter(item => !existingIds.has(item.id)).length; const newItemsCount = items.filter(item => !existingIds.has(item.id)).length;
const updatedItemsCount = items.length - newItemsCount; const updatedItemsCount = items.length - newItemsCount;
console.debug(`[Assignments job] Indexed ${items.length} assignment items (${newItemsCount} new, ${updatedItemsCount} updated)`); verboseDebug(`[Assignments job] Indexed ${items.length} assignment items (${newItemsCount} new, ${updatedItemsCount} updated)`);
return items; return items;
}, },
@@ -3,6 +3,7 @@ import { seqtaFetchPayload } from "../api";
import { buildIndexItem } from "../extract"; import { buildIndexItem } from "../extract";
import { htmlToPlainText } from "../utils"; import { htmlToPlainText } from "../utils";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes per-subject course content from `/seqta/student/load/courses`. * Indexes per-subject course content from `/seqta/student/load/courses`.
* *
@@ -106,7 +107,7 @@ export const coursesJob: Job = {
run: async (_ctx) => { run: async (_ctx) => {
const subjects = await fetchActiveSubjects(); const subjects = await fetchActiveSubjects();
if (subjects.length === 0) { if (subjects.length === 0) {
console.debug("[Courses job] No active subjects discovered."); verboseDebug("[Courses job] No active subjects discovered.");
return []; return [];
} }
@@ -169,7 +170,7 @@ export const coursesJob: Job = {
); );
} }
console.debug( verboseDebug(
`[Courses job] Indexed ${items.length} courses across ${subjects.length} subjects.`, `[Courses job] Indexed ${items.length} courses across ${subjects.length} subjects.`,
); );
return items; return items;
@@ -1,6 +1,7 @@
import type { IndexItem, Job } from "../types"; import type { IndexItem, Job } from "../types";
import { seqtaFetchPayload } from "../api"; import { seqtaFetchPayload } from "../api";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes file metadata from `/seqta/student/load/documents`. * Indexes file metadata from `/seqta/student/load/documents`.
* *
@@ -131,7 +132,7 @@ export const documentsJob: Job = {
} }
} }
console.debug(`[Documents job] Indexed ${items.length} document entries.`); verboseDebug(`[Documents job] Indexed ${items.length} document entries.`);
return items; return items;
}, },
@@ -3,6 +3,7 @@ import { seqtaFetchPayload } from "../api";
import { htmlToPlainText } from "../utils"; import { htmlToPlainText } from "../utils";
import { delay } from "@/seqta/utils/delay"; import { delay } from "@/seqta/utils/delay";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes student folio entries from `/seqta/student/folio`. * Indexes student folio entries from `/seqta/student/folio`.
* *
@@ -126,7 +127,7 @@ export const folioJob: Job = {
await delay(PER_ITEM_DELAY_MS); await delay(PER_ITEM_DELAY_MS);
} }
console.debug(`[Folio job] Indexed ${items.length} folio entries.`); verboseDebug(`[Folio job] Indexed ${items.length} folio entries.`);
return items; return items;
}, },
@@ -3,6 +3,7 @@ import { seqtaFetchPayload } from "../api";
import { extractTextFromValue } from "../extract"; import { extractTextFromValue } from "../extract";
import { delay } from "@/seqta/utils/delay"; import { delay } from "@/seqta/utils/delay";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes student goals from `/seqta/student/load/goals`. * Indexes student goals from `/seqta/student/load/goals`.
* *
@@ -42,7 +43,7 @@ export const goalsJob: Job = {
{ mode: "years" }, { mode: "years" },
); );
if (!Array.isArray(years) || years.length === 0) { if (!Array.isArray(years) || years.length === 0) {
console.debug("[Goals job] No goal years available; skipping."); verboseDebug("[Goals job] No goal years available; skipping.");
return []; return [];
} }
@@ -101,7 +102,7 @@ export const goalsJob: Job = {
await delay(PER_YEAR_DELAY_MS); await delay(PER_YEAR_DELAY_MS);
} }
console.debug(`[Goals job] Indexed ${items.length} goal entries.`); verboseDebug(`[Goals job] Indexed ${items.length} goal entries.`);
return items; return items;
}, },
@@ -7,6 +7,7 @@ import { loadAllStoredItems } from "../indexer";
import { renderComponentMap } from "../renderComponents"; import { renderComponentMap } from "../renderComponents";
import { jobs } from "../jobs"; import { jobs } from "../jobs";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const RATE_LIMIT_CONFIG = { const RATE_LIMIT_CONFIG = {
minDelay: 30, minDelay: 30,
maxDelay: 3000, maxDelay: 3000,
@@ -208,7 +209,7 @@ function checkCircuitBreaker(progress: MessagesProgress): boolean {
) { ) {
progress.circuitBreakerOpen = false; progress.circuitBreakerOpen = false;
progress.consecutiveFailures = 0; progress.consecutiveFailures = 0;
console.info( verboseInfo(
`[Messages job] Circuit breaker closed after ${RATE_LIMIT_CONFIG.circuitBreakerResetTime}ms`, `[Messages job] Circuit breaker closed after ${RATE_LIMIT_CONFIG.circuitBreakerResetTime}ms`,
); );
return false; return false;
@@ -352,7 +353,7 @@ async function processMessagesInParallel(
batchResponseTime, batchResponseTime,
); );
console.log( verboseLog(
`[Messages job] Processed parallel batch: ${batchSuccesses} successes, ${batchFailures} failures, ${batchResponseTime}ms total time`, `[Messages job] Processed parallel batch: ${batchSuccesses} successes, ${batchFailures} failures, ${batchResponseTime}ms total time`,
); );
} }
@@ -397,7 +398,7 @@ export const messagesJob: Job = {
await vectorWorker.startStreamingSession( await vectorWorker.startStreamingSession(
progress.totalEstimated, progress.totalEstimated,
(progressData) => { (progressData) => {
console.log( verboseLog(
`[Messages job] Vector streaming progress: ${progressData.processed}/${progressData.total} (${progressData.status})`, `[Messages job] Vector streaming progress: ${progressData.processed}/${progressData.total} (${progressData.status})`,
); );
}, },
@@ -405,7 +406,7 @@ export const messagesJob: Job = {
"messages", "messages",
); );
progress.streamingStarted = true; progress.streamingStarted = true;
console.log( verboseLog(
`[Messages job] Started streaming vectorization session for ~${progress.totalEstimated} items`, `[Messages job] Started streaming vectorization session for ~${progress.totalEstimated} items`,
); );
} catch (error) { } catch (error) {
@@ -422,7 +423,7 @@ export const messagesJob: Job = {
let itemsStreamedToVector = 0; let itemsStreamedToVector = 0;
if (progress.retryQueue.length > 0) { if (progress.retryQueue.length > 0) {
console.log( verboseLog(
`[Messages job] Processing ${Math.min(progress.retryQueue.length, 10)} items from retry queue`, `[Messages job] Processing ${Math.min(progress.retryQueue.length, 10)} items from retry queue`,
); );
@@ -505,7 +506,7 @@ export const messagesJob: Job = {
batchResponseTime, batchResponseTime,
); );
console.log( verboseLog(
`[Messages job] Processed retry batch: ${retrySuccesses} successes, ${retryFailures} failures`, `[Messages job] Processed retry batch: ${retrySuccesses} successes, ${retryFailures} failures`,
); );
} }
@@ -590,7 +591,7 @@ export const messagesJob: Job = {
try { try {
await vectorWorker.streamItems(itemsToStream); await vectorWorker.streamItems(itemsToStream);
itemsStreamedToVector += itemsToStream.length; itemsStreamedToVector += itemsToStream.length;
console.log( verboseLog(
`[Messages job] Streamed ${itemsToStream.length} items to vector worker (total: ${itemsStreamedToVector})`, `[Messages job] Streamed ${itemsToStream.length} items to vector worker (total: ${itemsStreamedToVector})`,
); );
} catch (error) { } catch (error) {
@@ -659,7 +660,7 @@ export const messagesJob: Job = {
await ctx.setProgress(progress); await ctx.setProgress(progress);
progressUpdateCounter = 0; progressUpdateCounter = 0;
console.log( verboseLog(
`[Messages job] Progress: offset=${progress.offset}, batchSize=${progress.currentBatchSize}, delay=${progress.currentDelay}ms, failures=${progress.failedRequests}, retryQueue=${progress.retryQueue.length}, vectorStreamed=${itemsStreamedToVector}, parallelRequests=${RATE_LIMIT_CONFIG.parallelRequests}`, `[Messages job] Progress: offset=${progress.offset}, batchSize=${progress.currentBatchSize}, delay=${progress.currentDelay}ms, failures=${progress.failedRequests}, retryQueue=${progress.retryQueue.length}, vectorStreamed=${itemsStreamedToVector}, parallelRequests=${RATE_LIMIT_CONFIG.parallelRequests}`,
); );
} }
@@ -673,7 +674,7 @@ export const messagesJob: Job = {
if (progress.streamingStarted) { if (progress.streamingStarted) {
try { try {
await vectorWorker.endStreamingSession(); await vectorWorker.endStreamingSession();
console.log( verboseLog(
`[Messages job] Ended streaming session. Total items streamed: ${itemsStreamedToVector}`, `[Messages job] Ended streaming session. Total items streamed: ${itemsStreamedToVector}`,
); );
} catch (error) { } catch (error) {
@@ -3,6 +3,7 @@ import { seqtaFetchPayload } from "../api";
import { htmlToPlainText } from "../utils"; import { htmlToPlainText } from "../utils";
import { delay } from "@/seqta/utils/delay"; import { delay } from "@/seqta/utils/delay";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes daily notices from `/seqta/student/load/notices`. * Indexes daily notices from `/seqta/student/load/notices`.
* *
@@ -205,7 +206,7 @@ export const noticesJob: Job = {
await ctx.setProgress(progress); await ctx.setProgress(progress);
const newCount = items.filter((i) => !existingIds.has(i.id)).length; const newCount = items.filter((i) => !existingIds.has(i.id)).length;
console.debug( verboseDebug(
`[Notices job] Indexed ${items.length} notices across ${dates.length} dates (${newCount} new).`, `[Notices job] Indexed ${items.length} notices across ${dates.length} dates (${newCount} new).`,
); );
return items; return items;
@@ -8,6 +8,7 @@ import { loadAllStoredItems } from "../indexer";
import { renderComponentMap } from "../renderComponents"; import { renderComponentMap } from "../renderComponents";
import { jobs } from "../jobs"; import { jobs } from "../jobs";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const NOTIFICATIONS_RATE_LIMIT = { const NOTIFICATIONS_RATE_LIMIT = {
baseDelay: 150, baseDelay: 150,
maxDelay: 3000, maxDelay: 3000,
@@ -201,7 +202,7 @@ export const notificationsJob: Job = {
await vectorWorker.startStreamingSession( await vectorWorker.startStreamingSession(
estimatedTotal, estimatedTotal,
(progressData) => { (progressData) => {
console.log( verboseLog(
`[Notifications job] Vector streaming progress: ${progressData.processed}/${progressData.total} (${progressData.status})`, `[Notifications job] Vector streaming progress: ${progressData.processed}/${progressData.total} (${progressData.status})`,
); );
}, },
@@ -209,7 +210,7 @@ export const notificationsJob: Job = {
"notifications", "notifications",
); );
progress.streamingStarted = true; progress.streamingStarted = true;
console.log( verboseLog(
`[Notifications job] Started streaming vectorization session for ~${estimatedTotal} items`, `[Notifications job] Started streaming vectorization session for ~${estimatedTotal} items`,
); );
} catch (error) { } catch (error) {
@@ -247,7 +248,7 @@ export const notificationsJob: Job = {
let itemsStreamedToVector = 0; let itemsStreamedToVector = 0;
if (progress.retryQueue.length > 0) { if (progress.retryQueue.length > 0) {
console.log( verboseLog(
`[Notifications job] Processing ${Math.min(progress.retryQueue.length, 3)} items from retry queue`, `[Notifications job] Processing ${Math.min(progress.retryQueue.length, 3)} items from retry queue`,
); );
@@ -352,7 +353,7 @@ export const notificationsJob: Job = {
try { try {
await vectorWorker.streamItems([...itemsToStream]); await vectorWorker.streamItems([...itemsToStream]);
itemsStreamedToVector += itemsToStream.length; itemsStreamedToVector += itemsToStream.length;
console.log( verboseLog(
`[Notifications job] Streamed ${itemsToStream.length} items to vector worker (total: ${itemsStreamedToVector})`, `[Notifications job] Streamed ${itemsToStream.length} items to vector worker (total: ${itemsStreamedToVector})`,
); );
itemsToStream.length = 0; itemsToStream.length = 0;
@@ -424,7 +425,7 @@ export const notificationsJob: Job = {
try { try {
await vectorWorker.streamItems([...itemsToStream]); await vectorWorker.streamItems([...itemsToStream]);
itemsStreamedToVector += itemsToStream.length; itemsStreamedToVector += itemsToStream.length;
console.log( verboseLog(
`[Notifications job] Streamed final ${itemsToStream.length} items to vector worker (total: ${itemsStreamedToVector})`, `[Notifications job] Streamed final ${itemsToStream.length} items to vector worker (total: ${itemsStreamedToVector})`,
); );
} catch (error) { } catch (error) {
@@ -438,7 +439,7 @@ export const notificationsJob: Job = {
if (progress.streamingStarted) { if (progress.streamingStarted) {
try { try {
await vectorWorker.endStreamingSession(); await vectorWorker.endStreamingSession();
console.log( verboseLog(
`[Notifications job] Ended streaming session. Total items streamed: ${itemsStreamedToVector}`, `[Notifications job] Ended streaming session. Total items streamed: ${itemsStreamedToVector}`,
); );
progress.streamingStarted = false; progress.streamingStarted = false;
@@ -459,7 +460,7 @@ export const notificationsJob: Job = {
} }
await ctx.setProgress(progress); await ctx.setProgress(progress);
console.log( verboseLog(
`[Notifications job] Processed ${processedCount} notifications, ${progress.retryQueue.length} in retry queue, ${progress.failedRequests} failures, ${itemsStreamedToVector} items streamed to vector worker`, `[Notifications job] Processed ${processedCount} notifications, ${progress.retryQueue.length} in retry queue, ${progress.failedRequests} failures, ${itemsStreamedToVector} items streamed to vector worker`,
); );
@@ -1,6 +1,7 @@
import type { IndexItem, Job } from "../types"; import type { IndexItem, Job } from "../types";
import { seqtaFetchPayload } from "../api"; import { seqtaFetchPayload } from "../api";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes the user's external portal entries from `/seqta/student/load/portals`. * Indexes the user's external portal entries from `/seqta/student/load/portals`.
* *
@@ -82,7 +83,7 @@ export const portalsJob: Job = {
}); });
} }
console.debug(`[Portals job] Indexed ${items.length} portal entries.`); verboseDebug(`[Portals job] Indexed ${items.length} portal entries.`);
return items; return items;
}, },
@@ -1,6 +1,7 @@
import type { IndexItem, Job } from "../types"; import type { IndexItem, Job } from "../types";
import { seqtaFetchPayload } from "../api"; import { seqtaFetchPayload } from "../api";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Indexes report metadata from `/seqta/student/load/reports`. * Indexes report metadata from `/seqta/student/load/reports`.
* *
@@ -89,7 +90,7 @@ export const reportsJob: Job = {
}); });
} }
console.debug(`[Reports job] Indexed ${items.length} reports.`); verboseDebug(`[Reports job] Indexed ${items.length} reports.`);
return items; return items;
}, },
@@ -1,5 +1,6 @@
import type { IndexItem, Job } from "../types"; import type { IndexItem, Job } from "../types";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const fetchSubjects = async () => { const fetchSubjects = async () => {
const res = await fetch(`${location.origin}/seqta/student/load/subjects`, { const res = await fetch(`${location.origin}/seqta/student/load/subjects`, {
method: "POST", method: "POST",
@@ -129,7 +130,7 @@ export const subjectsJob: Job = {
} }
} }
console.debug(`[Subjects job] Indexed ${items.length} subject items`); verboseDebug(`[Subjects job] Indexed ${items.length} subject items`);
return items; return items;
}, },
@@ -6,6 +6,7 @@ import {
pickId, pickId,
pickTitle, pickTitle,
} from "./extract"; } from "./extract";
import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
import { isSensitiveSeqtaPath, normalizeSeqtaPath } from "./api"; import { isSensitiveSeqtaPath, normalizeSeqtaPath } from "./api";
import { loadAllStoredItems } from "./indexer"; import { loadAllStoredItems } from "./indexer";
import { loadDynamicItems } from "../utils/dynamicItems"; import { loadDynamicItems } from "../utils/dynamicItems";
@@ -542,7 +543,7 @@ export function installPassiveObserver(): void {
} }
} catch (e) { } catch (e) {
// Never let observer errors bubble up to the host page. // Never let observer errors bubble up to the host page.
console.debug("[Passive Observer] fetch hook error:", e); verboseDebug("[Passive Observer] fetch hook error:", e);
} }
return response; return response;
@@ -605,7 +606,7 @@ export function installPassiveObserver(): void {
void persistItems(items); void persistItems(items);
} }
} catch (e) { } catch (e) {
console.debug("[Passive Observer] xhr load error:", e); verboseDebug("[Passive Observer] xhr load error:", e);
} }
}); });
} }
@@ -616,7 +617,7 @@ export function installPassiveObserver(): void {
}; };
} }
console.debug("[Passive Observer] Installed."); verboseDebug("[Passive Observer] Installed.");
} }
/** /**
@@ -7,6 +7,7 @@ import {
pickId, pickId,
buildIndexItem, buildIndexItem,
} from "./extract"; } from "./extract";
import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
import { isSensitiveSeqtaPath, normalizeSeqtaPath } from "./api"; import { isSensitiveSeqtaPath, normalizeSeqtaPath } from "./api";
import { import {
coursesPayload, coursesPayload,
@@ -320,7 +321,7 @@ export async function runGlobalSearchSelfTests(): Promise<SelfTestReport> {
report.failures, report.failures,
); );
} else { } else {
console.info( verboseInfo(
`[Global Search Self-Tests] All ${report.passed} cases passed`, `[Global Search Self-Tests] All ${report.passed} cases passed`,
); );
} }
@@ -1,3 +1,4 @@
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
/** /**
* Check which items are already vectorized in embeddia's IndexedDB * Check which items are already vectorized in embeddia's IndexedDB
* Returns a Set of item IDs that are already indexed * Returns a Set of item IDs that are already indexed
@@ -7,7 +8,7 @@ export async function getVectorizedItemIds(): Promise<Set<string>> {
const request = indexedDB.open("embeddiaDB"); const request = indexedDB.open("embeddiaDB");
request.onerror = () => { request.onerror = () => {
console.debug("Could not open embeddiaDB, assuming no items are vectorized"); verboseDebug("Could not open embeddiaDB, assuming no items are vectorized");
resolve(new Set()); resolve(new Set());
}; };
@@ -15,7 +16,7 @@ export async function getVectorizedItemIds(): Promise<Set<string>> {
const db = (event.target as IDBOpenDBRequest).result; const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains("embeddiaObjectStore")) { if (!db.objectStoreNames.contains("embeddiaObjectStore")) {
console.debug("embeddiaObjectStore not found, assuming no items are vectorized"); verboseDebug("embeddiaObjectStore not found, assuming no items are vectorized");
db.close(); db.close();
resolve(new Set()); resolve(new Set());
return; return;
@@ -34,7 +35,7 @@ export async function getVectorizedItemIds(): Promise<Set<string>> {
} }
}); });
console.debug(`Found ${vectorizedIds.size} already vectorized items in embeddia DB`); verboseDebug(`Found ${vectorizedIds.size} already vectorized items in embeddia DB`);
db.close(); db.close();
resolve(vectorizedIds); resolve(vectorizedIds);
}; };
@@ -1,6 +1,7 @@
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia"; import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { IndexItem } from "../types"; import type { IndexItem } from "../types";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
let vectorIndex: EmbeddingIndex | null = null; let vectorIndex: EmbeddingIndex | null = null;
let isInitialized = false; let isInitialized = false;
let initializationFailed = false; let initializationFailed = false;
@@ -33,27 +34,27 @@ let streamingSession: {
async function initWorker() { async function initWorker() {
if (isInitialized) { if (isInitialized) {
console.debug("Vector worker already initialized."); verboseDebug("Vector worker already initialized.");
return; return;
} }
// Skip initialization in Firefox // Skip initialization in Firefox
if (isFirefoxWorker()) { if (isFirefoxWorker()) {
console.debug("[Vector Worker] Vector search not supported in Firefox - skipping initialization"); verboseDebug("[Vector Worker] Vector search not supported in Firefox - skipping initialization");
isInitialized = true; isInitialized = true;
initializationFailed = true; initializationFailed = true;
vectorIndex = null; vectorIndex = null;
return; return;
} }
console.debug("Initializing vector worker..."); verboseDebug("Initializing vector worker...");
try { try {
await initializeModel(); await initializeModel();
vectorIndex = new EmbeddingIndex([]); vectorIndex = new EmbeddingIndex([]);
const stored = await vectorIndex.getAllObjectsFromIndexedDB(); const stored = await vectorIndex.getAllObjectsFromIndexedDB();
if (stored.length > 0) { if (stored.length > 0) {
console.debug(`Found ${stored.length} existing items in IndexedDB`); verboseDebug(`Found ${stored.length} existing items in IndexedDB`);
loadedItemIds.clear(); loadedItemIds.clear();
@@ -64,14 +65,14 @@ async function initWorker() {
} }
}); });
console.debug( verboseDebug(
`Vector index loaded ${loadedItemIds.size} unique items from IndexedDB.`, `Vector index loaded ${loadedItemIds.size} unique items from IndexedDB.`,
); );
} else { } else {
console.debug("No existing vector index found in IndexedDB."); verboseDebug("No existing vector index found in IndexedDB.");
} }
isInitialized = true; isInitialized = true;
console.debug("Vector worker initialized successfully."); verboseDebug("Vector worker initialized successfully.");
} catch (e) { } catch (e) {
console.warn("[Vector Worker] Failed to initialize vector worker (will use text search only):", e); console.warn("[Vector Worker] Failed to initialize vector worker (will use text search only):", e);
isInitialized = true; isInitialized = true;
@@ -149,7 +150,7 @@ async function startStreamingSession(
processingPromise: null, processingPromise: null,
}; };
console.debug( verboseDebug(
`Started streaming session for ${totalExpected} items with batch size ${batchSize}`, `Started streaming session for ${totalExpected} items with batch size ${batchSize}`,
); );
@@ -175,7 +176,7 @@ async function processStreamingBatch(
streamingSession.totalReceived += items.length; streamingSession.totalReceived += items.length;
streamingSession.pendingItems.push(...items); streamingSession.pendingItems.push(...items);
console.debug( verboseDebug(
`Received streaming batch: ${items.length} items (${streamingSession.totalReceived}/${streamingSession.totalExpected})`, `Received streaming batch: ${items.length} items (${streamingSession.totalReceived}/${streamingSession.totalExpected})`,
); );
@@ -208,7 +209,7 @@ async function processStreamingItems() {
if (unprocessedItems.length === 0) { if (unprocessedItems.length === 0) {
streamingSession.totalProcessed += batchToProcess.length; streamingSession.totalProcessed += batchToProcess.length;
console.debug(`Skipped ${batchToProcess.length} already processed items`); verboseDebug(`Skipped ${batchToProcess.length} already processed items`);
continue; continue;
} }
@@ -231,7 +232,7 @@ async function processStreamingItems() {
loadedItemIds.size % 200 === 0 loadedItemIds.size % 200 === 0
) { ) {
await vectorIndex!.saveIndex("indexedDB"); await vectorIndex!.saveIndex("indexedDB");
console.debug( verboseDebug(
`Saved streaming index at ${streamingSession.totalProcessed} processed items (${loadedItemIds.size} total unique items)`, `Saved streaming index at ${streamingSession.totalProcessed} processed items (${loadedItemIds.size} total unique items)`,
); );
} }
@@ -272,7 +273,7 @@ async function finalizeStreamingSession() {
try { try {
if (vectorIndex) { if (vectorIndex) {
await vectorIndex.saveIndex("indexedDB"); await vectorIndex.saveIndex("indexedDB");
console.debug("Final save of streaming index completed"); verboseDebug("Final save of streaming index completed");
} }
} catch (e) { } catch (e) {
console.error("Error in final streaming save:", e); console.error("Error in final streaming save:", e);
@@ -293,7 +294,7 @@ async function finalizeStreamingSession() {
}, },
}); });
console.debug( verboseDebug(
`Streaming session completed: ${totalProcessed}/${totalExpected} items processed`, `Streaming session completed: ${totalProcessed}/${totalExpected} items processed`,
); );
} }
@@ -303,14 +304,14 @@ async function endStreamingSession() {
return; return;
} }
console.debug("Ending streaming session..."); verboseDebug("Ending streaming session...");
if (streamingSession.processingPromise) { if (streamingSession.processingPromise) {
await streamingSession.processingPromise; await streamingSession.processingPromise;
} }
if (streamingSession.pendingItems.length > 0) { if (streamingSession.pendingItems.length > 0) {
console.debug( verboseDebug(
`Processing ${streamingSession.pendingItems.length} remaining items before ending session`, `Processing ${streamingSession.pendingItems.length} remaining items before ending session`,
); );
streamingSession.processingPromise = processStreamingItems(); streamingSession.processingPromise = processStreamingItems();
@@ -320,7 +321,7 @@ async function endStreamingSession() {
try { try {
if (vectorIndex) { if (vectorIndex) {
await vectorIndex.saveIndex("indexedDB"); await vectorIndex.saveIndex("indexedDB");
console.debug("Final save before ending streaming session"); verboseDebug("Final save before ending streaming session");
} }
} catch (e) { } catch (e) {
console.error("Error in final save before ending session:", e); console.error("Error in final save before ending session:", e);
@@ -341,7 +342,7 @@ async function endStreamingSession() {
} }
async function processItems(items: IndexItem[], signal: AbortSignal) { async function processItems(items: IndexItem[], signal: AbortSignal) {
console.debug("Worker received process request."); verboseDebug("Worker received process request.");
if (initializationFailed || isFirefoxWorker()) { if (initializationFailed || isFirefoxWorker()) {
self.postMessage({ self.postMessage({
@@ -378,7 +379,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
}); });
if (signal.aborted) { if (signal.aborted) {
console.debug("Processing cancelled before starting."); verboseDebug("Processing cancelled before starting.");
self.postMessage({ self.postMessage({
type: "progress", type: "progress",
data: { data: {
@@ -390,7 +391,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
} }
if (unprocessedItems.length === 0) { if (unprocessedItems.length === 0) {
console.debug( verboseDebug(
`No new items to process. ${loadedItemIds.size} items already in index.`, `No new items to process. ${loadedItemIds.size} items already in index.`,
); );
self.postMessage({ self.postMessage({
@@ -403,7 +404,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
return; return;
} }
console.debug( verboseDebug(
`Starting processing of ${unprocessedItems.length} items (${items.length - unprocessedItems.length} already processed).`, `Starting processing of ${unprocessedItems.length} items (${items.length - unprocessedItems.length} already processed).`,
); );
self.postMessage({ self.postMessage({
@@ -419,7 +420,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
let processedCount = 0; let processedCount = 0;
for (let i = 0; i < unprocessedItems.length; i += BATCH_SIZE) { for (let i = 0; i < unprocessedItems.length; i += BATCH_SIZE) {
if (signal.aborted) { if (signal.aborted) {
console.debug("Processing cancelled during batching."); verboseDebug("Processing cancelled during batching.");
self.postMessage({ self.postMessage({
type: "progress", type: "progress",
data: { data: {
@@ -437,7 +438,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
) as (IndexItem & { embedding: number[] })[]; ) as (IndexItem & { embedding: number[] })[];
if (signal.aborted) { if (signal.aborted) {
console.debug("Processing cancelled after vectorization batch."); verboseDebug("Processing cancelled after vectorization batch.");
self.postMessage({ self.postMessage({
type: "progress", type: "progress",
data: { data: {
@@ -464,7 +465,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
} }
if (signal.aborted) { if (signal.aborted) {
console.debug("Processing cancelled before saving batch."); verboseDebug("Processing cancelled before saving batch.");
self.postMessage({ self.postMessage({
type: "progress", type: "progress",
data: { data: {
@@ -481,7 +482,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
) { ) {
try { try {
await vectorIndex!.saveIndex("indexedDB"); await vectorIndex!.saveIndex("indexedDB");
console.debug( verboseDebug(
`Saved index after processing batch ${i / BATCH_SIZE + 1} (${loadedItemIds.size} total unique items)`, `Saved index after processing batch ${i / BATCH_SIZE + 1} (${loadedItemIds.size} total unique items)`,
); );
} catch (e) { } catch (e) {
@@ -505,7 +506,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
}); });
} }
console.debug( verboseDebug(
`Processing complete. Total unique items in index: ${loadedItemIds.size}`, `Processing complete. Total unique items in index: ${loadedItemIds.size}`,
); );
self.postMessage({ self.postMessage({
@@ -520,7 +521,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
} }
async function resetWorker() { async function resetWorker() {
console.debug("Resetting vector worker state..."); verboseDebug("Resetting vector worker state...");
loadedItemIds.clear(); loadedItemIds.clear();
@@ -532,7 +533,7 @@ async function resetWorker() {
if (vectorIndex) { if (vectorIndex) {
try { try {
await vectorIndex.saveIndex("indexedDB"); await vectorIndex.saveIndex("indexedDB");
console.debug("Saved index before reset"); verboseDebug("Saved index before reset");
} catch (e) { } catch (e) {
console.warn("Error saving index before reset:", e); console.warn("Error saving index before reset:", e);
} }
@@ -543,7 +544,7 @@ async function resetWorker() {
await initWorker(); await initWorker();
console.debug( verboseDebug(
`Vector worker reset complete. Loaded ${loadedItemIds.size} items.`, `Vector worker reset complete. Loaded ${loadedItemIds.size} items.`,
); );
@@ -3,6 +3,7 @@ import type { IndexItem } from "../types";
import { isVectorSearchSupported } from "../../utils/browserDetection"; import { isVectorSearchSupported } from "../../utils/browserDetection";
import vectorWorker from "./vectorWorker.ts?inlineWorker"; import vectorWorker from "./vectorWorker.ts?inlineWorker";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
export type ProgressCallback = (data: { export type ProgressCallback = (data: {
status: "started" | "processing" | "complete" | "error" | "cancelled"; status: "started" | "processing" | "complete" | "error" | "cancelled";
total?: number; total?: number;
@@ -38,7 +39,7 @@ export class VectorWorkerManager {
static getInstance(): VectorWorkerManager { static getInstance(): VectorWorkerManager {
if (!VectorWorkerManager.instance) { if (!VectorWorkerManager.instance) {
console.debug("Creating new VectorWorkerManager instance"); verboseDebug("Creating new VectorWorkerManager instance");
VectorWorkerManager.instance = new VectorWorkerManager(); VectorWorkerManager.instance = new VectorWorkerManager();
} }
return VectorWorkerManager.instance; return VectorWorkerManager.instance;
@@ -47,7 +48,7 @@ export class VectorWorkerManager {
private async initWorker(): Promise<void> { private async initWorker(): Promise<void> {
// Skip initialization if vector search is not supported (e.g., Firefox) // Skip initialization if vector search is not supported (e.g., Firefox)
if (!isVectorSearchSupported()) { if (!isVectorSearchSupported()) {
console.debug("[VectorWorkerManager] Vector search not supported - skipping worker initialization"); verboseDebug("[VectorWorkerManager] Vector search not supported - skipping worker initialization");
this.isInitialized = false; this.isInitialized = false;
return Promise.resolve(); return Promise.resolve();
} }
@@ -55,19 +56,19 @@ export class VectorWorkerManager {
if (this.isInitialized) return Promise.resolve(); if (this.isInitialized) return Promise.resolve();
if (this.readyPromise) return this.readyPromise; if (this.readyPromise) return this.readyPromise;
console.debug("Lazy-loading vector worker..."); verboseDebug("Lazy-loading vector worker...");
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (this.worker) { if (this.worker) {
console.debug("Terminating existing worker before creating new one"); verboseDebug("Terminating existing worker before creating new one");
this.worker.terminate(); this.worker.terminate();
this.worker = null; this.worker = null;
} }
console.debug("Creating new vector worker instance"); verboseDebug("Creating new vector worker instance");
this.worker = vectorWorker(); this.worker = vectorWorker();
console.log("Worker initialized", this.worker); verboseLog("Worker initialized", this.worker);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
console.error("Vector worker initialization timed out"); console.error("Vector worker initialization timed out");
@@ -82,14 +83,14 @@ export class VectorWorkerManager {
this.worker!.addEventListener("message", (e) => { this.worker!.addEventListener("message", (e) => {
const { type, data } = e.data; const { type, data } = e.data;
console.debug("Message from vector worker:", type, data); verboseDebug("Message from vector worker:", type, data);
switch (type) { switch (type) {
case "ready": case "ready":
this.isInitialized = true; this.isInitialized = true;
clearTimeout(timeout); clearTimeout(timeout);
this.updateActivity(); // Start idle timer after initialization this.updateActivity(); // Start idle timer after initialization
console.debug("Vector worker initialized and ready."); verboseDebug("Vector worker initialized and ready.");
resolve(); resolve();
break; break;
@@ -150,7 +151,7 @@ export class VectorWorkerManager {
} }
private resetWorkerState() { private resetWorkerState() {
console.debug("Resetting vector worker state"); verboseDebug("Resetting vector worker state");
if (this.worker) { if (this.worker) {
this.worker.terminate(); this.worker.terminate();
this.worker = null; this.worker = null;
@@ -176,7 +177,7 @@ export class VectorWorkerManager {
if (this.vectorizationLockCount > 0) return; if (this.vectorizationLockCount > 0) return;
if (this.streamingSession?.isActive) return; if (this.streamingSession?.isActive) return;
if (!this.isInitialized) return; if (!this.isInitialized) return;
console.debug("[VectorWorker] Auto-shutting down due to 2 minutes of inactivity"); verboseDebug("[VectorWorker] Auto-shutting down due to 2 minutes of inactivity");
this.resetWorkerState(); this.resetWorkerState();
}, 120000); // 2 minutes }, 120000); // 2 minutes
} }
@@ -208,7 +209,7 @@ export class VectorWorkerManager {
this.unloadTimer = setTimeout(() => { this.unloadTimer = setTimeout(() => {
if (this.vectorizationLockCount > 0) return; if (this.vectorizationLockCount > 0) return;
if (!this.streamingSession?.isActive && this.isInitialized) { if (!this.streamingSession?.isActive && this.isInitialized) {
console.debug("[VectorWorker] Auto-unloading after processing complete"); verboseDebug("[VectorWorker] Auto-unloading after processing complete");
this.resetWorkerState(); this.resetWorkerState();
} }
}, delay); }, delay);
@@ -295,7 +296,7 @@ export class VectorWorkerManager {
}); });
if (uniqueItems.length !== items.length) { if (uniqueItems.length !== items.length) {
console.debug( verboseDebug(
`Filtered out ${items.length - uniqueItems.length} duplicate items before processing`, `Filtered out ${items.length - uniqueItems.length} duplicate items before processing`,
); );
} }
@@ -350,7 +351,7 @@ export class VectorWorkerManager {
}; };
this.progressCallback = wrap; this.progressCallback = wrap;
console.debug( verboseDebug(
`Sending ${uniqueItems.length} unique items to worker for processing.`, `Sending ${uniqueItems.length} unique items to worker for processing.`,
); );
@@ -378,7 +379,7 @@ export class VectorWorkerManager {
): Promise<void> { ): Promise<void> {
// Skip if vector search is not supported // Skip if vector search is not supported
if (!isVectorSearchSupported()) { if (!isVectorSearchSupported()) {
console.debug("[VectorWorker] Vector search not supported - skipping streaming session"); verboseDebug("[VectorWorker] Vector search not supported - skipping streaming session");
if (onProgress) { if (onProgress) {
onProgress({ onProgress({
status: "complete", status: "complete",
@@ -390,7 +391,7 @@ export class VectorWorkerManager {
// Only initialize if we expect items to process // Only initialize if we expect items to process
if (totalExpectedItems === 0) { if (totalExpectedItems === 0) {
console.debug("[VectorWorker] No items expected, not starting streaming session"); verboseDebug("[VectorWorker] No items expected, not starting streaming session");
return; return;
} }
@@ -405,7 +406,7 @@ export class VectorWorkerManager {
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
} else { } else {
console.debug(`Streaming session for job ${jobId} already active`); verboseDebug(`Streaming session for job ${jobId} already active`);
return; return;
} }
} }
@@ -425,7 +426,7 @@ export class VectorWorkerManager {
lastActivityTime: Date.now(), lastActivityTime: Date.now(),
}; };
console.debug( verboseDebug(
`Starting streaming session for job ${jobId} with ${totalExpectedItems} items (batch size ${batchSize})`, `Starting streaming session for job ${jobId} with ${totalExpectedItems} items (batch size ${batchSize})`,
); );
@@ -456,7 +457,7 @@ export class VectorWorkerManager {
}); });
if (uniqueItems.length !== items.length) { if (uniqueItems.length !== items.length) {
console.debug( verboseDebug(
`[Streaming] Filtered out ${items.length - uniqueItems.length} duplicate items before streaming`, `[Streaming] Filtered out ${items.length - uniqueItems.length} duplicate items before streaming`,
); );
} }
@@ -472,7 +473,7 @@ export class VectorWorkerManager {
this.streamingSession.inactivityTimer = setTimeout(() => { this.streamingSession.inactivityTimer = setTimeout(() => {
if (this.streamingSession?.isActive) { if (this.streamingSession?.isActive) {
console.debug( verboseDebug(
"[VectorWorker] Auto-ending streaming session due to inactivity", "[VectorWorker] Auto-ending streaming session due to inactivity",
); );
this.endStreamingSession(); this.endStreamingSession();
@@ -513,7 +514,7 @@ export class VectorWorkerManager {
this.streamingSession.flushTimer = null; this.streamingSession.flushTimer = null;
} }
console.debug( verboseDebug(
`Streaming batch of ${batch.length} items to worker (${this.streamingSession.totalSent}/${this.streamingSession.totalExpected})`, `Streaming batch of ${batch.length} items to worker (${this.streamingSession.totalSent}/${this.streamingSession.totalExpected})`,
); );
@@ -549,7 +550,7 @@ export class VectorWorkerManager {
type: "endStreaming", type: "endStreaming",
}); });
console.debug("Streaming session ended"); verboseDebug("Streaming session ended");
if (this.progressCallback) { if (this.progressCallback) {
this.progressCallback({ this.progressCallback({
@@ -590,12 +591,12 @@ export class VectorWorkerManager {
} }
terminate() { terminate() {
console.debug("Terminating Vector Worker Manager..."); verboseDebug("Terminating Vector Worker Manager...");
this.resetWorkerState(); this.resetWorkerState();
} }
async resetWorker(): Promise<void> { async resetWorker(): Promise<void> {
console.debug("Resetting vector worker..."); verboseDebug("Resetting vector worker...");
if (this.streamingSession?.isActive) { if (this.streamingSession?.isActive) {
await this.endStreamingSession(); await this.endStreamingSession();
@@ -605,6 +606,6 @@ export class VectorWorkerManager {
this.worker!.postMessage({ type: "reset" }); this.worker!.postMessage({ type: "reset" });
console.debug("Reset command sent to worker"); verboseDebug("Reset command sent to worker");
} }
} }
@@ -10,6 +10,7 @@ import {
isStrongLexicalMatch, isStrongLexicalMatch,
STRONG_LEXICAL_THRESHOLD, STRONG_LEXICAL_THRESHOLD,
} from "./lexicalMatch"; } from "./lexicalMatch";
import { verboseDebug, verboseInfo, verboseLog } from "@/utils/verboseLog";
/** Same normalization as lexical matching (trim + lowercase). */ /** Same normalization as lexical matching (trim + lowercase). */
function normSearchKey(s: string): string { function normSearchKey(s: string): string {
@@ -91,7 +92,7 @@ function setCachedResults(query: string, results: CombinedResult[]) {
*/ */
export function clearSearchCache(): void { export function clearSearchCache(): void {
searchCache.clear(); searchCache.clear();
console.debug("[Search] Search result cache cleared"); verboseDebug("[Search] Search result cache cleared");
} }
// Listen for cache clear events (e.g., on extension update) // Listen for cache clear events (e.g., on extension update)
@@ -3,6 +3,7 @@ import type { IndexItem } from "../../indexing/types";
import type { SearchResult } from "embeddia"; import type { SearchResult } from "embeddia";
import { isVectorSearchSupported } from "../../utils/browserDetection"; import { isVectorSearchSupported } from "../../utils/browserDetection";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
let vectorIndex: EmbeddingIndex | null = null; let vectorIndex: EmbeddingIndex | null = null;
let initializationAttempted = false; let initializationAttempted = false;
let initializationFailed = false; let initializationFailed = false;
@@ -11,7 +12,7 @@ export async function initVectorSearch() {
// Skip initialization if already attempted and failed, or if not supported // Skip initialization if already attempted and failed, or if not supported
if (initializationFailed || !isVectorSearchSupported()) { if (initializationFailed || !isVectorSearchSupported()) {
if (!isVectorSearchSupported()) { if (!isVectorSearchSupported()) {
console.debug("[Vector Search] Vector search not supported in Firefox - using text search only"); verboseDebug("[Vector Search] Vector search not supported in Firefox - using text search only");
} }
return; return;
} }
@@ -26,7 +27,7 @@ export async function initVectorSearch() {
await initializeModel(); await initializeModel();
vectorIndex = new EmbeddingIndex([]); vectorIndex = new EmbeddingIndex([]);
vectorIndex.preloadIndexedDB(); vectorIndex.preloadIndexedDB();
console.debug("[Vector Search] Initialized successfully"); verboseDebug("[Vector Search] Initialized successfully");
} catch (e) { } catch (e) {
console.warn("[Vector Search] Failed to initialize vector search (will use text search only):", e); console.warn("[Vector Search] Failed to initialize vector search (will use text search only):", e);
initializationFailed = true; initializationFailed = true;
@@ -66,7 +67,7 @@ function setCachedEmbedding(query: string, embedding: number[]) {
*/ */
export function clearEmbeddingCache(): void { export function clearEmbeddingCache(): void {
embeddingCache.clear(); embeddingCache.clear();
console.debug("[Vector Search] Embedding cache cleared"); verboseDebug("[Vector Search] Embedding cache cleared");
} }
// Listen for cache clear events (e.g., on extension update) // Listen for cache clear events (e.g., on extension update)
@@ -1,6 +1,7 @@
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import { resetSearchIndexes } from "../indexing/resetIndexes"; import { resetSearchIndexes } from "../indexing/resetIndexes";
import { verboseDebug, verboseInfo, verboseLog } from '@/utils/verboseLog';
const VERSION_STORAGE_KEY = "betterseqta-global-search-version"; const VERSION_STORAGE_KEY = "betterseqta-global-search-version";
const VERSION_CACHE_KEY = "betterseqta-global-search-cache-version"; const VERSION_CACHE_KEY = "betterseqta-global-search-cache-version";
@@ -60,7 +61,7 @@ export async function checkAndHandleUpdate(): Promise<boolean> {
// First run: just remember the version, don't reset (the user likely // First run: just remember the version, don't reset (the user likely
// just installed the extension; the index is already empty). // just installed the extension; the index is already empty).
if (!storedVersion) { if (!storedVersion) {
console.debug( verboseDebug(
`[Version Check] First run detected, storing version ${currentVersion}`, `[Version Check] First run detected, storing version ${currentVersion}`,
); );
storeVersion(currentVersion); storeVersion(currentVersion);
@@ -71,7 +72,7 @@ export async function checkAndHandleUpdate(): Promise<boolean> {
return false; return false;
} }
console.log( verboseLog(
`[Version Check] Extension updated from ${storedVersion} to ${currentVersion}, resetting search index...`, `[Version Check] Extension updated from ${storedVersion} to ${currentVersion}, resetting search index...`,
); );
@@ -79,7 +80,7 @@ export async function checkAndHandleUpdate(): Promise<boolean> {
try { try {
await resetSearchIndexes(); await resetSearchIndexes();
console.log( verboseLog(
"[Version Check] Search index reset; next indexing pass will repopulate from scratch.", "[Version Check] Search index reset; next indexing pass will repopulate from scratch.",
); );
} catch (e) { } catch (e) {
@@ -112,7 +113,7 @@ export async function clearAllCaches(): Promise<void> {
} catch (e: any) { } catch (e: any) {
// Module might not be loaded yet, or CSS preload error - that's okay // Module might not be loaded yet, or CSS preload error - that's okay
if (!e?.message?.includes("preload CSS") && !e?.message?.includes("MIME type")) { if (!e?.message?.includes("preload CSS") && !e?.message?.includes("MIME type")) {
console.debug("[Version Check] Could not clear search cache:", e); verboseDebug("[Version Check] Could not clear search cache:", e);
} }
} }
@@ -122,12 +123,12 @@ export async function clearAllCaches(): Promise<void> {
} catch (e: any) { } catch (e: any) {
// Module might not be loaded yet, or CSS preload error - that's okay // Module might not be loaded yet, or CSS preload error - that's okay
if (!e?.message?.includes("preload CSS") && !e?.message?.includes("MIME type")) { if (!e?.message?.includes("preload CSS") && !e?.message?.includes("MIME type")) {
console.debug("[Version Check] Could not clear embedding cache:", e); verboseDebug("[Version Check] Could not clear embedding cache:", e);
} }
} }
}, 50); }, 50);
console.debug("[Version Check] All caches cleared"); verboseDebug("[Version Check] All caches cleared");
} catch (e) { } catch (e) {
console.error("[Version Check] Error clearing caches:", e); console.error("[Version Check] Error clearing caches:", e);
} }
@@ -1,5 +1,6 @@
import type { Plugin } from "../../core/types"; import type { Plugin } from "../../core/types";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage"; import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import { verboseInfo } from "@/utils/verboseLog";
interface NotificationCollectorStorage { interface NotificationCollectorStorage {
lastNotificationCount: number; lastNotificationCount: number;
@@ -75,7 +76,7 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
if (alertDiv) { if (alertDiv) {
alertDiv.textContent = notificationCount.toString(); alertDiv.textContent = notificationCount.toString();
} else { } else {
console.info("[BetterSEQTA+] No notifications currently"); verboseInfo("[BetterSEQTA+] No notifications currently");
} }
} catch (error) { } catch (error) {
console.error("[BetterSEQTA+] Error fetching notifications:", error); console.error("[BetterSEQTA+] Error fetching notifications:", error);
+86 -233
View File
@@ -17,6 +17,12 @@ import {
clearCustomThemeAdaptiveCssVariables, clearCustomThemeAdaptiveCssVariables,
setCustomThemeAdaptiveCssVariables, setCustomThemeAdaptiveCssVariables,
} from "@/seqta/ui/colors/customThemeAdaptiveBindings"; } from "@/seqta/ui/colors/customThemeAdaptiveBindings";
import {
clearThemeInPage,
syncThemeToPage,
type ThemePageSyncInput,
} from "@/seqta/utils/patchThemeImagesPageContext";
import { verboseDebug, verboseInfo } from "@/utils/verboseLog";
import { import {
clearThemeRuntime, clearThemeRuntime,
injectThemeDom, injectThemeDom,
@@ -56,18 +62,15 @@ export type InstallThemeMeta = {
export class ThemeManager { export class ThemeManager {
private static instance: ThemeManager; private static instance: ThemeManager;
private currentTheme: CustomTheme | null = null; private currentTheme: CustomTheme | null = null;
private styleElement: HTMLStyleElement | null = null;
private previewStyleElement: HTMLStyleElement | null = null;
private previousImageVariableNames: string[] = []; private previousImageVariableNames: string[] = [];
private lastSyncedImageKey: string | null = null;
private originalPreviewColor: string | null = null; private originalPreviewColor: string | null = null;
private originalPreviewTheme: boolean | null = null; private originalPreviewTheme: boolean | null = null;
private imageUrlCache: Map<string, string> = new Map();
private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 }; private lastTransitionPoint: { x: number; y: number } = { x: 0, y: 0 };
private storeUpdateCheckRunning = false; private storeUpdateCheckRunning = false;
private headObserver: MutationObserver | null = null;
private constructor() { private constructor() {
console.debug("[ThemeManager] Initializing..."); verboseDebug("[ThemeManager] Initializing...");
} }
public static getInstance(): ThemeManager { public static getInstance(): ThemeManager {
@@ -88,7 +91,7 @@ export class ThemeManager {
* Get a theme by ID from storage * Get a theme by ID from storage
*/ */
public async getTheme(themeId: string): Promise<CustomTheme | null> { public async getTheme(themeId: string): Promise<CustomTheme | null> {
console.debug("[ThemeManager] Getting theme:", themeId); verboseDebug("[ThemeManager] Getting theme:", themeId);
try { try {
const theme = (await localforage.getItem(themeId)) as CustomTheme; const theme = (await localforage.getItem(themeId)) as CustomTheme;
return theme; return theme;
@@ -164,17 +167,17 @@ export class ThemeManager {
* Disable the current theme without deleting it * Disable the current theme without deleting it
*/ */
public async disableTheme(): Promise<void> { public async disableTheme(): Promise<void> {
console.debug("[ThemeManager] Disabling current theme"); verboseDebug("[ThemeManager] Disabling current theme");
try { try {
if (!this.currentTheme) { if (!this.currentTheme) {
console.debug("[ThemeManager] No theme to disable"); verboseDebug("[ThemeManager] No theme to disable");
return; return;
} }
await this.removeTheme(this.currentTheme); await this.removeTheme(this.currentTheme);
this.currentTheme = null; this.currentTheme = null;
settingsState.selectedTheme = ""; settingsState.selectedTheme = "";
console.debug("[ThemeManager] Theme disabled successfully"); verboseDebug("[ThemeManager] Theme disabled successfully");
} catch (error) { } catch (error) {
console.error("[ThemeManager] Error disabling theme:", error); console.error("[ThemeManager] Error disabling theme:", error);
} }
@@ -211,7 +214,7 @@ export class ThemeManager {
* Initialize the theme system and restore previous state * Initialize the theme system and restore previous state
*/ */
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
console.debug("[ThemeManager] Starting initialization"); verboseDebug("[ThemeManager] Starting initialization");
try { try {
const neumorphicThemeId = "9a9786d1-b5fc-4a91-8c7a-f8bf7f7679ad"; const neumorphicThemeId = "9a9786d1-b5fc-4a91-8c7a-f8bf7f7679ad";
const migrationCSS = "#title {\nbackground: transparent !important;\n}"; const migrationCSS = "#title {\nbackground: transparent !important;\n}";
@@ -224,7 +227,7 @@ export class ThemeManager {
const themeCreatorOpen = localStorage.getItem("themeCreatorOpen"); const themeCreatorOpen = localStorage.getItem("themeCreatorOpen");
if (themeCreatorOpen === "true") { if (themeCreatorOpen === "true") {
console.debug( verboseDebug(
"[ThemeManager] Theme creator was open, clearing preview state", "[ThemeManager] Theme creator was open, clearing preview state",
); );
this.clearPreview(); this.clearPreview();
@@ -232,7 +235,7 @@ export class ThemeManager {
} }
if (settingsState.selectedTheme) { if (settingsState.selectedTheme) {
console.debug( verboseDebug(
"[ThemeManager] Found selected theme, restoring:", "[ThemeManager] Found selected theme, restoring:",
settingsState.selectedTheme, settingsState.selectedTheme,
); );
@@ -249,7 +252,7 @@ export class ThemeManager {
* Clean up theme system resources * Clean up theme system resources
*/ */
public async cleanup(): Promise<void> { public async cleanup(): Promise<void> {
console.debug("[ThemeManager] Cleaning up resources"); verboseDebug("[ThemeManager] Cleaning up resources");
try { try {
if (this.currentTheme) { if (this.currentTheme) {
await this.removeTheme(this.currentTheme, false); await this.removeTheme(this.currentTheme, false);
@@ -263,7 +266,7 @@ export class ThemeManager {
* Set and apply a theme by ID * Set and apply a theme by ID
*/ */
public async setTheme(themeId: string, applyViewTransition: boolean = true): Promise<void> { public async setTheme(themeId: string, applyViewTransition: boolean = true): Promise<void> {
console.debug("[ThemeManager] Setting theme:", themeId); verboseDebug("[ThemeManager] Setting theme:", themeId);
try { try {
const theme = (await localforage.getItem(themeId)) as CustomTheme; const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (!theme) { if (!theme) {
@@ -273,7 +276,7 @@ export class ThemeManager {
// Store original settings before applying new theme // Store original settings before applying new theme
if (!settingsState.selectedTheme) { if (!settingsState.selectedTheme) {
console.debug("[ThemeManager] Storing original settings"); verboseDebug("[ThemeManager] Storing original settings");
settingsState.originalSelectedColor = settingsState.selectedColor; settingsState.originalSelectedColor = settingsState.selectedColor;
if (shouldForceThemeAppearance(theme)) { if (shouldForceThemeAppearance(theme)) {
@@ -286,7 +289,7 @@ export class ThemeManager {
await this.applyViewTransition(async () => { await this.applyViewTransition(async () => {
// Remove current theme if exists // Remove current theme if exists
if (this.currentTheme) { if (this.currentTheme) {
console.debug("[ThemeManager] Removing current theme"); verboseDebug("[ThemeManager] Removing current theme");
await this.removeThemeWithoutTransition(this.currentTheme); await this.removeThemeWithoutTransition(this.currentTheme);
} }
@@ -298,7 +301,7 @@ export class ThemeManager {
} else { } else {
// Remove current theme if exists // Remove current theme if exists
if (this.currentTheme) { if (this.currentTheme) {
console.debug("[ThemeManager] Removing current theme"); verboseDebug("[ThemeManager] Removing current theme");
await this.removeThemeWithoutTransition(this.currentTheme); await this.removeThemeWithoutTransition(this.currentTheme);
} }
@@ -317,7 +320,7 @@ export class ThemeManager {
* Apply theme components (CSS, images, settings) * Apply theme components (CSS, images, settings)
*/ */
private async applyTheme(theme: CustomTheme): Promise<void> { private async applyTheme(theme: CustomTheme): Promise<void> {
console.debug("[ThemeManager] Applying theme:", theme.name); verboseDebug("[ThemeManager] Applying theme:", theme.name);
try { try {
// Run the theme script BEFORE injecting CustomCSS so any state the // Run the theme script BEFORE injecting CustomCSS so any state the
// script publishes (e.g. `data-city-state` and `--city-sky-color` for // script publishes (e.g. `data-city-state` and `--city-sky-color` for
@@ -326,40 +329,34 @@ export class ThemeManager {
// its previous state before snapping to the right colour. // its previous state before snapping to the right colour.
runThemeScript(theme.themeScript); runThemeScript(theme.themeScript);
// Apply custom CSS // Custom CSS + images must be applied in page context (Firefox).
if (theme.CustomCSS) { verboseDebug("[ThemeManager] Applying theme styles in page context");
console.debug("[ThemeManager] Applying custom CSS"); await syncThemeToPage({
this.applyCustomCSS(theme.CustomCSS); customCss: theme.CustomCSS || "",
} images: theme.CustomImages ?? [],
});
// Apply custom images if (theme.CustomImages?.length) {
if (theme.CustomImages) { this.lastSyncedImageKey = this.imageSyncKey(theme.CustomImages);
console.debug("[ThemeManager] Applying custom images"); } else {
theme.CustomImages.forEach((image) => { this.lastSyncedImageKey = null;
const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty(
"--" + image.variableName,
`url(${imageUrl})`,
);
});
} }
// Apply theme settings // Apply theme settings
if (shouldForceThemeAppearance(theme)) { if (shouldForceThemeAppearance(theme)) {
const dark = getForcedDarkMode(theme); const dark = getForcedDarkMode(theme);
console.debug("[ThemeManager] Setting dark mode:", dark); verboseDebug("[ThemeManager] Setting dark mode:", dark);
settingsState.DarkMode = dark; settingsState.DarkMode = dark;
} }
// Use the stored selected color if available, otherwise use the default // Use the stored selected color if available, otherwise use the default
if (theme.selectedColor) { if (theme.selectedColor) {
console.debug( verboseDebug(
"[ThemeManager] Restoring saved color:", "[ThemeManager] Restoring saved color:",
theme.selectedColor, theme.selectedColor,
); );
settingsState.selectedColor = theme.selectedColor; settingsState.selectedColor = theme.selectedColor;
} else if (theme.defaultColour) { } else if (theme.defaultColour) {
console.debug( verboseDebug(
"[ThemeManager] Using default color:", "[ThemeManager] Using default color:",
theme.defaultColour, theme.defaultColour,
); );
@@ -381,7 +378,7 @@ export class ThemeManager {
theme: CustomTheme, theme: CustomTheme,
clearSelectedTheme: boolean = true, clearSelectedTheme: boolean = true,
): Promise<void> { ): Promise<void> {
console.debug("[ThemeManager] Removing theme with transition:", theme.name); verboseDebug("[ThemeManager] Removing theme with transition:", theme.name);
try { try {
await this.applyViewTransition(async () => { await this.applyViewTransition(async () => {
await this.removeThemeWithoutTransition(theme, clearSelectedTheme); await this.removeThemeWithoutTransition(theme, clearSelectedTheme);
@@ -398,37 +395,13 @@ export class ThemeManager {
theme: CustomTheme, theme: CustomTheme,
clearSelectedTheme: boolean = true, clearSelectedTheme: boolean = true,
): Promise<void> { ): Promise<void> {
console.debug("[ThemeManager] Removing theme:", theme.name); verboseDebug("[ThemeManager] Removing theme:", theme.name);
try { try {
clearThemeRuntime(); clearThemeRuntime();
// Disconnect the head observer BEFORE removing the style element, verboseDebug("[ThemeManager] Removing theme page styles");
// otherwise the removal fires the observer and it would no-op only clearThemeInPage();
// because the style is already gone — wasted work, but harmless. this.lastSyncedImageKey = null;
this.disconnectStyleObserver();
// Remove custom CSS
if (this.styleElement) {
console.debug("[ThemeManager] Removing custom CSS");
this.styleElement.remove();
this.styleElement = null;
}
// Remove custom images
if (theme.CustomImages) {
console.debug("[ThemeManager] Removing custom images");
theme.CustomImages.forEach((image) => {
const value = document.documentElement.style.getPropertyValue(
"--" + image.variableName,
);
if (value) {
URL.revokeObjectURL(value.slice(4, -1)); // Remove url() wrapper
}
document.documentElement.style.removeProperty(
"--" + image.variableName,
);
});
}
if (this.currentTheme) { if (this.currentTheme) {
// Store the current color with the theme before removing it // Store the current color with the theme before removing it
@@ -449,7 +422,7 @@ export class ThemeManager {
// Restore original settings // Restore original settings
if (settingsState.originalSelectedColor) { if (settingsState.originalSelectedColor) {
console.debug( verboseDebug(
"[ThemeManager] Restoring original color:", "[ThemeManager] Restoring original color:",
settingsState.originalSelectedColor, settingsState.originalSelectedColor,
); );
@@ -457,7 +430,7 @@ export class ThemeManager {
} }
if (settingsState.originalDarkMode !== undefined) { if (settingsState.originalDarkMode !== undefined) {
console.debug( verboseDebug(
"[ThemeManager] Restoring original dark mode:", "[ThemeManager] Restoring original dark mode:",
settingsState.originalDarkMode, settingsState.originalDarkMode,
); );
@@ -476,58 +449,21 @@ export class ThemeManager {
} }
/** /**
* Apply custom CSS to the document. The `<style>` element is always * Stable key so preview updates can skip re-encoding image blobs when only CSS changed.
* re-appended to the end of `<head>` so it wins specificity ties
* against any styles SEQTA's late-loading injected.scss adds in dev
* mode (where `import("@/css/injected.scss")` is fire-and-forget and
* can resolve after the theme has already been applied). The head
* observer below keeps us at the end if anything else gets appended
* later (Vite HMR, another script-injected stylesheet, etc.).
*/ */
private applyCustomCSS(css: string): void { private imageSyncKey(
console.debug("[ThemeManager] Applying custom CSS"); images: Array<{ id: string; variableName: string; blob: Blob }>,
try { ): string {
if (!this.styleElement) { return images
this.styleElement = document.createElement("style"); .map((image) => `${image.id}:${image.variableName}:${image.blob.size}`)
this.styleElement.id = "custom-theme"; .join("|");
}
this.styleElement.textContent = css;
document.head.appendChild(this.styleElement);
this.ensureStyleStaysLast();
} catch (error) {
console.error("[ThemeManager] Error applying custom CSS:", error);
}
}
/**
* Watch `<head>` for any child-list changes and re-append the theme
* style element if anything has been added after it. Idempotent: the
* observer's own re-append fires the callback again, but the early
* `lastElementChild === style` check short-circuits the second pass.
*/
private ensureStyleStaysLast(): void {
if (this.headObserver) return;
this.headObserver = new MutationObserver(() => {
const style = this.styleElement;
if (!style || !document.head.contains(style)) return;
if (document.head.lastElementChild === style) return;
document.head.appendChild(style);
});
this.headObserver.observe(document.head, { childList: true });
}
private disconnectStyleObserver(): void {
if (this.headObserver) {
this.headObserver.disconnect();
this.headObserver = null;
}
} }
/** /**
* Get list of available themes * Get list of available themes
*/ */
public async getAvailableThemes(): Promise<CustomTheme[]> { public async getAvailableThemes(): Promise<CustomTheme[]> {
console.debug("[ThemeManager] Getting available themes"); verboseDebug("[ThemeManager] Getting available themes");
try { try {
const themeIds = (await localforage.getItem("customThemes")) as const themeIds = (await localforage.getItem("customThemes")) as
| string[] | string[]
@@ -553,7 +489,7 @@ export class ThemeManager {
* Save or update a theme * Save or update a theme
*/ */
public async saveTheme(theme: LoadedCustomTheme): Promise<void> { public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug("[ThemeManager] Saving theme:", theme.name); verboseDebug("[ThemeManager] Saving theme:", theme.name);
try { try {
const existing = (await localforage.getItem(theme.id)) as CustomTheme | null; const existing = (await localforage.getItem(theme.id)) as CustomTheme | null;
let toSave = theme; let toSave = theme;
@@ -583,7 +519,7 @@ export class ThemeManager {
* Delete a theme * Delete a theme
*/ */
public async deleteTheme(themeId: string): Promise<void> { public async deleteTheme(themeId: string): Promise<void> {
console.debug("[ThemeManager] Deleting theme:", themeId); verboseDebug("[ThemeManager] Deleting theme:", themeId);
try { try {
const theme = (await localforage.getItem(themeId)) as CustomTheme; const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (theme) { if (theme) {
@@ -653,7 +589,7 @@ export class ThemeManager {
theme_json_url?: string; theme_json_url?: string;
updated_at?: number; updated_at?: number;
}): Promise<void> { }): Promise<void> {
console.debug("[ThemeManager] Downloading theme:", themeContent.name); verboseDebug("[ThemeManager] Downloading theme:", themeContent.name);
if (!themeContent.id) { if (!themeContent.id) {
throw new Error("Missing theme id"); throw new Error("Missing theme id");
} }
@@ -689,7 +625,7 @@ export class ThemeManager {
themeData: ThemeContent, themeData: ThemeContent,
meta?: InstallThemeMeta, meta?: InstallThemeMeta,
): Promise<void> { ): Promise<void> {
console.debug("[ThemeManager] Installing theme:", themeData.name); verboseDebug("[ThemeManager] Installing theme:", themeData.name);
try { try {
// Validate required fields // Validate required fields
if (!themeData.id || !themeData.name) { if (!themeData.id || !themeData.name) {
@@ -860,7 +796,7 @@ export class ThemeManager {
name: theme.name, name: theme.name,
updated_at: serverUpdated, updated_at: serverUpdated,
}); });
console.log( verboseInfo(
"[ThemeManager] Theme auto-updated from store:", "[ThemeManager] Theme auto-updated from store:",
theme.name, theme.name,
); );
@@ -893,7 +829,7 @@ export class ThemeManager {
* Share a theme by exporting it * Share a theme by exporting it
*/ */
public async shareTheme(themeId: string): Promise<void> { public async shareTheme(themeId: string): Promise<void> {
console.debug("[ThemeManager] Sharing theme:", themeId); verboseDebug("[ThemeManager] Sharing theme:", themeId);
try { try {
const theme = (await localforage.getItem(themeId)) as LoadedCustomTheme; const theme = (await localforage.getItem(themeId)) as LoadedCustomTheme;
if (!theme) { if (!theme) {
@@ -947,7 +883,7 @@ export class ThemeManager {
* Preview a theme without applying it * Preview a theme without applying it
*/ */
public async previewTheme(theme: LoadedCustomTheme): Promise<void> { public async previewTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug("[ThemeManager] Previewing theme:", theme.name); verboseDebug("[ThemeManager] Previewing theme:", theme.name);
try { try {
const { CustomCSS, CustomImages, defaultColour } = theme; const { CustomCSS, CustomImages, defaultColour } = theme;
@@ -965,35 +901,16 @@ export class ThemeManager {
} }
} }
// Apply custom CSS // Apply custom CSS + images in page context (preview stylesheet)
if (CustomCSS) { await syncThemeToPage({
this.applyPreviewCSS(CustomCSS); previewCss: CustomCSS,
} images: CustomImages,
});
// Apply custom images this.lastSyncedImageKey = this.imageSyncKey(CustomImages);
const newImageVariableNames = CustomImages.map( this.previousImageVariableNames = CustomImages.map(
(image) => image.variableName, (image) => image.variableName,
); );
// Remove old preview images
this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName);
}
});
// Apply new images
CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
});
// Update previousImageVariableNames
this.previousImageVariableNames = newImageVariableNames;
// Apply theme settings // Apply theme settings
if (shouldForceThemeAppearance(theme)) { if (shouldForceThemeAppearance(theme)) {
settingsState.DarkMode = getForcedDarkMode(theme); settingsState.DarkMode = getForcedDarkMode(theme);
@@ -1014,7 +931,7 @@ export class ThemeManager {
* Update the preview of a theme in real-time (for theme creator) * Update the preview of a theme in real-time (for theme creator)
*/ */
public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> { public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> {
console.debug("[ThemeManager] Updating theme preview"); verboseDebug("[ThemeManager] Updating theme preview");
try { try {
// Only store original settings if this is a new theme (not editing) // Only store original settings if this is a new theme (not editing)
// We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded) // We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded)
@@ -1027,49 +944,28 @@ export class ThemeManager {
} }
} }
// Apply CSS if changed const newImageVariableNames =
theme.CustomImages?.map((image) => image.variableName) ?? [];
const syncInput: ThemePageSyncInput = {};
if (theme.CustomCSS !== undefined) { if (theme.CustomCSS !== undefined) {
this.applyPreviewCSS(theme.CustomCSS); syncInput.previewCss = theme.CustomCSS;
} }
// Handle images if present
if (theme.CustomImages) { if (theme.CustomImages) {
const newImageVariableNames = theme.CustomImages.map( const imageKey = this.imageSyncKey(theme.CustomImages);
(image) => image.variableName, if (imageKey !== this.lastSyncedImageKey) {
); syncInput.images = theme.CustomImages;
this.lastSyncedImageKey = imageKey;
// Remove old preview images that are no longer present }
this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName);
// Clean up cached URL
this.imageUrlCache.delete(variableName);
}
});
// Apply or update images
theme.CustomImages.forEach((image) => {
const existingUrl = this.imageUrlCache.get(image.variableName);
if (!existingUrl) {
// Only create new URL if one doesn't exist
const imageUrl = URL.createObjectURL(image.blob);
this.imageUrlCache.set(image.variableName, imageUrl);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
} else {
// Reuse existing URL
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${existingUrl})`,
);
}
});
this.previousImageVariableNames = newImageVariableNames; this.previousImageVariableNames = newImageVariableNames;
} }
if (Object.keys(syncInput).length > 0) {
await syncThemeToPage(syncInput);
}
// Always apply dark mode setting when theme forces appearance // Always apply dark mode setting when theme forces appearance
if (shouldForceThemeAppearance(theme as CustomTheme)) { if (shouldForceThemeAppearance(theme as CustomTheme)) {
settingsState.DarkMode = getForcedDarkMode(theme as CustomTheme); settingsState.DarkMode = getForcedDarkMode(theme as CustomTheme);
@@ -1102,23 +998,12 @@ export class ThemeManager {
* Clear theme preview * Clear theme preview
*/ */
public clearPreview(): void { public clearPreview(): void {
console.debug("[ThemeManager] Clearing theme preview"); verboseDebug("[ThemeManager] Clearing theme preview");
try { try {
// Remove preview images and revoke URLs void syncThemeToPage({ clearPreview: true, images: [] });
this.previousImageVariableNames.forEach((variableName) => { this.lastSyncedImageKey = null;
this.removeImageFromDocument(variableName);
});
// Clear all cached URLs
this.imageUrlCache.forEach((url) => URL.revokeObjectURL(url));
this.imageUrlCache.clear();
this.previousImageVariableNames = []; this.previousImageVariableNames = [];
// Remove preview CSS
if (this.previewStyleElement) {
this.previewStyleElement.remove();
this.previewStyleElement = null;
}
clearCustomThemeAdaptiveCssVariables(); clearCustomThemeAdaptiveCssVariables();
// Restore original settings // Restore original settings
@@ -1128,22 +1013,22 @@ export class ThemeManager {
settingsState.selectedColor = storedColor; settingsState.selectedColor = storedColor;
localStorage.removeItem("originalPreviewColor"); localStorage.removeItem("originalPreviewColor");
} else if (this.originalPreviewColor !== null) { } else if (this.originalPreviewColor !== null) {
console.debug( verboseDebug(
"[ThemeManager] Restoring color from memory:", "[ThemeManager] Restoring color from memory:",
this.originalPreviewColor, this.originalPreviewColor,
); );
settingsState.selectedColor = this.originalPreviewColor; settingsState.selectedColor = this.originalPreviewColor;
console.debug( verboseDebug(
"[ThemeManager] Color after restore:", "[ThemeManager] Color after restore:",
settingsState.selectedColor, settingsState.selectedColor,
); );
} else { } else {
console.debug("[ThemeManager] No color to restore found"); verboseDebug("[ThemeManager] No color to restore found");
} }
this.originalPreviewColor = null; this.originalPreviewColor = null;
if (this.originalPreviewTheme !== null) { if (this.originalPreviewTheme !== null) {
console.debug( verboseDebug(
"[ThemeManager] Restoring dark mode:", "[ThemeManager] Restoring dark mode:",
this.originalPreviewTheme, this.originalPreviewTheme,
); );
@@ -1218,36 +1103,4 @@ export class ThemeManager {
console.error("[ThemeManager] Error saving theme file:", err); console.error("[ThemeManager] Error saving theme file:", err);
} }
} }
private removeImageFromDocument(variableName: string): void {
try {
const value = document.documentElement.style.getPropertyValue(
"--" + variableName,
);
if (value) {
const url = this.imageUrlCache.get(variableName);
if (url) {
URL.revokeObjectURL(url);
this.imageUrlCache.delete(variableName);
}
}
document.documentElement.style.removeProperty("--" + variableName);
} catch (err) {
console.error("[ThemeManager] Error removing image from document:", err);
}
}
private applyPreviewCSS(css: string): void {
console.debug("[ThemeManager] Applying preview CSS");
try {
if (!this.previewStyleElement) {
this.previewStyleElement = document.createElement("style");
this.previewStyleElement.id = "custom-theme-preview";
document.head.appendChild(this.previewStyleElement);
}
this.previewStyleElement.textContent = css;
} catch (error) {
console.error("[ThemeManager] Error applying preview CSS:", error);
}
}
} }
@@ -0,0 +1,38 @@
/**
* Theme decorative images must use data URLs instead of blob URLs on Firefox:
* blob: URLs are tied to the origin where createObjectURL ran (page), while
* settings UI runs in extension shadow DOM (moz-extension://).
*/
export function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === "string") {
resolve(reader.result);
} else {
reject(new Error("FileReader did not return a string"));
}
};
reader.onerror = () =>
reject(reader.error ?? new Error("FileReader failed"));
reader.readAsDataURL(blob);
});
}
/** Base64 payload only (no `data:…;base64,` prefix) for the page-context bridge. */
export function blobToBase64Data(blob: Blob): Promise<string> {
return blobToDataUrl(blob).then((dataUrl) => {
const comma = dataUrl.indexOf(",");
return comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl;
});
}
export function themeCssUrlValue(url: string): string {
return `url("${url.replace(/"/g, "%22")}")`;
}
export function releaseThemeImageUrl(url: string): void {
if (url.startsWith("blob:")) {
URL.revokeObjectURL(url);
}
}
+2 -1
View File
@@ -3,6 +3,7 @@ import type { Plugin } from "../../core/types";
import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris"; import { attachTimetableColorisRecovery } from "@/seqta/utils/timetableColoris";
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat"; import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
import { verboseLog } from "@/utils/verboseLog";
const timetablePlugin: Plugin<{}, {}> = { const timetablePlugin: Plugin<{}, {}> = {
id: "timetable", id: "timetable",
@@ -87,7 +88,7 @@ async function handleTimetable(): Promise<void> {
} }
function handleTimetableZoom(): void { function handleTimetableZoom(): void {
console.log("Initializing timetable zoom controls"); verboseLog("Initializing timetable zoom controls");
// Create zoom controls // Create zoom controls
const zoomControls = document.createElement("div"); const zoomControls = document.createElement("div");
+3 -2
View File
@@ -1,4 +1,5 @@
import type { Plugin, PluginSettings } from "./types"; import type { Plugin, PluginSettings } from "./types";
import { verboseInfo } from "@/utils/verboseLog";
/** /**
* Interface for lazy-loaded plugin definitions * Interface for lazy-loaded plugin definitions
@@ -37,13 +38,13 @@ export function createLazyPlugin<T extends PluginSettings = PluginSettings, S =
beta: lazyPlugin.beta, beta: lazyPlugin.beta,
run: async (api) => { run: async (api) => {
console.info(`[BetterSEQTA+] Dynamically loading plugin "${lazyPlugin.id}"...`); verboseInfo(`[BetterSEQTA+] Dynamically loading plugin "${lazyPlugin.id}"...`);
try { try {
// Dynamically import the actual plugin implementation // Dynamically import the actual plugin implementation
const { default: actualPlugin } = await lazyPlugin.loader(); const { default: actualPlugin } = await lazyPlugin.loader();
console.info(`[BetterSEQTA+] Successfully loaded plugin "${lazyPlugin.id}"`); verboseInfo(`[BetterSEQTA+] Successfully loaded plugin "${lazyPlugin.id}"`);
// Execute the actual plugin's run function // Execute the actual plugin's run function
return await actualPlugin.run(api); return await actualPlugin.run(api);
+4 -3
View File
@@ -12,6 +12,7 @@ import type {
import { createPluginAPI } from "./createAPI"; import { createPluginAPI } from "./createAPI";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { verboseInfo } from "@/utils/verboseLog";
interface PluginSettingsStorage { interface PluginSettingsStorage {
enabled?: boolean; enabled?: boolean;
@@ -158,7 +159,7 @@ export class PluginManager {
const enabled = const enabled =
pluginSettings?.enabled ?? plugin.defaultEnabled ?? true; pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
if (!enabled) { if (!enabled) {
console.info( verboseInfo(
`Plugin "${pluginId}" is disabled, skipping initialization`, `Plugin "${pluginId}" is disabled, skipping initialization`,
); );
return; return;
@@ -181,7 +182,7 @@ export class PluginManager {
this.cleanupFunctions.set(plugin.id, result); this.cleanupFunctions.set(plugin.id, result);
} }
this.runningPlugins.set(pluginId, true); this.runningPlugins.set(pluginId, true);
console.info(`Plugin "${pluginId}" started successfully`); verboseInfo(`Plugin "${pluginId}" started successfully`);
// Process any backlogged events // Process any backlogged events
await this.processBackloggedEvents(pluginId); await this.processBackloggedEvents(pluginId);
@@ -238,7 +239,7 @@ export class PluginManager {
this.cleanupFunctions.delete(pluginId); this.cleanupFunctions.delete(pluginId);
} }
this.runningPlugins.set(pluginId, false); this.runningPlugins.set(pluginId, false);
console.info(`Plugin "${pluginId}" stopped`); verboseInfo(`Plugin "${pluginId}" stopped`);
this.emit("plugin.stopped", pluginId); this.emit("plugin.stopped", pluginId);
} }
+9 -8
View File
@@ -18,6 +18,7 @@ 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";
import { applySelectedFont } from "@/seqta/ui/fonts/Manager"; import { applySelectedFont } from "@/seqta/ui/fonts/Manager";
import { verboseInfo, verboseLog } from "@/utils/verboseLog";
import loading from "@/seqta/ui/Loading"; import loading from "@/seqta/ui/Loading";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage"; import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { getEngageRoutePage } from "@/seqta/utils/engageRoute"; import { getEngageRoutePage } from "@/seqta/utils/engageRoute";
@@ -300,7 +301,7 @@ async function handleSublink(sublink: string | undefined): Promise<void> {
break; break;
case "home": case "home":
window.location.replace(`${location.origin}/#?page=/home`); window.location.replace(`${location.origin}/#?page=/home`);
console.info("[BetterSEQTA+] Started Init (SEQTA Engage home)"); verboseInfo("[BetterSEQTA+] Started Init (SEQTA Engage home)");
if (settingsState.onoff) void loadEngageHomePage(); if (settingsState.onoff) void loadEngageHomePage();
finishLoad(); finishLoad();
break; break;
@@ -316,7 +317,7 @@ async function handleSublink(sublink: string | undefined): Promise<void> {
await handleNewsPage(); await handleNewsPage();
break; break;
case "analytics": case "analytics":
console.info("[BetterSEQTA+] Started Init (Analytics)"); verboseInfo("[BetterSEQTA+] Started Init (Analytics)");
if (settingsState.onoff) void loadAnalyticsPage(); if (settingsState.onoff) void loadAnalyticsPage();
finishLoad(); finishLoad();
break; break;
@@ -336,7 +337,7 @@ async function handleSublink(sublink: string | undefined): Promise<void> {
break; break;
case "home": case "home":
window.location.replace(`${location.origin}/#?page=/home`); window.location.replace(`${location.origin}/#?page=/home`);
console.info("[BetterSEQTA+] Started Init"); verboseInfo("[BetterSEQTA+] Started Init");
if (settingsState.onoff) loadHomePage(); if (settingsState.onoff) loadHomePage();
finishLoad(); finishLoad();
break; break;
@@ -353,7 +354,7 @@ async function handleNewsPage(): Promise<void> {
return; return;
} }
console.info("[BetterSEQTA+] Started Init"); verboseInfo("[BetterSEQTA+] Started Init");
try { try {
await SendNewsPage(); await SendNewsPage();
} catch (error) { } catch (error) {
@@ -682,7 +683,7 @@ export function init() {
}; };
if (settingsState.onoff) { if (settingsState.onoff) {
console.info("[BetterSEQTA+] Enabled"); verboseInfo("[BetterSEQTA+] Enabled");
if (settingsState.DarkMode) document.documentElement.classList.add("dark"); if (settingsState.DarkMode) document.documentElement.classList.add("dark");
if (settingsState.iconOnlySidebar) { if (settingsState.iconOnlySidebar) {
if (document.body) { if (document.body) {
@@ -783,7 +784,7 @@ export function init() {
".outside-container .bottom-container", ".outside-container .bottom-container",
); );
if (legacyElement) { if (legacyElement) {
console.log("Legacy extension detected"); verboseLog("Legacy extension detected");
showConflictPopup(); showConflictPopup();
} }
}, 1000); }, 1000);
@@ -795,7 +796,7 @@ export function init() {
} }
function InjectCustomIcons() { function InjectCustomIcons() {
console.info("[BetterSEQTA+] Injecting Icons"); verboseInfo("[BetterSEQTA+] Injecting Icons");
const style = document.createElement("style"); const style = document.createElement("style");
style.setAttribute("type", "text/css"); style.setAttribute("type", "text/css");
@@ -812,7 +813,7 @@ function InjectCustomIcons() {
export function AppendElementsToDisabledPage() { export function AppendElementsToDisabledPage() {
if (document.getElementById("AddedSettings")) return; if (document.getElementById("AddedSettings")) return;
console.info("[BetterSEQTA+] Appending elements to disabled page"); verboseInfo("[BetterSEQTA+] Appending elements to disabled page");
AddBetterSEQTAElements(); AddBetterSEQTAElements();
let settingsStyle = document.createElement("style"); let settingsStyle = document.createElement("style");
+2
View File
@@ -8,6 +8,7 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import pageState from "@/pageState.js?url"; import pageState from "@/pageState.js?url";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl"; import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours"; import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
import { installThemeImagePagePatch } from "@/seqta/utils/patchThemeImagesPageContext";
// Stylesheets // Stylesheets
import injectedCSS from "@/css/injected.scss?inline"; import injectedCSS from "@/css/injected.scss?inline";
@@ -18,6 +19,7 @@ export async function main() {
if (settingsState.onoff) { if (settingsState.onoff) {
injectPageState(); injectPageState();
installSeqtaMenuColourPatch(); 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 // 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") { 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 { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent"; import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent";
import { setupFixedTooltips } from "@/seqta/utils/fixedTooltip"; import { setupFixedTooltips } from "@/seqta/utils/fixedTooltip";
import { verboseInfo } from "@/utils/verboseLog";
let LessonInterval: any; let LessonInterval: any;
let currentSelectedDate = new Date(); let currentSelectedDate = new Date();
let loadingTimeout: any; let loadingTimeout: any;
export async function loadHomePage() { export async function loadHomePage() {
console.info("[BetterSEQTA+] Started Loading Home Page"); verboseInfo("[BetterSEQTA+] Started Loading Home Page");
currentSelectedDate = new Date(); currentSelectedDate = new Date();
+2 -1
View File
@@ -5,9 +5,10 @@ import { settingsState } from "./listeners/SettingsState";
import browser from "webextension-polyfill"; import browser from "webextension-polyfill";
import LogoLightOutline from "@/resources/icons/betterseqta-light-outline.png"; import LogoLightOutline from "@/resources/icons/betterseqta-light-outline.png";
import { animate, stagger } from "motion"; import { animate, stagger } from "motion";
import { verboseInfo } from "@/utils/verboseLog";
export async function SendNewsPage() { export async function SendNewsPage() {
console.info("[BetterSEQTA+] Started Loading News Page"); verboseInfo("[BetterSEQTA+] Started Loading News Page");
document.title = "News ― SEQTA Learn"; document.title = "News ― SEQTA Learn";
await delay(10); await delay(10);
@@ -29,6 +29,7 @@ const OPTIONAL_UNSET_MEANS_DEFAULT_KEYS = [
"themeOfTheMonthLastSeenId", "themeOfTheMonthLastSeenId",
"justupdated", "justupdated",
"devMode", "devMode",
"verboseLogging",
"hideSensitiveContent", "hideSensitiveContent",
"mockNotices", "mockNotices",
"devGhReleaseVersionOverride", "devGhReleaseVersionOverride",
+2 -1
View File
@@ -1,4 +1,5 @@
import { settingsState } from "./listeners/SettingsState"; import { settingsState } from "./listeners/SettingsState";
import { verboseInfo } from "@/utils/verboseLog";
const STYLE_ID = "bsplus-menuitem-visibility"; const STYLE_ID = "bsplus-menuitem-visibility";
@@ -28,7 +29,7 @@ export function applyMenuItemVisibility(): void {
)) { )) {
if (config && !config.toggle) { if (config && !config.toggle) {
css += hideRule(menuItem); 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."; var TUTOR_COLOUR_PREF_PREFIX = "timetable.tutor.";
function log(event, detail) { function log(event, detail) {
if (!document.documentElement.hasAttribute("data-bsplus-verbose-log")) return;
if (detail !== undefined) { if (detail !== undefined) {
console.info(LOG, event, detail); console.info(LOG, event, detail);
} else { } 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 { dismissStaleColourDialogs } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
import { verboseInfo } from "@/utils/verboseLog";
let attached = false; let attached = false;
let dismissTimer: ReturnType<typeof setTimeout> | null = null; let dismissTimer: ReturnType<typeof setTimeout> | null = null;
@@ -83,7 +84,7 @@ export function attachTimetableColorisRecovery(): void {
if (!pickerOpen) { if (!pickerOpen) {
const result = dismissTimetableUiBlockers(); const result = dismissTimetableUiBlockers();
if (result.slideRemoved > 0 || result.modalRemoved > 0) { if (result.slideRemoved > 0 || result.modalRemoved > 0) {
console.info( verboseInfo(
"[BetterSEQTA+] timetable colour: content-script cleanup", "[BetterSEQTA+] timetable colour: content-script cleanup",
result, result,
); );
+2
View File
@@ -49,6 +49,8 @@ export interface SettingsState {
animations: boolean; animations: boolean;
defaultPage: string; defaultPage: string;
devMode?: boolean; devMode?: boolean;
/** Dev-only: emit verboseDebug / verboseInfo / verboseLog output. */
verboseLogging?: boolean;
/** Dev-only: pretend this is the latest GitHub release version for update badge testing. */ /** Dev-only: pretend this is the latest GitHub release version for update badge testing. */
devGhReleaseVersionOverride?: string; devGhReleaseVersionOverride?: string;
/** ISO timestamp of the last acknowledged nightly release publish time. */ /** ISO timestamp of the last acknowledged nightly release publish time. */
+38
View File
@@ -0,0 +1,38 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
const VERBOSE_LOG_ATTR = "data-bsplus-verbose-log";
export function isVerboseLoggingEnabled(): boolean {
return Boolean(settingsState.devMode && settingsState.verboseLogging);
}
export function syncVerboseLogDomFlag(): void {
if (typeof document === "undefined") return;
document.documentElement.toggleAttribute(
VERBOSE_LOG_ATTR,
isVerboseLoggingEnabled(),
);
}
let initialized = false;
/** Register DOM flag sync when dev / verbose toggles change. Call after settings load. */
export function initVerboseLogging(): void {
if (initialized) return;
initialized = true;
syncVerboseLogDomFlag();
settingsState.register("devMode", () => syncVerboseLogDomFlag());
settingsState.register("verboseLogging", () => syncVerboseLogDomFlag());
}
export function verboseDebug(...args: unknown[]): void {
if (isVerboseLoggingEnabled()) console.debug(...args);
}
export function verboseInfo(...args: unknown[]): void {
if (isVerboseLoggingEnabled()) console.info(...args);
}
export function verboseLog(...args: unknown[]): void {
if (isVerboseLoggingEnabled()) console.log(...args);
}
+7
View File
@@ -140,6 +140,13 @@ export default defineConfig(({ command }) => ({
"utils", "utils",
"seqtaMenuColourPatch.js", "seqtaMenuColourPatch.js",
), ),
themeImagePagePatch: join(
__dirname,
"src",
"seqta",
"utils",
"themeImagePagePatch.js",
),
}, },
output: { output: {
assetFileNames: "assets/[name]-[hash][extname]", assetFileNames: "assets/[name]-[hash][extname]",