format: run prettify

This commit is contained in:
SethBurkart123
2025-05-05 18:04:10 +10:00
parent 771169348f
commit 0f9f618164
142 changed files with 28768 additions and 20790 deletions
@@ -1,7 +1,11 @@
import { BasePlugin } from '../../core/settings';
import { type Plugin } from '@/plugins/core/types';
import { defineSettings, numberSetting, Setting } from '@/plugins/core/settingsHelpers';
import styles from './styles.css?inline';
import { BasePlugin } from "../../core/settings";
import { type Plugin } from "@/plugins/core/types";
import {
defineSettings,
numberSetting,
Setting,
} from "@/plugins/core/settingsHelpers";
import styles from "./styles.css?inline";
const settings = defineSettings({
speed: numberSetting({
@@ -10,8 +14,8 @@ const settings = defineSettings({
description: "Controls how fast the background moves",
min: 0.1,
max: 2,
step: 0.05
})
step: 0.05,
}),
});
class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
@@ -22,10 +26,10 @@ class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
const instance = new AnimatedBackgroundPluginClass();
const animatedBackgroundPlugin: Plugin<typeof settings> = {
id: 'animated-background',
name: 'Animated Background',
description: 'Adds an animated background to BetterSEQTA+',
version: '1.0.0',
id: "animated-background",
name: "Animated Background",
description: "Adds an animated background to BetterSEQTA+",
version: "1.0.0",
disableToggle: true,
styles: styles,
settings: instance.settings,
@@ -34,7 +38,7 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
// Create the background elements
const container = document.getElementById("container");
const menu = document.getElementById("menu");
if (!container || !menu) {
return () => {};
}
@@ -42,12 +46,12 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
const backgrounds = [
{ classes: ["bg"] },
{ classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] }
{ classes: ["bg", "bg3"] },
];
backgrounds.forEach(({ classes }) => {
const bk = document.createElement("div");
classes.forEach(cls => bk.classList.add(cls));
classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu);
});
@@ -55,24 +59,27 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
updateAnimationSpeed(api.settings.speed);
// Listen for speed changes
const speedUnregister = api.settings.onChange('speed', updateAnimationSpeed);
const speedUnregister = api.settings.onChange(
"speed",
updateAnimationSpeed,
);
// Return cleanup function
return () => {
speedUnregister.unregister();
// Remove background elements
const backgrounds = document.getElementsByClassName('bg');
Array.from(backgrounds).forEach(element => element.remove());
const backgrounds = document.getElementsByClassName("bg");
Array.from(backgrounds).forEach((element) => element.remove());
};
}
},
};
function updateAnimationSpeed(speed: number) {
const bgElements = document.getElementsByClassName('bg');
const bgElements = document.getElementsByClassName("bg");
Array.from(bgElements).forEach((element, index) => {
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
});
}
export default animatedBackgroundPlugin;
export default animatedBackgroundPlugin;
@@ -28,4 +28,4 @@
100% {
transform: translateX(5%) rotate(-60deg);
}
}
}
@@ -7,18 +7,18 @@ export function CreateBackground() {
// Creating and inserting 3 divs containing the background applied to the pages
const container = document.getElementById("container");
const menu = document.getElementById("menu");
if (!container || !menu) return;
const backgrounds = [
{ classes: ["bg"] },
{ classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] }
{ classes: ["bg", "bg3"] },
];
backgrounds.forEach(({ classes }) => {
const bk = document.createElement("div");
classes.forEach(cls => bk.classList.add(cls));
classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu);
});
}
}
@@ -1,6 +1,6 @@
export function RemoveBackground() {
const backgrounds = document.getElementsByClassName("bg");
// Convert HTMLCollection to Array and remove each element
Array.from(backgrounds).forEach(element => element.remove());
}
Array.from(backgrounds).forEach((element) => element.remove());
}
@@ -1,5 +1,9 @@
import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, defineSettings, Setting } from "@/plugins/core/settingsHelpers";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
import { type Plugin } from "@/plugins/core/types";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForElm } from "@/seqta/utils/waitForElm";
@@ -8,7 +12,7 @@ const settings = defineSettings({
lettergrade: booleanSetting({
default: false,
title: "Letter Grades",
description: "Display the average as a letter instead of a percentage"
description: "Display the average as a letter instead of a percentage",
}),
});
@@ -34,62 +38,105 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
true,
10,
1000
1000,
);
// Helper function to find actual class names by their base pattern
const getClassByPattern = (element: Element | Document, basePattern: string): string => {
const getClassByPattern = (
element: Element | Document,
basePattern: string,
): string => {
// Find all classes on the element
const classes = Array.from(element.querySelectorAll('*'))
.flatMap(el => Array.from(el.classList))
.filter(className => className.startsWith(basePattern));
return classes.length ? classes[0] : '';
const classes = Array.from(element.querySelectorAll("*"))
.flatMap((el) => Array.from(el.classList))
.filter((className) => className.startsWith(basePattern));
return classes.length ? classes[0] : "";
};
// Find actual class names from the DOM
const sampleAssessmentItem = document.querySelector("[class*='AssessmentItem__AssessmentItem___']");
const sampleAssessmentItem = document.querySelector(
"[class*='AssessmentItem__AssessmentItem___']",
);
if (!sampleAssessmentItem) return;
// Extract all necessary class patterns from a sample assessment item
const assessmentItemClass = Array.from(sampleAssessmentItem.classList)
.find(c => c.startsWith('AssessmentItem__AssessmentItem___')) || '';
const metaContainerClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__metaContainer___');
const metaClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__meta___');
const simpleResultClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__simpleResult___');
const titleClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__title___');
const assessmentItemClass =
Array.from(sampleAssessmentItem.classList).find((c) =>
c.startsWith("AssessmentItem__AssessmentItem___"),
) || "";
const metaContainerClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__metaContainer___",
);
const metaClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__meta___",
);
const simpleResultClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__simpleResult___",
);
const titleClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__title___",
);
// Get Thermoscore classes
const thermoscoreElement = document.querySelector("[class*='Thermoscore__Thermoscore___']");
const thermoscoreElement = document.querySelector(
"[class*='Thermoscore__Thermoscore___']",
);
if (!thermoscoreElement) return;
const thermoscoreClass = Array.from(thermoscoreElement.classList)
.find(c => c.startsWith('Thermoscore__Thermoscore___')) || '';
const fillClass = getClassByPattern(thermoscoreElement, 'Thermoscore__fill___');
const textClass = getClassByPattern(thermoscoreElement, 'Thermoscore__text___');
const thermoscoreClass =
Array.from(thermoscoreElement.classList).find((c) =>
c.startsWith("Thermoscore__Thermoscore___"),
) || "";
const fillClass = getClassByPattern(
thermoscoreElement,
"Thermoscore__fill___",
);
const textClass = getClassByPattern(
thermoscoreElement,
"Thermoscore__text___",
);
// Find assessment list
const assessmentsList = document.querySelector("#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']");
const assessmentsList = document.querySelector(
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
);
if (!assessmentsList) return;
const gradeElements = document.querySelectorAll("[class*='Thermoscore__text___']");
const gradeElements = document.querySelectorAll(
"[class*='Thermoscore__text___']",
);
if (!gradeElements.length) return;
// Parse and average grades
const letterToNumber: Record<string, number> = {
"A+": 100, A: 95, "A-": 90,
"B+": 85, B: 80, "B-": 75,
"C+": 70, C: 65, "C-": 60,
"D+": 55, D: 50, "D-": 45,
"E+": 40, E: 35, "E-": 30,
"A+": 100,
A: 95,
"A-": 90,
"B+": 85,
B: 80,
"B-": 75,
"C+": 70,
C: 65,
"C-": 60,
"D+": 55,
D: 50,
"D-": 45,
"E+": 40,
E: 35,
"E-": 30,
F: 0,
};
function parseGrade(text: string): number {
const str = text.trim().toUpperCase();
if (str.includes("/")) {
const [raw, max] = str.split("/").map(n => parseFloat(n));
const [raw, max] = str.split("/").map((n) => parseFloat(n));
return (raw / max) * 100;
}
if (str.includes("%")) {
@@ -112,16 +159,23 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
const avg = total / count;
const rounded = Math.ceil(avg / 5) * 5;
const numberToLetter = Object.entries(letterToNumber).reduce((acc, [k, v]) => {
acc[v] = k;
return acc;
}, {} as Record<number, string>);
const numberToLetter = Object.entries(letterToNumber).reduce(
(acc, [k, v]) => {
acc[v] = k;
return acc;
},
{} as Record<number, string>,
);
const letterAvg = numberToLetter[rounded] ?? "N/A";
const display = api.settings.lettergrade ? letterAvg : `${avg.toFixed(2)}%`;
const display = api.settings.lettergrade
? letterAvg
: `${avg.toFixed(2)}%`;
// Prevent duplicate
const existing = assessmentsList.querySelector(`[class*='AssessmentItem__title___']`);
const existing = assessmentsList.querySelector(
`[class*='AssessmentItem__title___']`,
);
if (existing?.textContent === "Subject Average") return;
// Use the dynamic class names in the HTML template
@@ -144,7 +198,7 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
});
}
},
};
export default assessmentsAveragePlugin;
export default assessmentsAveragePlugin;
@@ -11,7 +11,7 @@ import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing } from "../indexing/indexer";
import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from 'embeddia';
import { IndexedDbManager } from "embeddia";
const settings = defineSettings({
searchHotkey: stringSetting({
@@ -65,12 +65,11 @@ const globalSearchPlugin: Plugin<typeof settings> = {
run: async (api) => {
const appRef = { current: null };
await IndexedDbManager.create(
'embeddiaDB',
'embeddiaObjectStore',
{ primaryKey: 'id', autoIncrement: false }
);
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
primaryKey: "id",
autoIncrement: false,
});
initVectorSearch();
if (api.settings.runIndexingOnLoad) {
@@ -6,7 +6,7 @@ import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
export function mountSearchBar(
titleElement: Element,
api: any,
appRef: { current: any }
appRef: { current: any },
) {
if (titleElement.querySelector(".search-trigger")) {
return;
@@ -15,7 +15,10 @@ async function loadProgress<T = any>(jobId: string): Promise<T | undefined> {
return rec?.progress as T | undefined;
}
async function saveProgress<T = any>(jobId: string, progress: T): Promise<void> {
async function saveProgress<T = any>(
jobId: string,
progress: T,
): Promise<void> {
await put(META_STORE, { jobId, progress }, `progress:${jobId}`);
}
/* ───────────────────────────────────────────── */
@@ -67,7 +70,13 @@ function stopHeartbeat() {
localStorage.removeItem(LOCK_KEY);
}
function dispatchProgress(completed: number, total: number, indexing: boolean, status?: string, detail?: string) {
function dispatchProgress(
completed: number,
total: number,
indexing: boolean,
status?: string,
detail?: string,
) {
const event = new CustomEvent("indexing-progress", {
detail: { completed, total, indexing, status, detail },
});
@@ -79,31 +88,41 @@ export async function loadAllStoredItems(): Promise<HydratedIndexItem[]> {
const jobIds = Object.keys(jobs);
for (const jobId of jobIds) {
try {
const items = await getAll(jobId) as IndexItem[];
const job = jobs[jobId];
const renderComponent = renderComponentMap[job.renderComponentId];
try {
const items = (await getAll(jobId)) as IndexItem[];
const job = jobs[jobId];
const renderComponent = renderComponentMap[job.renderComponentId];
if (!renderComponent) {
console.warn(`Render component not found for job ${jobId} (ID: ${job.renderComponentId})`);
}
for (const item of items) {
// Ensure item has all required fields before pushing
if (item && item.id && item.text && item.category && item.actionId && job.renderComponentId) {
all.push({
...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found
});
} else {
console.warn(`Skipping invalid item from job ${jobId}:`, item);
}
}
} catch (error) {
console.error(`Error loading items for job ${jobId}:`, error);
if (!renderComponent) {
console.warn(
`Render component not found for job ${jobId} (ID: ${job.renderComponentId})`,
);
}
for (const item of items) {
if (
item &&
item.id &&
item.text &&
item.category &&
item.actionId &&
job.renderComponentId
) {
all.push({
...item,
renderComponent: renderComponent || undefined,
});
} else {
console.warn(`Skipping invalid item from job ${jobId}:`, item);
}
}
} catch (error) {
console.error(`Error loading items for job ${jobId}:`, error);
}
}
console.debug(`[Indexer] Loaded ${all.length} items from non-vector storage.`);
console.debug(
`[Indexer] Loaded ${all.length} items from non-vector storage.`,
);
return all;
}
@@ -129,7 +148,12 @@ export async function runIndexing(): Promise<void> {
// --- Step 1: Run Fetching/Storing Jobs (Main Thread) ---
for (const jobId of jobIds) {
dispatchProgress(completedJobs, totalSteps, true, `Running job: ${jobs[jobId].label}`);
dispatchProgress(
completedJobs,
totalSteps,
true,
`Running job: ${jobs[jobId].label}`,
);
const job = jobs[jobId];
const lastRun = await getLastRunMeta(jobId);
@@ -139,26 +163,37 @@ export async function runIndexing(): Promise<void> {
"color: gray",
);
completedJobs++;
dispatchProgress(completedJobs, totalSteps, true, `Skipped job: ${job.label}`);
dispatchProgress(
completedJobs,
totalSteps,
true,
`Skipped job: ${job.label}`,
);
continue;
}
const getStoredItems = async (storeId?: string) => await getAll(storeId ?? jobId);
const getStoredItems = async (storeId?: string) =>
await getAll(storeId ?? jobId);
const setStoredItems = async (items: IndexItem[], storeId?: string) => {
const targetStore = storeId ?? jobId;
await clear(targetStore);
const validItems = items.filter(i => i && i.id);
const validItems = items.filter((i) => i && i.id);
if (validItems.length !== items.length) {
console.warn(`[Indexer Job ${jobId} -> Store ${targetStore}] Filtered out ${items.length - validItems.length} invalid items before storing.`);
console.warn(
`[Indexer Job ${jobId} -> Store ${targetStore}] Filtered out ${items.length - validItems.length} invalid items before storing.`,
);
}
await Promise.all(validItems.map((i) => put(targetStore, i, i.id)));
};
const addItem = async (item: IndexItem, storeId?: string) => {
const targetStore = storeId ?? jobId;
if (item && item.id) {
await put(targetStore, item, item.id);
await put(targetStore, item, item.id);
} else {
console.warn(`[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`, item);
console.warn(
`[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`,
item,
);
}
};
const removeItem = async (id: string, storeId?: string) => {
@@ -193,18 +228,30 @@ export async function runIndexing(): Promise<void> {
// Hydrate items for vector processing
const renderComponent = renderComponentMap[job.renderComponentId];
if (!renderComponent) {
console.warn(`Render component not found for job ${jobId} (ID: ${job.renderComponentId}) during hydration`);
console.warn(
`Render component not found for job ${jobId} (ID: ${job.renderComponentId}) during hydration`,
);
}
const hydratedItems = merged
.filter(item => item && item.id && item.text && item.category && item.actionId && job.renderComponentId) // Filter invalid before hydrating
.map((item) => ({
...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found
}));
.filter(
(item) =>
item &&
item.id &&
item.text &&
item.category &&
item.actionId &&
job.renderComponentId,
) // Filter invalid before hydrating
.map((item) => ({
...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found
}));
if (hydratedItems.length !== merged.length) {
console.warn(`[Indexer Job ${jobId}] Filtered out ${merged.length - hydratedItems.length} invalid items during hydration.`);
}
if (hydratedItems.length !== merged.length) {
console.warn(
`[Indexer Job ${jobId}] Filtered out ${merged.length - hydratedItems.length} invalid items during hydration.`,
);
}
allItemsFromJobs.push(...hydratedItems);
@@ -218,7 +265,12 @@ export async function runIndexing(): Promise<void> {
}
completedJobs++;
dispatchProgress(completedJobs, totalSteps, true, `Finished job: ${job.label}`);
dispatchProgress(
completedJobs,
totalSteps,
true,
`Finished job: ${job.label}`,
);
}
// --- Step 2: Delegate Vectorization to Worker (Off Main Thread) ---
@@ -233,54 +285,113 @@ export async function runIndexing(): Promise<void> {
const workerManager = VectorWorkerManager.getInstance();
// Pass a progress callback to the worker manager
await workerManager.processItems(allItemsFromJobs, (progress) => {
// Update overall progress based on worker feedback
let detailMessage = progress.message || '';
if (progress.status === 'processing' && progress.total && progress.processed !== undefined) {
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
// You could potentially update the 'completed' count more granularly here
// For simplicity, we'll just update the detail message
} else if (progress.status === 'complete') {
detailMessage = "Vectorization complete";
// Mark the vectorization step as complete
dispatchProgress(totalSteps, totalSteps, true, "Vectorization finished");
} else if (progress.status === 'error') {
detailMessage = `Vectorization error: ${progress.message}`;
dispatchProgress(completedJobs, totalSteps, true, "Vectorization failed", detailMessage); // Show error
} else if (progress.status === 'started') {
detailMessage = `Vectorization started for ${progress.total} items`;
} else if (progress.status === 'cancelled') {
detailMessage = `Vectorization cancelled: ${progress.message}`;
dispatchProgress(completedJobs, totalSteps, true, "Vectorization cancelled", detailMessage);
}
// Update overall progress based on worker feedback
let detailMessage = progress.message || "";
if (
progress.status === "processing" &&
progress.total &&
progress.processed !== undefined
) {
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
// You could potentially update the 'completed' count more granularly here
// For simplicity, we'll just update the detail message
} else if (progress.status === "complete") {
detailMessage = "Vectorization complete";
// Mark the vectorization step as complete
dispatchProgress(
totalSteps,
totalSteps,
true,
"Vectorization finished",
);
} else if (progress.status === "error") {
detailMessage = `Vectorization error: ${progress.message}`;
dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization failed",
detailMessage,
); // Show error
} else if (progress.status === "started") {
detailMessage = `Vectorization started for ${progress.total} items`;
} else if (progress.status === "cancelled") {
detailMessage = `Vectorization cancelled: ${progress.message}`;
dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization cancelled",
detailMessage,
);
}
// Update the status detail
dispatchProgress(completedJobs, totalSteps, true, "Vectorization in progress", detailMessage);
// Update the status detail
dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization in progress",
detailMessage,
);
// When worker signals completion of *its* task, mark the final step complete
if (progress.status === 'complete') {
completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress(completedJobs, totalSteps, false, "Indexing finished"); // Set indexing to false
} else if (progress.status === 'error' || progress.status === 'cancelled') {
// Don't increment completed count on failure/cancel, just stop indexing indicator
dispatchProgress(completedJobs, totalSteps, false, "Indexing stopped due to error/cancel");
}
// When worker signals completion of *its* task, mark the final step complete
if (progress.status === "complete") {
completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing finished",
); // Set indexing to false
} else if (
progress.status === "error" ||
progress.status === "cancelled"
) {
// Don't increment completed count on failure/cancel, just stop indexing indicator
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing stopped due to error/cancel",
);
}
});
console.debug("%c[Indexer] Vectorization task sent to worker.", "color: green");
console.debug(
"%c[Indexer] Vectorization task sent to worker.",
"color: green",
);
// Note: runIndexing might return *before* vectorization is complete now.
// The progress updates will signal the true end state.
} catch (error) {
console.error(`%c[Indexer] ❌ Failed to send items to vector worker:`, "color: red", error);
dispatchProgress(completedJobs, totalSteps, false, "Vectorization failed", String(error)); // Stop indexing indicator
console.error(
`%c[Indexer] ❌ Failed to send items to vector worker:`,
"color: red",
error,
);
dispatchProgress(
completedJobs,
totalSteps,
false,
"Vectorization failed",
String(error),
); // Stop indexing indicator
}
} else {
console.debug("%c[Indexer] No items to send for vectorization.", "color: gray");
console.debug(
"%c[Indexer] No items to send for vectorization.",
"color: gray",
);
// If no vectorization needed, indexing is done here.
completedJobs++; // Count the "skipped" vectorization step
dispatchProgress(completedJobs, totalSteps, false, "Indexing finished (no vectorization needed)");
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing finished (no vectorization needed)",
);
}
// Stop heartbeat ONLY when all jobs *and* the vectorization dispatch are done.
// The actual *completion* of vectorization is now asynchronous.
stopHeartbeat();
@@ -292,10 +403,10 @@ function mergeItems(existing: IndexItem[], incoming: IndexItem[]): IndexItem[] {
const map = new Map<string, IndexItem>();
// Prioritize incoming items if IDs clash
for (const item of existing) {
if (item && item.id) map.set(item.id, item);
if (item && item.id) map.set(item.id, item);
}
for (const item of incoming) {
if (item && item.id) map.set(item.id, item);
if (item && item.id) map.set(item.id, item);
}
return Array.from(map.values());
}
}
@@ -5,4 +5,4 @@ import { assessmentsJob } from "./jobs/assessments";
export const jobs: Record<string, Job> = {
messages: messagesJob,
assessments: assessmentsJob,
};
};
@@ -49,21 +49,27 @@ const fetchNotifications = async () => {
const fetchAssessmentName = async (
assessmentId: number,
metaclassId: number,
programmeId: number
programmeId: number,
): Promise<string> => {
const searchAssessment = (data: any): string | null => {
// Search syllabus
for (const item of data.syllabus || []) {
const found = (item.assessments || []).find((a: any) => a.id === assessmentId);
const found = (item.assessments || []).find(
(a: any) => a.id === assessmentId,
);
if (found) return found.title;
}
// Search pending
const foundPending = (data.pending || []).find((a: any) => a.id === assessmentId);
const foundPending = (data.pending || []).find(
(a: any) => a.id === assessmentId,
);
if (foundPending) return foundPending.title;
// Search tasks
const foundTask = (data.tasks || []).find((a: any) => a.id === assessmentId);
const foundTask = (data.tasks || []).find(
(a: any) => a.id === assessmentId,
);
if (foundTask) return foundTask.title;
return null;
@@ -88,11 +94,17 @@ const fetchAssessmentName = async (
if (title) return title;
// Try from /upcoming if not found in /past
const upcomingPayload = await fetchAssessments("/seqta/student/assessment/list/upcoming");
const foundUpcoming = (upcomingPayload || []).find((a: any) => a.id === assessmentId);
const upcomingPayload = await fetchAssessments(
"/seqta/student/assessment/list/upcoming",
);
const foundUpcoming = (upcomingPayload || []).find(
(a: any) => a.id === assessmentId,
);
if (foundUpcoming) return foundUpcoming.title;
throw new Error(`Assessment with ID ${assessmentId} not found in past or upcoming.`);
throw new Error(
`Assessment with ID ${assessmentId} not found in past or upcoming.`,
);
};
/* ------------- Job ------------- */
@@ -103,9 +115,10 @@ export const assessmentsJob: Job = {
frequency: { type: "expiry", afterMs: 15 * 60 * 1000 },
run: async (ctx) => {
const progress =
(await ctx.getProgress<AssessmentsProgress>()) ?? { lastTs: 0 };
const progress = (await ctx.getProgress<AssessmentsProgress>()) ?? {
lastTs: 0,
};
let notifications: Notification[];
try {
notifications = await fetchNotifications();
@@ -113,25 +126,33 @@ export const assessmentsJob: Job = {
console.error("[Assessments job] fetch failed:", e);
return [];
}
const notificationIsIndexed = async (id: string): Promise<boolean> => {
const [inAssessments, inMessages] = await Promise.all([
ctx.getStoredItems("assessments").then((items) => items.some((i) => i.id === id)),
ctx.getStoredItems("messages").then((items) => items.some((i) => i.id === id)),
ctx
.getStoredItems("assessments")
.then((items) => items.some((i) => i.id === id)),
ctx
.getStoredItems("messages")
.then((items) => items.some((i) => i.id === id)),
]);
return inAssessments || inMessages;
};
const items: IndexItem[] = [];
for (const notif of notifications) {
const id = notif.notificationID.toString();
if (await notificationIsIndexed(id)) continue;
if (notif.type === "coneqtassessments") {
const a = notif.coneqtAssessments;
const content = await fetchAssessmentName(a.assessmentID, a.metaclassID, a.programmeID);
const content = await fetchAssessmentName(
a.assessmentID,
a.metaclassID,
a.programmeID,
);
items.push({
id,
text: a.title,
@@ -168,11 +189,11 @@ export const assessmentsJob: Job = {
actionId: "message",
renderComponentId: "message",
},
"messages"
"messages",
);
}
}
if (items.length) {
const latest = Math.max(
...items.map((i) => i.dateAdded),
@@ -190,4 +211,4 @@ export const assessmentsJob: Job = {
date.setHours(0, 0, 0, 0);
return items.filter((i) => i.dateAdded >= date.getTime());
},
};
};
@@ -49,12 +49,12 @@ export const messagesJob: Job = {
run: async (ctx) => {
const limit = 100;
const progress =
(await ctx.getProgress<MessagesProgress>()) ?? { offset: 0, done: false };
const progress = (await ctx.getProgress<MessagesProgress>()) ?? {
offset: 0,
done: false,
};
const existingIds = new Set(
(await ctx.getStoredItems()).map((i) => i.id),
);
const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
let consecutiveExisting = 0;
@@ -129,4 +129,4 @@ export const messagesJob: Job = {
const fourYears = Date.now() - 4 * 365 * 24 * 60 * 60 * 1000;
return items.filter((i) => i.dateAdded >= fourYears);
},
};
};
@@ -36,4 +36,4 @@ export interface Job {
renderComponentId: string;
run: (ctx: JobContext) => Promise<IndexItem[]>;
purge?: (items: IndexItem[]) => IndexItem[];
}
}
@@ -1,11 +1,13 @@
export function htmlToPlainText(rawHtml: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(rawHtml, 'text/html');
const doc = parser.parseFromString(rawHtml, "text/html");
const { body } = doc;
body.querySelectorAll('script,style,template,noscript,meta,link').forEach(el => el.remove());
body
.querySelectorAll("script,style,template,noscript,meta,link")
.forEach((el) => el.remove());
body.querySelectorAll('.forward').forEach(el => {
body.querySelectorAll(".forward").forEach((el) => {
let n: ChildNode | null = el;
while (n) {
const next = n.nextSibling as ChildNode | null;
@@ -14,19 +16,19 @@ export function htmlToPlainText(rawHtml: string): string {
}
});
let text = body.innerText || '';
let text = body.innerText || "";
text = text
.replace(/\u00A0/g, ' ')
.replace(/[ \t]{2,}/g, ' ')
.replace(/\r\n|\r/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.replace(/^[.\w#][^{]{0,100}\{[^}]*\}$/gm, '')
.split('\n')
.map(line => line.trimEnd())
.filter(line => line.trim().length > 0 || line === '')
.join('\n')
.replace(/\u00A0/g, " ")
.replace(/[ \t]{2,}/g, " ")
.replace(/\r\n|\r/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/^[.\w#][^{]{0,100}\{[^}]*\}$/gm, "")
.split("\n")
.map((line) => line.trimEnd())
.filter((line) => line.trim().length > 0 || line === "")
.join("\n")
.trim();
return text;
}
}
@@ -1,8 +1,4 @@
import {
EmbeddingIndex,
getEmbedding,
initializeModel,
} from "embeddia";
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { HydratedIndexItem } from "../types";
let vectorIndex: EmbeddingIndex | null = null;
@@ -1,10 +1,10 @@
import { refreshVectorCache } from '../../search/vector/vectorSearch';
import type { HydratedIndexItem } from '../types';
import vectorWorker from './vectorWorker.ts?inlineWorker';
import type { SearchResult } from 'embeddia';
import { refreshVectorCache } from "../../search/vector/vectorSearch";
import type { HydratedIndexItem } from "../types";
import vectorWorker from "./vectorWorker.ts?inlineWorker";
import type { SearchResult } from "embeddia";
export type ProgressCallback = (data: {
status: 'started' | 'processing' | 'complete' | 'error' | 'cancelled';
status: "started" | "processing" | "complete" | "error" | "cancelled";
total?: number;
processed?: number;
message?: string;
@@ -16,10 +16,21 @@ export class VectorWorkerManager {
private isInitialized = false;
private readyPromise: Promise<void> | null = null; // To await initialization
private progressCallback: ProgressCallback | null = null;
private searchPromises = new Map<string, { resolve: (value: SearchResult[]) => void, reject: (reason?: any) => void, timer: NodeJS.Timeout }>();
private searchPromises = new Map<
string,
{
resolve: (value: SearchResult[]) => void;
reject: (reason?: any) => void;
timer: NodeJS.Timeout;
}
>();
private debounceTimer: NodeJS.Timeout | null = null;
private lastSearchParams: { query: string; topK: number; resolve: (results: SearchResult[]) => void, reject: (reason?: any) => void } | null = null;
private lastSearchParams: {
query: string;
topK: number;
resolve: (results: SearchResult[]) => void;
reject: (reason?: any) => void;
} | null = null;
private constructor() {
// Start initialization immediately, but allow awaiting it
@@ -39,101 +50,115 @@ export class VectorWorkerManager {
if (this.readyPromise) return this.readyPromise;
return new Promise<void>((resolve, reject) => {
// Create the worker
this.worker = vectorWorker();
// Create the worker
this.worker = vectorWorker();
console.log('Worker initialized', this.worker);
console.log("Worker initialized", this.worker);
const timeout = setTimeout(() => {
console.error('Vector worker initialization timed out');
this.worker?.terminate(); // Clean up worker if it exists
this.worker = null;
this.isInitialized = false; // Ensure state reflects failure
this.readyPromise = null; // Allow retrying init later
reject(new Error('Worker initialization timed out'));
}, 10000); // Increased timeout
const timeout = setTimeout(() => {
console.error("Vector worker initialization timed out");
this.worker?.terminate(); // Clean up worker if it exists
this.worker = null;
this.isInitialized = false; // Ensure state reflects failure
this.readyPromise = null; // Allow retrying init later
reject(new Error("Worker initialization timed out"));
}, 10000); // Increased timeout
// Set up message handling
this.worker!.addEventListener('message', (e) => {
const { type, data } = e.data;
console.debug("Message from vector worker:", type, data);
// Set up message handling
this.worker!.addEventListener("message", (e) => {
const { type, data } = e.data;
console.debug("Message from vector worker:", type, data);
switch (type) {
case 'ready':
this.isInitialized = true;
clearTimeout(timeout);
console.debug('Vector worker initialized and ready.');
resolve(); // Resolve the init promise
break;
switch (type) {
case "ready":
this.isInitialized = true;
clearTimeout(timeout);
console.debug("Vector worker initialized and ready.");
resolve(); // Resolve the init promise
break;
case 'progress':
if (this.progressCallback) {
this.progressCallback(data);
case "progress":
if (this.progressCallback) {
this.progressCallback(data);
if (data.status === 'complete') {
refreshVectorCache();
}
if (data.status === "complete") {
refreshVectorCache();
}
break;
}
break;
case 'searchResults':
const searchInfo = this.searchPromises.get(data.messageId);
if (searchInfo) {
clearTimeout(searchInfo.timer); // Clear timeout on success
searchInfo.resolve(data.results);
this.searchPromises.delete(data.messageId);
} else {
console.warn('Received search results for unknown messageId:', data.messageId);
}
break;
case "searchResults":
const searchInfo = this.searchPromises.get(data.messageId);
if (searchInfo) {
clearTimeout(searchInfo.timer); // Clear timeout on success
searchInfo.resolve(data.results);
this.searchPromises.delete(data.messageId);
} else {
console.warn(
"Received search results for unknown messageId:",
data.messageId,
);
}
break;
case 'searchError':
const errorInfo = this.searchPromises.get(data.messageId);
if (errorInfo) {
clearTimeout(errorInfo.timer); // Clear timeout on error
errorInfo.reject(new Error(data.error));
this.searchPromises.delete(data.messageId);
} else {
console.warn('Received search error for unknown messageId:', data.messageId);
}
break;
case "searchError":
const errorInfo = this.searchPromises.get(data.messageId);
if (errorInfo) {
clearTimeout(errorInfo.timer); // Clear timeout on error
errorInfo.reject(new Error(data.error));
this.searchPromises.delete(data.messageId);
} else {
console.warn(
"Received search error for unknown messageId:",
data.messageId,
);
}
break;
case 'searchCancelled':
const cancelledInfo = this.searchPromises.get(data.messageId);
if (cancelledInfo) {
clearTimeout(cancelledInfo.timer); // Clear timeout on cancel
// Reject with a specific cancellation error or resolve with empty? Let's reject.
cancelledInfo.reject(new Error('Search cancelled by worker'));
this.searchPromises.delete(data.messageId);
} else {
console.debug('Received cancellation for unknown messageId:', data.messageId);
}
break;
case "searchCancelled":
const cancelledInfo = this.searchPromises.get(data.messageId);
if (cancelledInfo) {
clearTimeout(cancelledInfo.timer); // Clear timeout on cancel
// Reject with a specific cancellation error or resolve with empty? Let's reject.
cancelledInfo.reject(new Error("Search cancelled by worker"));
this.searchPromises.delete(data.messageId);
} else {
console.debug(
"Received cancellation for unknown messageId:",
data.messageId,
);
}
break;
default:
console.warn('Unknown message from worker:', type, data);
}
});
default:
console.warn("Unknown message from worker:", type, data);
}
});
// Initialize the worker
this.worker!.postMessage({ type: 'init' });
// Initialize the worker
this.worker!.postMessage({ type: "init" });
});
}
// Ensures worker is ready before proceeding
private async ensureReady() {
if (!this.readyPromise) {
// If init wasn't called or failed, try again
console.warn("Worker not initialized, attempting init...");
this.readyPromise = this.initWorker();
// If init wasn't called or failed, try again
console.warn("Worker not initialized, attempting init...");
this.readyPromise = this.initWorker();
}
await this.readyPromise;
if (!this.isInitialized || !this.worker) {
throw new Error("Vector Worker is not available after initialization attempt.");
throw new Error(
"Vector Worker is not available after initialization attempt.",
);
}
}
async processItems(items: HydratedIndexItem[], onProgress?: ProgressCallback) {
async processItems(
items: HydratedIndexItem[],
onProgress?: ProgressCallback,
) {
await this.ensureReady(); // Wait for worker to be ready
this.progressCallback = onProgress || null;
@@ -146,13 +171,16 @@ export class VectorWorkerManager {
const serialisableItems = items.map(({ renderComponent, ...rest }) => rest);
this.worker!.postMessage({
type: 'process',
data: { items: serialisableItems }
type: "process",
data: { items: serialisableItems },
});
}
// Public search method
public async search(query: string, topK: number = 10): Promise<SearchResult[]> {
public async search(
query: string,
topK: number = 10,
): Promise<SearchResult[]> {
await this.ensureReady();
return new Promise((resolve, reject) => {
@@ -167,54 +195,62 @@ export class VectorWorkerManager {
// Set a timeout for the search operation itself
const searchTimeout = 10000; // e.g., 10 seconds
const searchTimer = setTimeout(() => {
if (this.searchPromises.has(messageId)) {
console.error(`Search timed out for messageId: ${messageId}`);
currentParams.reject(new Error(`Search timed out after ${searchTimeout}ms`));
this.searchPromises.delete(messageId);
}
if (this.searchPromises.has(messageId)) {
console.error(`Search timed out for messageId: ${messageId}`);
currentParams.reject(
new Error(`Search timed out after ${searchTimeout}ms`),
);
this.searchPromises.delete(messageId);
}
}, searchTimeout);
this.searchPromises.set(messageId, {
resolve: currentParams.resolve,
reject: currentParams.reject,
timer: searchTimer,
});
this.searchPromises.set(messageId, { resolve: currentParams.resolve, reject: currentParams.reject, timer: searchTimer });
console.debug(`Sending search request (ID: ${messageId}) to worker: "${currentParams.query}"`);
console.debug(
`Sending search request (ID: ${messageId}) to worker: "${currentParams.query}"`,
);
console.log(this.worker);
this.worker.postMessage({
type: "search",
data: { query: currentParams.query, topK: currentParams.topK },
messageId
messageId,
});
} else if (this.lastSearchParams) {
// This case might happen if ensureReady failed but didn't throw
console.error("Worker unavailable when trying to send search request.");
this.lastSearchParams.reject(new Error("Worker unavailable for search"));
this.lastSearchParams = null;
this.debounceTimer = null;
// This case might happen if ensureReady failed but didn't throw
console.error("Worker unavailable when trying to send search request.");
this.lastSearchParams.reject(
new Error("Worker unavailable for search"),
);
this.lastSearchParams = null;
this.debounceTimer = null;
}
});
}
// Method to cancel all pending/debounced searches
private cancelAllSearches(reason: string = "Cancelled") {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
if (this.lastSearchParams) {
this.lastSearchParams.reject(new Error(`Search cancelled: ${reason}`));
this.lastSearchParams = null;
}
}
// We might also want to tell the worker to cancel its *current* search
// if it supports it, but this requires worker modification.
// For now, just reject pending promises in the manager.
for (const [messageId, promiseInfo] of this.searchPromises.entries()) {
clearTimeout(promiseInfo.timer);
promiseInfo.reject(new Error(`Search cancelled: ${reason}`));
this.searchPromises.delete(messageId);
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
if (this.lastSearchParams) {
this.lastSearchParams.reject(new Error(`Search cancelled: ${reason}`));
this.lastSearchParams = null;
}
}
// We might also want to tell the worker to cancel its *current* search
// if it supports it, but this requires worker modification.
// For now, just reject pending promises in the manager.
for (const [messageId, promiseInfo] of this.searchPromises.entries()) {
clearTimeout(promiseInfo.timer);
promiseInfo.reject(new Error(`Search cancelled: ${reason}`));
this.searchPromises.delete(messageId);
}
}
terminate() {
console.debug("Terminating Vector Worker Manager...");
this.cancelAllSearches("Worker terminated"); // Cancel pending searches
@@ -229,4 +265,4 @@ export class VectorWorkerManager {
// Clear the static instance? Or assume app lifecycle handles this?
// VectorWorkerManager.instance = null; // Uncomment if needed
}
}
}
@@ -1,6 +1,6 @@
import { EmbeddingIndex, getEmbedding, initializeModel } from 'embeddia';
import type { HydratedIndexItem } from '../../indexing/types';
import type { SearchResult } from 'embeddia';
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { HydratedIndexItem } from "../../indexing/types";
import type { SearchResult } from "embeddia";
let vectorIndex: EmbeddingIndex | null = null;
@@ -10,7 +10,7 @@ export async function initVectorSearch() {
vectorIndex = new EmbeddingIndex([]);
vectorIndex.preloadIndexedDB();
} catch (e) {
console.error('Error initializing vector search', e);
console.error("Error initializing vector search", e);
}
}
@@ -18,17 +18,20 @@ export interface VectorSearchResult extends SearchResult {
object: HydratedIndexItem & { embedding: number[] };
}
export async function searchVectors(query: string, topK: number = 10): Promise<VectorSearchResult[]> {
export async function searchVectors(
query: string,
topK: number = 10,
): Promise<VectorSearchResult[]> {
if (!vectorIndex) await initVectorSearch();
const queryEmbedding = await getEmbedding(query.slice(0, 100));
const results = await vectorIndex!.search(queryEmbedding, {
const results = await vectorIndex!.search(queryEmbedding, {
topK,
useStorage: 'indexedDB',
dedupeEntries: true
useStorage: "indexedDB",
dedupeEntries: true,
});
return results as VectorSearchResult[];
}
@@ -36,4 +39,4 @@ export async function refreshVectorCache() {
if (!vectorIndex) await initVectorSearch();
vectorIndex!.clearIndexedDBCache();
vectorIndex!.preloadIndexedDB();
}
}
@@ -4,4 +4,3 @@ import type { HydratedIndexItem } from "../../indexing/types";
export interface VectorSearchResult extends SearchResult {
object: HydratedIndexItem & { embedding: number[] };
}
@@ -1,4 +1,4 @@
import type { Plugin } from '../../core/types';
import type { Plugin } from "../../core/types";
interface NotificationCollectorStorage {
lastNotificationCount: number;
@@ -6,10 +6,10 @@ interface NotificationCollectorStorage {
}
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
id: 'notificationCollector',
name: 'Notification Collector',
description: 'Collects and displays SEQTA notifications',
version: '1.0.0',
id: "notificationCollector",
name: "Notification Collector",
description: "Collects and displays SEQTA notifications",
version: "1.0.0",
settings: {},
disableToggle: true,
@@ -23,30 +23,35 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
const checkNotifications = async () => {
try {
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement;
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (api.storage.lastNotificationCount !== 0) {
alertDiv.textContent = api.storage.lastNotificationCount.toString();
}
const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
const response = await fetch(
`${location.origin}/seqta/student/heartbeat?`,
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
timestamp: "1970-01-01 00:00:00.0",
hash: "#?page=/home",
}),
},
body: JSON.stringify({
timestamp: "1970-01-01 00:00:00.0",
hash: "#?page=/home",
})
});
);
const data = await response.json();
// Store notification count for history
const notificationCount = data.payload.notifications.length;
api.storage.lastNotificationCount = notificationCount;
api.storage.lastCheckedTime = new Date().toISOString();
if (alertDiv) {
alertDiv.textContent = notificationCount.toString();
} else {
@@ -67,7 +72,9 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
if (pollInterval) {
window.clearInterval(pollInterval);
pollInterval = null;
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement;
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (alertDiv) {
if (api.storage.lastNotificationCount > 9) {
alertDiv.textContent = "9+";
@@ -85,7 +92,7 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
return () => {
stopPolling();
};
}
},
};
export default notificationCollectorPlugin;
export default notificationCollectorPlugin;
+20 -16
View File
@@ -1,6 +1,10 @@
import type { Plugin } from '@/plugins/core/types';
import { BasePlugin } from '@/plugins/core/settings';
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Step 1: Define settings with proper typing
const settings = defineSettings({
@@ -8,7 +12,7 @@ const settings = defineSettings({
default: true,
title: "Test Plugin",
description: "Some random setting",
})
}),
});
// Step 2: Create the plugin class with @Setting decorators
@@ -21,32 +25,32 @@ class TestPluginClass extends BasePlugin<typeof settings> {
const settingsInstance = new TestPluginClass();
const testPlugin: Plugin<typeof settings> = {
id: 'test',
name: 'Test Plugin',
description: 'A test plugin for BetterSEQTA+',
version: '1.0.0',
id: "test",
name: "Test Plugin",
description: "A test plugin for BetterSEQTA+",
version: "1.0.0",
settings: settingsInstance.settings,
disableToggle: true,
run: async (api) => {
console.log('Test plugin running');
console.log("Test plugin running");
api.events.on('ping', (data) => {
console.log('Ping received! Page changed to: ', data);
api.events.on("ping", (data) => {
console.log("Ping received! Page changed to: ", data);
});
const { unregister } = api.seqta.onPageChange((page) => {
//console.log('Page changed to', page);
api.events.emit('ping', page);
api.events.emit("ping", page);
console.log('Current setting value:', api.settings.someSetting);
console.log("Current setting value:", api.settings.someSetting);
});
return () => {
console.log('Test plugin stopped');
console.log("Test plugin stopped");
unregister();
}
}
};
},
};
export default testPlugin;
+65 -62
View File
@@ -1,10 +1,10 @@
import renderSvelte from "@/interface/main"
import themeCreator from "@/interface/pages/themeCreator.svelte"
import { unmount } from "svelte"
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager"
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import renderSvelte from "@/interface/main";
import themeCreator from "@/interface/pages/themeCreator.svelte";
import { unmount } from "svelte";
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let themeCreatorSvelteApp: any = null
let themeCreatorSvelteApp: any = null;
const themeManager = ThemeManager.getInstance();
/**
@@ -13,76 +13,79 @@ const themeManager = ThemeManager.getInstance();
* @returns void
*/
export function OpenThemeCreator(themeID: string = "") {
CloseThemeCreator()
CloseThemeCreator();
// Only store original color if we're not editing an existing theme
localStorage.setItem('themeCreatorOpen', 'true');
localStorage.setItem("themeCreatorOpen", "true");
if (!themeID) {
localStorage.setItem('originalPreviewColor', settingsState.selectedColor);
localStorage.setItem("originalPreviewColor", settingsState.selectedColor);
}
const width = "310px"
const width = "310px";
const themeCreatorDiv: HTMLDivElement = document.createElement("div")
themeCreatorDiv.id = "themeCreator"
themeCreatorDiv.style.width = width
const themeCreatorDiv: HTMLDivElement = document.createElement("div");
themeCreatorDiv.id = "themeCreator";
themeCreatorDiv.style.width = width;
const shadow = themeCreatorDiv.attachShadow({ mode: "open" })
const shadow = themeCreatorDiv.attachShadow({ mode: "open" });
themeCreatorSvelteApp = renderSvelte(themeCreator, shadow, {
themeID: themeID,
})
});
const mainContent = document.querySelector("#container") as HTMLDivElement
if (mainContent) mainContent.style.width = `calc(100% - ${width})`
const mainContent = document.querySelector("#container") as HTMLDivElement;
if (mainContent) mainContent.style.width = `calc(100% - ${width})`;
// close button
const closeButton = document.createElement("button")
closeButton.classList.add("themeCloseButton")
closeButton.textContent = "×"
const closeButton = document.createElement("button");
closeButton.classList.add("themeCloseButton");
closeButton.textContent = "×";
closeButton.addEventListener("click", () => {
CloseThemeCreator()
themeManager.clearPreview()
})
CloseThemeCreator();
themeManager.clearPreview();
});
document.body.appendChild(closeButton)
document.body.appendChild(closeButton);
const resizeBar = document.createElement("div")
resizeBar.classList.add("resizeBar")
resizeBar.style.right = "307.5px"
const resizeBar = document.createElement("div");
resizeBar.classList.add("resizeBar");
resizeBar.style.right = "307.5px";
let isDragging = false
let isDragging = false;
const mouseDownHandler = (_: MouseEvent) => {
isDragging = true
document.addEventListener("mousemove", mouseMoveHandler)
document.addEventListener("mouseup", mouseUpHandler)
document.body.style.userSelect = "none"
themeCreatorDiv.style.pointerEvents = "none"
}
isDragging = true;
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
document.body.style.userSelect = "none";
themeCreatorDiv.style.pointerEvents = "none";
};
const mouseMoveHandler = (e: MouseEvent) => {
if (!isDragging) return
const windowWidth = window.innerWidth
const newWidth = Math.max(310, windowWidth - e.clientX)
themeCreatorDiv.style.width = `${newWidth}px`
mainContent.style.width = `calc(100% - ${newWidth}px)`
resizeBar.style.right = `${newWidth - 2.5}px`
}
if (!isDragging) return;
const windowWidth = window.innerWidth;
const newWidth = Math.max(310, windowWidth - e.clientX);
themeCreatorDiv.style.width = `${newWidth}px`;
mainContent.style.width = `calc(100% - ${newWidth}px)`;
resizeBar.style.right = `${newWidth - 2.5}px`;
};
const mouseUpHandler = () => {
isDragging = false
document.removeEventListener("mousemove", mouseMoveHandler)
document.removeEventListener("mouseup", mouseUpHandler)
document.body.style.userSelect = ""
themeCreatorDiv.style.pointerEvents = "auto"
}
isDragging = false;
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
document.body.style.userSelect = "";
themeCreatorDiv.style.pointerEvents = "auto";
};
resizeBar.addEventListener("mousedown", mouseDownHandler)
resizeBar.addEventListener("mouseover", () => (resizeBar.style.opacity = "1"))
resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0"))
resizeBar.addEventListener("mousedown", mouseDownHandler);
resizeBar.addEventListener(
"mouseover",
() => (resizeBar.style.opacity = "1"),
);
resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0"));
document.body.appendChild(themeCreatorDiv)
document.body.appendChild(resizeBar)
document.body.appendChild(themeCreatorDiv);
document.body.appendChild(resizeBar);
}
/**
@@ -91,19 +94,19 @@ export function OpenThemeCreator(themeID: string = "") {
*/
export function CloseThemeCreator() {
// Remove the stored flag
localStorage.removeItem('themeCreatorOpen');
localStorage.removeItem("themeCreatorOpen");
const themeCreator = document.getElementById("themeCreator")
const themeCreator = document.getElementById("themeCreator");
const closeButton = document.querySelector(
".themeCloseButton",
) as HTMLButtonElement
const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement
) as HTMLButtonElement;
const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement;
if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp)
if (themeCreator) themeCreator.remove()
if (closeButton) closeButton.remove()
if (resizeBar) resizeBar.remove()
if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp);
if (themeCreator) themeCreator.remove();
if (closeButton) closeButton.remove();
if (resizeBar) resizeBar.remove();
const mainContent = document.querySelector("#container") as HTMLDivElement
if (mainContent) mainContent.style.width = "100%"
const mainContent = document.querySelector("#container") as HTMLDivElement;
if (mainContent) mainContent.style.width = "100%";
}
+8 -8
View File
@@ -1,17 +1,17 @@
import type { Plugin } from '../../core/types';
import { ThemeManager } from './theme-manager';
import type { Plugin } from "../../core/types";
import { ThemeManager } from "./theme-manager";
const themesPlugin: Plugin = {
id: 'themes',
name: 'Themes',
description: 'Adds a theme selector to the settings page',
version: '1.0.0',
id: "themes",
name: "Themes",
description: "Adds a theme selector to the settings page",
version: "1.0.0",
settings: {},
run: async (_) => {
const themeManager = ThemeManager.getInstance();
await themeManager.initialize();
}
},
};
export default themesPlugin;
export default themesPlugin;
+247 -169
View File
@@ -1,7 +1,7 @@
import localforage from 'localforage';
import type { CustomTheme, LoadedCustomTheme } from '@/types/CustomThemes';
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
import debounce from '@/seqta/utils/debounce';
import localforage from "localforage";
import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import debounce from "@/seqta/utils/debounce";
type ThemeContent = {
id: string;
@@ -13,7 +13,7 @@ type ThemeContent = {
CustomCSS?: string;
hideThemeName?: boolean;
forceDark?: boolean;
images: { id: string, variableName: string, data: string }[]; // data: base64
images: { id: string; variableName: string; data: string }[]; // data: base64
};
export class ThemeManager {
@@ -27,7 +27,7 @@ export class ThemeManager {
private imageUrlCache: Map<string, string> = new Map();
private constructor() {
console.debug('[ThemeManager] Initializing...');
console.debug("[ThemeManager] Initializing...");
}
public static getInstance(): ThemeManager {
@@ -48,12 +48,12 @@ export class ThemeManager {
* Get a theme by ID from storage
*/
public async getTheme(themeId: string): Promise<CustomTheme | null> {
console.debug('[ThemeManager] Getting theme:', themeId);
console.debug("[ThemeManager] Getting theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as CustomTheme;
const theme = (await localforage.getItem(themeId)) as CustomTheme;
return theme;
} catch (error) {
console.error('[ThemeManager] Error getting theme:', error);
console.error("[ThemeManager] Error getting theme:", error);
return null;
}
}
@@ -69,19 +69,19 @@ export class ThemeManager {
* Disable the current theme without deleting it
*/
public async disableTheme(): Promise<void> {
console.debug('[ThemeManager] Disabling current theme');
console.debug("[ThemeManager] Disabling current theme");
try {
if (!this.currentTheme) {
console.debug('[ThemeManager] No theme to disable');
console.debug("[ThemeManager] No theme to disable");
return;
}
await this.removeTheme(this.currentTheme);
this.currentTheme = null;
settingsState.selectedTheme = '';
console.debug('[ThemeManager] Theme disabled successfully');
settingsState.selectedTheme = "";
console.debug("[ThemeManager] Theme disabled successfully");
} catch (error) {
console.error('[ThemeManager] Error disabling theme:', error);
console.error("[ThemeManager] Error disabling theme:", error);
}
}
@@ -89,23 +89,28 @@ export class ThemeManager {
* Initialize the theme system and restore previous state
*/
public async initialize(): Promise<void> {
console.debug('[ThemeManager] Starting initialization');
console.debug("[ThemeManager] Starting initialization");
try {
// Check if theme creator was open during reload
const themeCreatorOpen = localStorage.getItem('themeCreatorOpen');
if (themeCreatorOpen === 'true') {
console.debug('[ThemeManager] Theme creator was open, clearing preview state');
const themeCreatorOpen = localStorage.getItem("themeCreatorOpen");
if (themeCreatorOpen === "true") {
console.debug(
"[ThemeManager] Theme creator was open, clearing preview state",
);
this.clearPreview();
// Clean up the flag
localStorage.removeItem('themeCreatorOpen');
localStorage.removeItem("themeCreatorOpen");
}
if (settingsState.selectedTheme) {
console.debug('[ThemeManager] Found selected theme, restoring:', settingsState.selectedTheme);
console.debug(
"[ThemeManager] Found selected theme, restoring:",
settingsState.selectedTheme,
);
await this.setTheme(settingsState.selectedTheme);
}
} catch (error) {
console.error('[ThemeManager] Error during initialization:', error);
console.error("[ThemeManager] Error during initialization:", error);
}
}
@@ -113,13 +118,13 @@ export class ThemeManager {
* Clean up theme system resources
*/
public async cleanup(): Promise<void> {
console.debug('[ThemeManager] Cleaning up resources');
console.debug("[ThemeManager] Cleaning up resources");
try {
if (this.currentTheme) {
await this.removeTheme(this.currentTheme, false);
}
} catch (error) {
console.error('[ThemeManager] Error during cleanup:', error);
console.error("[ThemeManager] Error during cleanup:", error);
}
}
@@ -127,24 +132,24 @@ export class ThemeManager {
* Set and apply a theme by ID
*/
public async setTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Setting theme:', themeId);
console.debug("[ThemeManager] Setting theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as CustomTheme;
const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (!theme) {
console.error('[ThemeManager] Theme not found:', themeId);
console.error("[ThemeManager] Theme not found:", themeId);
return;
}
// Store original settings before applying new theme
if (!settingsState.selectedTheme) {
console.debug('[ThemeManager] Storing original settings');
console.debug("[ThemeManager] Storing original settings");
settingsState.originalSelectedColor = settingsState.selectedColor;
settingsState.originalDarkMode = settingsState.DarkMode;
}
// Remove current theme if exists
if (this.currentTheme) {
console.debug('[ThemeManager] Removing current theme');
console.debug("[ThemeManager] Removing current theme");
await this.removeTheme(this.currentTheme);
}
@@ -153,9 +158,8 @@ export class ThemeManager {
await this.applyTheme(theme);
this.currentTheme = theme;
settingsState.selectedTheme = themeId;
} catch (error) {
console.error('[ThemeManager] Error setting theme:', error);
console.error("[ThemeManager] Error setting theme:", error);
}
}
@@ -163,65 +167,80 @@ export class ThemeManager {
* Apply theme components (CSS, images, settings)
*/
private async applyTheme(theme: CustomTheme): Promise<void> {
console.debug('[ThemeManager] Applying theme:', theme.name);
console.debug("[ThemeManager] Applying theme:", theme.name);
try {
// Apply custom CSS
if (theme.CustomCSS) {
console.debug('[ThemeManager] Applying custom CSS');
console.debug("[ThemeManager] Applying custom CSS");
this.applyCustomCSS(theme.CustomCSS);
}
// Apply custom images
if (theme.CustomImages) {
console.debug('[ThemeManager] Applying custom images');
console.debug("[ThemeManager] Applying custom images");
theme.CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty('--' + image.variableName, `url(${imageUrl})`);
document.documentElement.style.setProperty(
"--" + image.variableName,
`url(${imageUrl})`,
);
});
}
// Apply theme settings
if (theme.forceDark !== undefined) {
console.debug('[ThemeManager] Setting dark mode:', theme.forceDark);
console.debug("[ThemeManager] Setting dark mode:", theme.forceDark);
settingsState.DarkMode = theme.forceDark;
}
// Use the stored selected color if available, otherwise use the default
if (theme.selectedColor) {
console.debug('[ThemeManager] Restoring saved color:', theme.selectedColor);
console.debug(
"[ThemeManager] Restoring saved color:",
theme.selectedColor,
);
settingsState.selectedColor = theme.selectedColor;
} else if (theme.defaultColour) {
console.debug('[ThemeManager] Using default color:', theme.defaultColour);
console.debug(
"[ThemeManager] Using default color:",
theme.defaultColour,
);
settingsState.selectedColor = theme.defaultColour;
}
} catch (error) {
console.error('[ThemeManager] Error applying theme:', error);
console.error("[ThemeManager] Error applying theme:", error);
}
}
/**
* Remove theme and restore original settings
*/
private async removeTheme(theme: CustomTheme, clearSelectedTheme: boolean = true): Promise<void> {
console.debug('[ThemeManager] Removing theme:', theme.name);
private async removeTheme(
theme: CustomTheme,
clearSelectedTheme: boolean = true,
): Promise<void> {
console.debug("[ThemeManager] Removing theme:", theme.name);
try {
// Remove custom CSS
if (this.styleElement) {
console.debug('[ThemeManager] Removing custom CSS');
console.debug("[ThemeManager] Removing custom CSS");
this.styleElement.remove();
this.styleElement = null;
}
// Remove custom images
if (theme.CustomImages) {
console.debug('[ThemeManager] Removing custom images');
console.debug("[ThemeManager] Removing custom images");
theme.CustomImages.forEach((image) => {
const value = document.documentElement.style.getPropertyValue('--' + image.variableName);
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);
document.documentElement.style.removeProperty(
"--" + image.variableName,
);
});
}
@@ -229,29 +248,34 @@ export class ThemeManager {
// Store the current color with the theme before removing it
await localforage.setItem(this.currentTheme.id, {
...this.currentTheme,
selectedColor: settingsState.selectedColor
selectedColor: settingsState.selectedColor,
});
}
// Restore original settings
if (settingsState.originalSelectedColor) {
console.debug('[ThemeManager] Restoring original color:', settingsState.originalSelectedColor);
console.debug(
"[ThemeManager] Restoring original color:",
settingsState.originalSelectedColor,
);
settingsState.selectedColor = settingsState.originalSelectedColor;
}
if (settingsState.originalDarkMode !== undefined) {
console.debug('[ThemeManager] Restoring original dark mode:', settingsState.originalDarkMode);
console.debug(
"[ThemeManager] Restoring original dark mode:",
settingsState.originalDarkMode,
);
settingsState.DarkMode = settingsState.originalDarkMode;
settingsState.originalDarkMode = undefined;
}
this.currentTheme = null;
if (clearSelectedTheme) {
settingsState.selectedTheme = '';
settingsState.selectedTheme = "";
}
} catch (error) {
console.error('[ThemeManager] Error removing theme:', error);
console.error("[ThemeManager] Error removing theme:", error);
}
}
@@ -259,16 +283,16 @@ export class ThemeManager {
* Apply custom CSS to the document
*/
private applyCustomCSS(css: string): void {
console.debug('[ThemeManager] Applying custom CSS');
console.debug("[ThemeManager] Applying custom CSS");
try {
if (!this.styleElement) {
this.styleElement = document.createElement('style');
this.styleElement.id = 'custom-theme';
this.styleElement = document.createElement("style");
this.styleElement.id = "custom-theme";
document.head.appendChild(this.styleElement);
}
this.styleElement.textContent = css;
} catch (error) {
console.error('[ThemeManager] Error applying custom CSS:', error);
console.error("[ThemeManager] Error applying custom CSS:", error);
}
}
@@ -276,22 +300,24 @@ export class ThemeManager {
* Get list of available themes
*/
public async getAvailableThemes(): Promise<CustomTheme[]> {
console.debug('[ThemeManager] Getting available themes');
console.debug("[ThemeManager] Getting available themes");
try {
const themeIds = await localforage.getItem('customThemes') as string[] | null;
const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (!themeIds) {
return [];
}
const themes = await Promise.all(
themeIds.map(async (id) => {
return await localforage.getItem(id) as CustomTheme;
})
return (await localforage.getItem(id)) as CustomTheme;
}),
);
return themes.filter(theme => theme !== null);
return themes.filter((theme) => theme !== null);
} catch (error) {
console.error('[ThemeManager] Error getting available themes:', error);
console.error("[ThemeManager] Error getting available themes:", error);
return [];
}
}
@@ -300,21 +326,23 @@ export class ThemeManager {
* Save or update a theme
*/
public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug('[ThemeManager] Saving theme:', theme.name);
console.debug("[ThemeManager] Saving theme:", theme.name);
try {
await localforage.setItem(theme.id, theme);
const themeIds = await localforage.getItem('customThemes') as string[] | null;
const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (themeIds) {
if (!themeIds.includes(theme.id)) {
themeIds.push(theme.id);
await localforage.setItem('customThemes', themeIds);
await localforage.setItem("customThemes", themeIds);
}
} else {
await localforage.setItem('customThemes', [theme.id]);
await localforage.setItem("customThemes", [theme.id]);
}
} catch (error) {
console.error('[ThemeManager] Error saving theme:', error);
console.error("[ThemeManager] Error saving theme:", error);
}
}
@@ -322,40 +350,49 @@ export class ThemeManager {
* Delete a theme
*/
public async deleteTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Deleting theme:', themeId);
console.debug("[ThemeManager] Deleting theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as CustomTheme;
const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (theme) {
if (this.currentTheme?.id === themeId) {
await this.removeTheme(theme);
}
await localforage.removeItem(themeId);
const themeIds = await localforage.getItem('customThemes') as string[] | null;
const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (themeIds) {
const updatedThemeIds = themeIds.filter(id => id !== themeId);
await localforage.setItem('customThemes', updatedThemeIds);
const updatedThemeIds = themeIds.filter((id) => id !== themeId);
await localforage.setItem("customThemes", updatedThemeIds);
}
}
} catch (error) {
console.error('[ThemeManager] Error deleting theme:', error);
console.error("[ThemeManager] Error deleting theme:", error);
}
}
/**
* Download and install a theme from the store
*/
public async downloadTheme(themeContent: { id: string; name: string; description: string; coverImage: string; }): Promise<void> {
console.debug('[ThemeManager] Downloading theme:', themeContent.name);
public async downloadTheme(themeContent: {
id: string;
name: string;
description: string;
coverImage: string;
}): Promise<void> {
console.debug("[ThemeManager] Downloading theme:", themeContent.name);
try {
if (!themeContent.id) return;
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`);
const themeData = await response.json() as ThemeContent;
const response = await fetch(
`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`,
);
const themeData = (await response.json()) as ThemeContent;
await this.installTheme(themeData);
} catch (error) {
console.error('[ThemeManager] Error downloading theme:', error);
console.error("[ThemeManager] Error downloading theme:", error);
}
}
@@ -363,62 +400,67 @@ export class ThemeManager {
* Install a theme from theme data
*/
public async installTheme(themeData: ThemeContent): Promise<void> {
console.debug('[ThemeManager] Installing theme:', themeData.name);
console.debug("[ThemeManager] Installing theme:", themeData.name);
try {
// Validate required fields
if (!themeData.id || !themeData.name) {
throw new Error('Theme is missing required fields (id or name)');
throw new Error("Theme is missing required fields (id or name)");
}
// Handle cover image (optional)
let coverImageBlob = null;
if (themeData.coverImage) {
try {
const strippedCoverImage = this.stripBase64Prefix(themeData.coverImage);
const strippedCoverImage = this.stripBase64Prefix(
themeData.coverImage,
);
coverImageBlob = this.base64ToBlob(strippedCoverImage);
} catch (e) {
console.warn('[ThemeManager] Failed to process cover image:', e);
console.warn("[ThemeManager] Failed to process cover image:", e);
// Continue without cover image
}
}
// Handle images (optional)
const images = themeData.images?.map((image) => {
try {
if (!image.id || !image.variableName || !image.data) {
console.warn('[ThemeManager] Skipping invalid image:', image);
return null;
}
return {
...image,
blob: this.base64ToBlob(this.stripBase64Prefix(image.data))
};
} catch (e) {
console.warn('[ThemeManager] Failed to process image:', e);
return null;
}
}).filter(img => img !== null) ?? [];
const images =
themeData.images
?.map((image) => {
try {
if (!image.id || !image.variableName || !image.data) {
console.warn("[ThemeManager] Skipping invalid image:", image);
return null;
}
return {
...image,
blob: this.base64ToBlob(this.stripBase64Prefix(image.data)),
};
} catch (e) {
console.warn("[ThemeManager] Failed to process image:", e);
return null;
}
})
.filter((img) => img !== null) ?? [];
// Create theme with defaults for optional fields
const theme: LoadedCustomTheme = {
id: themeData.id,
name: themeData.name,
description: themeData.description || '',
description: themeData.description || "",
webURL: themeData.id,
coverImage: coverImageBlob,
CustomImages: images,
CustomCSS: themeData.CustomCSS || '',
defaultColour: themeData.defaultColour || 'rgba(0, 123, 255, 1)',
CustomCSS: themeData.CustomCSS || "",
defaultColour: themeData.defaultColour || "rgba(0, 123, 255, 1)",
CanChangeColour: themeData.CanChangeColour ?? true,
allowBackgrounds: true,
isEditable: false,
hideThemeName: themeData.hideThemeName ?? false,
forceDark: themeData.forceDark
forceDark: themeData.forceDark,
};
await this.saveTheme(theme);
} catch (error) {
console.error('[ThemeManager] Error installing theme:', error);
console.error("[ThemeManager] Error installing theme:", error);
throw error; // Re-throw to handle in UI
}
}
@@ -427,11 +469,11 @@ export class ThemeManager {
* Share a theme by exporting it
*/
public async shareTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Sharing theme:', themeId);
console.debug("[ThemeManager] Sharing theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as LoadedCustomTheme;
const theme = (await localforage.getItem(themeId)) as LoadedCustomTheme;
if (!theme) {
console.error('[ThemeManager] Theme not found');
console.error("[ThemeManager] Theme not found");
return;
}
@@ -447,26 +489,30 @@ export class ThemeManager {
} = theme;
// Convert images to base64
const finalImages = await Promise.all(CustomImages.map(async (image) => ({
id: image.id,
variableName: image.variableName,
data: await this.blobToBase64(image.blob)
})));
const finalImages = await Promise.all(
CustomImages.map(async (image) => ({
id: image.id,
variableName: image.variableName,
data: await this.blobToBase64(image.blob),
})),
);
// Convert cover image to base64
const coverImageBase64 = coverImage ? await this.blobToBase64(coverImage) : null;
const coverImageBase64 = coverImage
? await this.blobToBase64(coverImage)
: null;
// Create shareable theme data with only necessary fields
const shareableTheme = {
...themeBasics,
images: finalImages,
coverImage: coverImageBase64
coverImage: coverImageBase64,
};
// Save theme file
this.saveThemeFile(shareableTheme, theme.name || 'Unnamed_Theme');
this.saveThemeFile(shareableTheme, theme.name || "Unnamed_Theme");
} catch (error) {
console.error('[ThemeManager] Error sharing theme:', error);
console.error("[ThemeManager] Error sharing theme:", error);
}
}
@@ -474,7 +520,7 @@ export class ThemeManager {
* Preview a theme without applying it
*/
public async previewTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug('[ThemeManager] Previewing theme:', theme.name);
console.debug("[ThemeManager] Previewing theme:", theme.name);
try {
const { CustomCSS, CustomImages, defaultColour, forceDark } = theme;
@@ -482,7 +528,10 @@ export class ThemeManager {
if (!theme.webURL) {
if (this.originalPreviewColor === null) {
this.originalPreviewColor = settingsState.selectedColor;
localStorage.setItem('originalPreviewColor', settingsState.selectedColor);
localStorage.setItem(
"originalPreviewColor",
settingsState.selectedColor,
);
}
if (this.originalPreviewTheme === null) {
this.originalPreviewTheme = settingsState.DarkMode;
@@ -495,10 +544,12 @@ export class ThemeManager {
}
// Apply custom images
const newImageVariableNames = CustomImages.map(image => image.variableName);
const newImageVariableNames = CustomImages.map(
(image) => image.variableName,
);
// Remove old preview images
this.previousImageVariableNames.forEach(variableName => {
this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName);
}
@@ -507,7 +558,10 @@ export class ThemeManager {
// Apply new images
CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${imageUrl})`);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
});
// Update previousImageVariableNames
@@ -517,12 +571,12 @@ export class ThemeManager {
if (forceDark !== undefined) {
settingsState.DarkMode = forceDark;
}
if (defaultColour) {
settingsState.selectedColor = defaultColour;
}
} catch (error) {
console.error('[ThemeManager] Error previewing theme:', error);
console.error("[ThemeManager] Error previewing theme:", error);
}
}
@@ -530,7 +584,7 @@ export class ThemeManager {
* Update the preview of a theme in real-time (for theme creator)
*/
public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> {
console.debug('[ThemeManager] Updating theme preview');
console.debug("[ThemeManager] Updating theme preview");
try {
// 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)
@@ -550,10 +604,12 @@ export class ThemeManager {
// Handle images if present
if (theme.CustomImages) {
const newImageVariableNames = theme.CustomImages.map(image => image.variableName);
const newImageVariableNames = theme.CustomImages.map(
(image) => image.variableName,
);
// Remove old preview images that are no longer present
this.previousImageVariableNames.forEach(variableName => {
this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName);
// Clean up cached URL
@@ -568,10 +624,16 @@ export class ThemeManager {
// 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})`);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
} else {
// Reuse existing URL
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${existingUrl})`);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${existingUrl})`,
);
}
});
@@ -588,7 +650,7 @@ export class ThemeManager {
settingsState.selectedColor = theme.defaultColour;
}
} catch (error) {
console.error('[ThemeManager] Error updating theme preview:', error);
console.error("[ThemeManager] Error updating theme preview:", error);
}
}
@@ -596,22 +658,25 @@ export class ThemeManager {
* Update the preview of a theme (debounced)
* @param theme - The theme to update the preview of
*/
public updatePreviewDebounced = debounce((theme: Partial<LoadedCustomTheme>): void => {
this.updatePreview(theme);
}, 2);
public updatePreviewDebounced = debounce(
(theme: Partial<LoadedCustomTheme>): void => {
this.updatePreview(theme);
},
2,
);
/**
* Clear theme preview
*/
public clearPreview(): void {
console.debug('[ThemeManager] Clearing theme preview');
console.debug("[ThemeManager] Clearing theme preview");
try {
// Remove preview images and revoke URLs
this.previousImageVariableNames.forEach(variableName => {
this.previousImageVariableNames.forEach((variableName) => {
this.removeImageFromDocument(variableName);
});
// Clear all cached URLs
this.imageUrlCache.forEach(url => URL.revokeObjectURL(url));
this.imageUrlCache.forEach((url) => URL.revokeObjectURL(url));
this.imageUrlCache.clear();
this.previousImageVariableNames = [];
@@ -622,40 +687,51 @@ export class ThemeManager {
}
// Restore original settings
const storedColor = localStorage.getItem('originalPreviewColor');
const storedColor = localStorage.getItem("originalPreviewColor");
if (storedColor) {
settingsState.selectedColor = storedColor;
localStorage.removeItem('originalPreviewColor');
localStorage.removeItem("originalPreviewColor");
} else if (this.originalPreviewColor !== null) {
console.debug('[ThemeManager] Restoring color from memory:', this.originalPreviewColor);
console.debug(
"[ThemeManager] Restoring color from memory:",
this.originalPreviewColor,
);
settingsState.selectedColor = this.originalPreviewColor;
console.debug('[ThemeManager] Color after restore:', settingsState.selectedColor);
console.debug(
"[ThemeManager] Color after restore:",
settingsState.selectedColor,
);
} else {
console.debug('[ThemeManager] No color to restore found');
console.debug("[ThemeManager] No color to restore found");
}
this.originalPreviewColor = null;
if (this.originalPreviewTheme !== null) {
console.debug('[ThemeManager] Restoring dark mode:', this.originalPreviewTheme);
console.debug(
"[ThemeManager] Restoring dark mode:",
this.originalPreviewTheme,
);
settingsState.DarkMode = this.originalPreviewTheme;
this.originalPreviewTheme = null;
}
} catch (error) {
console.error('[ThemeManager] Error clearing preview:', error);
console.error("[ThemeManager] Error clearing preview:", error);
}
}
// Utility methods
private stripBase64Prefix(base64String: string): string {
if (!base64String) return '';
if (!base64String) return "";
const prefixRegex = /^data:[^;]+;base64,/;
try {
return prefixRegex.test(base64String) ? base64String.replace(prefixRegex, '') : base64String;
} catch(err) {
console.error('[ThemeManager] Error stripping base64 prefix:', err);
return '';
return prefixRegex.test(base64String)
? base64String.replace(prefixRegex, "")
: base64String;
} catch (err) {
console.error("[ThemeManager] Error stripping base64 prefix:", err);
return "";
}
}
@@ -664,14 +740,14 @@ export class ThemeManager {
const byteString = atob(base64);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: 'image/png' });
} catch(err) {
console.error('[ThemeManager] Error converting base64 to blob:', err);
return new Blob([ab], { type: "image/png" });
} catch (err) {
console.error("[ThemeManager] Error converting base64 to blob:", err);
return new Blob();
}
}
@@ -681,7 +757,7 @@ export class ThemeManager {
const reader = new FileReader();
reader.onloadend = () => {
const base64String = reader.result as string;
const base64Data = base64String.split(',')[1];
const base64Data = base64String.split(",")[1];
resolve(base64Data);
};
reader.onerror = reject;
@@ -692,23 +768,25 @@ export class ThemeManager {
private saveThemeFile(data: object, fileName: string): void {
try {
const fileData = JSON.stringify(data, null, 2);
const blob = new Blob([fileData], { type: 'application/json' });
const blob = new Blob([fileData], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `${fileName}.theme.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch(err) {
console.error('[ThemeManager] Error saving theme file:', err);
} catch (err) {
console.error("[ThemeManager] Error saving theme file:", err);
}
}
private removeImageFromDocument(variableName: string): void {
try {
const value = document.documentElement.style.getPropertyValue('--' + variableName);
const value = document.documentElement.style.getPropertyValue(
"--" + variableName,
);
if (value) {
const url = this.imageUrlCache.get(variableName);
if (url) {
@@ -716,23 +794,23 @@ export class ThemeManager {
this.imageUrlCache.delete(variableName);
}
}
document.documentElement.style.removeProperty('--' + variableName);
} catch(err) {
console.error('[ThemeManager] Error removing image from document:', err);
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');
console.debug("[ThemeManager] Applying preview CSS");
try {
if (!this.previewStyleElement) {
this.previewStyleElement = document.createElement('style');
this.previewStyleElement.id = 'custom-theme-preview';
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);
console.error("[ThemeManager] Error applying preview CSS:", error);
}
}
}
}
+149 -138
View File
@@ -1,268 +1,279 @@
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
import type { Plugin } from '../../core/types';
import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat';
import { waitForElm } from '@/seqta/utils/waitForElm';
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { Plugin } from "../../core/types";
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
import { waitForElm } from "@/seqta/utils/waitForElm";
const timetablePlugin: Plugin<{}, {}> = {
id: 'timetable',
name: 'Timetable Enhancer',
description: 'Adds extra features to the timetable view',
version: '1.0.0',
id: "timetable",
name: "Timetable Enhancer",
description: "Adds extra features to the timetable view",
version: "1.0.0",
settings: {},
disableToggle: true,
run: async (api) => {
const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable)
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
return () => {
// Call the unregister function to remove the mount listener
unregister();
const timetablePage = document.querySelector('.timetablepage')
const timetablePage = document.querySelector(".timetablepage");
if (timetablePage) {
const zoomControls = document.querySelector('.timetable-zoom-controls')
if (zoomControls) zoomControls.remove()
const hideControls = document.querySelector('.timetable-hide-controls')
if (hideControls) hideControls.remove()
resetTimetableStyles()
const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) zoomControls.remove();
const hideControls = document.querySelector(".timetable-hide-controls");
if (hideControls) hideControls.remove();
resetTimetableStyles();
}
}
}
};
},
};
// Store event handlers globally for cleanup
const zoomHandlers = new WeakMap<Element, { zoomIn: () => void; zoomOut: () => void }>()
const zoomHandlers = new WeakMap<
Element,
{ zoomIn: () => void; zoomOut: () => void }
>();
function resetTimetableStyles(): void {
const firstDayColumn = document.querySelector(".dailycal .content .days td") as HTMLElement
if (!firstDayColumn) return
const baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
const firstDayColumn = document.querySelector(
".dailycal .content .days td",
) as HTMLElement;
if (!firstDayColumn) return;
const baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${baseContainerHeight}px`
})
const timeColumn = document.querySelector(".times")
(td as HTMLElement).style.height = `${baseContainerHeight}px`;
});
const timeColumn = document.querySelector(".times");
if (timeColumn) {
const times = timeColumn.querySelectorAll(".time")
const timeHeight = baseContainerHeight / times.length
const times = timeColumn.querySelectorAll(".time");
const timeHeight = baseContainerHeight / times.length;
times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px`
})
(time as HTMLElement).style.height = `${timeHeight}px`;
});
}
const lessons = document.querySelectorAll(".dailycal .lesson")
const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement
const originalHeight = lessonEl.getAttribute('data-original-height')
const lessonEl = lesson as HTMLElement;
const originalHeight = lessonEl.getAttribute("data-original-height");
if (originalHeight) {
lessonEl.style.height = `${originalHeight}px`
lessonEl.style.height = `${originalHeight}px`;
}
})
});
const entries = document.querySelectorAll(".entry")
const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
entryEl.style.opacity = '1'
})
const entryEl = entry as HTMLElement;
entryEl.style.opacity = "1";
});
const zoomControls = document.querySelector('.timetable-zoom-controls')
const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) {
const handlers = zoomHandlers.get(zoomControls)
const handlers = zoomHandlers.get(zoomControls);
if (handlers) {
const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)')
const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)')
if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn)
if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut)
zoomHandlers.delete(zoomControls)
const zoomIn = zoomControls.querySelector(".timetable-zoom:nth-child(2)");
const zoomOut = zoomControls.querySelector(
".timetable-zoom:nth-child(1)",
);
if (zoomIn) zoomIn.removeEventListener("click", handlers.zoomIn);
if (zoomOut) zoomOut.removeEventListener("click", handlers.zoomOut);
zoomHandlers.delete(zoomControls);
}
}
}
async function handleTimetable(): Promise<void> {
await waitForElm(".time", true, 10)
await waitForElm(".time", true, 10);
// Store original heights when timetable loads
const lessons = document.querySelectorAll(".dailycal .lesson")
const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement
const lessonEl = lesson as HTMLElement;
lessonEl.setAttribute(
"data-original-height",
lessonEl.offsetHeight.toString(),
)
})
);
});
// Existing time format code
if (settingsState.timeFormat == "12") {
const times = document.querySelectorAll(".timetablepage .times .time")
const times = document.querySelectorAll(".timetablepage .times .time");
for (const time of times) {
if (!time.textContent) continue
time.textContent = convertTo12HourFormat(time.textContent, true)
if (!time.textContent) continue;
time.textContent = convertTo12HourFormat(time.textContent, true);
}
}
handleTimetableZoom()
handleTimetableAssessmentHide()
handleTimetableZoom();
handleTimetableAssessmentHide();
}
function handleTimetableZoom(): void {
console.log("Initializing timetable zoom controls")
console.log("Initializing timetable zoom controls");
// Lazy initialize state variables only when function is first called
let timetableZoomLevel = 1
let baseContainerHeight: number | null = null
let timetableZoomLevel = 1;
let baseContainerHeight: number | null = null;
const originalEntryPositions = new Map<
Element,
{ topRatio: number; heightRatio: number }
>()
>();
// Create zoom controls
const zoomControls = document.createElement("div")
zoomControls.className = "timetable-zoom-controls"
const zoomControls = document.createElement("div");
zoomControls.className = "timetable-zoom-controls";
const zoomIn = document.createElement("button")
zoomIn.className = "uiButton timetable-zoom iconFamily"
zoomIn.innerHTML = "&#xed93;" // Unicode for zoom in icon (custom iconfamily)
const zoomIn = document.createElement("button");
zoomIn.className = "uiButton timetable-zoom iconFamily";
zoomIn.innerHTML = "&#xed93;"; // Unicode for zoom in icon (custom iconfamily)
const zoomOut = document.createElement("button")
zoomOut.className = "uiButton timetable-zoom iconFamily"
zoomOut.innerHTML = "&#xed94;" // Unicode for zoom out icon (custom iconfamily)
const zoomOut = document.createElement("button");
zoomOut.className = "uiButton timetable-zoom iconFamily";
zoomOut.innerHTML = "&#xed94;"; // Unicode for zoom out icon (custom iconfamily)
zoomControls.appendChild(zoomOut)
zoomControls.appendChild(zoomIn)
zoomControls.appendChild(zoomOut);
zoomControls.appendChild(zoomIn);
const toolbar = document.getElementById("toolbar")
toolbar?.appendChild(zoomControls)
const toolbar = document.getElementById("toolbar");
toolbar?.appendChild(zoomControls);
// Store event listener references
const zoomInHandler = () => {
if (timetableZoomLevel < 2) {
timetableZoomLevel += 0.2
updateZoom()
timetableZoomLevel += 0.2;
updateZoom();
}
}
};
const zoomOutHandler = () => {
if (timetableZoomLevel > 0.6) {
timetableZoomLevel -= 0.2
updateZoom()
timetableZoomLevel -= 0.2;
updateZoom();
}
}
};
zoomIn.addEventListener("click", zoomInHandler)
zoomOut.addEventListener("click", zoomOutHandler)
zoomIn.addEventListener("click", zoomInHandler);
zoomOut.addEventListener("click", zoomOutHandler);
// Store references for cleanup
zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler })
zoomHandlers.set(zoomControls, {
zoomIn: zoomInHandler,
zoomOut: zoomOutHandler,
});
const initializePositions = () => {
// Get the base container height from the first TD
const firstDayColumn = document.querySelector(
".dailycal .content .days td",
) as HTMLElement
if (!firstDayColumn) return false
) as HTMLElement;
if (!firstDayColumn) return false;
baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
// Store original ratios
const entries = document.querySelectorAll(".entriesWrapper .entry")
const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
const entryEl = entry as HTMLElement;
// Calculate ratios relative to detected base height
if (baseContainerHeight === null) return
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight
if (baseContainerHeight === null) return;
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight;
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight;
originalEntryPositions.set(entry, { topRatio, heightRatio })
})
originalEntryPositions.set(entry, { topRatio, heightRatio });
});
return true
}
return true;
};
const updateZoom = () => {
// Initialize positions if not already done
if (baseContainerHeight === null && !initializePositions()) {
console.error("Failed to initialize positions")
return
console.error("Failed to initialize positions");
return;
}
console.debug(`Updating zoom level to: ${timetableZoomLevel}`)
console.debug(`Updating zoom level to: ${timetableZoomLevel}`);
// Calculate new container height
if (baseContainerHeight === null) return
const newContainerHeight = baseContainerHeight * timetableZoomLevel
if (baseContainerHeight === null) return;
const newContainerHeight = baseContainerHeight * timetableZoomLevel;
// Update all day columns (TDs)
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${newContainerHeight}px`
})
(td as HTMLElement).style.height = `${newContainerHeight}px`;
});
// Update all entries using stored ratios
const entries = document.querySelectorAll(".entriesWrapper .entry")
const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
const originalRatios = originalEntryPositions.get(entry)
const entryEl = entry as HTMLElement;
const originalRatios = originalEntryPositions.get(entry);
if (originalRatios) {
// Calculate new positions from original ratios
const newTop = originalRatios.topRatio * newContainerHeight
const newHeight = originalRatios.heightRatio * newContainerHeight
const newTop = originalRatios.topRatio * newContainerHeight;
const newHeight = originalRatios.heightRatio * newContainerHeight;
// Apply new values
entryEl.style.top = `${Math.round(newTop)}px`
entryEl.style.height = `${Math.round(newHeight)}px`
entryEl.style.top = `${Math.round(newTop)}px`;
entryEl.style.height = `${Math.round(newHeight)}px`;
}
})
});
// Update time column to match
const timeColumn = document.querySelector(".times")
const timeColumn = document.querySelector(".times");
if (timeColumn) {
const times = timeColumn.querySelectorAll(".time")
const timeHeight = newContainerHeight / times.length
const times = timeColumn.querySelectorAll(".time");
const timeHeight = newContainerHeight / times.length;
times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px`
})
(time as HTMLElement).style.height = `${timeHeight}px`;
});
}
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
behavior: "instant",
block: "center",
})
}
});
};
}
function handleTimetableAssessmentHide(): void {
const hideControls = document.createElement("div")
hideControls.className = "timetable-hide-controls"
const hideControls = document.createElement("div");
hideControls.className = "timetable-hide-controls";
const hideOn = document.createElement("button")
hideOn.className = "uiButton timetable-hide iconFamily"
hideOn.innerHTML = "&#128065;"
const hideOn = document.createElement("button");
hideOn.className = "uiButton timetable-hide iconFamily";
hideOn.innerHTML = "&#128065;";
hideControls.appendChild(hideOn)
hideControls.appendChild(hideOn);
const toolbar = document.getElementById("toolbar")
toolbar?.appendChild(hideControls)
const toolbar = document.getElementById("toolbar");
toolbar?.appendChild(hideControls);
function hideElements(): void {
const entries = document.querySelectorAll(".entry")
const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
const entryEl = entry as HTMLElement;
if (!entryEl.classList.contains("assessment")) {
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3"
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3";
}
})
});
}
hideOn.addEventListener("click", hideElements)
hideOn.addEventListener("click", hideElements);
}
export default timetablePlugin;
export default timetablePlugin;
+109 -62
View File
@@ -1,7 +1,16 @@
import type { EventsAPI, Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, SettingValue, StorageAPI } from './types';
import { eventManager } from '@/seqta/utils/listeners/EventManager';
import ReactFiber from '@/seqta/utils/ReactFiber';
import browser from 'webextension-polyfill';
import type {
EventsAPI,
Plugin,
PluginAPI,
PluginSettings,
SEQTAAPI,
SettingsAPI,
SettingValue,
StorageAPI,
} from "./types";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
import ReactFiber from "@/seqta/utils/ReactFiber";
import browser from "webextension-polyfill";
function createSEQTAAPI(): SEQTAAPI {
return {
@@ -11,41 +20,46 @@ function createSEQTAAPI(): SEQTAAPI {
{
customCheck: (element) => element.matches(selector),
},
callback
callback,
);
},
getFiber: (selector) => {
return ReactFiber.find(selector);
},
getCurrentPage: () => {
const path = window.location.hash.split('?page=/')[1] || '';
return path.split('/')[0];
const path = window.location.hash.split("?page=/")[1] || "";
return path.split("/")[0];
},
onPageChange: (callback) => {
const handler = () => {
const page = window.location.hash.split('?page=/')[1] || '';
callback(page.split('/')[0]);
const page = window.location.hash.split("?page=/")[1] || "";
callback(page.split("/")[0]);
};
window.addEventListener('hashchange', handler);
window.addEventListener("hashchange", handler);
// Return an unregister function
return {
unregister: () => {
window.removeEventListener('hashchange', handler);
}
window.removeEventListener("hashchange", handler);
},
};
}
},
};
}
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } {
function createSettingsAPI<T extends PluginSettings>(
plugin: Plugin<T>,
): SettingsAPI<T> & { loaded: Promise<void> } {
const storageKey = `plugin.${plugin.id}.settings`;
const listeners = new Map<keyof T, Set<(value: any) => void>>();
// Initialize with default values
const settingsWithMeta: any = {
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
onChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => {
if (!listeners.has(key)) {
listeners.set(key, new Set());
}
@@ -53,13 +67,16 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return {
unregister: () => {
listeners.get(key)!.delete(callback);
}
},
};
},
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
offChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => {
listeners.get(key)?.delete(callback);
},
loaded: Promise.resolve() // will be replaced below
loaded: Promise.resolve(), // will be replaced below
};
// Fill with defaults first
@@ -71,33 +88,45 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
const loaded = (async () => {
try {
const stored = await browser.storage.local.get(storageKey);
const storedSettings = stored[storageKey] as Partial<Record<keyof T, any>>;
const storedSettings = stored[storageKey] as Partial<
Record<keyof T, any>
>;
if (storedSettings) {
for (const key in storedSettings) {
if (key in settingsWithMeta) {
settingsWithMeta[key] = storedSettings[key];
listeners.get(key as keyof T)?.forEach(cb => cb(storedSettings[key]));
listeners
.get(key as keyof T)
?.forEach((cb) => cb(storedSettings[key]));
}
}
}
} catch (error) {
console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error);
console.error(
`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`,
error,
);
}
})();
settingsWithMeta.loaded = loaded;
// Listen for storage changes and update settingsWithMeta
const handleStorageChange = (changes: { [key: string]: browser.Storage.StorageChange }, area: string) => {
if (area !== 'local' || !(storageKey in changes)) return;
const handleStorageChange = (
changes: { [key: string]: browser.Storage.StorageChange },
area: string,
) => {
if (area !== "local" || !(storageKey in changes)) return;
const newValue = changes[storageKey].newValue as Partial<Record<keyof T, any>> | undefined;
const newValue = changes[storageKey].newValue as
| Partial<Record<keyof T, any>>
| undefined;
if (!newValue) return;
for (const key in newValue) {
const typedKey = key as keyof T;
settingsWithMeta[typedKey] = newValue[typedKey];
listeners.get(typedKey)?.forEach(cb => cb(newValue[typedKey]));
listeners.get(typedKey)?.forEach((cb) => cb(newValue[typedKey]));
}
};
@@ -108,7 +137,8 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return target[prop];
},
set(target, prop, value) {
if (['onChange', 'offChange', 'loaded'].includes(prop as string)) return false;
if (["onChange", "offChange", "loaded"].includes(prop as string))
return false;
target[prop] = value;
@@ -120,25 +150,29 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
browser.storage.local.set({ [storageKey]: dataToStore });
listeners.get(prop as keyof T)?.forEach(cb => cb(value));
listeners.get(prop as keyof T)?.forEach((cb) => cb(value));
return true;
}
},
}) as SettingsAPI<T> & { loaded: Promise<void> };
return proxy;
}
function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in keyof T]: T[K] } {
function createStorageAPI<T = any>(
pluginId: string,
): StorageAPI<T> & { [K in keyof T]: T[K] } {
const prefix = `plugin.${pluginId}.storage.`;
const cache: Record<string, any> = {};
const listeners = new Map<string, Set<(value: any) => void>>();
const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>();
const storageListeners = new Set<
(changes: { [key: string]: any }, area: string) => void
>();
// Load all existing storage values for this plugin
const loadStoragePromise = (async () => {
try {
const allStorage = await browser.storage.local.get(null);
// Filter for this plugin's storage keys and populate cache
Object.entries(allStorage).forEach(([key, value]) => {
if (key.startsWith(prefix)) {
@@ -147,31 +181,39 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
}
});
} catch (error) {
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error);
console.error(
`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`,
error,
);
}
})();
// Listen for storage changes
const handleStorageChange = (changes: { [key: string]: any }, area: string) => {
if (area === 'local') {
const handleStorageChange = (
changes: { [key: string]: any },
area: string,
) => {
if (area === "local") {
Object.entries(changes).forEach(([key, change]) => {
if (key.startsWith(prefix)) {
const shortKey = key.slice(prefix.length);
cache[shortKey] = change.newValue;
// Notify listeners
listeners.get(shortKey)?.forEach(callback => callback(change.newValue));
listeners
.get(shortKey)
?.forEach((callback) => callback(change.newValue));
}
});
}
};
browser.storage.onChanged.addListener(handleStorageChange);
storageListeners.add(handleStorageChange);
// Create the proxy for direct property access
return new Proxy(cache, {
get(target, prop: string) {
if (prop === 'onChange') {
if (prop === "onChange") {
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
if (!listeners.has(key as string)) {
listeners.set(key as string, new Set());
@@ -180,79 +222,84 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
return {
unregister: () => {
listeners.get(key as string)?.delete(callback);
}
},
};
};
}
if (prop === 'offChange') {
if (prop === "offChange") {
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
listeners.get(key as string)?.delete(callback);
};
}
if (prop === 'loaded') {
if (prop === "loaded") {
return loadStoragePromise;
}
// Direct property access
return target[prop];
},
set(target, prop: string, value: any) {
if (['onChange', 'offChange', 'loaded'].includes(prop)) {
if (["onChange", "offChange", "loaded"].includes(prop)) {
return false;
}
// Update cache and store in browser storage
target[prop] = value;
browser.storage.local.set({ [prefix + prop]: value });
// Notify listeners
listeners.get(prop)?.forEach(callback => callback(value));
listeners.get(prop)?.forEach((callback) => callback(value));
return true;
}
},
}) as StorageAPI<T> & { [K in keyof T]: T[K] };
}
function createEventsAPI(pluginId: string): EventsAPI {
const prefix = `plugin.${pluginId}.`;
const eventListeners = new Map<string, Set<{ callback: (...args: any[]) => void, listener: EventListener }>>();
const eventListeners = new Map<
string,
Set<{ callback: (...args: any[]) => void; listener: EventListener }>
>();
return {
on: (event, callback) => {
const fullEventName = prefix + event;
const listener = ((e: CustomEvent) => {
callback(...(e.detail || []));
}) as EventListener;
document.addEventListener(fullEventName, listener);
if (!eventListeners.has(event)) {
eventListeners.set(event, new Set());
}
eventListeners.get(event)!.add({ callback, listener });
return {
unregister: () => {
document.removeEventListener(fullEventName, listener);
eventListeners.get(event)?.delete({ callback, listener });
}
},
};
},
emit: (event, ...args) => {
document.dispatchEvent(
new CustomEvent(prefix + event, {
detail: args.length > 0 ? args : null
})
detail: args.length > 0 ? args : null,
}),
);
},
};
}
export function createPluginAPI<T extends PluginSettings, S = any>(plugin: Plugin<T, S>): PluginAPI<T, S> {
export function createPluginAPI<T extends PluginSettings, S = any>(
plugin: Plugin<T, S>,
): PluginAPI<T, S> {
return {
seqta: createSEQTAAPI(),
settings: createSettingsAPI(plugin),
storage: createStorageAPI<S>(plugin.id),
events: createEventsAPI(plugin.id),
};
}
}
+108 -74
View File
@@ -1,6 +1,13 @@
import type { BooleanSetting, NumberSetting, Plugin, PluginSettings, SelectSetting, StringSetting } from './types';
import { createPluginAPI } from './createAPI';
import browser from 'webextension-polyfill';
import type {
BooleanSetting,
NumberSetting,
Plugin,
PluginSettings,
SelectSetting,
StringSetting,
} from "./types";
import { createPluginAPI } from "./createAPI";
import browser from "webextension-polyfill";
interface PluginSettingsStorage {
enabled?: boolean;
@@ -34,7 +41,7 @@ export class PluginManager {
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
const fullEventName = `plugin.${pluginId}.${event}`;
// Dispatch plugin event if it's running otherwise queue it
if (this.runningPlugins.get(pluginId)) {
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
@@ -49,7 +56,7 @@ export class PluginManager {
private async processBackloggedEvents(pluginId: string) {
for (const [key, argsList] of this.eventBacklog.entries()) {
const [eventPluginId, event] = key.split(':');
const [eventPluginId, event] = key.split(":");
if (eventPluginId === pluginId) {
for (const args of argsList) {
this.dispatchPluginEvent(pluginId, event, args);
@@ -59,7 +66,9 @@ export class PluginManager {
}
}
public registerPlugin<T extends PluginSettings, S>(plugin: Plugin<T, S>): void {
public registerPlugin<T extends PluginSettings, S>(
plugin: Plugin<T, S>,
): void {
if (this.plugins.has(plugin.id)) {
throw new Error(`Plugin with id "${plugin.id}" is already registered`);
}
@@ -79,53 +88,60 @@ export class PluginManager {
try {
const api = createPluginAPI(plugin);
// Check if plugin is enabled before starting
if (plugin.disableToggle) {
const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`);
const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined;
const enabled = pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
const settings = await browser.storage.local.get(
`plugin.${pluginId}.settings`,
);
const pluginSettings = settings[`plugin.${pluginId}.settings`] as
| PluginSettingsStorage
| undefined;
const enabled =
pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
if (!enabled) {
console.info(`Plugin "${pluginId}" is disabled, skipping initialization`);
console.info(
`Plugin "${pluginId}" is disabled, skipping initialization`,
);
return;
}
}
// Inject plugin styles if provided
if (plugin.styles) {
const styleElement = document.createElement('style');
const styleElement = document.createElement("style");
styleElement.textContent = plugin.styles;
document.head.appendChild(styleElement);
this.styleElements.set(pluginId, styleElement);
}
// Wait for both settings and storage to be loaded before starting the plugin
await Promise.all([
(api.settings as any).loaded,
api.storage.loaded
]);
await Promise.all([(api.settings as any).loaded, api.storage.loaded]);
const result = await plugin.run(api);
if (typeof result === 'function') {
if (typeof result === "function") {
this.cleanupFunctions.set(plugin.id, result);
}
this.runningPlugins.set(pluginId, true);
console.info(`Plugin "${pluginId}" started successfully`);
// Process any backlogged events
await this.processBackloggedEvents(pluginId);
} catch (error) {
console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error);
console.error(
`[BetterSEQTA+] Failed to start plugin ${pluginId}:`,
error,
);
throw error;
}
}
public async startAllPlugins(): Promise<void> {
const startPromises = Array.from(this.plugins.keys()).map(id =>
this.startPlugin(id).catch(error => {
const startPromises = Array.from(this.plugins.keys()).map((id) =>
this.startPlugin(id).catch((error) => {
console.error(`Failed to start plugin "${id}":`, error);
return Promise.reject(error);
})
}),
);
await Promise.allSettled(startPromises);
@@ -146,11 +162,11 @@ export class PluginManager {
}
this.runningPlugins.set(pluginId, false);
console.info(`Plugin "${pluginId}" stopped`);
this.emit('plugin.stopped', pluginId);
this.emit("plugin.stopped", pluginId);
}
public stopAllPlugins(): void {
Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id));
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
}
public getPlugin(pluginId: string): Plugin | undefined {
@@ -166,40 +182,49 @@ export class PluginManager {
name: string;
description: string;
settings: {
[key: string]: (Omit<BooleanSetting, 'type'> & { type: 'boolean', id: string }) |
(Omit<StringSetting, 'type'> & { type: 'string', id: string }) |
(Omit<NumberSetting, 'type'> & { type: 'number', id: string }) |
(Omit<SelectSetting<string>, 'type'> & { type: 'select', id: string, options: Array<{ value: string, label: string }> });
}
[key: string]:
| (Omit<BooleanSetting, "type"> & { type: "boolean"; id: string })
| (Omit<StringSetting, "type"> & { type: "string"; id: string })
| (Omit<NumberSetting, "type"> & { type: "number"; id: string })
| (Omit<SelectSetting<string>, "type"> & {
type: "select";
id: string;
options: Array<{ value: string; label: string }>;
});
};
}> {
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
const settingsEntries = Object.entries(plugin.settings).map(([key, setting]) => {
const settingObj = setting as any;
// Create a copy of the setting object without any functions
const result: any = Object.fromEntries(
Object.entries(settingObj)
.filter(([_, value]) => typeof value !== 'function')
);
// Ensure required properties are present
result.id = key;
result.title = result.title || key;
result.description = result.description || '';
result.defaultEnabled = plugin.defaultEnabled ?? true;
return [key, result];
});
const settingsEntries = Object.entries(plugin.settings).map(
([key, setting]) => {
const settingObj = setting as any;
// Create a copy of the setting object without any functions
const result: any = Object.fromEntries(
Object.entries(settingObj).filter(
([_, value]) => typeof value !== "function",
),
);
// Ensure required properties are present
result.id = key;
result.title = result.title || key;
result.description = result.description || "";
result.defaultEnabled = plugin.defaultEnabled ?? true;
return [key, result];
},
);
if (plugin.disableToggle) {
settingsEntries.push([
'enabled', {
id: 'enabled',
"enabled",
{
id: "enabled",
title: plugin.name,
description: plugin.description,
type: 'boolean',
default: plugin.defaultEnabled ?? true
}
])
type: "boolean",
default: plugin.defaultEnabled ?? true,
},
]);
}
return {
pluginId: id,
@@ -218,7 +243,7 @@ export class PluginManager {
private emit(event: string, ...args: any[]): void {
const listeners = this.listeners.get(event);
if (listeners) {
listeners.forEach(listener => listener(...args));
listeners.forEach((listener) => listener(...args));
}
}
@@ -237,7 +262,10 @@ export class PluginManager {
}
// Add handler for plugin enable/disable state changes
private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise<void> {
private async handlePluginStateChange(
pluginId: string,
enabled: boolean,
): Promise<void> {
if (enabled) {
await this.startPlugin(pluginId);
} else {
@@ -247,24 +275,30 @@ export class PluginManager {
// Add listener for plugin settings changes
private setupPluginStateListener(): void {
browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => {
if (area !== 'local') return;
for (const [key, change] of Object.entries(changes)) {
const match = key.match(/^plugin\.(.+)\.settings$/);
if (!match) continue;
const pluginId = match[1];
const plugin = this.plugins.get(pluginId);
if (!plugin?.disableToggle) continue;
const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true;
const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? plugin.defaultEnabled ?? true;
if (enabled !== wasEnabled) {
this.handlePluginStateChange(pluginId, enabled);
browser.storage.onChanged.addListener(
(changes: { [key: string]: StorageChange }, area: string) => {
if (area !== "local") return;
for (const [key, change] of Object.entries(changes)) {
const match = key.match(/^plugin\.(.+)\.settings$/);
if (!match) continue;
const pluginId = match[1];
const plugin = this.plugins.get(pluginId);
if (!plugin?.disableToggle) continue;
const enabled =
(change.newValue as PluginSettingsStorage)?.enabled ?? true;
const wasEnabled =
(change.oldValue as PluginSettingsStorage)?.enabled ??
plugin.defaultEnabled ??
true;
if (enabled !== wasEnabled) {
this.handlePluginStateChange(pluginId, enabled);
}
}
}
});
},
);
}
}
}
+6 -6
View File
@@ -1,14 +1,14 @@
import type { PluginSettings } from './types';
import type { PluginSettings } from "./types";
export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => {
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
Object.defineProperty(proto, 'settings', {
if (!proto.hasOwnProperty("settings")) {
Object.defineProperty(proto, "settings", {
value: {},
writable: true,
configurable: true,
enumerable: true
enumerable: true,
});
}
@@ -27,7 +27,7 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
// Copy settings from the prototype to the instance
// This ensures that each instance has its own settings object
// IMPORTANT: Ensure the prototype actually HAS settings before copying
if (this.constructor.prototype.hasOwnProperty('settings')) {
if (this.constructor.prototype.hasOwnProperty("settings")) {
// Deep clone might be safer if settings objects become complex,
// but a shallow clone is usually fine for this structure.
this.settings = { ...this.constructor.prototype.settings } as T;
@@ -36,4 +36,4 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
this.settings = {} as T;
}
}
}
}
+29 -17
View File
@@ -1,30 +1,43 @@
import type { BooleanSetting, NumberSetting, SelectSetting, StringSetting } from './types';
import type {
BooleanSetting,
NumberSetting,
SelectSetting,
StringSetting,
} from "./types";
export function numberSetting(options: Omit<NumberSetting, 'type'>): NumberSetting {
export function numberSetting(
options: Omit<NumberSetting, "type">,
): NumberSetting {
return {
type: 'number',
...options
type: "number",
...options,
};
}
export function booleanSetting(options: Omit<BooleanSetting, 'type'>): BooleanSetting {
export function booleanSetting(
options: Omit<BooleanSetting, "type">,
): BooleanSetting {
return {
type: 'boolean',
...options
type: "boolean",
...options,
};
}
export function stringSetting(options: Omit<StringSetting, 'type'>): StringSetting {
export function stringSetting(
options: Omit<StringSetting, "type">,
): StringSetting {
return {
type: 'string',
...options
type: "string",
...options,
};
}
export function selectSetting<T extends string>(options: Omit<SelectSetting<T>, 'type'>): SelectSetting<T> {
export function selectSetting<T extends string>(
options: Omit<SelectSetting<T>, "type">,
): SelectSetting<T> {
return {
type: 'select',
...options
type: "select",
...options,
};
}
@@ -35,16 +48,15 @@ export function defineSettings<T extends Record<string, any>>(settings: T): T {
export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => {
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
Object.defineProperty(proto, 'settings', {
if (!proto.hasOwnProperty("settings")) {
Object.defineProperty(proto, "settings", {
value: {},
writable: true,
configurable: true,
enumerable: true
enumerable: true,
});
}
proto.settings[propertyKey] = settingDef;
};
}
+53 -26
View File
@@ -1,14 +1,14 @@
import ReactFiber from '@/seqta/utils/ReactFiber';
import ReactFiber from "@/seqta/utils/ReactFiber";
export interface BooleanSetting {
type: 'boolean';
type: "boolean";
default: boolean;
title: string;
description?: string;
}
export interface StringSetting {
type: 'string';
type: "string";
default: string;
title: string;
description?: string;
@@ -17,7 +17,7 @@ export interface StringSetting {
}
export interface NumberSetting {
type: 'number';
type: "number";
default: number;
title: string;
description?: string;
@@ -27,47 +27,69 @@ export interface NumberSetting {
}
export interface SelectSetting<T extends string> {
type: 'select';
type: "select";
options: readonly T[];
default: T;
title: string;
description?: string;
}
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
export type PluginSetting =
| BooleanSetting
| StringSetting
| NumberSetting
| SelectSetting<string>;
export type PluginSettings = {
[key: string]: PluginSetting;
}
};
// Helper type to extract the actual value type from a setting
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
T extends StringSetting ? string :
T extends NumberSetting ? number :
T extends SelectSetting<infer O> ? O :
never;
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
? boolean
: T extends StringSetting
? string
: T extends NumberSetting
? number
: T extends SelectSetting<infer O>
? O
: never;
export type SettingsAPI<T extends PluginSettings> = {
[K in keyof T]: SettingValue<T[K]>;
} & {
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => { unregister: () => void };
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
onChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => { unregister: () => void };
offChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => void;
loaded: Promise<void>;
}
};
export interface SEQTAAPI {
onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void };
onMount: (
selector: string,
callback: (element: Element) => void,
) => { unregister: () => void };
getFiber: (selector: string) => ReactFiber;
getCurrentPage: () => string;
onPageChange: (callback: (page: string) => void) => { unregister: () => void };
onPageChange: (callback: (page: string) => void) => {
unregister: () => void;
};
}
export interface StorageAPI<T = any> {
/**
* Register a callback to be called when a storage value changes
*/
onChange: <K extends keyof T>(key: K, callback: (value: T[K]) => void) => { unregister: () => void };
onChange: <K extends keyof T>(
key: K,
callback: (value: T[K]) => void,
) => { unregister: () => void };
/**
* Promise that resolves when storage values are loaded
*/
@@ -76,10 +98,13 @@ export interface StorageAPI<T = any> {
export type TypedStorageAPI<T> = StorageAPI<T> & {
[K in keyof T]: T[K];
}
};
export interface EventsAPI {
on: (event: string, callback: (...args: any[]) => void) => { unregister: () => void };
on: (
event: string,
callback: (...args: any[]) => void,
) => { unregister: () => void };
emit: (event: string, ...args: any[]) => void;
}
@@ -96,8 +121,10 @@ export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
description: string;
version: string;
settings: T;
styles?: string; // Optional CSS styles for the plugin
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
}
styles?: string; // Optional CSS styles for the plugin
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
run: (
api: PluginAPI<T, S>,
) => void | Promise<void> | (() => void) | Promise<() => void>;
}
+9 -9
View File
@@ -1,12 +1,12 @@
import { PluginManager } from './core/manager';
import { PluginManager } from "./core/manager";
// plugins
import timetablePlugin from './built-in/timetable';
import notificationCollectorPlugin from './built-in/notificationCollector';
import themesPlugin from './built-in/themes';
import animatedBackgroundPlugin from './built-in/animatedBackground';
import assessmentsAveragePlugin from './built-in/assessmentsAverage';
import globalSearchPlugin from './built-in/globalSearch/src/core';
import timetablePlugin from "./built-in/timetable";
import notificationCollectorPlugin from "./built-in/notificationCollector";
import themesPlugin from "./built-in/themes";
import animatedBackgroundPlugin from "./built-in/animatedBackground";
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
import globalSearchPlugin from "./built-in/globalSearch/src/core";
//import testPlugin from './built-in/test';
// Initialize plugin manager
@@ -21,7 +21,7 @@ pluginManager.registerPlugin(timetablePlugin);
pluginManager.registerPlugin(globalSearchPlugin);
//pluginManager.registerPlugin(testPlugin);
export { init as Monofile } from './monofile';
export { init as Monofile } from "./monofile";
export async function initializePlugins(): Promise<void> {
await pluginManager.startAllPlugins();
@@ -31,4 +31,4 @@ export { pluginManager };
export function getAllPluginSettings() {
return pluginManager.getAllPluginSettings();
}
}
+247 -244
View File
@@ -1,135 +1,134 @@
// Third-party libraries
import browser from "webextension-polyfill"
import { animate, stagger } from "motion"
import browser from "webextension-polyfill";
import { animate, stagger } from "motion";
// Internal utilities and functions
import { ChangeMenuItemPositions, MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions"
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"
import { waitForElm } from "@/seqta/utils/waitForElm"
import { delay } from "@/seqta/utils/delay"
import stringToHTML from "@/seqta/utils/stringToHTML"
import { MessageHandler } from "@/seqta/utils/listeners/MessageListener"
import {
settingsState,
} from "@/seqta/utils/listeners/SettingsState"
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges"
import { eventManager } from "@/seqta/utils/listeners/EventManager"
ChangeMenuItemPositions,
MenuOptionsOpen,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { delay } from "@/seqta/utils/delay";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { MessageHandler } from "@/seqta/utils/listeners/MessageListener";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
// UI and theme management
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners"
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements"
import { updateAllColors } from "@/seqta/ui/colors/Manager"
import loading from "@/seqta/ui/Loading"
import { SendNewsPage } from "@/seqta/utils/SendNewsPage"
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew"
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners";
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements";
import { updateAllColors } from "@/seqta/ui/colors/Manager";
import loading from "@/seqta/ui/Loading";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
// JSON content
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json"
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
// Icons and fonts
import IconFamily from "@/resources/fonts/IconFamily.woff"
import IconFamily from "@/resources/fonts/IconFamily.woff";
// Stylesheets
import iframeCSS from "@/css/iframe.scss?raw"
import iframeCSS from "@/css/iframe.scss?raw";
function SetDisplayNone(ElementName: string) {
return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}`
return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}`;
}
async function HideMenuItems(): Promise<void> {
try {
let stylesheetInnerText: string = ""
let stylesheetInnerText: string = "";
for (const [menuItem, { toggle }] of Object.entries(
settingsState.menuitems,
)) {
if (!toggle) {
stylesheetInnerText += SetDisplayNone(menuItem)
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`)
stylesheetInnerText += SetDisplayNone(menuItem);
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
}
}
const menuItemStyle: HTMLStyleElement = document.createElement("style")
menuItemStyle.innerText = stylesheetInnerText
document.head.appendChild(menuItemStyle)
const menuItemStyle: HTMLStyleElement = document.createElement("style");
menuItemStyle.innerText = stylesheetInnerText;
document.head.appendChild(menuItemStyle);
} catch (error) {
console.error("[BetterSEQTA+] An error occurred:", error)
console.error("[BetterSEQTA+] An error occurred:", error);
}
}
export function hideSideBar() {
const sidebar = document.getElementById("menu") // The sidebar element to be closed
const main = document.getElementById("main") // The main content element that must be resized to fill the page
const sidebar = document.getElementById("menu"); // The sidebar element to be closed
const main = document.getElementById("main"); // The main content element that must be resized to fill the page
const currentMenuWidth = window.getComputedStyle(sidebar!).width // Get the styles of the different elements
const currentContentPosition = window.getComputedStyle(main!).position
const currentMenuWidth = window.getComputedStyle(sidebar!).width; // Get the styles of the different elements
const currentContentPosition = window.getComputedStyle(main!).position;
if (currentMenuWidth != "0") {
// Actually modify it to collapse the sidebar
sidebar!.style.width = "0"
sidebar!.style.width = "0";
} else {
sidebar!.style.width = "100%"
sidebar!.style.width = "100%";
}
if (currentContentPosition != "relative") {
main!.style.position = "relative"
main!.style.position = "relative";
} else {
main!.style.position = "absolute"
main!.style.position = "absolute";
}
}
export async function finishLoad() {
try {
document.querySelector(".legacy-root")?.classList.remove("hidden")
document.querySelector(".legacy-root")?.classList.remove("hidden");
const loadingbk = document.getElementById("loading")
loadingbk?.classList.add("closeLoading")
await delay(501)
loadingbk?.remove()
const loadingbk = document.getElementById("loading");
loadingbk?.classList.add("closeLoading");
await delay(501);
loadingbk?.remove();
} catch (err) {
console.error("Error during loading cleanup:", err)
console.error("Error during loading cleanup:", err);
}
if (settingsState.justupdated && !document.getElementById("whatsnewbk")) {
OpenWhatsNewPopup()
OpenWhatsNewPopup();
}
}
export function GetCSSElement(file: string) {
const cssFile = browser.runtime.getURL(file)
const fileref = document.createElement("link")
fileref.setAttribute("rel", "stylesheet")
fileref.setAttribute("type", "text/css")
fileref.setAttribute("href", cssFile)
const cssFile = browser.runtime.getURL(file);
const fileref = document.createElement("link");
fileref.setAttribute("rel", "stylesheet");
fileref.setAttribute("type", "text/css");
fileref.setAttribute("href", cssFile);
return fileref
return fileref;
}
function removeThemeTagsFromNotices() {
// Grabs an array of the notice iFrames
const userHTMLArray = document.getElementsByClassName("userHTML")
const userHTMLArray = document.getElementsByClassName("userHTML");
// Iterates through the array, applying the iFrame css
for (const item of userHTMLArray) {
// Grabs the HTML of the body tag
const item1 = item as HTMLIFrameElement
const body = item1.contentWindow!.document.querySelectorAll("body")[0]
const item1 = item as HTMLIFrameElement;
const body = item1.contentWindow!.document.querySelectorAll("body")[0];
if (body) {
// Replaces the theme tag with nothing
const bodyText = body.innerHTML
const bodyText = body.innerHTML;
body.innerHTML = bodyText
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/ +/, " ")
.replace(/ +/, " ");
}
}
}
async function updateIframesWithDarkMode(): Promise<void> {
const cssLink = document.createElement("style")
cssLink.classList.add("iframecss")
const cssContent = document.createTextNode(iframeCSS)
cssLink.appendChild(cssContent)
const cssLink = document.createElement("style");
cssLink.classList.add("iframecss");
const cssContent = document.createTextNode(iframeCSS);
cssLink.appendChild(cssContent);
eventManager.register(
"iframeAdded",
@@ -139,63 +138,63 @@ async function updateIframesWithDarkMode(): Promise<void> {
!element.classList.contains("iframecss"),
},
(element) => {
const iframe = element as HTMLIFrameElement
const iframe = element as HTMLIFrameElement;
try {
applyDarkModeToIframe(iframe, cssLink)
applyDarkModeToIframe(iframe, cssLink);
if (element.classList.contains("cke_wysiwyg_frame")) {
(async () => {
await delay(100)
iframe.contentDocument?.body.setAttribute("spellcheck", "true")
})()
await delay(100);
iframe.contentDocument?.body.setAttribute("spellcheck", "true");
})();
}
} catch (error) {
console.error("Error applying dark mode:", error)
console.error("Error applying dark mode:", error);
}
},
)
);
}
function applyDarkModeToIframe(
iframe: HTMLIFrameElement,
cssLink: HTMLStyleElement,
): void {
const iframeDocument = iframe.contentDocument
if (!iframeDocument) return
const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return;
iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink)
}
applyDarkModeToIframe(iframe, cssLink);
};
if (settingsState.DarkMode) {
iframeDocument.documentElement.classList.add("dark")
iframeDocument.documentElement.classList.add("dark");
}
const head = iframeDocument.head
const head = iframeDocument.head;
if (head && !head.innerHTML.includes("iframecss")) {
head.innerHTML += cssLink.outerHTML
head.innerHTML += cssLink.outerHTML;
}
}
function SortMessagePageItems(messagesParentElement: any) {
try {
let filterbutton = document.createElement("div")
filterbutton.classList.add("messages-filterbutton")
filterbutton.innerText = "Filter"
let filterbutton = document.createElement("div");
filterbutton.classList.add("messages-filterbutton");
filterbutton.innerText = "Filter";
let header = document.querySelector(
"[class*='MessageList__MessageList___']",
) as HTMLElement
header.append(filterbutton)
messagesParentElement
) as HTMLElement;
header.append(filterbutton);
messagesParentElement;
} catch (error) {
console.error("Error sorting message page items:", error)
console.error("Error sorting message page items:", error);
}
}
async function LoadPageElements(): Promise<void> {
await AddBetterSEQTAElements()
const sublink: string | undefined = window.location.href.split("/")[4]
await AddBetterSEQTAElements();
const sublink: string | undefined = window.location.href.split("/")[4];
eventManager.register(
"messagesAdded",
@@ -204,7 +203,7 @@ async function LoadPageElements(): Promise<void> {
className: "messages",
},
handleMessages,
)
);
eventManager.register(
"noticesAdded",
@@ -213,7 +212,7 @@ async function LoadPageElements(): Promise<void> {
className: "notices",
},
CheckNoticeTextColour,
)
);
eventManager.register(
"dashboardAdded",
@@ -222,7 +221,7 @@ async function LoadPageElements(): Promise<void> {
className: "dashboard",
},
handleDashboard,
)
);
eventManager.register(
"documentsAdded",
@@ -231,7 +230,7 @@ async function LoadPageElements(): Promise<void> {
className: "documents",
},
handleDocuments,
)
);
eventManager.register(
"reportsAdded",
@@ -240,7 +239,7 @@ async function LoadPageElements(): Promise<void> {
className: "reports",
},
handleReports,
)
);
/* eventManager.register(
"timetableAdded",
@@ -258,21 +257,21 @@ async function LoadPageElements(): Promise<void> {
className: "notice",
},
handleNotices,
)
);
RegisterClickListeners()
RegisterClickListeners();
await handleSublink(sublink)
await handleSublink(sublink);
}
async function handleNotices(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
node.style.opacity = "0"
node.style.opacity = "0";
// get index of node in relation to parent
const index = Array.from(node.parentElement!.children).indexOf(node)
const index = Array.from(node.parentElement!.children).indexOf(node);
animate(
node,
@@ -283,71 +282,73 @@ async function handleNotices(node: Element): Promise<void> {
stiffness: 250,
damping: 20,
},
)
);
}
async function handleSublink(sublink: string | undefined): Promise<void> {
switch (sublink) {
case "news":
await handleNewsPage()
break
await handleNewsPage();
break;
case undefined:
window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`)
if (settingsState.defaultPage === "home") loadHomePage()
window.location.replace(
`${location.origin}/#?page=/${settingsState.defaultPage}`,
);
if (settingsState.defaultPage === "home") loadHomePage();
if (settingsState.defaultPage === "documents")
handleDocuments(document.querySelector(".documents")!)
handleDocuments(document.querySelector(".documents")!);
if (settingsState.defaultPage === "reports")
handleReports(document.querySelector(".reports")!)
handleReports(document.querySelector(".reports")!);
if (settingsState.defaultPage === "messages")
handleMessages(document.querySelector(".messages")!)
handleMessages(document.querySelector(".messages")!);
finishLoad()
break
finishLoad();
break;
case "home":
window.location.replace(`${location.origin}/#?page=/home`)
console.info("[BetterSEQTA+] Started Init")
if (settingsState.onoff) loadHomePage()
finishLoad()
break
window.location.replace(`${location.origin}/#?page=/home`);
console.info("[BetterSEQTA+] Started Init");
if (settingsState.onoff) loadHomePage();
finishLoad();
break;
default:
await handleDefault()
break
await handleDefault();
break;
}
}
async function handleNewsPage(): Promise<void> {
console.info("[BetterSEQTA+] Started Init")
console.info("[BetterSEQTA+] Started Init");
if (settingsState.onoff) {
SendNewsPage()
finishLoad()
SendNewsPage();
finishLoad();
}
}
async function handleDefault(): Promise<void> {
finishLoad()
finishLoad();
}
async function handleMessages(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!(node instanceof HTMLElement)) return;
const element = document.getElementById("title")!.firstChild as HTMLElement
element.innerText = "Direct Messages"
document.title = "Direct Messages ― SEQTA Learn"
SortMessagePageItems(node)
if (!settingsState.animations) return
const element = document.getElementById("title")!.firstChild as HTMLElement;
element.innerText = "Direct Messages";
document.title = "Direct Messages ― SEQTA Learn";
SortMessagePageItems(node);
if (!settingsState.animations) return;
// Hides messages on page load
const style = document.createElement("style")
style.classList.add("messageHider")
style.innerHTML = "[data-message]{opacity: 0 !important;}"
document.head.append(style)
const style = document.createElement("style");
style.classList.add("messageHider");
style.innerHTML = "[data-message]{opacity: 0 !important;}";
document.head.append(style);
await waitForElm("[data-message]", true, 10)
await waitForElm("[data-message]", true, 10);
const messages = Array.from(
document.querySelectorAll("[data-message]"),
).slice(0, 35)
).slice(0, 35);
animate(
messages,
{ opacity: [0, 1], y: [10, 0] },
@@ -356,21 +357,21 @@ async function handleMessages(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
document.head.querySelector("style.messageHider")?.remove()
document.head.querySelector("style.messageHider")?.remove();
}
async function handleDashboard(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
const style = document.createElement("style")
style.classList.add("dashboardHider")
style.innerHTML = ".dashboard{opacity: 0 !important;}"
document.head.append(style)
const style = document.createElement("style");
style.classList.add("dashboardHider");
style.innerHTML = ".dashboard{opacity: 0 !important;}";
document.head.append(style);
await waitForElm(".dashlet", true, 10)
await waitForElm(".dashlet", true, 10);
animate(
".dashboard > *",
{ opacity: [0, 1], y: [10, 0] },
@@ -379,16 +380,16 @@ async function handleDashboard(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
document.head.querySelector("style.dashboardHider")?.remove()
document.head.querySelector("style.dashboardHider")?.remove();
}
async function handleDocuments(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
await waitForElm(".document", true, 10)
await waitForElm(".document", true, 10);
animate(
".documents tbody tr.document",
{ opacity: [0, 1], y: [10, 0] },
@@ -397,14 +398,14 @@ async function handleDocuments(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
}
async function handleReports(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
await waitForElm(".report", true, 10)
await waitForElm(".report", true, 10);
animate(
".reports .item",
{ opacity: [0, 1], y: [10, 0] },
@@ -413,7 +414,7 @@ async function handleReports(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
}
function CheckNoticeTextColour(notice: any) {
@@ -425,132 +426,134 @@ function CheckNoticeTextColour(notice: any) {
parentElement: notice,
},
(node) => {
var hex = (node as HTMLElement).style.cssText.split(" ")[1]
var hex = (node as HTMLElement).style.cssText.split(" ")[1];
if (hex) {
const hex1 = hex.slice(0, -1)
var threshold = GetThresholdOfColor(hex1)
const hex1 = hex.slice(0, -1);
var threshold = GetThresholdOfColor(hex1);
if (settingsState.DarkMode && threshold < 100) {
(node as HTMLElement).style.cssText = "--color: undefined;"
(node as HTMLElement).style.cssText = "--color: undefined;";
}
}
},
)
);
}
export function tryLoad() {
waitForElm(".login").then(() => {
finishLoad()
})
finishLoad();
});
waitForElm(".day-container").then(() => {
finishLoad()
})
finishLoad();
});
waitForElm("[data-key=welcome]").then((elm: any) => {
elm.classList.remove("active")
})
elm.classList.remove("active");
});
waitForElm(".code", true, 50).then((elm: any) => {
if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements()
})
if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements();
});
updateIframesWithDarkMode()
updateIframesWithDarkMode();
// Waits for page to call on load, run scripts
document.addEventListener(
"load",
function () {
removeThemeTagsFromNotices()
removeThemeTagsFromNotices();
},
true,
)
);
}
function ReplaceMenuSVG(element: HTMLElement, svg: string) {
let item = element.firstChild as HTMLElement
item!.firstChild!.remove()
let item = element.firstChild as HTMLElement;
item!.firstChild!.remove();
item.innerHTML = `<span>${item.innerHTML}</span>`
item.innerHTML = `<span>${item.innerHTML}</span>`;
let newsvg = stringToHTML(svg).firstChild
item.insertBefore(newsvg as Node, item.firstChild)
let newsvg = stringToHTML(svg).firstChild;
item.insertBefore(newsvg as Node, item.firstChild);
}
const processedSymbol = Symbol('processed')
const processedSymbol = Symbol("processed");
export async function ObserveMenuItemPosition() {
await waitForElm("#menu > ul > li")
await waitForElm("#menu > ul > li");
eventManager.register(
"menuList",
{
parentElement: document.querySelector("#menu")!.firstChild as Element,
},
(element: Element) => {
const node = element as HTMLElement
const node = element as HTMLElement;
// Only process top-level menu items and skip everything else
if (!node.classList.contains('item') ||
node.nodeName !== 'LI' ||
node.parentElement?.parentElement?.id !== 'menu') {
return
if (
!node.classList.contains("item") ||
node.nodeName !== "LI" ||
node.parentElement?.parentElement?.id !== "menu"
) {
return;
}
// Early exit if already processed
if ((element as any)[processedSymbol]) {
return
return;
}
if (!node?.dataset?.checked && !MenuOptionsOpen) {
const key =
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey]
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
if (key) {
ReplaceMenuSVG(
node,
MenuitemSVGKey[node.dataset.key as keyof typeof MenuitemSVGKey],
)
);
} else if (node?.firstChild?.nodeName === "LABEL") {
const label = node.firstChild as HTMLElement
let textNode = label.lastChild as HTMLElement
const label = node.firstChild as HTMLElement;
let textNode = label.lastChild as HTMLElement;
if (
textNode.nodeType === 3 &&
textNode.parentNode &&
textNode.parentNode.nodeName !== "SPAN"
) {
const span = document.createElement("span")
span.textContent = textNode.nodeValue
const span = document.createElement("span");
span.textContent = textNode.nodeValue;
label.replaceChild(span, textNode)
label.replaceChild(span, textNode);
}
}
ChangeMenuItemPositions(settingsState.menuorder);
(element as any)[processedSymbol] = true
(element as any)[processedSymbol] = true;
}
},
)
);
}
export function showConflictPopup() {
if (document.getElementById("conflict-popup")) return
document.body.classList.remove("hidden")
if (document.getElementById("conflict-popup")) return;
document.body.classList.remove("hidden");
const background = document.createElement("div")
background.id = "conflict-popup"
background.classList.add("whatsnewBackground")
background.style.zIndex = "10000000"
const background = document.createElement("div");
background.id = "conflict-popup";
background.classList.add("whatsnewBackground");
background.style.zIndex = "10000000";
const container = document.createElement("div")
container.classList.add("whatsnewContainer")
container.style.height = "auto"
const container = document.createElement("div");
container.classList.add("whatsnewContainer");
container.style.height = "auto";
const headerHTML = /* html */ `
<div class="whatsnewHeader">
<h1>Extension Conflict Detected</h1>
<p>Legacy BetterSEQTA Installed</p>
</div>
`
const header = stringToHTML(headerHTML).firstChild
`;
const header = stringToHTML(headerHTML).firstChild;
const textHTML = /* html */ `
<div class="whatsnewTextContainer" style="overflow-y: auto; font-size: 1.3rem;">
@@ -562,91 +565,91 @@ export function showConflictPopup() {
Please remove the older BetterSEQTA extension to ensure that BetterSEQTA+ works correctly.
</p>
</div>
`
const text = stringToHTML(textHTML).firstChild
`;
const text = stringToHTML(textHTML).firstChild;
const exitButton = document.createElement("div")
exitButton.id = "whatsnewclosebutton"
const exitButton = document.createElement("div");
exitButton.id = "whatsnewclosebutton";
if (header) container.append(header)
if (text) container.append(text)
container.append(exitButton)
if (header) container.append(header);
if (text) container.append(text);
container.append(exitButton);
background.append(container)
background.append(container);
document.getElementById("container")?.append(background)
document.getElementById("container")?.append(background);
if (settingsState.animations) {
animate([background as HTMLElement], { opacity: [0, 1] })
animate([background as HTMLElement], { opacity: [0, 1] });
}
background.addEventListener("click", (event) => {
if (event.target === background) {
background.remove()
background.remove();
}
})
});
exitButton.addEventListener("click", () => {
background.remove()
})
background.remove();
});
}
export function init() {
const handleDisabled = () => {
waitForElm(".code", true, 50).then(AppendElementsToDisabledPage)
}
waitForElm(".code", true, 50).then(AppendElementsToDisabledPage);
};
if (settingsState.onoff) {
console.info("[BetterSEQTA+] Enabled")
console.info("[BetterSEQTA+] Enabled");
if (settingsState.DarkMode) document.documentElement.classList.add("dark");
document.querySelector(".legacy-root")?.classList.add("hidden")
document.querySelector(".legacy-root")?.classList.add("hidden");
ObserveMenuItemPosition();
new StorageChangeHandler()
new MessageHandler()
new StorageChangeHandler();
new MessageHandler();
updateAllColors()
loading()
InjectCustomIcons()
HideMenuItems()
tryLoad()
updateAllColors();
loading();
InjectCustomIcons();
HideMenuItems();
tryLoad();
setTimeout(() => {
const legacyElement = document.querySelector(
".outside-container .bottom-container",
)
);
if (legacyElement) {
console.log("Legacy extension detected")
showConflictPopup()
console.log("Legacy extension detected");
showConflictPopup();
}
}, 1000)
}, 1000);
} else {
handleDisabled()
window.addEventListener("load", handleDisabled)
handleDisabled();
window.addEventListener("load", handleDisabled);
}
}
function InjectCustomIcons() {
console.info("[BetterSEQTA+] Injecting Icons")
console.info("[BetterSEQTA+] Injecting Icons");
const style = document.createElement("style")
style.setAttribute("type", "text/css")
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.innerHTML = `
@font-face {
font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal;
font-style: normal;
}`
document.head.appendChild(style)
}`;
document.head.appendChild(style);
}
export function AppendElementsToDisabledPage() {
console.info("[BetterSEQTA+] Appending elements to disabled page")
AddBetterSEQTAElements()
console.info("[BetterSEQTA+] Appending elements to disabled page");
AddBetterSEQTAElements();
let settingsStyle = document.createElement("style")
let settingsStyle = document.createElement("style");
settingsStyle.innerHTML = /* css */ `
.addedButton {
position: absolute !important;
@@ -671,6 +674,6 @@ export function AppendElementsToDisabledPage() {
box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6);
transform-origin: 70% 0;
}
`
document.head.append(settingsStyle)
}
`;
document.head.append(settingsStyle);
}