mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
format: run prettify
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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%";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = "" // Unicode for zoom in icon (custom iconfamily)
|
||||
const zoomIn = document.createElement("button");
|
||||
zoomIn.className = "uiButton timetable-zoom iconFamily";
|
||||
zoomIn.innerHTML = ""; // Unicode for zoom in icon (custom iconfamily)
|
||||
|
||||
const zoomOut = document.createElement("button")
|
||||
zoomOut.className = "uiButton timetable-zoom iconFamily"
|
||||
zoomOut.innerHTML = "" // Unicode for zoom out icon (custom iconfamily)
|
||||
const zoomOut = document.createElement("button");
|
||||
zoomOut.className = "uiButton timetable-zoom iconFamily";
|
||||
zoomOut.innerHTML = ""; // 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 = "👁"
|
||||
const hideOn = document.createElement("button");
|
||||
hideOn.className = "uiButton timetable-hide iconFamily";
|
||||
hideOn.innerHTML = "👁";
|
||||
|
||||
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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user