feat: optimisations

This commit is contained in:
2026-07-19 18:08:52 +09:30
parent 10db972ef4
commit 824cedca2f
63 changed files with 1098 additions and 1719 deletions
-5
View File
@@ -1,5 +0,0 @@
{
"transformers": {
"raw:*": ["@parcel/transformer-raw"]
}
}
+18 -38
View File
@@ -27,13 +27,6 @@
"publish": "bun lib/publish.js --b",
"zip": "bedframe zip"
},
"targets": {
"prod": {
"sourceMap": {
"inline": false
}
}
},
"keywords": [],
"author": {
"name": "SethBurkart123",
@@ -42,35 +35,44 @@
},
"license": "MIT",
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.26.9",
"@babel/runtime": "^7.26.9",
"@bedframe/cli": "^0.1.2",
"@crxjs/vite-plugin": "^2.6.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10",
"@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.1.4",
"@types/color": "^4.2.0",
"@types/d3-scale": "^4.0.9",
"@types/d3-shape": "^3.1.8",
"@types/jest": "^30.0.0",
"@types/mime-types": "^3.0.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/node": "^24.3.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/sortablejs": "^1.15.8",
"@types/webextension-polyfill": "^0.12.3",
"@typescript-eslint/eslint-plugin": "^8.60.1",
"@typescript-eslint/parser": "^8.60.1",
"autoprefixer": "^10.4.21",
"cross-env": "^10.0.0",
"dependency-cruiser": "^17.0.1",
"esbuild": "^0.25.3",
"eslint": "^9.33.0",
"eslint-plugin-import": "^2.31.0",
"glob": "^13.0.6",
"jest": "^30.4.2",
"jest-environment-jsdom": "^30.4.1",
"mime-types": "^3.0.1",
"postcss": "^8.5.3",
"prettier": "^3.5.3",
"process": "^0.11.10",
"publish-browser-extension": "^4.0.4",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"semver": "^7.7.1",
"tailwindcss": "3",
"ts-jest": "^29.4.11",
"url": "^0.11.4"
"typescript": "^5.8.2",
"vite": "^6.2.1"
},
"dependencies": {
"@bedframe/core": "^0.1.0",
@@ -81,53 +83,31 @@
"@codemirror/search": "^6.5.10",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.4",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10",
"@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.1.4",
"@types/color": "^4.2.0",
"@types/lodash": "^4.17.16",
"@types/node": "^24.3.0",
"@types/qrcode": "^1.5.6",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^10.0.0",
"@types/webextension-polyfill": "^0.12.3",
"@huggingface/transformers": "^3.8.1",
"@uiw/codemirror-extensions-color": "^4.23.10",
"@uiw/codemirror-theme-github": "^4.23.10",
"autoprefixer": "^10.4.21",
"canvas-confetti": "^1.9.3",
"codemirror": "^6.0.1",
"color": "^5.0.0",
"d3-scale": "^4.0.2",
"d3-shape": "^3.2.0",
"dompurify": "^3.2.4",
"@huggingface/transformers": "^3.8.1",
"embeddia": "^1.3.0",
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-svelte": "^8.5.2",
"esbuild": "^0.25.3",
"events": "^3.3.0",
"flexsearch": "^0.8.147",
"fuse.js": "^7.1.0",
"idb": "^8.0.2",
"layerchart": "2.0.0-next.27",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"mathjs": "^14.4.0",
"million": "^3.1.11",
"motion": "^12.4.12",
"pdfjs-dist": "^5.4.530",
"postcss": "^8.5.3",
"qrcode": "^1.5.4",
"react": "17",
"react-best-gradient-color-picker": "3.0.11",
"react-dom": "17",
"rss-parser": "^3.13.0",
"semver": "^7.7.1",
"sortablejs": "^1.15.6",
"svelte": "^5.46.4",
"typescript": "^5.8.2",
"uuid": "^11.1.0",
"vite": "^6.2.1",
"webextension-polyfill": "^0.12.0"
},
"overrides": {
+3 -6
View File
@@ -6,7 +6,6 @@ import documentLoadCSS from "@/css/documentload.scss?inline";
import icon48 from "@/resources/icons/icon-48.png?base64";
import browser from "webextension-polyfill";
import { init as Monofile } from "@/plugins/monofile";
import { main } from "@/seqta/main";
import { delay } from "./seqta/utils/delay";
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
@@ -66,10 +65,6 @@ async function init() {
IsSEQTAPage = true;
verboseInfo("[BetterSEQTA+] Verified SEQTA Page");
if (typeof window !== "undefined" && window === window.top) {
void browser.runtime.sendMessage({ type: "cloudSettingsPoll" }).catch(() => {});
}
registerFetchSeqtaAppLinkListener();
const documentLoadStyle = document.createElement("style");
@@ -112,10 +107,12 @@ async function init() {
}
await main();
const { init: Monofile } = await import("@/plugins/monofile");
Monofile();
if (settingsState.onoff) {
const { initializePlugins } = await import("@/plugins/runtime");
const { initializePlugins } = await import("@/plugins/index");
await initializePlugins();
}
+3 -3
View File
@@ -39,7 +39,7 @@ import {
clearGoogleCalendarState,
readGoogleCalendarState,
writeGoogleCalendarState,
} from "@/seqta/utils/googleCalendar/storage";
} from "@/seqta/utils/calendarSync/providerStorage";
import { ensureGoogleAppCalendar } from "@/seqta/utils/googleCalendar/calendarProvisioning";
import type {
GoogleCalendarStatus,
@@ -49,8 +49,8 @@ import {
clearOutlookCalendarState,
readOutlookCalendarState,
writeOutlookCalendarState,
} from "@/seqta/utils/outlookCalendar/storage";
import type { OutlookCalendarStatus } from "@/seqta/utils/outlookCalendar/storage";
type OutlookCalendarStatus,
} from "@/seqta/utils/calendarSync/providerStorage";
import browser from "webextension-polyfill";
const WEEKLY_PERIOD_MINUTES = 7 * 24 * 60;
+1
View File
@@ -473,5 +473,6 @@ export function initCloudSettingsAutoSync(deps: { reloadSeqtaPages: () => void }
if (autoSyncInitialized) return;
autoSyncInitialized = true;
browser.storage.onChanged.addListener(onStorageChanged);
void runCloudSettingsPoll();
}
@@ -2,7 +2,6 @@
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader';
import Spinner from '../Spinner.svelte';
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import { Index } from 'flexsearch';
import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
@@ -20,8 +19,6 @@
let savedBackgrounds = $state<string[]>([]);
let installingBackgrounds = $state<Set<string>>(new Set());
let debugInfo = $state<string>('');
let searchIndex = $state<Index | null>(null);
// New state variables
let activeTab = $state<'all' | 'installed' | 'photos' | 'videos'>('all');
let sortBy = $state<'newest' | 'popular' | 'name'>('newest');
@@ -36,19 +33,6 @@
}
const data = await response.json();
backgrounds = data.backgrounds;
// Initialize FlexSearch index
const index = new Index({
tokenize: "forward",
preset: "score"
});
// Add backgrounds to the index
backgrounds.forEach((bg, i) => {
index.add(i, bg.name + " " + bg.description);
});
searchIndex = index;
debugInfo = `Loaded ${backgrounds.length} backgrounds`;
await loadSavedBackgrounds();
} catch (e) {
@@ -79,10 +63,11 @@
let filteredBackgrounds = $derived((() => {
let filtered = backgrounds;
// Use FlexSearch if there's a search term
if (searchTerm.trim() && searchIndex) {
const results = searchIndex.search(searchTerm) as number[];
filtered = results.map(i => backgrounds[i]);
if (searchTerm.trim()) {
const q = searchTerm.trim().toLowerCase();
filtered = backgrounds.filter((bg) =>
`${bg.name} ${bg.description}`.toLowerCase().includes(q),
);
}
// Apply category filtering
+24 -119
View File
@@ -1,149 +1,55 @@
import { type DBSchema, type IDBPDatabase, openDB } from "idb";
import localforage from "localforage";
/**
* Defines the schema for the IndexedDB database used for storing background image data.
*
* @interface BackgroundDB
* @extends {DBSchema}
* @property {object} backgrounds - The object store for background images.
* @property {string} backgrounds.key - The type of the key for the object store (in this case, it's `id` as defined in `keyPath`).
* @property {object} backgrounds.value - The structure of the objects stored.
* @property {string} backgrounds.value.id - The unique identifier for the background image record.
* @property {string} backgrounds.value.type - The MIME type of the image (e.g., "image/png", "image/jpeg").
* @property {Blob} backgrounds.value.blob - The binary large object (Blob) containing the image data.
*/
interface BackgroundDB extends DBSchema {
backgrounds: {
key: string; // Corresponds to the 'id' property due to keyPath: "id"
value: {
id: string;
type: string;
blob: Blob;
};
};
type BackgroundRecord = { id: string; type: string; blob: Blob };
const store = localforage.createInstance({
name: "BackgroundDB",
storeName: "backgrounds",
});
export async function openDatabase(): Promise<typeof store> {
await store.ready();
return store;
}
let db: IDBPDatabase<BackgroundDB> | null = null;
/**
* Initializes and opens an IndexedDB connection or returns an existing one.
* If the database doesn't exist or needs an upgrade, the `upgrade` callback
* creates the 'backgrounds' object store with 'id' as the keyPath.
*
* @async
* @returns {Promise<IDBPDatabase<BackgroundDB>>} A promise that resolves with the database instance.
*/
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
if (db) return db;
db = await openDB<BackgroundDB>("BackgroundDB", 1, {
upgrade(db: IDBPDatabase<BackgroundDB>) {
db.createObjectStore("backgrounds", { keyPath: "id" });
},
export async function readAllData(): Promise<BackgroundRecord[]> {
await store.ready();
const items: BackgroundRecord[] = [];
await store.iterate<BackgroundRecord, void>((value) => {
if (value?.id && value.blob) items.push(value);
});
return db;
return items;
}
/**
* Retrieves all background image records from the 'backgrounds' object store in IndexedDB.
*
* @async
* @returns {Promise<Array<{id: string, type: string, blob: Blob}>>} A promise that resolves with an array of all background image records.
*/
export async function readAllData(): Promise<
Array<{ id: string; type: string; blob: Blob }>
> {
const db = await openDatabase();
return db.getAll("backgrounds");
}
/**
* Writes or updates a background image record in the 'backgrounds' object store.
* If a record with the given `id` already exists, it will be updated. Otherwise, a new record is created.
*
* @async
* @param {string} id - The unique identifier for the background image record.
* @param {string} type - The MIME type of the image (e.g., "image/png").
* @param {Blob} blob - The Blob object containing the image data.
* @returns {Promise<void>} A promise that resolves when the data has been successfully written.
*/
export async function writeData(
id: string,
type: string,
blob: Blob,
): Promise<void> {
const db = await openDatabase();
await db.put("backgrounds", { id, type, blob });
await store.setItem(id, { id, type, blob });
}
/**
* Deletes a background image record from the 'backgrounds' object store by its ID.
*
* @async
* @param {string} id - The unique identifier of the background image record to delete.
* @returns {Promise<void>} A promise that resolves when the data has been successfully deleted.
*/
export async function deleteData(id: string): Promise<void> {
const db = await openDatabase();
await db.delete("backgrounds", id);
await store.removeItem(id);
}
/**
* Clears all records from the 'backgrounds' object store in IndexedDB.
*
* @async
* @returns {Promise<void>} A promise that resolves when all data has been successfully cleared.
*/
export async function clearAllData(): Promise<void> {
const db = await openDatabase();
await db.clear("backgrounds");
await store.clear();
}
/**
* Retrieves a single background image record from the 'backgrounds' object store by its ID.
*
* @async
* @param {string} id - The unique identifier of the background image record to retrieve.
* @returns {Promise<{id: string, type: string, blob: Blob} | undefined>} A promise that resolves with the
* background image record if found, or undefined otherwise.
*/
export async function getDataById(
id: string,
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
const db = await openDatabase();
return db.get("backgrounds", id);
): Promise<BackgroundRecord | undefined> {
const item = await store.getItem<BackgroundRecord>(id);
return item ?? undefined;
}
/**
* Closes the active IndexedDB connection and nullifies the global `db` variable.
* This is important to release resources and allow for proper database management.
*/
export function closeDatabase(): void {
if (db) {
db.close();
db = null;
}
}
export function closeDatabase(): void {}
/**
* Checks if IndexedDB is supported by the current browser environment.
*
* @returns {boolean} True if IndexedDB is supported, false otherwise.
*/
export function isIndexedDBSupported(): boolean {
return "indexedDB" in window;
}
/**
* Estimates available storage space and checks if it's sufficient for the specified `requiredSpace`.
* Uses the `navigator.storage.estimate()` API if available.
* If the API is not available or cannot determine space, it defaults to assuming enough space is available.
*
* @async
* @param {number} requiredSpace - The amount of storage space required, in bytes.
* @returns {Promise<boolean>} A promise that resolves with true if enough space is estimated to be available, false otherwise.
*/
export async function hasEnoughStorageSpace(
requiredSpace: number,
): Promise<boolean> {
@@ -153,6 +59,5 @@ export async function hasEnoughStorageSpace(
return quota - usage > requiredSpace;
}
}
// If we can't determine, assume there's enough space
return true;
}
+11 -2
View File
@@ -1,11 +1,20 @@
import { mount } from "svelte";
import type { SvelteComponent } from "svelte";
import style from "./index.css?inline";
import settingsStyle from "./index.css?inline";
import contentShadowStyle from "./contentShadow.css?inline";
const shadowStyles = {
settings: settingsStyle,
content: contentShadowStyle,
} as const;
export type ShadowStyleVariant = keyof typeof shadowStyles;
export default function renderSvelte(
Component: SvelteComponent | any,
mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {},
shadowStyle: ShadowStyleVariant = "settings",
) {
const app = mount(Component, {
target: mountPoint,
@@ -17,7 +26,7 @@ export default function renderSvelte(
if (mountPoint instanceof ShadowRoot) {
const styleElement = document.createElement("style");
styleElement.textContent = style;
styleElement.textContent = shadowStyles[shadowStyle];
mountPoint.appendChild(styleElement);
}
-140
View File
@@ -12,7 +12,6 @@
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
//import { OpenMinecraftServerPopup } from "@/seqta/utils/Openers/OpenMinecraftServerPopup";
import type { Component } from "svelte";
import FontPickerModal from "../components/FontPickerModal.svelte";
@@ -87,11 +86,6 @@
closeExtensionPopup();
};
/* const openMinecraftServer = () => {
OpenMinecraftServerPopup();
closeExtensionPopup();
}; */
const openPrivacyStatement = () => {
window.open("https://betterseqta.org/privacy", "_blank");
closeExtensionPopup();
@@ -221,140 +215,6 @@
{"\uecba"}
</button>
</div>
<!-- <button
onclick={openMinecraftServer}
class="flex justify-center items-center p-1 w-8 h-8 rounded-xl bg-zinc-100 dark:bg-zinc-700"
aria-label="Open Minecraft Server"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 70"
fill="none"
class="w-full h-full"
>
<path
d="M0 0 C3.96 0 7.92 0 12 0 C12 3.96 12 7.92 12 12 C10.68 12 9.36 12 8 12 C8 10.68 8 9.36 8 8 C6.68 8 5.36 8 4 8 C4 6.68 4 5.36 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(42,10)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 6.6 4 13.2 4 20 C2.68 20 1.36 20 0 20 C0 13.4 0 6.8 0 0 Z "
fill="currentColor"
transform="translate(54,22)"
/>
<path
d="M0 0 C6.6 0 13.2 0 20 0 C20 1.32 20 2.64 20 4 C13.4 4 6.8 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(22,6)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 5.28 4 10.56 4 16 C2.68 16 1.36 16 0 16 C0 10.72 0 5.44 0 0 Z "
fill="currentColor"
transform="translate(46,26)"
/>
<path
d="M0 0 C5.28 0 10.56 0 16 0 C16 1.32 16 2.64 16 4 C10.72 4 5.44 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(22,14)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C5.32 4 6.64 4 8 4 C8 5.32 8 6.64 8 8 C5.36 8 2.72 8 0 8 C0 5.36 0 2.72 0 0 Z "
fill="currentColor"
transform="translate(6,50)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(14,50)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(18,46)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(10,46)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(50,42)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(22,42)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(14,42)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(26,38)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(18,38)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(30,34)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(22,34)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(34,30)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(26,30)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(38,26)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(30,26)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(42,22)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(34,22)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(38,18)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(18,10)"
/>
</svg>
</button> -->
</div>
{/if}
</div>
+1 -2
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
import { fade } from 'svelte/transition';
@@ -35,7 +34,7 @@
const themeManager = ThemeManager.getInstance();
let theme = $state<LoadedCustomTheme>({
id: uuidv4(),
id: crypto.randomUUID(),
name: '',
description: '',
defaultColour: 'blue',
-26
View File
@@ -1,26 +0,0 @@
import { mount } from "svelte";
import type { SvelteComponent } from "svelte";
import style from "./contentShadow.css?inline";
/** Mount Svelte UI inside a shadow root from content scripts (decoupled from settings popup CSS). */
export default function renderInShadow(
Component: SvelteComponent | any,
mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {},
) {
const app = mount(Component, {
target: mountPoint,
props: {
standalone: false,
...props,
},
});
if (mountPoint instanceof ShadowRoot) {
const styleElement = document.createElement("style");
styleElement.textContent = style;
mountPoint.appendChild(styleElement);
}
return app;
}
@@ -1,9 +1,3 @@
import { BasePlugin } from "@/plugins/core/settings";
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";
@@ -25,34 +19,12 @@ interface weightingsStorage {
weightingOverrides: Record<string, string>;
}
const settings = defineSettings({
lettergrade: booleanSetting({
default: false,
title: "Letter Grades",
description: "Display the average as a letter instead of a percentage",
}),
});
class AssessmentsAveragePluginClass extends BasePlugin<typeof settings> {
@Setting(settings.lettergrade)
lettergrade!: boolean;
}
const instance = new AssessmentsAveragePluginClass();
let overrideListenerController: AbortController | null = null;
let wrapperColourObserver: MutationObserver | null = null;
let wrapperColourObserverTimeout: ReturnType<typeof setTimeout> | null = null;
const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
id: "assessments-average",
name: "Assessment Averages",
description: "Adds an average grade to the Assessments page",
version: "1.0.0",
disableToggle: true,
settings: instance.settings,
run: async (api) => {
const assessmentsAveragePlugin = {
run: async (api: Parameters<NonNullable<Plugin["run"]>>[0]) => {
await initStorage(api);
clearStuck(api);
@@ -0,0 +1,20 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { booleanSetting, defineSettings } from "../../core/settingsHelpers";
const settings = defineSettings({
lettergrade: booleanSetting({
default: false,
title: "Letter Grades",
description: "Display the average as a letter instead of a percentage",
}),
});
export default defineLazyPlugin({
id: "assessments-average",
name: "Assessment Averages",
description: "Adds an average grade to the Assessments page",
version: "1.0.0",
disableToggle: true,
settings,
loader: () => import("./index"),
});
@@ -1,3 +1,5 @@
import debounce from "@/seqta/utils/debounce";
const RUBRIC_SELECTOR =
"[class*='AssessableCriterion__rubric___'][class*='Rubric__Rubric___'], [class*='Rubric__Rubric___'][class*='AssessableCriterion__rubric___']";
const ENHANCED_ATTR = "data-betterseqta-rubric-copy";
@@ -365,13 +367,13 @@ function enhanceRubrics(root: ParentNode = document) {
root.querySelectorAll<HTMLElement>(RUBRIC_SELECTOR).forEach(enhanceRubric);
}
const debouncedEnhanceRubrics = debounce(enhanceRubrics, 50);
function watchRubrics(root: ParentNode) {
observer?.disconnect();
enhanceRubrics(root);
observer = new MutationObserver(() => {
enhanceRubrics(root);
});
observer = new MutationObserver(() => debouncedEnhanceRubrics(root));
observer.observe(root, { childList: true, subtree: true });
}
@@ -15,8 +15,6 @@ import { extractWeightFromCoversheetText } from "./extractWeightFromCoversheetTe
export { extractWeightFromCoversheetText };
ensurePdfjsWorker();
export const WEIGHTING_SCHEMA_VERSION = 1;
export interface WeightingEntry {
@@ -655,6 +653,7 @@ export async function extractPDFText(url: string): Promise<string> {
throw new Error("PDF response is empty");
}
ensurePdfjsWorker();
const pdf = await pdfjs.getDocument({
data: arrayBuffer,
useSystemFonts: true,
@@ -53,7 +53,7 @@ const assessmentsOverviewPlugin: Plugin<{}> = {
"Adds an overview option to the assessments page that organizes assessments by status",
version: "1.0.0",
settings: {},
disableToggle: false,
disableToggle: true,
styles,
run: async () => {
@@ -304,7 +304,6 @@ const enhancedNavigationPlugin: Plugin<typeof settings> = {
injectStyles();
window.addEventListener("resize", positionArrows);
window.addEventListener("scroll", positionArrows, true);
const navObservers: MutationObserver[] = [];
const courseObservers: MutationObserver[] = [];
@@ -355,7 +354,6 @@ const enhancedNavigationPlugin: Plugin<typeof settings> = {
return () => {
window.removeEventListener("resize", positionArrows);
window.removeEventListener("scroll", positionArrows, true);
courseMount.unregister();
navObservers.forEach((observer) => observer.disconnect());
courseObservers.forEach((observer) => observer.disconnect());
@@ -0,0 +1,23 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { booleanSetting, defineSettings } from "../../core/settingsHelpers";
const settings = defineSettings({
autoScrollOnClick: booleanSetting({
default: false,
title: "Auto-scroll navigator on click",
description:
"When you click a lesson directly in the side panel, automatically scroll it to the centre. The prev/next arrows always centre the selected lesson regardless of this setting.",
}),
});
export default defineLazyPlugin({
id: "enhanced-navigation",
name: "Enhanced Navigation",
description:
"Keeps the course navigator focused on the current lesson and adds prev/next lesson arrows.",
version: "1.0.0",
disableToggle: true,
settings,
beta: false,
loader: () => import("./index"),
});
@@ -10,10 +10,11 @@
import Calculator from './Calculator.svelte';
import { actionMap } from '../indexing/actions';
import type { IndexItem } from '../indexing/types';
import debounce from 'lodash/debounce';
import debounce from '@/seqta/utils/debounce';
import { renderComponentMap } from '../indexing/renderComponents';
import HighlightedText from '../utils/HighlightedText.svelte';
import { matchesHotkey } from '../utils/hotkeyUtils';
import { warmUpVectorSearchOnInteraction } from '../search/vector/vectorSearch';
import browser from 'webextension-polyfill';
const {
@@ -93,6 +94,7 @@
keydownHandler = (e: KeyboardEvent) => {
if (matchesHotkey(e, currentSearchHotkey)) {
e.preventDefault();
warmUpVectorSearchOnInteraction();
commandPalleteOpen = true;
tick().then(() => searchbar?.focus());
}
@@ -146,6 +148,7 @@
// @ts-ignore - Intentionally adding to window
window.setCommandPalleteOpen = (open: boolean) => {
if (open) warmUpVectorSearchOnInteraction();
commandPalleteOpen = open;
};
@@ -5,10 +5,8 @@ import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing, ensureSchemaCurrent } from "../indexing/indexer";
import { installResetIndexMessageListener } from "../indexing/resetIndexes";
import { isIndexingPaused } from "../indexing/indexingPause";
import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from "embeddia";
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
import { checkAndHandleUpdate } from "../utils/versionCheck";
import {
getStoredPassiveItems,
@@ -68,20 +66,10 @@ const globalSearchPlugin: Plugin<{}> = {
console.error("Failed to create IndexedDB:", error);
}
initVectorSearch();
setTimeout(async () => {
try {
const { isVectorSearchSupported } = await import("../utils/browserDetection");
if (isVectorSearchSupported()) VectorWorkerManager.getInstance();
} catch (error) {
console.warn("[Global Search] Vector worker warm-up failed:", error);
}
}, 1000);
// @ts-ignore
window.globalSearchDebug = {
resetWorker: () => VectorWorkerManager.getInstance().resetWorker(),
resetWorker: async () =>
(await import("../indexing/worker/vectorWorkerManager")).VectorWorkerManager.getInstance().resetWorker(),
passiveItems: getStoredPassiveItems,
runSelfTests: async () =>
(await import("../indexing/selfTests")).runGlobalSearchSelfTests(),
@@ -1,6 +1,6 @@
import SearchBar from "../components/SearchBar.svelte";
import { unmount } from "svelte";
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
import { warmUpVectorSearchOnInteraction } from "../search/vector/vectorSearch";
import { formatHotkeyForDisplay, isValidHotkey } from "../utils/hotkeyUtils";
import browser from "webextension-polyfill";
@@ -253,17 +253,18 @@ export async function mountSearchBar(
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
searchButton.addEventListener("click", () => {
warmUpVectorSearchOnInteraction();
// @ts-ignore - Intentionally adding to window
window.setCommandPalleteOpen(true);
});
try {
const { default: renderSvelte } = await import("@/interface/renderInShadow");
const { default: renderSvelte } = await import("@/interface/main");
appRef.current = renderSvelte(SearchBar, searchRootShadow, {
transparencyEffects: api.settings.transparencyEffects,
showRecentFirst: api.settings.showRecentFirst,
searchHotkey: currentHotkey,
});
}, "content");
} catch (error) {
console.error("Error rendering Svelte component:", error);
}
@@ -314,8 +315,10 @@ export function cleanupSearchBar(appRef: {
searchRoot.remove();
}
// Clean up vector worker
// Clean up vector worker when it was started (indexing or search interaction)
void import("../indexing/worker/vectorWorkerManager").then(({ VectorWorkerManager }) => {
VectorWorkerManager.getInstance().terminate();
}).catch(() => {});
if (appRef.storageChangeHandler) {
browser.storage.onChanged.removeListener(appRef.storageChangeHandler);
@@ -8,6 +8,13 @@ import { verboseDebug } from "@/utils/verboseLog";
let vectorIndex: EmbeddingIndex | null = null;
let initializationAttempted = false;
let initializationFailed = false;
let interactionWarmupStarted = false;
export function warmUpVectorSearchOnInteraction(): void {
if (interactionWarmupStarted) return;
interactionWarmupStarted = true;
void initVectorSearch();
}
export async function initVectorSearch() {
if (initializationFailed || !isVectorSearchSupported()) {
@@ -1,4 +1,10 @@
import { create, all, typeOf as mathTypeOf, format as mathFormat } from 'mathjs';
import {
create, absDependencies, addDependencies, cosDependencies, divideDependencies, eDependencies,
evaluateDependencies, formatDependencies, log10Dependencies, logDependencies, modDependencies,
multiplyDependencies, piDependencies, powDependencies, sinDependencies, sqrtDependencies,
subtractDependencies, tanDependencies, toDependencies, typeOfDependencies, unaryMinusDependencies,
unaryPlusDependencies, unitDependencies,
} from 'mathjs';
import { unitFullNames } from './unitMap';
export interface CalculatorResult {
@@ -10,38 +16,34 @@ export interface CalculatorResult {
error?: string;
}
/** Hard cap on calculator input length to limit parse/eval cost. */
export const CALCULATOR_MAX_INPUT_LENGTH = 128;
/**
* Functions safe to replace with stubs. Do not block type constructors
* (`complex`, `typed`, `fraction`, `bignumber`, `sparse`) or parse pipeline
* (`parse`, `compile`, `parser`) — mathjs needs those internally and
* `evaluate()` depends on them.
*/
const BLOCKED_MATH_FUNCTIONS = [
'import',
'createUnit',
'random',
'pickRandom',
'chain',
'help',
] as const;
const CALCULATOR_MATH_CONFIG = Object.assign(
{},
evaluateDependencies, formatDependencies, unitDependencies, typeOfDependencies,
addDependencies, subtractDependencies, multiplyDependencies, divideDependencies,
powDependencies, modDependencies, unaryMinusDependencies, unaryPlusDependencies,
absDependencies, sqrtDependencies, logDependencies, log10Dependencies,
sinDependencies, cosDependencies, tanDependencies, toDependencies,
piDependencies, eDependencies,
);
const BLOCKED_MATH_FUNCTIONS = ['import', 'createUnit', 'random', 'pickRandom', 'chain', 'help'] as const;
function createSandboxedMath() {
const sandbox = create(all);
const blockFn = () => {
throw new Error('Function not allowed');
};
const sandbox = create(CALCULATOR_MATH_CONFIG);
const blockFn = () => { throw new Error('Function not allowed'); };
const blocked: Record<string, () => never> = {};
for (const name of BLOCKED_MATH_FUNCTIONS) {
blocked[name] = blockFn;
}
for (const name of BLOCKED_MATH_FUNCTIONS) blocked[name] = blockFn;
sandbox.import(blocked, { override: true });
return sandbox;
}
const calculatorMath = createSandboxedMath();
const FORMAT_OPTS = { precision: 14, lowerExp: -15, upperExp: 15 } as const;
const emptyResult = (error?: string): CalculatorResult => ({
result: null, isValid: false, isPartial: false, inputUnit: '', outputUnit: '', error,
});
function detectUnit(expression: string): string {
try {
@@ -50,161 +52,63 @@ function detectUnit(expression: string): string {
const unitStr = unit.formatUnits();
return unitFullNames[unitStr] || unitStr;
}
} catch (e) {
// Not a unit or invalid expression
}
} catch {}
return '';
}
function isLikelyMathExpression(input: string): boolean {
const trimmed = input.trim();
// Must contain at least one digit or mathematical operator
if (!/[\d+\-*/^()=.]/.test(trimmed)) {
return false;
}
// Check for common non-math words that shouldn't trigger calculator
if (!/[\d+\-*/^()=.]/.test(trimmed)) return false;
const nonMathWords = ['abs', 'function', 'class', 'const', 'let', 'var', 'if', 'else', 'while', 'for', 'return', 'import', 'export'];
const words = trimmed.toLowerCase().split(/\s+/);
// If it's just a single non-math word, skip it
if (words.length === 1 && nonMathWords.includes(words[0]) && !/\d/.test(trimmed)) {
return false;
}
// Must have some mathematical content
const mathPattern = /(\d+\.?\d*|\+|\-|\*|\/|\^|\(|\)|sin|cos|tan|log|sqrt|pi|e|=)/i;
return mathPattern.test(trimmed);
if (words.length === 1 && nonMathWords.includes(words[0]) && !/\d/.test(trimmed)) return false;
return /(\d+\.?\d*|\+|\-|\*|\/|\^|\(|\)|sin|cos|tan|log|sqrt|pi|e|=)/i.test(trimmed);
}
function tryCompleteExpression(expression: string): string | null {
const trimmed = expression.trim();
// Common patterns for incomplete expressions
const incompletePatterns = [
/[\+\-\*\/\^]\s*$/, // ends with operator
/\(\s*$/, // ends with opening parenthesis
/[\+\-\*\/\^]\s*\(/, // operator followed by opening parenthesis
];
for (const pattern of incompletePatterns) {
if (pattern.test(trimmed)) {
// Try to evaluate what we have so far by removing the incomplete part
let partial = trimmed.replace(/[\+\-\*\/\^]\s*$/, '').trim();
// Handle cases like "4 + 3 *" -> evaluate "4 + 3"
if (partial && !partial.match(/[\+\-\*\/\^]\s*$/)) {
for (const pattern of [/[\+\-\*\/\^]\s*$/, /\(\s*$/, /[\+\-\*\/\^]\s*\(/]) {
if (!pattern.test(trimmed)) continue;
const partial = trimmed.replace(/[\+\-\*\/\^]\s*$/, '').trim();
if (!partial || partial.match(/[\+\-\*\/\^]\s*$/)) continue;
try {
const result = calculatorMath.evaluate(partial);
if (typeof result === 'number' && !isNaN(result)) {
return calculatorMath.format(result, { precision: 14, lowerExp: -15, upperExp: 15 });
return calculatorMath.format(result, FORMAT_OPTS);
}
} catch (e) {
// Continue to other attempts
} catch {}
}
}
}
}
return null;
}
export function calculateExpression(input: string): CalculatorResult {
const trimmed = input.trim();
// Early exit for empty or very short inputs
if (!trimmed || (trimmed.length <= 2 && !/\d/.test(trimmed))) {
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
};
}
if (!trimmed || (trimmed.length <= 2 && !/\d/.test(trimmed))) return emptyResult();
if (trimmed.length > CALCULATOR_MAX_INPUT_LENGTH) {
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
error: `Expression too long (max ${CALCULATOR_MAX_INPUT_LENGTH} characters)`,
};
}
// Check if this looks like a math expression at all
if (!isLikelyMathExpression(trimmed)) {
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
};
return emptyResult(`Expression too long (max ${CALCULATOR_MAX_INPUT_LENGTH} characters)`);
}
if (!isLikelyMathExpression(trimmed)) return emptyResult();
try {
// First try to evaluate the expression as-is
const evaluated = calculatorMath.evaluate(trimmed.replace('**', '^'));
if (evaluated !== undefined) {
let result: string;
let inputUnit = '';
let outputUnit = '';
if (mathTypeOf(evaluated) === 'Unit') {
// Handle unit conversion results
result = calculatorMath.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
inputUnit = detectUnit(trimmed);
outputUnit = detectUnit(result);
} else if (typeof evaluated === 'number') {
// Handle regular numbers
result = mathFormat(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
} else {
result = mathFormat(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
}
const result = calculatorMath.format(evaluated, FORMAT_OPTS);
const isUnit = calculatorMath.typeOf(evaluated) === 'Unit';
return {
result,
isValid: true,
isPartial: false,
inputUnit,
outputUnit,
inputUnit: isUnit ? detectUnit(trimmed) : '',
outputUnit: isUnit ? detectUnit(result) : '',
};
}
} catch (error) {
// Try to handle incomplete expressions
const partialResult = tryCompleteExpression(trimmed);
if (partialResult) {
return {
result: partialResult,
isValid: true,
isPartial: true,
inputUnit: '',
outputUnit: '',
};
return { result: partialResult, isValid: true, isPartial: true, inputUnit: '', outputUnit: '' };
}
return emptyResult(error instanceof Error ? error.message : 'Invalid expression');
}
// If it still looks like math but failed, return the error
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
error: error instanceof Error ? error.message : 'Invalid expression',
};
}
return {
result: null,
isValid: false,
isPartial: false,
inputUnit: '',
outputUnit: '',
};
return emptyResult();
}
+33 -107
View File
@@ -1,22 +1,5 @@
import type { Plugin } from "../../core/types";
import { booleanSetting } from "@/plugins/core/settingsHelpers";
import { waitForElm } from "@/seqta/utils/waitForElm";
import styles from "./styles.css?inline";
const messageFoldersSettings = {
showTagsInAllMessages: booleanSetting({
default: true,
title: "Show folder tags in All Messages",
description:
"When off, folder tags are not shown on the message list until you select a folder.",
}),
hideFolderedMessagesInAll: booleanSetting({
default: true,
title: "Hide foldered messages in All Messages",
description:
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
}),
} as const;
interface Folder {
id: string;
@@ -25,33 +8,31 @@ interface Folder {
emoji: string;
}
interface MessageFoldersStorage {
folders: Folder[];
messageAssignments: Record<string, string[]>;
}
const FOLDER_COLORS = [
"#3b82f6", "#ef4444", "#22c55e", "#f59e0b",
"#8b5cf6", "#ec4899", "#14b8a6", "#f97316",
];
const folderHeroicon = (inner: string) =>
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
const FOLDER_HEROICONS = [
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
`<svg style="width:16px;height:16px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>`,
folderHeroicon('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>'),
folderHeroicon('<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>'),
folderHeroicon('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>'),
folderHeroicon('<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>'),
folderHeroicon('<path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/>'),
folderHeroicon('<path d="M9 18h6"/><path d="M10 22h4"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0 0 18 8 6 6 0 0 0 6 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 0 1 8.91 14"/>'),
folderHeroicon('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>'),
folderHeroicon('<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>'),
folderHeroicon('<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>'),
folderHeroicon('<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>'),
folderHeroicon('<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>'),
folderHeroicon('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'),
folderHeroicon('<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>'),
folderHeroicon('<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>'),
folderHeroicon('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'),
folderHeroicon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
];
const FOLDER_ICON_SVG = `<svg style="width:24px;height:24px;flex-shrink:0" viewBox="0 0 24 24"><path fill="#888" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
@@ -177,20 +158,8 @@ function createFolderBadge(
return badge;
}
const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFoldersStorage> = {
id: "messageFolders",
name: "Message Folders",
description: "Organize direct messages into custom folders",
version: "2.0.0",
settings: messageFoldersSettings,
disableToggle: true,
defaultEnabled: true,
run: async (api) => {
const styleEl = document.createElement("style");
styleEl.textContent = styles;
document.head.appendChild(styleEl);
const messageFoldersPlugin = {
run: async (api: Parameters<NonNullable<Plugin["run"]>>[0]) => {
await api.storage.loaded;
if (!api.storage.folders) api.storage.folders = [];
@@ -202,7 +171,6 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
let actionsObserver: MutationObserver | null = null;
let openDropdown: HTMLElement | null = null;
let dropdownCloseHandler: ((e: MouseEvent) => void) | null = null;
let foldedSection: HTMLElement | null = null;
const unregisters: Array<{ unregister: () => void }> = [];
const getFolders = (): Folder[] =>
@@ -217,23 +185,11 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
api.storage.messageAssignments = { ...assignments };
};
const getMessageFolderIds = (messageId: string): string[] => {
const assignments = getAssignments();
const ids: string[] = [];
for (const [folderId, msgIds] of Object.entries(assignments)) {
if (msgIds.includes(messageId)) ids.push(folderId);
}
return ids;
};
const assignMessageToFolder = (messageId: string, folderId: string, add: boolean) => {
const assignMessageToFolder = (messageId: string, folderId: string) => {
const assignments = getAssignments();
if (!assignments[folderId]) assignments[folderId] = [];
const idx = assignments[folderId].indexOf(messageId);
if (add && idx < 0) {
if (!assignments[folderId].includes(messageId)) {
assignments[folderId].push(messageId);
} else if (!add && idx >= 0) {
assignments[folderId].splice(idx, 1);
}
saveAssignments(assignments);
};
@@ -262,36 +218,13 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
}
};
const isMessageInAnyCustomFolder = (messageId: string): boolean => {
for (const msgIds of Object.values(getAssignments())) {
if (msgIds.includes(messageId)) return true;
}
return false;
};
const isMessageInAnyCustomFolder = (messageId: string): boolean =>
getAssignedFolderIds(messageId, getAssignments()).length > 0;
const shouldShowBadgesInList = (): boolean => {
return api.settings.showTagsInAllMessages || activeFolderId !== null;
};
const getSelectedMessageId = (): string | null => {
const selectedMsg = document.querySelector("[class*='MessageList__selected___']");
return selectedMsg?.getAttribute("data-message") ?? null;
};
const getMessageIdFromEvent = (target: HTMLElement): string | null => {
const li = target.closest("li[data-message]");
return li?.getAttribute("data-message") ?? null;
};
const getAllVisibleMessageIds = (): string[] => {
const ids: string[] = [];
document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => {
const id = li.getAttribute("data-message");
if (id) ids.push(id);
});
return ids;
};
const showConfirmModal = (title: string, message: string, onConfirm: () => void) => {
const overlay = document.createElement("div");
overlay.className = "bsplus-modal-overlay";
@@ -338,13 +271,11 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
ol.appendChild(section);
}
foldedSection = section;
const folders = getFolders();
section.innerHTML = "";
const header = document.createElement("div");
header.className = "bsplus-folders-header";
header.dataset.folded = "false";
const collapseBtn = document.createElement("button");
collapseBtn.className = "bsplus-folders-collapse";
@@ -426,7 +357,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
editBtn.innerHTML = EDIT_SVG;
editBtn.addEventListener("click", (e) => {
e.stopPropagation();
showEditFolderInput(section!, folder);
showNewFolderInput(section!, folder);
});
actions.appendChild(editBtn);
@@ -518,7 +449,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
const messageId = data.replace("msg:", "");
const folderId = (e.target as HTMLElement).closest("[data-folder-id]")?.getAttribute("data-folder-id");
if (messageId && folderId) {
assignMessageToFolder(messageId, folderId, true);
assignMessageToFolder(messageId, folderId);
applyBadges();
applyFolderFilter();
renderSidebarFolders();
@@ -530,7 +461,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
};
const attachDragListeners = () => {
document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => {
getMessageListItems().forEach((li) => {
if (li.getAttribute("data-bsplus-drag") === "true") return;
li.setAttribute("data-bsplus-drag", "true");
li.draggable = true;
@@ -665,10 +596,6 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
container.appendChild(picker);
};
const showEditFolderInput = (container: Element, folder: Folder) => {
showNewFolderInput(container, folder);
};
const attachNativeSidebarListeners = () => {
const sidebar = document.querySelector("[class*='Viewer__sidebar___']");
if (!sidebar) return;
@@ -707,7 +634,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
dropdown.dataset.msgId = messageId;
const folders = getFolders();
const currentFolderIds = getMessageFolderIds(messageId);
const currentFolderIds = getAssignedFolderIds(messageId, getAssignments());
if (folders.length === 0) {
const empty = document.createElement("div");
@@ -747,7 +674,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
e.stopPropagation();
e.preventDefault();
toggleMessageInFolder(messageId, folder.id);
const nowChecked = getMessageFolderIds(messageId).includes(folder.id);
const nowChecked = getAssignedFolderIds(messageId, getAssignments()).includes(folder.id);
item.classList.toggle("bsplus-checked", nowChecked);
check.style.borderColor = nowChecked ? folder.color : "";
check.style.background = nowChecked ? folder.color : "";
@@ -819,7 +746,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
menu.appendChild(title);
const folders = getFolders();
const currentFolderIds = getMessageFolderIds(messageId);
const currentFolderIds = getAssignedFolderIds(messageId, getAssignments());
if (folders.length === 0) {
const empty = document.createElement("div");
@@ -907,7 +834,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
};
const applyFolderFilter = () => {
const messageItems = document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]");
const messageItems = getMessageListItems();
const moreBtn = document.querySelector("[class*='MessageList__MessageList___'] ol > button");
if (activeFolderId === null) {
if (api.settings.hideFolderedMessagesInAll) {
@@ -952,7 +879,7 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
};
const attachContextMenuListeners = () => {
document.querySelectorAll("[class*='MessageList__MessageList___'] ol > li[data-message]").forEach((li) => {
getMessageListItems().forEach((li) => {
if (li.getAttribute("data-bsplus-ctx") === "true") return;
li.setAttribute("data-bsplus-ctx", "true");
li.addEventListener("contextmenu", (e) => {
@@ -1022,7 +949,6 @@ const messageFoldersPlugin: Plugin<typeof messageFoldersSettings, MessageFolders
sidebarObserver?.disconnect();
actionsObserver?.disconnect();
closeDropdown();
styleEl.remove();
document.querySelectorAll(".bsplus-folders-section").forEach((el) => el.remove());
document.querySelectorAll(".bsplus-folder-btn").forEach((el) => el.remove());
document.querySelectorAll(".bsplus-msg-badges").forEach((el) => el.remove());
@@ -0,0 +1,30 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import { booleanSetting, defineSettings } from "../../core/settingsHelpers";
import styles from "./styles.css?inline";
const settings = defineSettings({
showTagsInAllMessages: booleanSetting({
default: true,
title: "Show folder tags in All Messages",
description:
"When off, folder tags are not shown on the message list until you select a folder.",
}),
hideFolderedMessagesInAll: booleanSetting({
default: true,
title: "Hide foldered messages in All Messages",
description:
"When on, messages assigned to a custom folder are hidden from the inbox until you open that folder.",
}),
});
export default defineLazyPlugin({
id: "messageFolders",
name: "Message Folders",
description: "Organize direct messages into custom folders",
version: "2.0.0",
settings,
disableToggle: true,
defaultEnabled: true,
styles,
loader: () => import("./index"),
});
-57
View File
@@ -1,57 +0,0 @@
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({
someSetting: booleanSetting({
default: true,
title: "Test Plugin",
description: "Some random setting",
}),
});
// Step 2: Create the plugin class with @Setting decorators
class TestPluginClass extends BasePlugin<typeof settings> {
@Setting(settings.someSetting)
someSetting!: boolean;
}
// Step 3: Instantiate and plug it in
const settingsInstance = new TestPluginClass();
const testPlugin: Plugin<typeof settings> = {
id: "test",
name: "Test Plugin",
description: "A test plugin for BetterSEQTA+",
version: "1.0.0",
settings: settingsInstance.settings,
disableToggle: true,
beta: true,
run: async (api) => {
console.log("Test plugin running");
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);
console.log("Current setting value:", api.settings.someSetting);
});
return () => {
console.log("Test plugin stopped");
unregister();
};
},
};
export default testPlugin;
@@ -67,7 +67,6 @@ export type InstallThemeMeta = {
export class ThemeManager {
private static instance: ThemeManager;
private currentTheme: CustomTheme | null = null;
private previousImageVariableNames: string[] = [];
private lastSyncedImageKey: string | null = null;
private originalPreviewColor: string | null = null;
private originalPreviewTheme: boolean | null = null;
@@ -914,9 +913,6 @@ export class ThemeManager {
images: CustomImages,
});
this.lastSyncedImageKey = this.imageSyncKey(CustomImages);
this.previousImageVariableNames = CustomImages.map(
(image) => image.variableName,
);
// Apply theme settings
if (shouldForceThemeAppearance(theme)) {
@@ -951,9 +947,6 @@ export class ThemeManager {
}
}
const newImageVariableNames =
theme.CustomImages?.map((image) => image.variableName) ?? [];
const syncInput: ThemePageSyncInput = {};
if (theme.CustomCSS !== undefined) {
@@ -966,7 +959,6 @@ export class ThemeManager {
syncInput.images = theme.CustomImages;
this.lastSyncedImageKey = imageKey;
}
this.previousImageVariableNames = newImageVariableNames;
}
if (Object.keys(syncInput).length > 0) {
@@ -1009,7 +1001,6 @@ export class ThemeManager {
try {
void syncThemeToPage({ clearPreview: true, images: [] });
this.lastSyncedImageKey = null;
this.previousImageVariableNames = [];
clearCustomThemeAdaptiveCssVariables();
+5 -41
View File
@@ -35,18 +35,9 @@ const DEFAULT_INTERVAL_MS = 600_000;
/**
* IDs of decorative city layers injected when `themeDom.cityLayers` is on.
* Order matters: earlier entries paint behind later ones. Buildings sit
* behind the lit batches (so windows draw over the silhouettes); flicker
* frames sit over the lit batches so blinking windows hide steady ones.
* Sun and moon are last so they paint over everything else in the
* wallpaper stack (still behind #main via z-index 0).
* `city-buildings` paints the night panorama; `city-day` paints the day
* panorama on top with opacity controlled by `--city-day-opacity`.
*/
/**
* `city-buildings` always paints the night panorama; `city-day` paints
* the day panorama on top with opacity controlled by `--city-day-
* opacity`. The two stack so we can CSS-transition opacity between
* them at the day boundary, instead of snapping background-image (which
* doesn't animate). */
const CITY_LAYER_IDS = [
"city-buildings",
"city-day",
@@ -61,7 +52,7 @@ const CITY_LAYER_IDS = [
// Built-in functions themes may reference by exact string match.
const BUILTINS: Record<string, () => void> = {
"setTimeState()": setTimeState,
"setTimeState()": setCityTime,
"setCityTime()": setCityTime,
};
@@ -137,10 +128,7 @@ function readDevOverride(): number | null {
* 19:00 .. 21:00 evening (deep indigo, fading toward night)
* 21:00 .. 24:00 night
*
* The discrete bucket here drives `data-city-state` (used by the car
* sprite swap and the day panorama). The continuous sky-colour lerp in
* `TIME_BOUNDARIES` MUST use the same minute markers so the boundary
* the user sees in the dev slider matches the visible colour change.
* The discrete bucket here drives `data-city-state` (car sprites, day panorama).
*/
function timeStateForMinutes(minutes: number): TimeState {
if (minutes < 5 * 60 + 30) return "night";
@@ -165,19 +153,6 @@ function clamp01(value: number): number {
return Math.max(0, Math.min(1, value));
}
/**
* Sky colour publishing is intentionally NOT time-interpolated. We
* publish exactly one of the five `SKY_COLOR` anchors based on the
* discrete state from `timeStateForMinutes()`, and let CSS handle the
* cross-fade via its own `transition: background-color` on #content.
*
* That way the sky is a flat colour for the entire duration of each
* phase, and only animates between two anchors AT THE MOMENT the state
* boundary is crossed. The animation duration is decoupled from how
* long the phase lasts.
*
* Removed: the previous `lerpColor` + `TIME_BOUNDARIES` minute table.
*/
function skyColorForState(state: TimeState): string {
return SKY_COLOR[state];
}
@@ -247,18 +222,7 @@ function formatMinutes(minutes: number): string {
}
/**
* Read the clock and update `data-city-state` + sky colour on <html>. Only
* writes when the state actually changes, so CSS transitions are driven by
* real state changes rather than every tick.
*/
export function setTimeState(): void {
setCityTime();
}
/**
* Read the clock and publish continuous city variables plus the discrete
* `data-city-state` bucket used by CSS that still needs hard cuts (e.g. day
* panorama swap, night car sprites).
* Read the clock and publish city variables plus `data-city-state`.
*/
export function setCityTime(): void {
const minutes = getMinutesOfDay();
@@ -2,24 +2,30 @@
import { onMount } from "svelte";
import type { Snippet } from "svelte";
import { fade, fly } from "svelte/transition";
import browser from "webextension-polyfill";
import {
GOOGLE_CALENDAR_SYNC_WEEKS_MAX,
GOOGLE_CALENDAR_SYNC_WEEKS_MIN,
} from "@/config/googleCalendar";
import { maybeRunDueWeeklySync } from "@/seqta/utils/googleCalendar/calendarSyncListener";
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
import {
deleteSyncedEventsFromGoogleCalendar,
deleteSyncedEventsFromOutlookCalendar,
} from "@/seqta/utils/calendarSync/syncEngine";
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
import { runGoogleCalendarSync, runOutlookCalendarSync } from "@/seqta/utils/calendarSync/syncRunner";
import {
connectCalendarProvider,
disconnectCalendarProvider,
fetchCalendarStatuses,
getCalendarAccessToken,
runGoogleCalendarSync,
runOutlookCalendarSync,
updateGoogleSyncSettings,
} from "@/seqta/utils/calendarSync/syncRunner";
import type {
GoogleCalendarStatus,
GoogleCalendarSyncProgress,
GoogleCalendarSyncResult,
} from "@/seqta/utils/googleCalendar/types";
import type { OutlookCalendarStatus } from "@/seqta/utils/outlookCalendar/storage";
import type { OutlookCalendarStatus } from "@/seqta/utils/calendarSync/providerStorage";
import OutlookCalendarIcon from "./OutlookCalendarIcon.svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
@@ -31,10 +37,11 @@
syncProgressPercent,
type CalendarProvider,
} from "./calendarSyncUi";
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
type BusyState = { provider: CalendarProvider; phase: BusyPhase } | null;
type ProviderStatus = { configured: boolean; connected: boolean; lastSyncAt?: number };
type BusyPhase = "connect" | "sync" | "delete" | "disconnect" | null;
type BusyState = { provider: CalendarProvider; phase: BusyPhase } | null;
function setProviderStatus(provider: CalendarProvider, patch: Partial<ProviderStatus>) {
if (provider === "google") googleStatus = { ...googleStatus, ...patch };
else outlookStatus = { ...outlookStatus, ...patch };
@@ -128,30 +135,13 @@
}
async function refreshStatus() {
const [google, outlook] = await Promise.all([
browser.runtime.sendMessage({ type: "googleCalendarStatus" }) as Promise<GoogleCalendarStatus>,
browser.runtime.sendMessage({ type: "outlookCalendarStatus" }) as Promise<OutlookCalendarStatus>,
]);
const { google, outlook } = await fetchCalendarStatuses();
googleStatus = google;
outlookStatus = outlook;
syncWeeksAhead = google.syncWeeksAhead ?? 12;
autoSyncWeekly = google.autoSyncWeekly !== false;
}
async function getAccessToken(provider: CalendarProvider): Promise<string> {
const messageType =
provider === "google" ? "googleCalendarGetAccessToken" : "outlookCalendarGetAccessToken";
const res = (await browser.runtime.sendMessage({ type: messageType })) as {
success?: boolean;
accessToken?: string;
error?: string;
};
if (!res?.success || !res.accessToken) {
throw new Error(res?.error ?? "Could not get calendar access token.");
}
return res.accessToken;
}
function handleSyncProgress(progress: GoogleCalendarSyncProgress) {
syncProgress = progress;
}
@@ -160,10 +150,7 @@
syncWeeksAhead?: number;
autoSyncWeekly?: boolean;
}) {
const result = (await browser.runtime.sendMessage({
type: "googleCalendarUpdateSyncSettings",
...patch,
})) as GoogleCalendarStatus & { success?: boolean };
const result = await updateGoogleSyncSettings(patch);
if (result.syncWeeksAhead != null) syncWeeksAhead = result.syncWeeksAhead;
if (result.autoSyncWeekly != null) autoSyncWeekly = result.autoSyncWeekly;
googleStatus = { ...googleStatus, ...result };
@@ -174,12 +161,6 @@
mode: "full" | "incremental" = "full",
): Promise<boolean> {
const run = provider === "google" ? runGoogleCalendarSync : runOutlookCalendarSync;
const format = (result: GoogleCalendarSyncResult) =>
formatLessonSyncResultMessage(
result,
`${calendarProviderLabel(provider)} Calendar`,
);
const result = await run({ mode, onProgress: handleSyncProgress });
syncProgress = null;
@@ -193,7 +174,9 @@
lastSyncAt: result.lastSyncAt ?? providerStatus(provider).lastSyncAt,
});
showToastMessage(format(result));
showToastMessage(
formatLessonSyncResultMessage(result, `${calendarProviderLabel(provider)} Calendar`),
);
return true;
}
@@ -202,12 +185,8 @@
if (!status.configured || isBusy) return;
menuOpen = false;
busy = { provider, phase: "connect" };
const connectType =
provider === "google" ? "googleCalendarConnect" : "outlookCalendarConnect";
try {
const result = (await browser.runtime.sendMessage({
type: connectType,
})) as GoogleCalendarSyncResult;
const result = await connectCalendarProvider(provider);
if (!result.success) {
showToastMessage(
result.error ?? `Could not connect to ${calendarProviderLabel(provider)} Calendar.`,
@@ -225,7 +204,6 @@
busy = null;
}
}
async function syncProvider(provider: CalendarProvider) {
const status = providerStatus(provider);
if (!status.configured || isBusy) return;
@@ -263,7 +241,7 @@
provider === "google"
? deleteSyncedEventsFromGoogleCalendar
: deleteSyncedEventsFromOutlookCalendar;
const result = await deleteFn(location.origin, () => getAccessToken(provider), {
const result = await deleteFn(location.origin, () => getCalendarAccessToken(provider), {
onProgress: handleSyncProgress,
});
@@ -275,11 +253,11 @@
const removed = result.deleted ?? 0;
modalProvider = null;
const label = calendarProviderLabel(provider);
if (removed === 0) {
showToastMessage("No synced events to remove.");
} else {
showToastMessage(`Removed ${removed} event${removed === 1 ? "" : "s"} from ${label} Calendar.`);
}
showToastMessage(
removed === 0
? "No synced events to remove."
: `Removed ${removed} event${removed === 1 ? "" : "s"} from ${label} Calendar.`,
);
} catch (err) {
showToastMessage(err instanceof Error ? err.message : "Remove failed.", true);
} finally {
@@ -304,13 +282,9 @@
if (isBusy || !modalProvider) return;
const provider = modalProvider;
busy = { provider, phase: "disconnect" };
const disconnectType =
provider === "google" ? "googleCalendarDisconnect" : "outlookCalendarDisconnect";
const label = calendarProviderLabel(provider);
try {
const result = (await browser.runtime.sendMessage({
type: disconnectType,
})) as { success?: boolean };
const result = await disconnectCalendarProvider(provider);
if (!result?.success) {
showToastMessage(`Could not disconnect ${label} Calendar.`, true);
return;
@@ -3,9 +3,8 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { extractSolidColor } from "@/seqta/ui/colors/parseCssColor";
import { ensureFontLoaded } from "@/seqta/ui/fonts/Manager";
import { getFontPreset } from "@/seqta/ui/fonts/presets";
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
import CalendarSyncControl from "./CalendarSyncControl.svelte";
import { registerCalendarContentHandlers } from "@/seqta/utils/googleCalendar/calendarSyncListener";
import type { GoogleCalendarSyncProgress } from "@/seqta/utils/googleCalendar/types";
import hostStyles from "./calendarSyncHost.css?inline";
export type CalendarProvider = "google" | "outlook";
@@ -164,6 +163,7 @@ export async function mountGoogleCalendarButton(): Promise<void> {
syncCalendarSyncTheme(mountRoot);
controls.appendChild(mountRoot);
const { default: CalendarSyncControl } = await import("./CalendarSyncControl.svelte");
currentApp = mount(CalendarSyncControl, { target: mountRoot });
}
+2 -16
View File
@@ -1,6 +1,5 @@
import type { Plugin } from "../../core/types";
import { waitForElm } from "@/seqta/utils/waitForElm";
import styles from "./styles.css?inline";
interface TimetableEntryData {
ci: number;
@@ -127,20 +126,8 @@ function showEditModal(
roomInput?.focus();
}
const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
id: "timetableEdit",
name: "Edit Rooms & Teachers",
description: "Edit room and teacher names in timetable classes",
version: "1.0.0",
settings: {},
disableToggle: true,
defaultEnabled: true,
run: async (api) => {
const styleEl = document.createElement("style");
styleEl.textContent = styles;
document.head.appendChild(styleEl);
const timetableEditPlugin = {
run: async (api: Parameters<NonNullable<Plugin["run"]>>[0]) => {
await api.storage.loaded;
let observer: MutationObserver | null = null;
@@ -514,7 +501,6 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
observer?.disconnect();
quickbarObserver?.disconnect();
if (quickbarSyncTimer !== null) clearTimeout(quickbarSyncTimer);
styleEl.remove();
document.querySelectorAll("[data-timetable-edit-processed]").forEach((el) => {
el.removeAttribute("data-timetable-edit-processed");
});
@@ -0,0 +1,14 @@
import { defineLazyPlugin } from "../../core/dynamicLoader";
import styles from "./styles.css?inline";
export default defineLazyPlugin({
id: "timetableEdit",
name: "Edit Rooms & Teachers",
description: "Edit room and teacher names in timetable classes",
version: "1.0.0",
settings: {},
disableToggle: true,
defaultEnabled: true,
styles,
loader: () => import("./index"),
});
+1
View File
@@ -20,6 +20,7 @@ function createSEQTAAPI(): SEQTAAPI {
return eventManager.register(
`${selector}Added`,
{
selector,
customCheck: (element) => element.matches(selector),
},
callback,
+8 -12
View File
@@ -2,19 +2,17 @@ import { PluginManager } from "./core/manager";
// Lightweight plugins (load immediately)
import timetablePlugin from "./built-in/timetable";
import timetableEditPlugin from "./built-in/timetableEdit";
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 profilePicturePlugin from "./built-in/profilePicture";
import assessmentsOverviewPlugin from "./built-in/assessmentsOverview";
import backgroundMusicPlugin from "./built-in/backgroundMusic";
import messageFoldersPlugin from "./built-in/messageFolders";
import enhancedNavigationPlugin from "./built-in/enhancedNavigation";
//import testPlugin from './built-in/test';
// Heavy plugins (lazy-loaded only when enabled)
import assessmentsAveragePluginLazy from "./built-in/assessmentsAverage/lazy";
import timetableEditPluginLazy from "./built-in/timetableEdit/lazy";
import messageFoldersPluginLazy from "./built-in/messageFolders/lazy";
import enhancedNavigationPluginLazy from "./built-in/enhancedNavigation/lazy";
import globalSearchPluginLazy from "./built-in/globalSearch/lazy";
import gradeAnalyticsPluginLazy from "./built-in/gradeAnalytics/lazy";
@@ -24,18 +22,16 @@ const pluginManager = PluginManager.getInstance();
// Register built-in plugins
pluginManager.registerPlugin(themesPlugin);
pluginManager.registerPlugin(animatedBackgroundPlugin);
pluginManager.registerPlugin(assessmentsAveragePlugin);
pluginManager.registerPlugin(notificationCollectorPlugin);
pluginManager.registerPlugin(timetablePlugin);
pluginManager.registerPlugin(timetableEditPlugin);
pluginManager.registerPlugin(profilePicturePlugin);
pluginManager.registerPlugin(assessmentsOverviewPlugin);
pluginManager.registerPlugin(backgroundMusicPlugin);
pluginManager.registerPlugin(messageFoldersPlugin);
pluginManager.registerPlugin(enhancedNavigationPlugin);
//pluginManager.registerPlugin(testPlugin);
// Register heavy plugins with lazy loading
pluginManager.registerPlugin(assessmentsAveragePluginLazy);
pluginManager.registerPlugin(timetableEditPluginLazy);
pluginManager.registerPlugin(messageFoldersPluginLazy);
pluginManager.registerPlugin(enhancedNavigationPluginLazy);
pluginManager.registerPlugin(globalSearchPluginLazy);
pluginManager.registerPlugin(gradeAnalyticsPluginLazy);
+47 -81
View File
@@ -11,6 +11,7 @@ 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";
import debounce from "@/seqta/utils/debounce";
// UI and theme management
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
@@ -29,7 +30,10 @@ import {
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { runStartupPopupQueue } from "@/seqta/utils/Openers/StartupPopupQueue";
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
import {
syncTimetableUrlMonitoring,
updateTimetableTimes,
} from "@/seqta/utils/updateTimetableTimes";
import { attachTimetableColorisRecovery } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
// JSON content
@@ -123,7 +127,7 @@ async function updateIframesWithDarkMode(): Promise<void> {
eventManager.register(
"iframeAdded",
{
elementType: "iframe",
selector: "iframe",
customCheck: (element: Element) =>
!element.classList.contains("iframecss"),
},
@@ -161,8 +165,8 @@ function applyDarkModeToIframe(
}
const head = iframeDocument.head;
if (head && !head.innerHTML.includes("iframecss")) {
head.innerHTML += cssLink.outerHTML;
if (head && !head.querySelector(".iframecss")) {
head.appendChild(cssLink.cloneNode(true));
}
}
@@ -197,71 +201,26 @@ async function LoadPageElements(): Promise<void> {
});
}
eventManager.register(
"messagesAdded",
{
elementType: "div",
className: "messages",
},
handleMessages,
);
eventManager.register("messagesAdded", { selector: "div.messages" }, handleMessages);
eventManager.register(
"noticesAdded",
{
elementType: "div",
className: "notices",
},
CheckNoticeTextColour,
);
eventManager.register("noticesAdded", { selector: "div.notices" }, CheckNoticeTextColour);
eventManager.register(
"dashboardAdded",
{
elementType: "div",
className: "dashboard",
},
handleDashboard,
);
eventManager.register("dashboardAdded", { selector: "div.dashboard" }, handleDashboard);
eventManager.register(
"documentsAdded",
{
elementType: "div",
className: "documents",
},
handleDocuments,
);
eventManager.register("documentsAdded", { selector: "div.documents" }, handleDocuments);
eventManager.register(
"reportsAdded",
{
elementType: "div",
className: "reports",
},
handleReports,
);
eventManager.register("reportsAdded", { selector: "div.reports" }, handleReports);
eventManager.register(
"timetableAdded",
{
elementType: "div",
className: "timetablepage",
},
{ selector: "div.timetablepage" },
async () => {
attachTimetableColorisRecovery();
await updateTimetableTimes();
},
);
eventManager.register(
"noticesAdded",
{
elementType: "div",
className: "notice",
},
handleNotices,
);
eventManager.register("noticesAdded", { selector: "div.notice" }, handleNotices);
RegisterClickListeners();
@@ -480,37 +439,42 @@ async function handleReports(node: Element): Promise<void> {
}
}
function CheckNoticeTextColour(notice: Element) {
const adjustNoticeColor = (node: Element) => {
const noticeContainers = new Set<Element>();
let noticeColourObserver: MutationObserver | null = null;
const adjustNoticeColor = (node: Element) => {
const hex = (node as HTMLElement).style.cssText.split(" ")[1];
if (hex) {
const hex1 = hex.slice(0, -1);
const threshold = GetThresholdOfColor(hex1);
if (settingsState.DarkMode && threshold < 100) {
if (!hex || !settingsState.DarkMode || GetThresholdOfColor(hex.slice(0, -1)) >= 100) return;
(node as HTMLElement).style.cssText = "--color: undefined;";
}
}
};
};
for (const node of notice.querySelectorAll("div.notice")) {
adjustNoticeColor(node);
}
const scanNotices = (el: Element) => {
if (el.matches("div.notice")) adjustNoticeColor(el);
el.querySelectorAll("div.notice").forEach(adjustNoticeColor);
};
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const added of mutation.addedNodes) {
if (!(added instanceof Element)) continue;
if (added.matches("div.notice")) {
adjustNoticeColor(added);
}
for (const node of added.querySelectorAll("div.notice")) {
adjustNoticeColor(node);
}
}
const scanAddedNotices = debounce((mutations: MutationRecord[]) => {
for (const m of mutations)
for (const node of m.addedNodes)
if (node instanceof Element) scanNotices(node);
}, 50);
function CheckNoticeTextColour(notice: Element) {
scanNotices(notice);
if (noticeContainers.has(notice)) return;
if (!noticeColourObserver) {
noticeColourObserver = new MutationObserver((mutations) => {
scanAddedNotices(mutations);
for (const m of mutations)
for (const node of m.removedNodes)
if (node instanceof Element && noticeContainers.has(node)) {
noticeColourObserver!.unobserve(node);
noticeContainers.delete(node);
}
});
observer.observe(notice, { childList: true, subtree: true });
}
noticeContainers.add(notice);
noticeColourObserver.observe(notice, { childList: true, subtree: true });
}
function watchForEngageLogin() {
@@ -726,12 +690,14 @@ export function init() {
loading();
InjectCustomIcons();
applyMenuItemVisibility();
syncTimetableUrlMonitoring();
tryLoad();
// Auto-focus WISP direct online submission editor when pane opens
eventManager.register(
"wispassessmentAdded",
{
selector: ".uiSlidePane, .wispassessment",
customCheck: (el) =>
el.classList.contains("wispassessment") ||
el.querySelector(".wispassessment") !== null,
-6
View File
@@ -1,6 +0,0 @@
export { init as Monofile } from "./monofile";
export async function initializePlugins(): Promise<void> {
const { pluginManager } = await import("./index");
await pluginManager.startAllPlugins();
}
-4
View File
@@ -7,8 +7,6 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
// UI and theme management
import pageState from "@/pageState.js?script&iife";
import { extensionPageScriptUrl } from "@/lib/extensionPageScriptUrl";
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
import { installThemeImagePagePatch } from "@/seqta/utils/patchThemeImagesPageContext";
// Stylesheets
import injectedCSS from "@/css/injected.scss?inline";
@@ -18,8 +16,6 @@ export async function main() {
try {
if (settingsState.onoff) {
injectPageState();
installSeqtaMenuColourPatch();
installThemeImagePagePatch();
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
if (import.meta.env.MODE === "development") {
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,4 +1,4 @@
import { debounce } from "lodash";
import debounce from "@/seqta/utils/debounce";
/**
* Automatically resizes the popup to fit the screen, checks on resize but is debounced to prevent intense utilisation.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

+23 -10
View File
@@ -23,11 +23,22 @@ import { resolveNoticeFilterTokens } from "@/seqta/utils/notices/noticeLabelFilt
import { setupNoticesSection } from "@/seqta/utils/notices/noticeHomeUi";
import { lessonsSubtitleForViewDate } from "@/seqta/utils/Loaders/timetableSubtitle";
let LessonInterval: any;
let lessonInterval: ReturnType<typeof setInterval> | null = null;
let homeTeardown: (() => void) | null = null;
let currentSelectedDate = new Date();
let loadingTimeout: any;
let loadingTimeout: ReturnType<typeof setTimeout> | null = null;
function clearLessonInterval(): void {
if (lessonInterval !== null) {
clearInterval(lessonInterval);
lessonInterval = null;
}
}
export async function loadHomePage() {
homeTeardown?.();
homeTeardown = null;
clearLessonInterval();
verboseInfo("[BetterSEQTA+] Started Loading Home Page");
currentSelectedDate = new Date();
@@ -103,7 +114,11 @@ export async function loadHomePage() {
);
}
const cleanup = setupTimetableListeners();
const timetableCleanup = setupTimetableListeners();
homeTeardown = () => {
clearLessonInterval();
timetableCleanup();
};
renderShortcuts();
@@ -148,7 +163,7 @@ export async function loadHomePage() {
});
}
return cleanup;
return homeTeardown;
}
let upcomingRefreshTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -395,14 +410,12 @@ function callHomeTimetable(date: string, change?: any) {
}
function CheckCurrentLessonAll(lessons: any) {
LessonInterval = setInterval(
function () {
clearLessonInterval();
lessonInterval = setInterval(() => {
for (let i = 0; i < lessons.length; i++) {
CheckCurrentLesson(lessons[i], i + 1);
}
}.bind(lessons),
60000,
);
}, 60000);
}
async function CheckCurrentLesson(lesson: any, num: number) {
@@ -431,7 +444,7 @@ async function CheckCurrentLesson(lesson: any, num: number) {
const element = document.getElementById(elementId);
if (!element) {
clearInterval(LessonInterval);
clearLessonInterval();
return;
}
+2 -30
View File
@@ -1,6 +1,7 @@
import stringToHTML from "../stringToHTML";
import { settingsState } from "../listeners/SettingsState";
import { openPopup } from "./PopupManager";
import { createPopupSocialFooter } from "./createPopupSocialFooter";
export function OpenAboutPage() {
const header = stringToHTML(
@@ -31,36 +32,7 @@ export function OpenAboutPage() {
</div>
`).firstChild as HTMLElement;
const footer = stringToHTML(/* html */ `
<div class="whatsnewFooter">
<div>
Resources and Feedback:
<a class="socials" href="https://betterseqta.org" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 658 656" version="1.1">
<path d="M 296 6.079 C 222.099 14.147, 156.177 44.962, 103.631 96 C 75.901 122.933, 55.863 150.195, 39.039 183.877 C 6.713 248.596, -2.990 322.811, 11.567 394 C 24.458 457.036, 54.499 512.622, 100.472 558.501 C 152.711 610.633, 218.648 642.109, 294.500 651.123 C 308.578 652.796, 349.167 652.807, 363.500 651.143 C 457.686 640.203, 538.776 592.815, 592.980 517.037 C 642.593 447.677, 662.695 361.034, 648.904 276 C 633.968 183.904, 580.183 103.524, 499.640 52.932 C 470.832 34.836, 435.045 20.244, 400.531 12.522 C 375.717 6.970, 364.646 5.804, 333.500 5.466 C 317.550 5.293, 300.675 5.568, 296 6.079 M 300.500 148.106 C 261.812 152.166, 225.171 169.425, 197.296 196.717 C 171.447 222.025, 154.115 255.340, 147.986 291.500 C 146.044 302.958, 145.844 306.932, 146.301 325 C 147.060 355.042, 151.117 371.665, 163.998 397.500 C 187.801 445.243, 230.082 477.905, 283.388 489.727 L 295.500 492.414 411.250 492.742 L 527 493.071 527 469.536 L 527 446 482.433 446 L 437.866 446 445.596 437.554 C 457.097 424.987, 465.208 413.133, 473.002 397.500 C 485.883 371.665, 489.940 355.042, 490.699 325 C 491.154 307.015, 490.951 302.933, 489.050 291.729 C 473.693 201.254, 391.395 138.565, 300.500 148.106 M 304.500 195.620 C 270.564 200.792, 243.575 215.251, 223.612 238.956 C 203.303 263.071, 193.650 289.377, 193.690 320.500 C 193.770 381.750, 237.341 433.004, 298.364 443.631 C 311.912 445.990, 335.206 445.075, 348.221 441.672 C 361.455 438.211, 373.637 433.094, 383.671 426.781 C 413.787 407.833, 433.890 379.189, 441.066 345 C 443.682 332.536, 444.161 311.707, 442.101 300 C 434.241 255.323, 402.917 217.681, 361 202.541 C 347.818 197.780, 337.607 195.947, 322 195.540 C 314.025 195.333, 306.150 195.369, 304.500 195.620" fill="currentColor" fill-rule="evenodd"/>
</svg>
</a>
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 256 250" preserveAspectRatio="xMidYMid" style="vertical-align: middle;">
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
</svg>
</a>
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
</svg>
</a>
<a class="socials" href="https://www.youtube.com/@BetterSEQTAPlus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"/></svg>
</a>
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</a>
</div>
</div>
`).firstChild as HTMLElement;
const footer = createPopupSocialFooter();
openPopup({
header,
@@ -1,72 +0,0 @@
import stringToHTML from "../stringToHTML";
import { settingsState } from "../listeners/SettingsState";
import { openPopup } from "./PopupManager";
import { attachPopupMediaFullscreen } from "./attachPopupMediaFullscreen";
/** Same hosting pattern as the What's New update video (GitHub raw). */
const BS_CLOUD_DEMO_VIDEO_URL =
"https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/bsclouddemo.webm";
export function shouldShowBsCloudAutoSyncAnnouncement(): boolean {
return !settingsState.bsCloudAutoSyncAnnouncementShown;
}
/**
* One-time announcement for BetterSEQTA Cloud automatic settings sync (after other startup popups).
* Video layout matches {@link OpenWhatsNewPopup} (`whatsnewImgContainer` / `whatsnewImg`).
*/
export function showBsCloudAutoSyncAnnouncement(onDismissed?: () => void) {
if (document.getElementById("whatsnewbk")) {
onDismissed?.();
return;
}
if (!shouldShowBsCloudAutoSyncAnnouncement()) {
onDismissed?.();
return;
}
const header = stringToHTML(
/* html */
`<div class="whatsnewHeader bsCloudAutoSyncAnnouncementHeader">
<h1>BetterSEQTA Cloud</h1>
</div>`,
).firstChild as HTMLElement;
const imageContainer = document.createElement("div");
imageContainer.classList.add("whatsnewImgContainer");
const video = document.createElement("video");
const source = document.createElement("source");
source.setAttribute("src", BS_CLOUD_DEMO_VIDEO_URL);
source.setAttribute("type", "video/webm");
video.autoplay = true;
video.muted = true;
video.loop = true;
video.appendChild(source);
video.classList.add("whatsnewImg");
imageContainer.appendChild(video);
attachPopupMediaFullscreen(video);
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer privacyStatement" style="height: 50%; overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
<p>
<strong class="bsCloudAccent">BetterSEQTA Cloud</strong> can keep your BetterSEQTA+ settings backed up and in
sync across browsers. Optional <strong>automatic settings sync</strong> runs when you are signed in (passwords
and tokens are never included).
</p>
<p>
Close this dialog when you are done. We will not show this announcement again.
</p>
<p class="bsCloudAutoSyncSignupCallout">Sign up in BetterSEQTA settings</p>
</div>
`).firstChild as HTMLElement;
openPopup({
header,
content: [imageContainer, text],
afterClose: () => {
settingsState.bsCloudAutoSyncAnnouncementShown = true;
onDismissed?.();
},
});
}
@@ -1,49 +0,0 @@
import stringToHTML from "../stringToHTML";
import { openPopup } from "./PopupManager";
export function OpenPrivacyStatement() {
const header = stringToHTML(
/* html */
`<div class="whatsnewHeader">
<h1>Privacy Statement</h1>
<p>Our commitment to your privacy</p>
</div>`,
).firstChild as HTMLElement;
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer privacyStatement">
<h2 style="margin-top: 0;">Privacy Policy</h2>
<p>At BetterSEQTA+, we take your privacy seriously. We want to be completely transparent about how we handle your data.</p>
<h3>Data Collection</h3>
<p><strong>We never collect any information from you.</strong> BetterSEQTA+ is designed to work entirely on your device. All processing happens locally in your browser, and we do not send any data to external servers.</p>
<h3>What We Don't Do</h3>
<ul style="text-align: left; margin: 10px 0;">
<li>We do not track your browsing activity</li>
<li>We do not collect personal information</li>
<li>We do not store your SEQTA credentials</li>
<li>We do not send data to third-party services</li>
<li>We do not use analytics or tracking cookies</li>
</ul>
<h3>Local Storage</h3>
<p>BetterSEQTA+ uses your browser's local storage to save your preferences and settings. This data remains on your device and is never transmitted anywhere. You can clear this data at any time through your browser's settings.</p>
<h3>Open Source</h3>
<p>BetterSEQTA+ is an open-source project. You can review our code on <a href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">GitHub</a> to verify our privacy practices. We believe in transparency and encourage you to inspect the code yourself.</p>
<h3>Our Commitment</h3>
<p>We are committed to providing the best features possible while respecting your privacy. We understand that schools and students have concerns about data privacy, and we want to assure you that BetterSEQTA+ is designed with privacy as a core principle.</p>
<p style="margin-top: 20px; font-weight: bold;">If you have any questions or concerns about our privacy practices, please reach out to us through our <a href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">GitHub repository</a>.</p>
</div>
`).firstChild as HTMLElement;
openPopup({
header,
content: [text],
containerClass: "whatsnewContainer--scrollBody",
});
}
+4 -406
View File
@@ -1,8 +1,9 @@
import stringToHTML from "../stringToHTML";
import browser from "webextension-polyfill";
import kofi from "@/resources/kofi.png?base64";
import { openPopup } from "./PopupManager";
import { attachPopupMediaFullscreen } from "./attachPopupMediaFullscreen";
import { createPopupSocialFooter } from "./createPopupSocialFooter";
import { renderWhatsNewChangelogHtml } from "./whatsNewChangelog";
export function OpenWhatsNewPopup(onDismissed?: () => void) {
const header = stringToHTML(
@@ -16,21 +17,6 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
const imageContainer = document.createElement("div");
imageContainer.classList.add("whatsnewImgContainer");
const video = document.createElement("video");
const source = document.createElement("source");
//source.setAttribute(
// "src",
// "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/update-video.webm",
//);
//video.autoplay = true;
//video.muted = true;
//video.loop = true;
//video.appendChild(source);
//video.classList.add("whatsnewImg");
//imageContainer.appendChild(video);
//attachPopupMediaFullscreen(video);
const heroImage = document.createElement("img");
heroImage.src =
"https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/updateimage1.webp";
@@ -42,399 +28,11 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
<h1>3.7.3 Timetable sync to Calendar & Bugfix Bundle</h1>
<li>Added an option in the Timetable to sync to Google Calendar and Outlook Calendar</li>
<li>Fixed dropdown contrast and readability in settings and across SEQTA pages.</li>
<li>Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.</li>
<li>Fixed timetable subject colour picker not reopening after closing (#221).</li>
<li>Fixed theme wallpaper images not applying on Firefox (#206).</li>
<li>Fixed missing subjects in upcoming assessments on the home page (#454).</li>
<li>Added home page assessment limits (subjects, per-subject count, include past).</li>
<li>Fixed assessment overview showing <code>Undefined%</code> for letter grades (#430).</li>
<li>Fixed notifications older than a year being removed; added local per-account archive (#443).</li>
<li>Fixed notices on the home screen sometimes failing to load (#388).</li>
<li>Fixed nightly/CI release builds (Windows runners, zip packaging, layerchart compile).</li>
<li>Fixed multi-target builds exiting early and masking Vite errors.</li>
<li>Improved background music autoplay with a tap-to-start hint when blocked.</li>
<li>Added verbose logging toggle under Developer settings.</li>
<li>Restored theme settings Edit/Done label and improved custom Select keyboard accessibility.</li>
<li>Cloud sign-in now sends a device name for account management.</li>
<h1>3.7.2 Analytics Fixes</h1>
<li>Fixed analytics page breaking on certain configurations.</li>
<li>Added safer DOM handling for analytics to prevent crashes.</li>
<li>Fixed hovering tooltip for forecast analytics chart.</li>
<li>Improved grade analytics layout, controls, and forecast chart rendering.</li>
<li>Updated update image for new layout.</li>
<h1>3.7.0 Grade Analytics, Enhanced Navigation, fonts, Global Search & SEQTA Engage Improvements</h1>
<li>Added Enhanced Navigation for courses: the navigator now auto-scrolls to the selected lesson (e.g. inside the "Go to…" popup) and prev/next arrows for jumping between lessons.</li>
<li>Added Grade Analytics, new sidebar page with grade trend charts synced from SEQTA.</li>
<li>Added Grade distribution auto-detects your schools letter scale from released marks for analytics page.</li>
<li>Added documents, notices, portals, folios, goals, and more to Global Search.</li>
<li>Added shortcuts to SEQTA Engage home page.</li>
<li>Added assessments overview and assessment weighting overrides for SEQTA Engage.</li>
<li>Added BetterSEQTA sidebar icons to SEQTA Engage.</li>
<li>Added more capabilities for an upcoming interactive theme.</li>
<li>Added font picker in settings.</li>
<li>Added rubric copy on assessment detail pages.</li>
<li>Added manual weight entry when an assessment weight is N/A.</li>
<li>Added an automatic reindex of assessments if any of a series of tracked values change (title, release state, etc). Helps keep weightings up to date.</li>
<li>Fixed BetterSEQTA sidebar injection issues on some pages.</li>
<li>Tweak Theme of the Month popup making it more clear about dismissals and respecting Dont show again.</li>
<li>Fixed duplicate-result fixes.</li>
<li>Improved assessments overview with a better look.</li>
<li>Fixed assement overview showing old subjects.</li>
<h1>3.6.5 - Theme of the Month, custom message folders & assessment weighting overrides</h1>
<li>Added Theme of the Month a monthly featured theme popup with a link to view it in the theme store.</li>
<li>Added custom message folders for organising direct DM's with drag to reorder.</li>
<li>Added the ability to override/add weightings to assessments (on assessment page).</li>
<li>Fixed custom room and teacher names not showing in the timetable popup.</li>
<li>Fixed assessment averages treating N/A weightings incorrectly in subject average calculations.</li>
<li>Fixed the display of weightings that could not automatically be discovered.</li>
<li>Fixed the formatting of the weighting tag that was broken due to a SEQTA update.</li>
<h1>3.6.4 - Theme flavours and fixes, Upcoming Assements improvement</h1>
<li>Added advanced colour adjustments variables for theme customisation.</li>
<li>Improved logic for upcoming assements dashlet to improve compatibility.</li>
<li>BS Cloud can now automatically download themes from other devices.</li>
<li>Added theme flavours for multiple colour variations of the same theme.</li>
<h1>3.6.3 - Assessment overview fix</h1>
<li>Fixed assessments overview failing to load.</li>
<h1>3.6.2 - Cloud backup, various fixes & SEQTA Engage support</h1>
<li>BetterSEQTA Cloud: back up and restore extension settings from your account (General settings).</li>
<li>Optional automatic cloud sync if signed in (on by default).</li>
<li>Option to use cloud profile photo as the local SEQTA profile picture</li>
<li>Firefox: fixed the extension settings popup.</li>
<li>SEQTA Engage: Added BetterSEQTA Plus support for SEQTA Engage for Parents.</li>
<li>Added smooth transitions to adaptive themes (on by default)</li>
<li>Added adaptive theme variables to custom themes (try it out with the Windows XP theme)</li>
<li>Fixed today's lessons on the homepage misbehaving in developer mode.</li>
<li>Reduced overlap between BetterSEQTA subject averages and SEQTA's built-in averages UI.</li>
<li>Updated outdated in-app links and update some under the hood code (Vite 8).</li>
<li>Added a notifications panel animation to work like settings.</li>
<li>Fix timetable edit plugin not working correctly.</li>
<h1>3.5.3 - Adaptive theme updates</h1>
<li>Fixed adaptive theming on current-year course and assessment pages.</li>
<h1>3.5.2 - PDF & store compliance</h1>
<li>Put PDF.js with the extension so assessment weighting stays compatible with Chrome Web Store rules</li>
<h1>3.5.1 - QR & session link fix</h1>
<li>Fixed DesQTA Connect Mobile App QR generation on Chrome</li>
<h1>3.5.0 - Adaptive Theme, Timetable Editor & More</h1>
<li>Added adaptive theme colour</li>
<li>Added optional soft gradient for adaptive theme when viewing a class</li>
<li>Added timetable editor</li>
<li>Added icon-only sidebar option for a compact layout</li>
<li>Added empty states for notices and homepage cards</li>
<li>Added BetterSEQTA Cloud sign-in and improved store browsing details</li>
<li>Improved popup rendering to better handle floating UI elements</li>
<li>Fixed assessment colouring issues</li>
<li>Fixed icon loading on SEQTA pages and improved Windows dropdown styling</li>
<li>Fixed sidebar issues with compact mode, keyboard focus, and tab navigation</li>
<li>Fixed unnecessary notice modal scrolling and other popup styling issues</li>
<li>Added new kanban categories to the assessments overview</li>
<li>Added DesQTA QR code generation in settings for linking to the DesQTA (BetterSEQTA) mobile app</li>
<li>New store with improved theme browsing and backgrounds</li>
<li>Other minor bug fixes and improvements</li>
<h1>3.4.16 - Subject Averages Fixes</h1>
<li>Fixed subject averages not showing correctly with letter grades and weighted marks</li>
<h1>3.4.15 - SEQTA New UI Patch</h1>
<li>Fixed compatibility issues caused by the new SEQTA UI update</li>
<li>Adjusted styling to match updated SEQTA layout changes</li>
<li>Other minor bug fixes and stability improvements</li>
<h1>3.4.14 - Search & Assessments Update</h1>
<li>Global Search improvements: indexing progress, more accurate results, and now includes past assessments/assignments</li>
<li>Assessment Averages now parse weightings when possible for more accurate subject averages</li>
<li>Added weight labels to assessment items (including proper handling of 0% and missing weights)</li>
<li>Fixed homepage tutor lesson colours and assessments/courses visibility issues</li>
<li>Fixed upcoming lessons tutorial room not displaying</li>
<li>Fixed favicon not showing / race condition issues</li>
<li>Other minor styling and stability improvements</li>
<h1>3.4.13 - Bug Fixes & Styling Improvements</h1>
<li>Fixed house/year box hard failing when house_colour does not exist</li>
<li>Fixed message of the day being unreadable in light mode</li>
<li>Fixed global font styling issues due to SEQTA updates</li>
<li>Fixed styling issues with title bar and other elements</li>
<li>Other minor bug fixes and improvements</li>
<h1>3.4.12 - Privacy Updates & Bug Fixes</h1>
<li>Added privacy statement</li>
<li>Added disclaimer modal to assessment averages switch</li>
<li>Improved popup management system</li>
<li>Other minor bug fixes and improvements</li>
<h1>3.4.11 - New Features & Bug Fixes</h1>
<li>Added Background Music plugin</li>
<li>Added empty state for assessments on homepage</li>
<li>Added Colour Picker hex/rgba controls</li>
<li>Fixed custom shortcuts positioning (moved above regular shortcuts)</li>
<li>Fixed Go to popup not scrolling properly</li>
<li>Made theme edit mode more plain</li>
<li>Other minor bug fixes and improvements</li>
<h1>3.4.10 - Minor bug fixes</h1>
<li>Fixed UI file styling incorrectly applying to documents</li>
<li>Fixed missing styles in global search</li>
<li>Added icons for image files in file viewer</li>
<li>Added rounded corners when dragging calendar events</li>
<li>Improved performance of element scanning</li>
<li>Other minor improvements</li>
<h1>3.4.9 - Bug Fixes and Performance Improvements</h1>
<li>Fixed performance issues with large notices on the homepage</li>
<li>Improved performance when global search is disabled</li>
<li>Improved performance of storage handling</li>
<li>Other bug fixes and improvements</li>
<h1>3.4.8 - Improvements!</h1>
<li>Added new assessments kanban overview</li>
<li>Added custom profile pictures</li>
<li>Added custom shortcut icons</li>
<li>Added modern and animated notices on homepage</li>
<li>Improved global search performance and bug fixes</li>
<li>Fixed sidebar icons reverting to old style after reload</li>
<li>Fixed settings popup not appearing on disabled pages</li>
<li>Fixed 12-hour time not applying correctly in timetable</li>
<li>Fixed background flickering on page load</li>
<li>Fixed homepage lessons not properly changing days</li>
<li>Performance improvements for global search</li>
<li>Performance improvements across the extension</li>
<li>Other bug fixes and improvements</li>
<h1>3.4.7 - Global Search</h1>
<li>Added a new global search bar (enable in settings)
<span class="beta">beta</span>
</li>
<li>Fixed news feed not loading</li>
<li>Style changes and improvements</li>
<li>Other bug fixes</li>
<h1>3.4.6.1 - Hot patch!</h1>
<li>Fixed storage not updating and sometimes being replaced with default values</li>
<h1>3.4.6 - Massive internal reworks!</h1>
<li>Fix SEQTA classes not being applied correctly causing a totally broken experience</li>
<li>Reworked internals to function as a plugin system (more on this soon)</li>
<li>Rebuilt theme system that is significantly less buggy</li>
<li>Performance improvements</li>
<li>Other minor bug fixes</li>
<h1>3.4.5 - News, Bug Fixes, and improvements!</h1>
<li>Added alternative news sources</li>
<li>Notifications now open direct messages</li>
<li>Added Toggle for Letter/Percent Grades</li>
<li>Added fullscreen to the theme creator CSS editor</li>
<li>Added warning if BetterSEQTA is installed</li>
<li>Removed max width from theme creator</li>
<li>Fixed discord icon colour in light mode</li>
<li>Fixed subject averages not showing up with letter grades</li>
<li>Tweaked compose UI</li>
<h1>3.4.4 - Bug Fixes and Improvements</h1>
<li>Added vertical zoom to the timetable</li>
<li>Fixed theme importing failing when images were included</li>
<li>Removed broken gradients on the backgrounds of certain buttons</li>
<li>Fixed timetable quickbar arrow receiving the wrong colour</li>
<li>Auto-applied selected theme after saving in theme creator</li>
<li>Fixed a bug where timetable was clipped at certain times</li>
<li>Fixed custom sidebar layouts not applying on page load</li>
<li>Improved spacing of the message editor buttons</li>
<li>Added HEX colour input to the theme creator</li>
<li>Fixed theme application in the creator</li>
<li>Performance improvements</li>
<li>Other minor bug fixes</li>
<h1>3.4.3 - Minor Bug Fixes</h1>
<li>Fixed a bug where timetable colours couldn't be changed</li>
<li>Other minor bug fixes</li>
<h1>3.4.2 - Minor Bug Fixes</h1>
<li>Fixed a bug where Assessment Average wasn't enabled by default</li>
<li>Fixed floating menus would sometimes be placed behind other elements</li>
<h1>3.4.1 - Bug Fixes and Performance Improvements</h1>
<li>Added a new "Subject Average" section to the assessments page</li>
<li>Fixed a bug where animations wouldn't play correctly</li>
<li>Added loading animations to the home page</li>
<li>Under the hood performance improvements</li>
<li>Improved animation performance</li>
<li>Better Animations!</li>
<li>Minor style tweaks</li>
<h1>3.4.0 - Major Performance Update</h1>
<li>Completely rebuilt the extension popup using Svelte for dramatically improved performance</li>
<li>Added a brand new background store with search functionality and downloadable backgrounds</li>
<li>Significant code cleanup and optimization across the extension</li>
<li>Improved overall responsiveness and load times</li>
<li>Smoother animations and improved scrolling</li>
<li>Fixed Firefox compatibility issues</li>
<li>Other minor bug fixes and under the hood improvements</li>
<h1>3.3.1 - Hot Fix</h1>
<li>Fixed assessments not loading when no notices are available</li>
<h1>3.3.0 - Overhauled Theming System</h1>
<li>Added a theme store!</li>
<li>Added the new theme creator!</li>
<li>Fixed Notices not working on home page</li>
<li>Fixed dark/light button labels inverted</li>
<li>Switched to GitHub for hosting the update video</li>
<li>Fixed an issue where the settings menu wouldn't change theme</li>
<li>Fixed custom shortcuts not allowing ports to be used</li>
<li>Fixed occasional flashing when using animations</li>
<li>Fixed loading of the tab icon</li>
<li>Made animations toggle apply to settings</li>
<li>Small styling improvements</li>
<li>Other minor bug fixes</li>
<h1>3.2.7 - Minor Improvements</h1>
<li>Improved performance!</li>
<li>Fixed a bug where the icon wasn't showing up</li>
<h1>3.2.6 - Bug fixes and performance improvements</h1>
<li>Improved contrast for notifications</li>
<li>Added 12-hour time format toggle</li>
<li>Using external update video to ensure smaller package size</li>
<li>Refactored underlying code to improve performance</li>
<li>Removed old theme system <span style="font-style: italic;">*revamp coming soon*</span></li>
<li>Improved notices contrast</li>
<li>Remove Telemetry completely - as we weren't using it too much</li>
<li>Added Error handling to settings interface</li>
<li>Fixed HTML message editor cursor becoming misaligned</li>
<li>Enabled spellcheck inside of direct messages</li>
<li>Fixed timetable dates being misaligned</li>
<li>Other minor bug fixes and under the hood improvements</li>
<h1>3.2.5 - More Bug Fixes</h1>
<li>New direct message scroll animations</li>
<li>Added error message for brave browser shields breaking backgrounds</li>
<li>Fixed homepage assessment tooltips being cut off</li>
<li>Improved direct message styling</li>
<li>Made settings panel auto size to height of screen</li>
<li>Fixed timetable dates not visible</li>
<li>Other minor bug fixes</li>
<h1>3.2.4 - Bug Fixes</h1>
<li>Added an open changelog button to settings</li>
<li>Fixed a memory overflow bug with Education Perfect</li>
<li>Fixed a bug where the background wouldn't change instantly</li>
<li>Fixed news feed not loading</li>
<li>Fixed home items duplicating</li>
<li>Fixed Upcoming assessments not showing</li>
<h1>3.2.2 - Minor Improvements</h1>
<li>Added Settings open-close animation</li>
<li>Minor Bug Fixes</li>
<h1>3.2.0 - Custom Themes</h1>
<li>Added transparency (blur) effects</li>
<li>Added custom themes</li>
<li>Added colour picker history</li>
<li>Heaps of bug fixes</li>
<h1>3.1.3 - Custom Backgrounds</h1>
<li>Added custom backgrounds with support for images and videos</li>
<li>Overhauled topbar</li>
<li>New animated hamburger icon</li>
<li>Minor bug fixes</li>
<h1>3.1.2 - New settings menu!</h1>
<li>Overhauled the settings menu</li>
<li>Added custom gradients</li>
<li>Added HEAPS of animations</li>
<li>Fixed a bug where shortcuts don't show up</li>
<li>Other minor bugs fixed</li>
<h1>3.1.1 - Minor Bug fixes</h1>
<li>Fixed assessments overlapping</li>
<li>Fixed houses not displaying if they aren't a specific color</li>
<li>Fixed Chrome Webstore Link</li>
<h1>3.1.0 - Design Improvements</h1>
<li>Minor UI improvements</li>
<li>Added Animation Speed Slider</li>
<li>Animation now enables and disables without reloading SEQTA</li>
<li>Changed logo</li>
<h1>3.0.0 - BetterSEQTA+ *Complete Overhaul*</h1>
<li>Redesigned appearance</li>
<li>Upgraded to manifest V3 (longer support)</li>
<li>Fixed transitional glitches</li>
<li>Under the hood improvements</li>
<li>Fixed News Feed</li>
<h1>2.0.7 - Added support to other domains + Minor bug fixes</h1>
<li>Fixed BetterSEQTA+ not loading on some pages</li>
<li>Fixed text colour of notices being unreadable</li>
<li>Fixed pages not reloading when saving changes</li>
<h1>2.0.2 - Minor bug fixes</h1>
<li>Fixed indicator for current lesson</li>
<li>Fixed text colour for DM messages list in Light mode</li>
<li>Fixed user info text colour</li>
<h1>Sleek New Layout</h1>
<li>Updated with a new font and presentation, BetterSEQTA+ has never looked better.</li>
<h1>New Updated Sidebar</h1>
<li>Condensed appearance with new updated icons.</li>
<h1>Independent Light Mode and Dark Mode</h1>
<li>Dark mode and Light mode are now available to pick alongside your chosen Theme Colour. Your Theme Colour will now become an accent colour for the page.
Light/Dark mode can be toggled with the new button, found in the top-right of the menu bar.</li>
<h1>Create Custom Shortcuts</h1>
<li>Found in the BetterSEQTA+ Settings menu, custom shortcuts can now be created with a name and URL of your choice.</li>
${renderWhatsNewChangelogHtml()}
</div>
`).firstChild as HTMLElement;
const footer = stringToHTML(/* html */ `
<div class="whatsnewFooter">
<div>
Resources and Feedback:
<a class="socials" href="https://betterseqta.org" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 658 656" version="1.1">
<path d="M 296 6.079 C 222.099 14.147, 156.177 44.962, 103.631 96 C 75.901 122.933, 55.863 150.195, 39.039 183.877 C 6.713 248.596, -2.990 322.811, 11.567 394 C 24.458 457.036, 54.499 512.622, 100.472 558.501 C 152.711 610.633, 218.648 642.109, 294.500 651.123 C 308.578 652.796, 349.167 652.807, 363.500 651.143 C 457.686 640.203, 538.776 592.815, 592.980 517.037 C 642.593 447.677, 662.695 361.034, 648.904 276 C 633.968 183.904, 580.183 103.524, 499.640 52.932 C 470.832 34.836, 435.045 20.244, 400.531 12.522 C 375.717 6.970, 364.646 5.804, 333.500 5.466 C 317.550 5.293, 300.675 5.568, 296 6.079 M 300.500 148.106 C 261.812 152.166, 225.171 169.425, 197.296 196.717 C 171.447 222.025, 154.115 255.340, 147.986 291.500 C 146.044 302.958, 145.844 306.932, 146.301 325 C 147.060 355.042, 151.117 371.665, 163.998 397.500 C 187.801 445.243, 230.082 477.905, 283.388 489.727 L 295.500 492.414 411.250 492.742 L 527 493.071 527 469.536 L 527 446 482.433 446 L 437.866 446 445.596 437.554 C 457.097 424.987, 465.208 413.133, 473.002 397.500 C 485.883 371.665, 489.940 355.042, 490.699 325 C 491.154 307.015, 490.951 302.933, 489.050 291.729 C 473.693 201.254, 391.395 138.565, 300.500 148.106 M 304.500 195.620 C 270.564 200.792, 243.575 215.251, 223.612 238.956 C 203.303 263.071, 193.650 289.377, 193.690 320.500 C 193.770 381.750, 237.341 433.004, 298.364 443.631 C 311.912 445.990, 335.206 445.075, 348.221 441.672 C 361.455 438.211, 373.637 433.094, 383.671 426.781 C 413.787 407.833, 433.890 379.189, 441.066 345 C 443.682 332.536, 444.161 311.707, 442.101 300 C 434.241 255.323, 402.917 217.681, 361 202.541 C 347.818 197.780, 337.607 195.947, 322 195.540 C 314.025 195.333, 306.150 195.369, 304.500 195.620" fill="currentColor" fill-rule="evenodd"/>
</svg>
</a>
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 256 250" preserveAspectRatio="xMidYMid" style="vertical-align: middle;">
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
</svg>
</a>
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
</svg>
</a>
<a class="socials" href="https://www.youtube.com/@BetterSEQTAPlus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"/></svg>
</a>
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
</svg>
</a>
</div>
<div>
<a href="https://ko-fi.com/sethburkart" target="_blank" rel="noopener noreferrer" style="background: none !important; margin:0;margin-left:6px;padding:0; display: flex; align-items: center;">
<img height="25" style="border:0px; height:25px; margin-right: -6px;" src="${kofi}" border="0" alt="Buy Me a Coffee at ko-fi.com" />
</a>
</div>
</div>
`).firstChild as HTMLElement;
const footer = createPopupSocialFooter({ kofi: true });
openPopup({
header,
@@ -1,90 +1,16 @@
import stringToHTML from "../stringToHTML";
import { openPopup } from "./PopupManager";
import kofi from "@/resources/kofi.png?base64";
export function OpenMinecraftServerPopup() {
if (!document.querySelector('link[href*="minecraftia"]')) {
const fontLink = document.createElement("link");
fontLink.href = "https://fonts.cdnfonts.com/css/minecraftia";
fontLink.rel = "stylesheet";
document.head.appendChild(fontLink);
}
export function createPopupSocialFooter(options?: { kofi?: boolean }): HTMLElement {
const kofiBlock = options?.kofi
? `<div>
<a href="https://ko-fi.com/sethburkart" target="_blank" rel="noopener noreferrer" style="background: none !important; margin:0;margin-left:6px;padding:0; display: flex; align-items: center;">
<img height="25" style="border:0px; height:25px; margin-right: -6px;" src="${kofi}" border="0" alt="Buy Me a Coffee at ko-fi.com" />
</a>
</div>`
: "";
const header = stringToHTML(
/* html */
`<div class="whatsnewHeader">
<h1>Minecraft Server</h1>
<p>The official BetterSEQTA+ Minecraft Server</p>
</div>`,
).firstChild as HTMLElement;
const imageContainer = document.createElement("div");
imageContainer.classList.add("whatsnewImgContainer");
const video = document.createElement("video");
video.style.aspectRatio = "16/9";
video.style.background = "black";
const source = document.createElement("source");
source.setAttribute(
"src",
"https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/server-video.mp4",
);
video.autoplay = true;
video.muted = true;
video.loop = true;
video.appendChild(source);
video.classList.add("whatsnewImg");
imageContainer.appendChild(video);
const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%; overflow-y: hidden;">
<h1>Join our community in Minecraft!</h1>
<p style="margin-left: 0;">Join the official BetterSEQTA+ Minecraft Server community now!</p>
<h1>Server Features</h1>
<ul>
<li>SMP as our first release gamemode</li>
<li>Community events and competitions</li>
<li>Custom world generation</li>
<li>Shop system with buying and selling</li>
<li>Regular updates and maintenance</li>
<li>The End dimension will be enabled during an upcoming live event</li>
</ul>
<p style="
font-family: 'Minecraftia', sans-serif;
color: white;
font-weight: bold;
font-size: 34px;
text-align: center;
margin-top: 0.5em;
margin-bottom: 0.1em;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;">
mc.betterseqta.org
</p>
<p style="
font-family: 'Minecraftia', sans-serif;
color: white;
font-weight: bold;
font-size: 12px;
text-align: center;
margin-top: 0;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;">
Version: 1.21.4
</p>
</div>
`).firstChild as HTMLElement;
const footer = stringToHTML(/* html */ `
return stringToHTML(/* html */ `
<div class="whatsnewFooter">
<div>
Resources and Feedback:
@@ -112,13 +38,7 @@ export function OpenMinecraftServerPopup() {
</svg>
</a>
</div>
<div>
</div>
${kofiBlock}
</div>
`).firstChild as HTMLElement;
openPopup({
header,
content: [imageContainer, text, footer],
});
}
@@ -0,0 +1,510 @@
export type WhatsNewRelease = {
title: string;
items: string[];
};
export const WHATS_NEW_CHANGELOG: WhatsNewRelease[] = [
{
"title": "3.7.3 Timetable sync to Calendar & Bugfix Bundle",
"items": [
"Added an option in the Timetable to sync to Google Calendar and Outlook Calendar",
"Fixed dropdown contrast and readability in settings and across SEQTA pages.",
"Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.",
"Fixed timetable subject colour picker not reopening after closing (#221).",
"Fixed theme wallpaper images not applying on Firefox (#206).",
"Fixed missing subjects in upcoming assessments on the home page (#454).",
"Added home page assessment limits (subjects, per-subject count, include past).",
"Fixed assessment overview showing <code>Undefined%</code> for letter grades (#430).",
"Fixed notifications older than a year being removed; added local per-account archive (#443).",
"Fixed notices on the home screen sometimes failing to load (#388).",
"Fixed nightly/CI release builds (Windows runners, zip packaging, layerchart compile).",
"Fixed multi-target builds exiting early and masking Vite errors.",
"Improved background music autoplay with a tap-to-start hint when blocked.",
"Added verbose logging toggle under Developer settings.",
"Restored theme settings Edit/Done label and improved custom Select keyboard accessibility.",
"Cloud sign-in now sends a device name for account management."
]
},
{
"title": "3.7.2 Analytics Fixes",
"items": [
"Fixed analytics page breaking on certain configurations.",
"Added safer DOM handling for analytics to prevent crashes.",
"Fixed hovering tooltip for forecast analytics chart.",
"Improved grade analytics layout, controls, and forecast chart rendering.",
"Updated update image for new layout."
]
},
{
"title": "3.7.0 Grade Analytics, Enhanced Navigation, fonts, Global Search & SEQTA Engage Improvements",
"items": [
"Added Enhanced Navigation for courses: the navigator now auto-scrolls to the selected lesson (e.g. inside the \"Go to…\" popup) and prev/next arrows for jumping between lessons.",
"Added Grade Analytics, new sidebar page with grade trend charts synced from SEQTA.",
"Added Grade distribution auto-detects your schools letter scale from released marks for analytics page.",
"Added documents, notices, portals, folios, goals, and more to Global Search.",
"Added shortcuts to SEQTA Engage home page.",
"Added assessments overview and assessment weighting overrides for SEQTA Engage.",
"Added BetterSEQTA sidebar icons to SEQTA Engage.",
"Added more capabilities for an upcoming interactive theme.",
"Added font picker in settings.",
"Added rubric copy on assessment detail pages.",
"Added manual weight entry when an assessment weight is N/A.",
"Added an automatic reindex of assessments if any of a series of tracked values change (title, release state, etc). Helps keep weightings up to date.",
"Fixed BetterSEQTA sidebar injection issues on some pages.",
"Tweak Theme of the Month popup making it more clear about dismissals and respecting “Dont show again”.",
"Fixed duplicate-result fixes.",
"Improved assessments overview with a better look.",
"Fixed assement overview showing old subjects."
]
},
{
"title": "3.6.5 - Theme of the Month, custom message folders & assessment weighting overrides",
"items": [
"Added Theme of the Month — a monthly featured theme popup with a link to view it in the theme store.",
"Added custom message folders for organising direct DM's with drag to reorder.",
"Added the ability to override/add weightings to assessments (on assessment page).",
"Fixed custom room and teacher names not showing in the timetable popup.",
"Fixed assessment averages treating N/A weightings incorrectly in subject average calculations.",
"Fixed the display of weightings that could not automatically be discovered.",
"Fixed the formatting of the weighting tag that was broken due to a SEQTA update."
]
},
{
"title": "3.6.4 - Theme flavours and fixes, Upcoming Assements improvement",
"items": [
"Added advanced colour adjustments variables for theme customisation.",
"Improved logic for upcoming assements dashlet to improve compatibility.",
"BS Cloud can now automatically download themes from other devices.",
"Added theme flavours for multiple colour variations of the same theme."
]
},
{
"title": "3.6.3 - Assessment overview fix",
"items": [
"Fixed assessments overview failing to load."
]
},
{
"title": "3.6.2 - Cloud backup, various fixes & SEQTA Engage support",
"items": [
"BetterSEQTA Cloud: back up and restore extension settings from your account (General settings).",
"Optional automatic cloud sync if signed in (on by default).",
"Option to use cloud profile photo as the local SEQTA profile picture",
"Firefox: fixed the extension settings popup.",
"SEQTA Engage: Added BetterSEQTA Plus support for SEQTA Engage for Parents.",
"Added smooth transitions to adaptive themes (on by default)",
"Added adaptive theme variables to custom themes (try it out with the Windows XP theme)",
"Fixed today's lessons on the homepage misbehaving in developer mode.",
"Reduced overlap between BetterSEQTA subject averages and SEQTA's built-in averages UI.",
"Updated outdated in-app links and update some under the hood code (Vite 8).",
"Added a notifications panel animation to work like settings.",
"Fix timetable edit plugin not working correctly."
]
},
{
"title": "3.5.3 - Adaptive theme updates",
"items": [
"Fixed adaptive theming on current-year course and assessment pages."
]
},
{
"title": "3.5.2 - PDF & store compliance",
"items": [
"Put PDF.js with the extension so assessment weighting stays compatible with Chrome Web Store rules"
]
},
{
"title": "3.5.1 - QR & session link fix",
"items": [
"Fixed DesQTA Connect Mobile App QR generation on Chrome"
]
},
{
"title": "3.5.0 - Adaptive Theme, Timetable Editor & More",
"items": [
"Added adaptive theme colour",
"Added optional soft gradient for adaptive theme when viewing a class",
"Added timetable editor",
"Added icon-only sidebar option for a compact layout",
"Added empty states for notices and homepage cards",
"Added BetterSEQTA Cloud sign-in and improved store browsing details",
"Improved popup rendering to better handle floating UI elements",
"Fixed assessment colouring issues",
"Fixed icon loading on SEQTA pages and improved Windows dropdown styling",
"Fixed sidebar issues with compact mode, keyboard focus, and tab navigation",
"Fixed unnecessary notice modal scrolling and other popup styling issues",
"Added new kanban categories to the assessments overview",
"Added DesQTA QR code generation in settings for linking to the DesQTA (BetterSEQTA) mobile app",
"New store with improved theme browsing and backgrounds",
"Other minor bug fixes and improvements"
]
},
{
"title": "3.4.16 - Subject Averages Fixes",
"items": [
"Fixed subject averages not showing correctly with letter grades and weighted marks"
]
},
{
"title": "3.4.15 - SEQTA New UI Patch",
"items": [
"Fixed compatibility issues caused by the new SEQTA UI update",
"Adjusted styling to match updated SEQTA layout changes",
"Other minor bug fixes and stability improvements"
]
},
{
"title": "3.4.14 - Search & Assessments Update",
"items": [
"Global Search improvements: indexing progress, more accurate results, and now includes past assessments/assignments",
"Assessment Averages now parse weightings when possible for more accurate subject averages",
"Added weight labels to assessment items (including proper handling of 0% and missing weights)",
"Fixed homepage tutor lesson colours and assessments/courses visibility issues",
"Fixed upcoming lessons tutorial room not displaying",
"Fixed favicon not showing / race condition issues",
"Other minor styling and stability improvements"
]
},
{
"title": "3.4.13 - Bug Fixes & Styling Improvements",
"items": [
"Fixed house/year box hard failing when house_colour does not exist",
"Fixed message of the day being unreadable in light mode",
"Fixed global font styling issues due to SEQTA updates",
"Fixed styling issues with title bar and other elements",
"Other minor bug fixes and improvements"
]
},
{
"title": "3.4.12 - Privacy Updates & Bug Fixes",
"items": [
"Added privacy statement",
"Added disclaimer modal to assessment averages switch",
"Improved popup management system",
"Other minor bug fixes and improvements"
]
},
{
"title": "3.4.11 - New Features & Bug Fixes",
"items": [
"Added Background Music plugin",
"Added empty state for assessments on homepage",
"Added Colour Picker hex/rgba controls",
"Fixed custom shortcuts positioning (moved above regular shortcuts)",
"Fixed Go to popup not scrolling properly",
"Made theme edit mode more plain",
"Other minor bug fixes and improvements"
]
},
{
"title": "3.4.10 - Minor bug fixes",
"items": [
"Fixed UI file styling incorrectly applying to documents",
"Fixed missing styles in global search",
"Added icons for image files in file viewer",
"Added rounded corners when dragging calendar events",
"Improved performance of element scanning",
"Other minor improvements"
]
},
{
"title": "3.4.9 - Bug Fixes and Performance Improvements",
"items": [
"Fixed performance issues with large notices on the homepage",
"Improved performance when global search is disabled",
"Improved performance of storage handling",
"Other bug fixes and improvements"
]
},
{
"title": "3.4.8 - Improvements!",
"items": [
"Added new assessments kanban overview",
"Added custom profile pictures",
"Added custom shortcut icons",
"Added modern and animated notices on homepage",
"Improved global search performance and bug fixes",
"Fixed sidebar icons reverting to old style after reload",
"Fixed settings popup not appearing on disabled pages",
"Fixed 12-hour time not applying correctly in timetable",
"Fixed background flickering on page load",
"Fixed homepage lessons not properly changing days",
"Performance improvements for global search",
"Performance improvements across the extension",
"Other bug fixes and improvements"
]
},
{
"title": "3.4.7 - Global Search",
"items": [
"Added a new global search bar (enable in settings)\n <span class=\"beta\">beta</span>\n ",
"Fixed news feed not loading",
"Style changes and improvements",
"Other bug fixes"
]
},
{
"title": "3.4.6.1 - Hot patch!",
"items": [
"Fixed storage not updating and sometimes being replaced with default values"
]
},
{
"title": "3.4.6 - Massive internal reworks!",
"items": [
"Fix SEQTA classes not being applied correctly causing a totally broken experience",
"Reworked internals to function as a plugin system (more on this soon)",
"Rebuilt theme system that is significantly less buggy",
"Performance improvements",
"Other minor bug fixes"
]
},
{
"title": "3.4.5 - News, Bug Fixes, and improvements!",
"items": [
"Added alternative news sources",
"Notifications now open direct messages",
"Added Toggle for Letter/Percent Grades",
"Added fullscreen to the theme creator CSS editor",
"Added warning if BetterSEQTA is installed",
"Removed max width from theme creator",
"Fixed discord icon colour in light mode",
"Fixed subject averages not showing up with letter grades",
"Tweaked compose UI"
]
},
{
"title": "3.4.4 - Bug Fixes and Improvements",
"items": [
"Added vertical zoom to the timetable",
"Fixed theme importing failing when images were included",
"Removed broken gradients on the backgrounds of certain buttons",
"Fixed timetable quickbar arrow receiving the wrong colour",
"Auto-applied selected theme after saving in theme creator",
"Fixed a bug where timetable was clipped at certain times",
"Fixed custom sidebar layouts not applying on page load",
"Improved spacing of the message editor buttons",
"Added HEX colour input to the theme creator",
"Fixed theme application in the creator",
"Performance improvements",
"Other minor bug fixes"
]
},
{
"title": "3.4.3 - Minor Bug Fixes",
"items": [
"Fixed a bug where timetable colours couldn't be changed",
"Other minor bug fixes"
]
},
{
"title": "3.4.2 - Minor Bug Fixes",
"items": [
"Fixed a bug where Assessment Average wasn't enabled by default",
"Fixed floating menus would sometimes be placed behind other elements"
]
},
{
"title": "3.4.1 - Bug Fixes and Performance Improvements",
"items": [
"Added a new \"Subject Average\" section to the assessments page",
"Fixed a bug where animations wouldn't play correctly",
"Added loading animations to the home page",
"Under the hood performance improvements",
"Improved animation performance",
"Better Animations!",
"Minor style tweaks"
]
},
{
"title": "3.4.0 - Major Performance Update",
"items": [
"Completely rebuilt the extension popup using Svelte for dramatically improved performance",
"Added a brand new background store with search functionality and downloadable backgrounds",
"Significant code cleanup and optimization across the extension",
"Improved overall responsiveness and load times",
"Smoother animations and improved scrolling",
"Fixed Firefox compatibility issues",
"Other minor bug fixes and under the hood improvements"
]
},
{
"title": "3.3.1 - Hot Fix",
"items": [
"Fixed assessments not loading when no notices are available"
]
},
{
"title": "3.3.0 - Overhauled Theming System",
"items": [
"Added a theme store!",
"Added the new theme creator!",
"Fixed Notices not working on home page",
"Fixed dark/light button labels inverted",
"Switched to GitHub for hosting the update video",
"Fixed an issue where the settings menu wouldn't change theme",
"Fixed custom shortcuts not allowing ports to be used",
"Fixed occasional flashing when using animations",
"Fixed loading of the tab icon",
"Made animations toggle apply to settings",
"Small styling improvements",
"Other minor bug fixes"
]
},
{
"title": "3.2.7 - Minor Improvements",
"items": [
"Improved performance!",
"Fixed a bug where the icon wasn't showing up"
]
},
{
"title": "3.2.6 - Bug fixes and performance improvements",
"items": [
"Improved contrast for notifications",
"Added 12-hour time format toggle",
"Using external update video to ensure smaller package size",
"Refactored underlying code to improve performance",
"Removed old theme system <span style=\"font-style: italic;\">*revamp coming soon*</span>",
"Improved notices contrast",
"Remove Telemetry completely - as we weren't using it too much",
"Added Error handling to settings interface",
"Fixed HTML message editor cursor becoming misaligned",
"Enabled spellcheck inside of direct messages",
"Fixed timetable dates being misaligned",
"Other minor bug fixes and under the hood improvements"
]
},
{
"title": "3.2.5 - More Bug Fixes",
"items": [
"New direct message scroll animations",
"Added error message for brave browser shields breaking backgrounds",
"Fixed homepage assessment tooltips being cut off",
"Improved direct message styling",
"Made settings panel auto size to height of screen",
"Fixed timetable dates not visible",
"Other minor bug fixes"
]
},
{
"title": "3.2.4 - Bug Fixes",
"items": [
"Added an open changelog button to settings",
"Fixed a memory overflow bug with Education Perfect",
"Fixed a bug where the background wouldn't change instantly",
"Fixed news feed not loading",
"Fixed home items duplicating",
"Fixed Upcoming assessments not showing"
]
},
{
"title": "3.2.2 - Minor Improvements",
"items": [
"Added Settings open-close animation",
"Minor Bug Fixes"
]
},
{
"title": "3.2.0 - Custom Themes",
"items": [
"Added transparency (blur) effects",
"Added custom themes",
"Added colour picker history",
"Heaps of bug fixes"
]
},
{
"title": "3.1.3 - Custom Backgrounds",
"items": [
"Added custom backgrounds with support for images and videos",
"Overhauled topbar",
"New animated hamburger icon",
"Minor bug fixes"
]
},
{
"title": "3.1.2 - New settings menu!",
"items": [
"Overhauled the settings menu",
"Added custom gradients",
"Added HEAPS of animations",
"Fixed a bug where shortcuts don't show up",
"Other minor bugs fixed"
]
},
{
"title": "3.1.1 - Minor Bug fixes",
"items": [
"Fixed assessments overlapping",
"Fixed houses not displaying if they aren't a specific color",
"Fixed Chrome Webstore Link"
]
},
{
"title": "3.1.0 - Design Improvements",
"items": [
"Minor UI improvements",
"Added Animation Speed Slider",
"Animation now enables and disables without reloading SEQTA",
"Changed logo"
]
},
{
"title": "3.0.0 - BetterSEQTA+ *Complete Overhaul*",
"items": [
"Redesigned appearance",
"Upgraded to manifest V3 (longer support)",
"Fixed transitional glitches",
"Under the hood improvements",
"Fixed News Feed"
]
},
{
"title": "2.0.7 - Added support to other domains + Minor bug fixes",
"items": [
"Fixed BetterSEQTA+ not loading on some pages",
"Fixed text colour of notices being unreadable",
"Fixed pages not reloading when saving changes"
]
},
{
"title": "2.0.2 - Minor bug fixes",
"items": [
"Fixed indicator for current lesson",
"Fixed text colour for DM messages list in Light mode",
"Fixed user info text colour"
]
},
{
"title": "Sleek New Layout",
"items": [
"Updated with a new font and presentation, BetterSEQTA+ has never looked better."
]
},
{
"title": "New Updated Sidebar",
"items": [
"Condensed appearance with new updated icons."
]
},
{
"title": "Independent Light Mode and Dark Mode",
"items": [
"Dark mode and Light mode are now available to pick alongside your chosen Theme Colour. Your Theme Colour will now become an accent colour for the page.\n Light/Dark mode can be toggled with the new button, found in the top-right of the menu bar."
]
},
{
"title": "Create Custom Shortcuts",
"items": [
"Found in the BetterSEQTA+ Settings menu, custom shortcuts can now be created with a name and URL of your choice."
]
}
];
export function renderWhatsNewChangelogHtml(): string {
return WHATS_NEW_CHANGELOG.map(
(release) =>
`<h1>${release.title}</h1>\n${release.items.map((item) => `<li>${item}</li>`).join("\n")}`,
).join("\n\n ");
}
+4 -2
View File
@@ -4,8 +4,10 @@ import {
GOOGLE_CALENDAR_SYNC_WEEKS_MAX,
GOOGLE_CALENDAR_SYNC_WEEKS_MIN,
} from "@/config/googleCalendar";
import { readGoogleCalendarState } from "@/seqta/utils/googleCalendar/storage";
import { readOutlookCalendarState } from "@/seqta/utils/outlookCalendar/storage";
import {
readGoogleCalendarState,
readOutlookCalendarState,
} from "@/seqta/utils/calendarSync/providerStorage";
export const BSPLUS_CALENDAR_SYNC_SETTINGS_KEY = "bsplus_calendar_sync_settings";
export const CALENDAR_WEEKLY_ALARM = "bsplus_calendar_weekly";
+2 -2
View File
@@ -37,7 +37,7 @@ import { ensureGoogleAppCalendar } from "@/seqta/utils/googleCalendar/calendarPr
import {
readGoogleCalendarState,
writeGoogleCalendarState,
} from "@/seqta/utils/googleCalendar/storage";
} from "@/seqta/utils/calendarSync/providerStorage";
import {
syncWindowRange,
trailingWeekRange,
@@ -54,7 +54,7 @@ import type {
import {
readOutlookCalendarState,
writeOutlookCalendarState,
} from "@/seqta/utils/outlookCalendar/storage";
} from "@/seqta/utils/calendarSync/providerStorage";
import { verboseLog } from "@/utils/verboseLog";
type CalendarStoredState = {
+63 -24
View File
@@ -1,57 +1,96 @@
import browser from "webextension-polyfill";
import { fetchTimetableForSync, fetchTimetableLessons } from "@/seqta/utils/googleCalendar/fetchTimetable";
import { trailingWeekRange } from "@/seqta/utils/googleCalendar/syncDateRange";
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
import {
googleLessonSyncProvider,
outlookLessonSyncProvider,
syncLessonsToCalendar,
type CalendarLessonSyncProvider,
} from "@/seqta/utils/calendarSync/syncEngine";
import { reportSyncProgress } from "@/seqta/utils/calendarSync/lessonSyncShared";
import type { OutlookCalendarStatus } from "@/seqta/utils/calendarSync/providerStorage";
import { getSyncWeeksAhead } from "@/seqta/utils/calendarSync/settings";
import type {
GoogleCalendarStatus,
GoogleCalendarSyncProgress,
GoogleCalendarSyncResult,
} from "@/seqta/utils/googleCalendar/types";
export type CalendarProvider = "google" | "outlook";
const MSG = {
accessToken: { google: "googleCalendarGetAccessToken", outlook: "outlookCalendarGetAccessToken" },
connect: { google: "googleCalendarConnect", outlook: "outlookCalendarConnect" },
disconnect: { google: "googleCalendarDisconnect", outlook: "outlookCalendarDisconnect" },
status: { google: "googleCalendarStatus", outlook: "outlookCalendarStatus" },
} as const;
async function sendMessage<T>(type: string, payload: Record<string, unknown> = {}): Promise<T> {
return browser.runtime.sendMessage({ type, ...payload }) as Promise<T>;
}
export async function getCalendarAccessToken(provider: CalendarProvider): Promise<string> {
const res = await sendMessage<{
success?: boolean;
accessToken?: string;
error?: string;
}>(MSG.accessToken[provider]);
if (!res?.success || !res.accessToken) {
throw new Error(res?.error ?? "Could not get calendar access token.");
}
return res.accessToken;
}
export async function fetchCalendarStatuses(): Promise<{
google: GoogleCalendarStatus;
outlook: OutlookCalendarStatus;
}> {
const [google, outlook] = await Promise.all([
sendMessage<GoogleCalendarStatus>(MSG.status.google),
sendMessage<OutlookCalendarStatus>(MSG.status.outlook),
]);
return { google, outlook };
}
export async function updateGoogleSyncSettings(patch: {
syncWeeksAhead?: number;
autoSyncWeekly?: boolean;
}): Promise<GoogleCalendarStatus & { success?: boolean }> {
return sendMessage("googleCalendarUpdateSyncSettings", patch);
}
export async function connectCalendarProvider(
provider: CalendarProvider,
): Promise<GoogleCalendarSyncResult> {
return sendMessage(MSG.connect[provider]);
}
export async function disconnectCalendarProvider(
provider: CalendarProvider,
): Promise<{ success?: boolean }> {
return sendMessage(MSG.disconnect[provider]);
}
export interface RunCalendarSyncParams {
mode?: "full" | "incremental";
onProgress?: (progress: GoogleCalendarSyncProgress) => void;
}
type CalendarSyncRunnerConfig = {
accessTokenMessageType: string;
accessTokenError: string;
provider: CalendarProvider;
lessonSyncProvider: CalendarLessonSyncProvider;
};
const GOOGLE_SYNC_RUNNER: CalendarSyncRunnerConfig = {
accessTokenMessageType: "googleCalendarGetAccessToken",
accessTokenError: "Could not get Google Calendar access token.",
provider: "google",
lessonSyncProvider: googleLessonSyncProvider,
};
const OUTLOOK_SYNC_RUNNER: CalendarSyncRunnerConfig = {
accessTokenMessageType: "outlookCalendarGetAccessToken",
accessTokenError: "Could not get Outlook Calendar access token.",
provider: "outlook",
lessonSyncProvider: outlookLessonSyncProvider,
};
async function getAccessTokenFromBackground(
messageType: string,
errorMessage: string,
): Promise<string> {
const res = (await browser.runtime.sendMessage({ type: messageType })) as {
success?: boolean;
accessToken?: string;
error?: string;
};
if (!res?.success || !res.accessToken) {
throw new Error(res?.error ?? errorMessage);
}
return res.accessToken;
}
export async function runCalendarSync(
config: CalendarSyncRunnerConfig,
params: RunCalendarSyncParams = {},
@@ -74,7 +113,7 @@ export async function runCalendarSync(
return syncLessonsToCalendar(
config.lessonSyncProvider,
{ origin: location.origin, lessons, mode, weeksAhead },
() => getAccessTokenFromBackground(config.accessTokenMessageType, config.accessTokenError),
() => getCalendarAccessToken(config.provider),
{ onProgress: params.onProgress },
);
}
+1 -1
View File
@@ -1,5 +1,5 @@
import browser from "webextension-polyfill";
import isEqual from "lodash/isEqual";
import isEqual from "@/seqta/utils/isEqual";
/** Matches the contract in docs/CLOUD_SETTINGS_SYNC_SERVER.md */
export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
@@ -5,8 +5,10 @@ import {
} from "@/seqta/utils/calendarSync/settings";
import { formatLessonSyncResultMessage } from "@/seqta/utils/calendarSync/lessonSyncShared";
import { runGoogleCalendarSync, runOutlookCalendarSync } from "@/seqta/utils/calendarSync/syncRunner";
import { readGoogleCalendarState } from "@/seqta/utils/googleCalendar/storage";
import { readOutlookCalendarState } from "@/seqta/utils/outlookCalendar/storage";
import {
readGoogleCalendarState,
readOutlookCalendarState,
} from "@/seqta/utils/calendarSync/providerStorage";
import type { GoogleCalendarSyncResult } from "@/seqta/utils/googleCalendar/types";
let listenerRegistered = false;
@@ -1,9 +0,0 @@
export {
BSPLUS_GOOGLE_CALENDAR_STORAGE_KEY,
clearGoogleCalendarState,
readGoogleCalendarState,
writeGoogleCalendarState,
type GoogleCalendarStoredState,
} from "@/seqta/utils/calendarSync/providerStorage";
export type { EventMapRecord as GoogleCalendarEventMapRecord } from "@/seqta/utils/calendarSync/eventMap";
@@ -11,9 +11,9 @@ jest.mock("@/utils/verboseLog", () => ({
verboseLog: jest.fn(),
}));
jest.mock("@/seqta/utils/googleCalendar/storage", () => {
const actual = jest.requireActual<typeof import("@/seqta/utils/googleCalendar/storage")>(
"@/seqta/utils/googleCalendar/storage",
jest.mock("@/seqta/utils/calendarSync/providerStorage", () => {
const actual = jest.requireActual<typeof import("@/seqta/utils/calendarSync/providerStorage")>(
"@/seqta/utils/calendarSync/providerStorage",
);
return {
...actual,
@@ -32,7 +32,7 @@ jest.mock("@/seqta/utils/calendarSync/remoteEvents", () => ({
listGoogleSyncedEvents: jest.fn(async () => []),
}));
import { readGoogleCalendarState } from "@/seqta/utils/googleCalendar/storage";
import { readGoogleCalendarState } from "@/seqta/utils/calendarSync/providerStorage";
import {
deleteGoogleCalendarEvent,
listGoogleSyncedEvents,
+24
View File
@@ -0,0 +1,24 @@
import isEqual from "./isEqual";
describe("isEqual", () => {
it("treats primitives by value", () => {
expect(isEqual(1, 1)).toBe(true);
expect(isEqual("a", "a")).toBe(true);
expect(isEqual(1, 2)).toBe(false);
expect(isEqual(null, null)).toBe(true);
expect(isEqual(undefined, undefined)).toBe(true);
expect(isEqual(null, undefined)).toBe(false);
});
it("compares flat objects deeply", () => {
expect(isEqual({ a: 1, b: "x" }, { a: 1, b: "x" })).toBe(true);
expect(isEqual({ a: 1 }, { a: 2 })).toBe(false);
expect(isEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
});
it("compares arrays deeply", () => {
expect(isEqual([1, { x: 2 }], [1, { x: 2 }])).toBe(true);
expect(isEqual([1, 2], [1, 3])).toBe(false);
expect(isEqual([1], [1, 2])).toBe(false);
});
});
+20
View File
@@ -0,0 +1,20 @@
/** Deep equality for JSON-like sync payloads (objects, arrays, primitives). */
export default function isEqual(a: unknown, b: unknown): boolean {
if (a === b) return true;
if (a == null || b == null) return false;
if (typeof a !== typeof b) return false;
if (typeof a !== "object") return false;
if (Array.isArray(a)) {
if (!Array.isArray(b) || a.length !== b.length) return false;
return a.every((v, i) => isEqual(v, b[i]));
}
if (Array.isArray(b)) return false;
const aObj = a as Record<string, unknown>;
const bObj = b as Record<string, unknown>;
const aKeys = Object.keys(aObj);
if (aKeys.length !== Object.keys(bObj).length) return false;
return aKeys.every((k) => isEqual(aObj[k], bObj[k]));
}
+2 -5
View File
@@ -101,12 +101,9 @@ class EventManager {
if (selector) {
elements = Array.from(root.querySelectorAll(selector));
if (selector && root.matches && root.matches(selector)) {
elements.unshift(root);
}
if (root.matches?.(selector)) elements.unshift(root);
} else {
elements = Array.from(root.getElementsByTagName("*"));
elements.unshift(root);
elements = [root];
}
for (let i = 0; i < elements.length; i += this.chunkSize) {
+40 -1
View File
@@ -1,7 +1,46 @@
import browser from "webextension-polyfill";
import { SYNCABLE_PLUGIN_SETTING_DEFAULTS } from "@/plugins/syncablePluginDefaults";
import { getDefaultSettingsState } from "@/seqta/utils/defaultSettings";
import type { SettingsState } from "@/types/storage";
import type { Subscriber, Unsubscriber } from "svelte/store";
const PLUGIN_SETTINGS_KEYS = Object.keys(SYNCABLE_PLUGIN_SETTING_DEFAULTS).map(
(id) => `plugin.${id}.settings`,
);
const PLUGIN_STORAGE_KEYS = [
"plugin.assessments-average.storage.assessments",
"plugin.assessments-average.storage.weightings",
"plugin.assessments-average.storage.weightingOverrides",
"plugin.notificationCollector.storage.lastNotificationCount",
"plugin.notificationCollector.storage.lastCheckedTime",
"plugin.notificationCollector.storage.consecutiveErrors",
"plugin.notificationCollector.storage.archivesByUser",
"plugin.timetableEdit.storage.timetableOverrides",
"plugin.timetableEdit.storage.timetableOverridesBySubject",
"plugin.messageFolders.storage.folders",
"plugin.messageFolders.storage.messageAssignments",
] as const;
const SETTINGS_STORAGE_KEYS = [
...new Set([
...Object.keys(getDefaultSettingsState()),
"justupdated",
"devMode",
"verboseLogging",
"devGhReleaseVersionOverride",
"lastSeenNightlyPublishedAt",
"originalDarkMode",
"mockNotices",
"hideSensitiveContent",
"privacyStatementLastUpdated",
"themeOfTheMonthDismissedMonth",
"themeOfTheMonthLastSeenId",
...PLUGIN_SETTINGS_KEYS,
...PLUGIN_STORAGE_KEYS,
]),
];
/** Auth/session keys live in `chrome.storage.local` only — not on the settingsState proxy. */
const EXCLUDED_FROM_SETTINGS_SURFACE = new Set([
"bsplus_token",
@@ -163,7 +202,7 @@ class StorageManager {
}
private async loadFromStorage(): Promise<void> {
const result = await browser.storage.local.get();
const result = await browser.storage.local.get(SETTINGS_STORAGE_KEYS);
Object.entries(result).forEach(([key, value]) => {
if (isExcludedSettingsKey(key)) return;
Reflect.set(this.data, key, value);
@@ -1,8 +0,0 @@
export {
BSPLUS_OUTLOOK_CALENDAR_STORAGE_KEY,
clearOutlookCalendarState,
readOutlookCalendarState,
writeOutlookCalendarState,
type OutlookCalendarStatus,
type OutlookCalendarStoredState,
} from "@/seqta/utils/calendarSync/providerStorage";
+6 -10
View File
@@ -207,15 +207,11 @@
}
if (!tryPatch()) {
var interval = setInterval(function () {
if (tryPatch()) {
clearInterval(interval);
} else if (window.msg) {
neutralizeBrokenListeners(window.msg);
}
}, 25);
setTimeout(function () {
clearInterval(interval);
}, 120000);
var delay = 25, waited = 0;
(function poll() {
if (tryPatch() || (waited += delay) >= 120000) return;
if (window.msg) neutralizeBrokenListeners(window.msg);
setTimeout(poll, delay = Math.min(delay * 2, 2000));
})();
}
})();
+1
View File
@@ -177,6 +177,7 @@ export async function observeMenuItemPosition() {
eventManager.register(
"menuList",
{
selector: "li.item, section.item",
parentElement: document.querySelector("#menu")!.firstChild as Element,
},
(element: Element) => {
+6 -7
View File
@@ -155,8 +155,13 @@ function stopUrlMonitoring(): void {
stopTimetableMonitoring();
}
export function syncTimetableUrlMonitoring(): void {
if (settingsState.timeFormat === "12") startUrlMonitoring();
else stopUrlMonitoring();
}
export async function updateTimetableTimes(): Promise<void> {
if (!settingsState.timeFormat) return;
if (settingsState.timeFormat !== "12") return;
const timetablePage = document.querySelector(".timetablepage");
if (!timetablePage) return;
@@ -178,12 +183,6 @@ export async function updateTimetableTimes(): Promise<void> {
}
}
// Initialize monitoring on page load
if (typeof window !== "undefined") {
// Start URL monitoring immediately
startUrlMonitoring();
}
// Cleanup function for when the module is unloaded
export function cleanup(): void {
stopUrlMonitoring();
-18
View File
@@ -10,8 +10,6 @@ import fixCrxWorkerLiveReload from "./lib/fixCrxWorkerLiveReload";
import { firefoxStripFunctionProbe } from "./lib/firefoxStripFunctionProbe";
import { extensionChunkUrls } from "./lib/extensionChunkUrls";
import million from "million/compiler";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { chrome } from "./src/manifests/chrome";
@@ -57,9 +55,6 @@ function withDevManifestCsp(manifest: Manifest, command: string): Manifest {
const mode = process.env.MODE || "chrome"; // Check the environment variable to determine which build type to use.
//const sourcemap = (process.env.SOURCEMAP === "true") || false; // Check whether we want sourcemaps.
/** Million's compiler can emit `new Function()`, which Firefox extension pages block (strict CSP, no unsafe-eval). */
const useMillion = mode.toLowerCase() !== "firefox";
const repoRoot = __dirname;
export default defineConfig(({ command, mode: viteMode }) => {
@@ -89,19 +84,6 @@ export default defineConfig(({ command, mode: viteMode }) => {
extensionChunkUrls(),
base64Loader,
InlineWorkerPlugin(),
...(useMillion && command !== "build"
? [
million.vite({
auto: true,
filter: {
exclude: [
"**/*.svelte",
"node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}",
],
},
}),
]
: []),
crx({
manifest: withDevManifestCsp(
targets.find((t) => t.browser === mode.toLowerCase())?.manifest ??