From 75e687f934517d80c040d89b82cb838ad56a1256 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 11 Mar 2025 19:53:56 +0800 Subject: [PATCH 01/72] bump every package, remove postcss --- .postcssrc.json | 5 --- package.json | 89 ++++++++++++++++++++++------------------- postcss.config.js | 6 --- src/interface/index.css | 2 + vite.config.ts | 3 ++ 5 files changed, 53 insertions(+), 52 deletions(-) delete mode 100644 .postcssrc.json delete mode 100644 postcss.config.js diff --git a/.postcssrc.json b/.postcssrc.json deleted file mode 100644 index d42c370f..00000000 --- a/.postcssrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": { - "tailwindcss": {} - } -} \ No newline at end of file diff --git a/package.json b/package.json index ba2f81b3..66f6a1dd 100644 --- a/package.json +++ b/package.json @@ -32,66 +32,73 @@ }, "license": "MIT", "devDependencies": { - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/runtime": "^7.26.7", - "@bedframe/cli": "^0.0.85", + "@babel/plugin-transform-runtime": "^7.26.9", + "@babel/runtime": "^7.26.9", + "@bedframe/cli": "^0.0.91", "@crxjs/vite-plugin": "2.0.0-beta.25", "@types/mime-types": "^2.1.4", - "@vitejs/plugin-react-swc": "^3.7.2", + "@vitejs/plugin-react-swc": "^3.8.0", "cross-env": "^7.0.3", "dependency-cruiser": "^16.10.0", - "eslint": "^8.57.1", + "eslint": "9.22.0", "glob": "^11.0.1", "mime-types": "^2.1.35", - "prettier": "^3.4.2", + "prettier": "^3.5.3", "process": "^0.11.10", "publish-browser-extension": "^3.0.0", - "sass": "^1.83.4", - "sass-loader": "^13.3.3", + "sass": "^1.85.1", + "sass-loader": "^16.0.5", "semver": "^7.7.1", "url": "^0.11.4" }, "dependencies": { - "@codemirror/lang-css": "^6.3.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "@tailwindcss/forms": "^0.5.9", + "@codemirror/autocomplete": "^6.18.6", + "@codemirror/commands": "^6.8.0", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/language": "^6.10.8", + "@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", + "@tailwindcss/postcss": "^4.0.12", + "@tailwindcss/vite": "^4.0.12", "@tsconfig/svelte": "^5.0.4", - "@types/chrome": "^0.0.270", - "@types/color": "^3.0.6", - "@types/dompurify": "^3.2.0", - "@types/lodash": "^4.17.15", - "@types/node": "^20.17.17", - "@types/react": "^17.0.83", - "@types/react-dom": "^17.0.26", + "@types/chrome": "^0.0.308", + "@types/color": "^4.2.0", + "@types/lodash": "^4.17.16", + "@types/node": "^22.13.10", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "@types/sortablejs": "^1.15.8", - "@types/uuid": "^9.0.8", - "@types/webextension-polyfill": "^0.10.7", - "@uiw/codemirror-extensions-color": "^4.23.8", - "@uiw/codemirror-theme-github": "^4.23.8", + "@types/uuid": "^10.0.0", + "@types/webextension-polyfill": "^0.12.3", + "@uiw/codemirror-extensions-color": "^4.23.10", + "@uiw/codemirror-theme-github": "^4.23.10", "@vitejs/plugin-react": "^4.3.4", - "autoprefixer": "^10.4.20", + "autoprefixer": "^10.4.21", "codemirror": "^6.0.1", - "color": "^4.2.3", - "dompurify": "^3.1.6", - "embla-carousel-autoplay": "^8.3.1", - "embla-carousel-svelte": "^8.3.1", - "fuse.js": "^7.0.0", - "idb": "^8.0.0", + "color": "^5.0.0", + "dompurify": "^3.2.4", + "embla-carousel-autoplay": "^8.5.2", + "embla-carousel-svelte": "^8.5.2", + "fuse.js": "^7.1.0", + "idb": "^8.0.2", "localforage": "^1.10.0", "lodash": "^4.17.21", "million": "^3.1.11", - "motion": "^11.12.0", - "postcss": "^8.4.45", - "react": "17", - "react-best-gradient-color-picker": "^3.0.10", - "react-dom": "17", + "motion": "^12.4.12", + "postcss": "^8.5.3", + "react": "^19.0.0", + "react-best-gradient-color-picker": "^3.0.14", + "react-dom": "^19.0.0", "rss-parser": "^3.13.0", - "sortablejs": "^1.15.3", - "svelte": "^5.1.9", - "tailwindcss": "^3.4.11", - "typescript": "^5.6.2", - "uuid": "^9.0.1", - "vite": "^5.4.14", - "webextension-polyfill": "^0.10.0" + "sortablejs": "^1.15.6", + "svelte": "^5.22.6", + "tailwindcss": "^4.0.12", + "typescript": "^5.8.2", + "uuid": "^11.1.0", + "vite": "^6.2.1", + "webextension-polyfill": "^0.12.0" } } diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index 2e7af2b7..00000000 --- a/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/src/interface/index.css b/src/interface/index.css index dedb557e..07b9d1ad 100644 --- a/src/interface/index.css +++ b/src/interface/index.css @@ -1,4 +1,6 @@ @import './components/ColourPicker.css'; +@import "tailwindcss"; + @tailwind base; @tailwind components; diff --git a/vite.config.ts b/vite.config.ts index 25582b7f..2fb94acf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,8 @@ import { defineConfig } from 'vite'; import { join, resolve } from 'path'; +import tailwindcss from '@tailwindcss/vite' + import { updateManifestPlugin } from './lib/patchPackage'; import { base64Loader } from './lib/base64loader'; import type { BuildTarget } from './lib/types'; @@ -30,6 +32,7 @@ export default defineConfig(({ command }) => ({ plugins: [ base64Loader, react(), + tailwindcss(), svelte({ emitCss: false }), From 8c2f36033fe9e76dbae1f6054f3436856060c6a2 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:13:44 +0800 Subject: [PATCH 02/72] add support for hiding non-assessments (discord issue) --- src/SEQTA.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 6c45cbc2..59062728 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -991,6 +991,37 @@ function handleTimetableZoom(): void { }) } +function handleTimetableAssessmentHide(): void { + const hideControls = document.createElement("div") + hideControls.className = "timetable-hide-controls" + + const hideOn = document.createElement("button") + hideOn.className = "uiButton timetable-hide iconFamily" + hideOn.innerHTML = "" // Using unicode for zoom in icon + + hideControls.appendChild(hideOn) + + const toolbar = document.getElementById("toolbar") + toolbar?.appendChild(hideControls) + + function hideElements(): void { + const entries = document.querySelectorAll(".entry") + entries.forEach((entry: Element) => { + const entryEl = entry as HTMLElement + if (!entryEl.classList.contains("assessment") && !(entryEl.style.display === "none")) { + entryEl.style.display = "none" + } else { + entryEl.style.display = "" + } + }) + } + + hideOn.addEventListener("click", () => { + hideElements() + }) + +} + async function handleNotices(node: Element): Promise { if (!(node instanceof HTMLElement)) return if (!settingsState.animations) return @@ -1068,6 +1099,7 @@ async function handleTimetable(): Promise { } handleTimetableZoom() + handleTimetableAssessmentHide() } async function handleNewsPage(): Promise { From f9209809486653eb1592858c2be4704881d0d9c3 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:20:42 +0800 Subject: [PATCH 03/72] change to an eye icon --- src/SEQTA.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 59062728..123b507f 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -997,7 +997,7 @@ function handleTimetableAssessmentHide(): void { const hideOn = document.createElement("button") hideOn.className = "uiButton timetable-hide iconFamily" - hideOn.innerHTML = "" // Using unicode for zoom in icon + hideOn.innerHTML = "👁" // Using unicode for hide icon hideControls.appendChild(hideOn) From c7bdd869671c794bc569ab2691fd7efc4fdaec72 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 11 Mar 2025 20:44:39 +0800 Subject: [PATCH 04/72] code commenting --- src/SEQTA.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 123b507f..700ed49a 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -992,31 +992,31 @@ function handleTimetableZoom(): void { } function handleTimetableAssessmentHide(): void { - const hideControls = document.createElement("div") + const hideControls = document.createElement("div") // Creates the div element which houses the eye icon hideControls.className = "timetable-hide-controls" - const hideOn = document.createElement("button") + const hideOn = document.createElement("button") // Creates the actual button which is clicked hideOn.className = "uiButton timetable-hide iconFamily" hideOn.innerHTML = "👁" // Using unicode for hide icon hideControls.appendChild(hideOn) - const toolbar = document.getElementById("toolbar") + const toolbar = document.getElementById("toolbar") // Appends the new button to the toolbar toolbar?.appendChild(hideControls) function hideElements(): void { - const entries = document.querySelectorAll(".entry") + const entries = document.querySelectorAll(".entry") // Gets all the timetables entries on the page, and loops through entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement - if (!entryEl.classList.contains("assessment") && !(entryEl.style.display === "none")) { + if (!entryEl.classList.contains("assessment") && !(entryEl.style.display === "none")) { // If the entry is not an assessment, and hasn't already been hidden, hide it. entryEl.style.display = "none" - } else { + } else { // Otherwise, it should be shown. entryEl.style.display = "" } }) } - hideOn.addEventListener("click", () => { + hideOn.addEventListener("click", () => { // Listen for when the button is pressed hideElements() }) From 5eb92bc87a7105bc0ed182e8308b3541cb076ac9 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Wed, 12 Mar 2025 20:52:48 +1100 Subject: [PATCH 05/72] fix: builds failing and css failing to load in frontend --- .postcss.config.js | 6 ++++++ .postcssrc.json | 5 +++++ package.json | 10 +++++----- src/SEQTA.ts | 4 ++-- src/interface/index.css | 2 -- src/interface/main.ts | 2 +- src/manifests/manifest.json | 2 +- vite.config.ts | 3 --- 8 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 .postcss.config.js create mode 100644 .postcssrc.json diff --git a/.postcss.config.js b/.postcss.config.js new file mode 100644 index 00000000..e99ebc2c --- /dev/null +++ b/.postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/.postcssrc.json b/.postcssrc.json new file mode 100644 index 00000000..d42c370f --- /dev/null +++ b/.postcssrc.json @@ -0,0 +1,5 @@ +{ + "plugins": { + "tailwindcss": {} + } +} \ No newline at end of file diff --git a/package.json b/package.json index 66f6a1dd..953229a8 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "sass": "^1.85.1", "sass-loader": "^16.0.5", "semver": "^7.7.1", + "tailwindcss": "3.4.11", "url": "^0.11.4" }, "dependencies": { @@ -60,7 +61,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.36.4", "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/forms": "^0.5.9", "@tailwindcss/postcss": "^4.0.12", "@tailwindcss/vite": "^4.0.12", "@tsconfig/svelte": "^5.0.4", @@ -89,13 +90,12 @@ "million": "^3.1.11", "motion": "^12.4.12", "postcss": "^8.5.3", - "react": "^19.0.0", - "react-best-gradient-color-picker": "^3.0.14", - "react-dom": "^19.0.0", + "react": "17", + "react-best-gradient-color-picker": "3.0.11", + "react-dom": "17", "rss-parser": "^3.13.0", "sortablejs": "^1.15.6", "svelte": "^5.22.6", - "tailwindcss": "^4.0.12", "typescript": "^5.8.2", "uuid": "^11.1.0", "vite": "^6.2.1", diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 700ed49a..306036eb 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -3046,10 +3046,10 @@ export async function SendNewsPage() { ;(titlediv! as HTMLElement).innerText = "News" AppendLoadingSymbol("newsloading", "#news-container") - const response = await browser.runtime.sendMessage({ + const response = (await browser.runtime.sendMessage({ type: "sendNews", source: settingsState.newsSource, - }) + })) as any const newscontainer = document.querySelector("#news-container") document.getElementById("newsloading")?.remove() diff --git a/src/interface/index.css b/src/interface/index.css index 07b9d1ad..dedb557e 100644 --- a/src/interface/index.css +++ b/src/interface/index.css @@ -1,6 +1,4 @@ @import './components/ColourPicker.css'; -@import "tailwindcss"; - @tailwind base; @tailwind components; diff --git a/src/interface/main.ts b/src/interface/main.ts index 0fe7d1c9..63ec7ce1 100644 --- a/src/interface/main.ts +++ b/src/interface/main.ts @@ -19,6 +19,6 @@ export default function renderSvelte( style.setAttribute("type", "text/css") style.innerHTML = styles mountPoint.appendChild(style) - + return app } diff --git a/src/manifests/manifest.json b/src/manifests/manifest.json index 5e6fca90..82f6c37e 100644 --- a/src/manifests/manifest.json +++ b/src/manifests/manifest.json @@ -32,7 +32,7 @@ ], "web_accessible_resources": [ { - "resources": ["*://*/*"], + "resources": ["*/*"], "matches": ["*://*/*"] }, { diff --git a/vite.config.ts b/vite.config.ts index 2fb94acf..25582b7f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,6 @@ import { defineConfig } from 'vite'; import { join, resolve } from 'path'; -import tailwindcss from '@tailwindcss/vite' - import { updateManifestPlugin } from './lib/patchPackage'; import { base64Loader } from './lib/base64loader'; import type { BuildTarget } from './lib/types'; @@ -32,7 +30,6 @@ export default defineConfig(({ command }) => ({ plugins: [ base64Loader, react(), - tailwindcss(), svelte({ emitCss: false }), From 1263c1c8ef7d9afa1a89ed55b31b6d60f3b492de Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Wed, 12 Mar 2025 20:57:33 +1100 Subject: [PATCH 06/72] feat: remove colour pallete flattening --- tailwind.config.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/tailwind.config.js b/tailwind.config.js index 383a3120..7b550250 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,5 +1,3 @@ -import flattenColorPalette from "tailwindcss/lib/util/flattenColorPalette"; - /** @type {import('tailwindcss').Config} */ export default { content: [ @@ -42,17 +40,5 @@ export default { }, plugins: [ require('@tailwindcss/forms'), - addVariablesForColors, ], -}; - -function addVariablesForColors({ addBase, theme }) { - let allColors = flattenColorPalette(theme("colors")); - let newVars = Object.fromEntries( - Object.entries(allColors).map(([key, val]) => [`--${key}`, val]) - ); - - addBase({ - ":root": newVars, - }); -} \ No newline at end of file +}; \ No newline at end of file From ca7e6b913795cce8d90bea6885037f20a853ebed Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Wed, 12 Mar 2025 21:46:01 +1100 Subject: [PATCH 07/72] feat: upgrade to tailwindcss v4 --- .postcss.config.js | 6 --- .postcssrc.json | 5 --- package.json | 4 +- .../components/TabbedContainer.svelte | 8 ++-- .../components/store/Backgrounds.svelte | 26 +++++------ .../components/store/CoverSwiper.svelte | 14 +++--- .../components/store/FilterPanel.svelte | 4 +- src/interface/components/store/Header.svelte | 10 ++--- .../components/store/ThemeCard.svelte | 2 +- .../components/store/ThemeModal.svelte | 4 +- .../components/themes/BackgroundItem.svelte | 2 +- .../components/themes/ThemeSelector.svelte | 2 +- src/interface/index.css | 42 ++++++++++++++++-- src/interface/pages/themeCreator.svelte | 10 ++--- tailwind.config.js | 44 ------------------- 15 files changed, 81 insertions(+), 102 deletions(-) delete mode 100644 .postcss.config.js delete mode 100644 .postcssrc.json delete mode 100644 tailwind.config.js diff --git a/.postcss.config.js b/.postcss.config.js deleted file mode 100644 index e99ebc2c..00000000 --- a/.postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/.postcssrc.json b/.postcssrc.json deleted file mode 100644 index d42c370f..00000000 --- a/.postcssrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": { - "tailwindcss": {} - } -} \ No newline at end of file diff --git a/package.json b/package.json index 953229a8..c5897e9c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "sass": "^1.85.1", "sass-loader": "^16.0.5", "semver": "^7.7.1", - "tailwindcss": "3.4.11", + "tailwindcss": "^4.0.13", "url": "^0.11.4" }, "dependencies": { @@ -62,7 +62,7 @@ "@codemirror/view": "^6.36.4", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/postcss": "^4.0.12", + "@tailwindcss/postcss": "^4.0.13", "@tailwindcss/vite": "^4.0.12", "@tsconfig/svelte": "^5.0.4", "@types/chrome": "^0.0.308", diff --git a/src/interface/components/TabbedContainer.svelte b/src/interface/components/TabbedContainer.svelte index 5771af5b..c5866562 100644 --- a/src/interface/components/TabbedContainer.svelte +++ b/src/interface/components/TabbedContainer.svelte @@ -41,7 +41,7 @@
-
+
{#each tabs as { title }, index}
-
+
{#each tabs as { Content, props }, index} -
diff --git a/src/interface/components/store/Backgrounds.svelte b/src/interface/components/store/Backgrounds.svelte index 8dbc4412..26cf2805 100644 --- a/src/interface/components/store/Backgrounds.svelte +++ b/src/interface/components/store/Backgrounds.svelte @@ -176,7 +176,7 @@
-
+

Categories

-
+
-
+

Explore Backgrounds {searchTerm ? `- "${searchTerm}"` : ''}

-
+
setSearchTerm(e.target.value)} - class="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-none focus:border-transparent" /> + class="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-hidden focus:border-transparent" /> {theme.name}
-
+
Theme Preview
diff --git a/src/interface/components/store/ThemeModal.svelte b/src/interface/components/store/ThemeModal.svelte index 7796568d..e7752e70 100644 --- a/src/interface/components/store/ThemeModal.svelte +++ b/src/interface/components/store/ThemeModal.svelte @@ -54,7 +54,7 @@
{ if (e.target === e.currentTarget) hideModal(); }} @@ -115,7 +115,7 @@
{relatedTheme.name}
-
+
Theme Preview
diff --git a/src/interface/components/themes/BackgroundItem.svelte b/src/interface/components/themes/BackgroundItem.svelte index e03945a0..a8a194bb 100644 --- a/src/interface/components/themes/BackgroundItem.svelte +++ b/src/interface/components/themes/BackgroundItem.svelte @@ -15,7 +15,7 @@ onkeydown={onClick} tabindex="-1" role="button" - class="relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-zinc-500/50 ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-4 ring-4' : 'ring-0'}" + class="relative w-16 h-16 cursor-pointer rounded-xl transition ring-3 dark:ring-zinc-500/50 ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-4 ring-4' : 'ring-0'}" > {#if isEditMode}
handleThemeClick(theme)} > {#if isEditMode} diff --git a/src/interface/index.css b/src/interface/index.css index dedb557e..447cfa1d 100644 --- a/src/interface/index.css +++ b/src/interface/index.css @@ -1,8 +1,42 @@ -@import './components/ColourPicker.css'; +@import './components/ColourPicker.css' layer(base); +@import 'tailwindcss'; -@tailwind base; -@tailwind components; -@tailwind utilities; +@plugin '@tailwindcss/forms'; + +@custom-variant dark (&:is(.dark *)); + +@theme { + --text-*: initial; + --text-xs: 0.65rem; + --text-sm: 0.775rem; + --text-base: 0.65rem; + --text-md: 0.65rem; + --text-lg: 1rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + --text-6xl: 4rem; + --text-7xl: 5rem; + --text-8xl: 6rem; + --text-9xl: 8rem; + --text-10xl: 10rem; + --text-11xl: 12rem; + --text-12xl: 14rem; + --text-13xl: 16rem; + --text-14xl: 18rem; + + --font-IconFamily: IconFamily; + + --animate-spin-fast: spin 0.4s linear infinite; + + --aspect-theme: 5 / 1; +} + +button { + @apply cursor-pointer; +} :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index 3248b7b2..d200bd45 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -210,14 +210,14 @@ {#each theme.CustomImages as image (image.id)}
- {image.variableName} + {image.variableName}
onImageVariableChange(image.id, e.currentTarget.value)} placeholder="CSS Variable Name" - class="flex-grow flex-[3] w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-600/50 focus:bg-zinc-300/50 dark:focus:bg-zinc-600" + class="grow flex-3 w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-600/50 focus:bg-zinc-300/50 dark:focus:bg-zinc-600" /> + +
+ +
+ + {#if currentMode === 'gradient'} +
+ +
+ +
+ + + {#each gradientStops as stop, i} + + {/each} +
+ + +
+
+ + + {gradientAngle}° +
+ + {#if gradientStops.length > 2} + + {/if} +
+
+ {/if}
{:else} - +
{ e.key === 'Enter' && handleBackgroundClick }} + role="button" + aria-label="Close color picker" + tabindex="0" >
- changeColour()} /> + +
+ + +
+ +
+ + {#if currentMode === 'gradient'} +
+ +
+ +
+ + + {#each gradientStops as stop, i} + + {/each} +
+ + +
+
+ + + {gradientAngle}° +
+ + {#if gradientStops.length > 2} + + {/if} +
+
+ {/if}
{/if} diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index a1a59716..d4cc659c 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -97,7 +97,7 @@ export async function init() { // TEMP 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") { - import("./css/injected.scss") + import("../css/injected.scss") } else { const injectedStyle = document.createElement("style") injectedStyle.textContent = injectedCSS diff --git a/vite.config.ts b/vite.config.ts index 4f80b022..0e408fcf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,7 +6,6 @@ import { base64Loader } from './lib/base64loader'; import type { BuildTarget } from './lib/types'; import ClosePlugin from './lib/closePlugin'; -import react from '@vitejs/plugin-react'; import million from "million/compiler"; //import MillionLint from '@million/lint'; @@ -31,7 +30,6 @@ const sourcemap = (process.env.SOURCEMAP === "true") || false; // Check whether export default defineConfig(({ command }) => ({ plugins: [ base64Loader, - react(), tailwindcss(), svelte({ emitCss: false From 9f7b46d2adfccaf8ff35be6aaab50c5e92cf884d Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Mon, 17 Mar 2025 13:45:16 +1100 Subject: [PATCH 21/72] feat: add back react colour picker --- package.json | 4 +- src/interface/components/ColourPicker.svelte | 510 ++----------------- src/interface/components/ColourPicker.tsx | 108 ++++ vite.config.ts | 4 +- 4 files changed, 146 insertions(+), 480 deletions(-) create mode 100644 src/interface/components/ColourPicker.tsx diff --git a/package.json b/package.json index abcfdadb..08e34ad9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "@codemirror/search": "^6.5.10", "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.36.4", - "@jaames/iro": "^5.5.2", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@tailwindcss/forms": "^0.5.9", "@tailwindcss/vite": "^4.0.12", @@ -86,6 +85,9 @@ "lodash": "^4.17.21", "million": "^3.1.11", "motion": "^12.4.12", + "react": "17", + "react-best-gradient-color-picker": "3.0.11", + "react-dom": "17", "rss-parser": "^3.13.0", "sortablejs": "^1.15.6", "svelte": "^5.22.6", diff --git a/src/interface/components/ColourPicker.svelte b/src/interface/components/ColourPicker.svelte index 4095fc8e..fde37b7f 100644 --- a/src/interface/components/ColourPicker.svelte +++ b/src/interface/components/ColourPicker.svelte @@ -1,93 +1,20 @@ {#if standalone} -
-
- - -
- -
- - {#if currentMode === 'gradient'} -
- -
- -
- - - {#each gradientStops as stop, i} - - {/each} -
- - -
-
- - - {gradientAngle}° -
- - {#if gradientStops.length > 2} - - {/if} -
-
- {/if} +
+
{:else} - +
{ e.key === 'Enter' && handleBackgroundClick }} - role="button" - aria-label="Close color picker" - tabindex="0" >
- -
- - -
- -
- - {#if currentMode === 'gradient'} -
- -
- -
- - - {#each gradientStops as stop, i} - - {/each} -
- - -
-
- - - {gradientAngle}° -
- - {#if gradientStops.length > 2} - - {/if} -
-
- {/if} +
{/if} diff --git a/src/interface/components/ColourPicker.tsx b/src/interface/components/ColourPicker.tsx new file mode 100644 index 00000000..46eaf224 --- /dev/null +++ b/src/interface/components/ColourPicker.tsx @@ -0,0 +1,108 @@ +import ColorPicker from "react-best-gradient-color-picker" +import { useEffect, useRef, useState } from "react" +import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" + +const defaultPresets = [ + "linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)", + "linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)", + "linear-gradient(40deg, rgba(0, 141, 201, 0.76) 0%, rgba(8, 5, 170, 0.66) 100%)", + "linear-gradient(40deg, rgba(0, 201, 20, 0.76) 0%, rgba(4, 160, 105, 0.66) 100%)", + "linear-gradient(40deg, rgba(199, 20, 55, 0.76) 0%, rgba(95, 11, 160, 0.66) 100%)", + "linear-gradient(40deg, rgba(24, 20, 199, 0.76) 0%, rgba(23, 173, 65, 0.66) 100%)", + "radial-gradient(circle, rgba(20, 199, 178, 0.76) 32%, rgba(3, 120, 57, 0.66) 100%)", + "radial-gradient(circle, rgba(13, 15, 145, 0.76) 12%, rgba(103, 3, 120, 0.66) 100%)", + "linear-gradient(20deg, rgb(230, 21, 21) 0%, rgb(230, 109, 21) 12%, rgb(230, 34, 21) 26%, rgb(230, 21, 21) 39%, rgb(230, 84, 21) 48%, rgb(230, 34, 21) 58%, rgb(230, 96, 21) 69%, rgb(230, 34, 21) 80%, rgb(230, 71, 21) 89%, rgb(230, 21, 21) 100%)", + "rgba(114, 1, 170, 0.89)", + "rgba(93, 135, 63, 0.89)", + "rgba(4, 4, 138, 0.77)", + "rgba(21, 20, 20, 0.89)", + "linear-gradient(340deg, rgb(205, 74, 82) 18%, rgba(132, 8, 8, 0.89) 46%, rgb(204, 78, 85) 72%)", + "radial-gradient(circle, rgb(74, 205, 158) 0%, rgba(8, 72, 132, 0.89) 99%)", + "rgba(17, 94, 89, 1)", + "rgba(30, 64, 175, 0.89)", + "rgba(134, 25, 143, 1)", + "rgba(14, 165, 233, 0.9)", +] + +interface PickerProps { + customOnChange?: (color: string) => void + customState?: string + savePresets?: boolean +} + +export default function Picker({ + customOnChange, + customState, + savePresets = true, +}: PickerProps) { + const [customThemeColor, setCustomThemeColor] = useState() + const [presets, setPresets] = useState() + + const latestValuesRef = useRef({ customThemeColor, customOnChange, savePresets, presets }); + + useEffect(() => { + if (customState !== undefined && customState !== null) { + setCustomThemeColor(customState) + } else { + setCustomThemeColor(settingsState.selectedColor ?? null) + } + + if (presets === undefined) { + const savedPresets = localStorage.getItem("colorPickerPresets") + setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets) + } + }, []) + + useEffect(() => { + latestValuesRef.current = { customThemeColor, customOnChange, savePresets, presets }; + }, [customThemeColor, customOnChange, savePresets, presets]); + + useEffect(() => { + return () => { + const { customThemeColor, customOnChange, savePresets, presets } = latestValuesRef.current; + if (!(customThemeColor && !customOnChange && savePresets && presets)) return; + + // Only proceed if presets are different (avoid unnecessary updates) + const existingIndex = presets.indexOf(customThemeColor); + let updatedPresets; + + if (existingIndex === 0) { + // No need to update if the selected color is already the first element + return; + } else if (existingIndex > -1) { + updatedPresets = [ + customThemeColor, + ...presets.slice(0, existingIndex), + ...presets.slice(existingIndex + 1), + ]; + } else { + updatedPresets = [customThemeColor, ...presets].slice(0, 18); + } + + localStorage.setItem("colorPickerPresets", JSON.stringify(updatedPresets)); + } + }, []) + + useEffect(() => { + if (customThemeColor && !customOnChange) { + settingsState.selectedColor = customThemeColor + } + }, [customThemeColor, customOnChange]) + + return ( + { + if (customOnChange) { + customOnChange(color) + setCustomThemeColor(color) + } else { + setCustomThemeColor(color) + } + }} + /> + ) +} diff --git a/vite.config.ts b/vite.config.ts index 0e408fcf..90ec170a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,7 +25,7 @@ const targets: BuildTarget[] = [ ] 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. +//const sourcemap = (process.env.SOURCEMAP === "true") || false; // Check whether we want sourcemaps. export default defineConfig(({ command }) => ({ plugins: [ @@ -74,7 +74,7 @@ export default defineConfig(({ command }) => ({ outDir: resolve(__dirname, 'dist', mode), emptyOutDir: false, minify: false, - sourcemap: sourcemap, + //sourcemap: sourcemap, rollupOptions: { input: { settings: join(__dirname, 'src', 'interface', 'index.html'), From fe2fa87cb519952437a5c2db399a954c93c796ce Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Mon, 17 Mar 2025 13:46:38 +1100 Subject: [PATCH 22/72] feat: add ReactAdaptor.svelte --- package.json | 2 ++ .../components/utils/ReactAdapter.svelte | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/interface/components/utils/ReactAdapter.svelte diff --git a/package.json b/package.json index 08e34ad9..f8225af7 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "@bedframe/cli": "^0.0.91", "@crxjs/vite-plugin": "2.0.0-beta.25", "@types/mime-types": "^2.1.4", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", "cross-env": "^7.0.3", "dependency-cruiser": "^16.10.0", "eslint": "9.22.0", diff --git a/src/interface/components/utils/ReactAdapter.svelte b/src/interface/components/utils/ReactAdapter.svelte new file mode 100644 index 00000000..2a7de2fc --- /dev/null +++ b/src/interface/components/utils/ReactAdapter.svelte @@ -0,0 +1,27 @@ + + +
\ No newline at end of file From 75446c68558d6e3c1e5fd6a0262765a0e33b99bd Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Mon, 17 Mar 2025 13:55:29 +1100 Subject: [PATCH 23/72] chore: clean up imports in monofile.ts --- src/css/injected.scss | 1 + src/plugins/monofile.ts | 37 +++++++++++-------------------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/css/injected.scss b/src/css/injected.scss index ac7768da..16746f30 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -2174,6 +2174,7 @@ body { > .entriesWrapper > .entry { padding: 3px; + transition: opacity 0.2s ease-in-out; } .Viewer__Viewer___32BH- { background: unset; diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index d4cc659c..ca26d42e 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -1,12 +1,11 @@ // Third-party libraries - - import browser from "webextension-polyfill" import { animate, stagger } from "motion" // Internal utilities and functions -import { waitForElm } from "@/seqta/utils/waitForElm" +import { ChangeMenuItemPositions, MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions" import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour" +import { waitForElm } from "@/seqta/utils/waitForElm" import { delay } from "@/seqta/utils/delay" import stringToHTML from "@/seqta/utils/stringToHTML" import { MessageHandler } from "@/seqta/utils/listeners/MessageListener" @@ -15,47 +14,33 @@ import { settingsState, } from "@/seqta/utils/listeners/SettingsState" import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges" +import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat" import { eventManager } from "@/seqta/utils/listeners/EventManager" // UI and theme management +import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners" import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements" import { enableCurrentTheme } from "@/seqta/ui/themes/enableCurrent" -import loading from "@/seqta/ui/Loading" import { updateAllColors } from "@/seqta/ui/colors/Manager" import pageState from "@/pageState.js?url" +import loading from "@/seqta/ui/Loading" +import { SendNewsPage } from "@/seqta/utils/SendNewsPage" +import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage" +import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew" // JSON content import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json" - // Icons and fonts import IconFamily from "@/resources/fonts/IconFamily.woff" - import icon48 from "@/resources/icons/icon-48.png?base64" -import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat" - // Stylesheets import iframeCSS from "@/css/iframe.scss?raw" import injectedCSS from "@/css/injected.scss?inline" import documentLoadCSS from "@/css/documentload.scss?inline" - - - -import { ChangeMenuItemPositions, MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions" -import { SendNewsPage } from "@/seqta/utils/SendNewsPage" - -import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage" - -import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" - -import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew" - - - - var IsSEQTAPage = false let hasSEQTAText = false @@ -509,10 +494,10 @@ function handleTimetableAssessmentHide(): void { const entries = document.querySelectorAll(".entry") // Gets all the timetables entries on the page, and loops through entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement - if (!entryEl.classList.contains("assessment") && !(entryEl.style.display === "none")) { // If the entry is not an assessment, and hasn't already been hidden, hide it. - entryEl.style.display = "none" + if (!entryEl.classList.contains("assessment") && !(entryEl.style.opacity === "0.3")) { // If the entry is not an assessment, and hasn't already been hidden, hide it. + entryEl.style.opacity = "0.3" } else { // Otherwise, it should be shown. - entryEl.style.display = "" + entryEl.style.opacity = "1" } }) } From 5c0044a4d4fa2ee26ff769994e0ffd39d262e8cc Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Mon, 17 Mar 2025 15:06:26 +1100 Subject: [PATCH 24/72] feat: cleanup work on plugins system --- src/SEQTA.ts | 10 ++--- src/plugins/hello.ts | 3 -- src/plugins/index.ts | 2 +- src/plugins/monofile.ts | 83 +---------------------------------------- src/plugins/themes.ts | 5 +++ src/seqta/main.ts | 49 ++++++++++++++++++++++++ 6 files changed, 59 insertions(+), 93 deletions(-) delete mode 100644 src/plugins/hello.ts create mode 100644 src/plugins/themes.ts create mode 100644 src/seqta/main.ts diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 7ffd0cc1..9712adf4 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -1,6 +1,5 @@ import { - initializeSettingsState, settingsState, } from "@/seqta/utils/listeners/SettingsState" import documentLoadCSS from "@/css/documentload.scss?inline" @@ -21,9 +20,8 @@ if (document.childNodes[1]) { init() } -import * as plugins from "@/plugins" // Import the plugins from folder - - +import * as plugins from "@/plugins" +import { main } from "@/seqta/main" async function init() { const hasSEQTATitle = document.title.includes("SEQTA Learn") @@ -40,14 +38,12 @@ async function init() { icon.href = icon48 // Change the icon try { - // wait until settingsState has been loaded from storage - await initializeSettingsState() + await main() if (settingsState.onoff) { Object.values(plugins).forEach(plugin => { plugin(); }) - } console.info( "[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.", diff --git a/src/plugins/hello.ts b/src/plugins/hello.ts deleted file mode 100644 index 6eade5c2..00000000 --- a/src/plugins/hello.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function init() { - console.log('Hello, world!') -} \ No newline at end of file diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 6b498f25..0e84ec2e 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,2 +1,2 @@ export { init as Monofile } from './monofile' -export { init as Hello } from './hello' \ No newline at end of file +export { init as Themes } from './themes' \ No newline at end of file diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index ca26d42e..59470914 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -10,7 +10,6 @@ import { delay } from "@/seqta/utils/delay" import stringToHTML from "@/seqta/utils/stringToHTML" import { MessageHandler } from "@/seqta/utils/listeners/MessageListener" import { - initializeSettingsState, settingsState, } from "@/seqta/utils/listeners/SettingsState" import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges" @@ -21,9 +20,7 @@ import { eventManager } from "@/seqta/utils/listeners/EventManager" import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners" import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements" -import { enableCurrentTheme } from "@/seqta/ui/themes/enableCurrent" import { updateAllColors } from "@/seqta/ui/colors/Manager" -import pageState from "@/pageState.js?url" import loading from "@/seqta/ui/Loading" import { SendNewsPage } from "@/seqta/utils/SendNewsPage" import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage" @@ -34,77 +31,14 @@ import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json" // Icons and fonts import IconFamily from "@/resources/fonts/IconFamily.woff" -import icon48 from "@/resources/icons/icon-48.png?base64" // Stylesheets import iframeCSS from "@/css/iframe.scss?raw" -import injectedCSS from "@/css/injected.scss?inline" -import documentLoadCSS from "@/css/documentload.scss?inline" - -var IsSEQTAPage = false -let hasSEQTAText = false - -// This check is placed outside of the document load event due to issues with EP (https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/84) -if (document.childNodes[1]) { - hasSEQTAText = - document.childNodes[1].textContent?.includes( - "Copyright (c) SEQTA Software", - ) ?? false - init() -} - -export async function init() { - CheckForMenuList() - const hasSEQTATitle = document.title.includes("SEQTA Learn") - - if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) { - IsSEQTAPage = true - console.info("[BetterSEQTA+] Verified SEQTA Page") - - const documentLoadStyle = document.createElement("style") - documentLoadStyle.textContent = documentLoadCSS - document.head.appendChild(documentLoadStyle) - - const icon = document.querySelector('link[rel*="icon"]')! as HTMLLinkElement - icon.href = icon48 - - try { - // wait until settingsState has been loaded from storage - await initializeSettingsState() - - if (settingsState.onoff) { - injectMainScript() - enableCurrentTheme() - - if (typeof settingsState.assessmentsAverage == "undefined") { - settingsState.assessmentsAverage = true - } - - // TEMP 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") { - import("../css/injected.scss") - } else { - const injectedStyle = document.createElement("style") - injectedStyle.textContent = injectedCSS - document.head.appendChild(injectedStyle) - } - } - console.info( - "[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.", - ) - main() - } catch (error: any) { - console.error(error) - } - } -} function SetDisplayNone(ElementName: string) { return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}` } - - async function HideMenuItems(): Promise { try { let stylesheetInnerText: string = "" @@ -125,14 +59,6 @@ async function HideMenuItems(): Promise { } } - - -function injectMainScript() { - const mainScript = document.createElement("script") - mainScript.src = browser.runtime.getURL(pageState) - document.head.appendChild(mainScript) -} - export function hideSideBar() { const sidebar = document.getElementById("menu") // The sidebar element to be closed const main = document.getElementById("main") // The main content element that must be resized to fill the page @@ -173,13 +99,6 @@ export async function finishLoad() { } } - - - - - - - export function GetCSSElement(file: string) { const cssFile = browser.runtime.getURL(file) const fileref = document.createElement("link") @@ -855,7 +774,7 @@ export function showConflictPopup() { }) } -function main() { +export function init() { if (typeof settingsState.onoff === "undefined") { browser.runtime.sendMessage({ type: "setDefaultStorage" }) } diff --git a/src/plugins/themes.ts b/src/plugins/themes.ts new file mode 100644 index 00000000..83d368fa --- /dev/null +++ b/src/plugins/themes.ts @@ -0,0 +1,5 @@ +import { enableCurrentTheme } from "@/seqta/ui/themes/enableCurrent"; + +export async function init() { + enableCurrentTheme(); +} \ No newline at end of file diff --git a/src/seqta/main.ts b/src/seqta/main.ts new file mode 100644 index 00000000..5921ed6b --- /dev/null +++ b/src/seqta/main.ts @@ -0,0 +1,49 @@ +// Third-party libraries +import browser from "webextension-polyfill" + +// Internal utilities and functions +import { + initializeSettingsState, + settingsState, +} from "@/seqta/utils/listeners/SettingsState" + +// UI and theme management +import pageState from "@/pageState.js?url" + +// Stylesheets +import injectedCSS from "@/css/injected.scss?inline" + +export async function main() { + return new Promise(async (resolve, reject) => { + try { + await initializeSettingsState() + + if (settingsState.onoff) { + injectPageState() + + if (typeof settingsState.assessmentsAverage == "undefined") { + settingsState.assessmentsAverage = true + } + + // TEMP 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") { + import("../css/injected.scss") + } else { + const injectedStyle = document.createElement("style") + injectedStyle.textContent = injectedCSS + document.head.appendChild(injectedStyle) + } + } + resolve(true) + } catch (error: any) { + console.error(error) + reject(error) + } + }) +} + +function injectPageState() { + const mainScript = document.createElement("script") + mainScript.src = browser.runtime.getURL(pageState) + document.head.appendChild(mainScript) +} \ No newline at end of file From 6fb4ea53729856a07116e43c6e727fd4903f3e76 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Mon, 17 Mar 2025 19:23:59 +0800 Subject: [PATCH 25/72] feat min: fix spelling mistake --- src/SEQTA.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 9712adf4..66fd570f 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -46,7 +46,7 @@ async function init() { }) } console.info( - "[BetterSEQTA+] Successfully initalised BetterSEQTA+, starting to load assets.", + "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", ) } catch (error: any) { console.error(error) From 77c3761947a97335c8ed5f8bb9669765b1d70ad7 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Mon, 17 Mar 2025 20:35:17 +0800 Subject: [PATCH 26/72] codefix: comment out unused function (may be required later) --- src/plugins/monofile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 59470914..84fb054d 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -861,14 +861,14 @@ export function AppendElementsToDisabledPage() { document.head.append(settingsStyle) } -async function CheckForMenuList() { +/*async function CheckForMenuList() { try { await waitForElm("#menu > ul") ObserveMenuItemPosition() } catch (error) { return } -} +}*/ async function handleAssessments(node: Element): Promise { if (!(node instanceof HTMLElement)) return From da3a68045592eb9d768741a0b3177b0c9d6c9894 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Mon, 17 Mar 2025 20:54:40 +0800 Subject: [PATCH 27/72] refactor: small code quality update --- src/plugins/monofile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 84fb054d..59eba8bd 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -918,7 +918,7 @@ async function handleAssessments(node: Element): Promise { } // Check if it's a letter grade - if (letterGradeMap.hasOwnProperty(trimmedGrade)) { + if (Object.prototype.hasOwnProperty.call(letterGradeMap, trimmedGrade)) { return letterGradeMap[trimmedGrade] } From 587aa5eb8944264439550174e79f6543c0e6849a Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Tue, 18 Mar 2025 07:51:12 +1100 Subject: [PATCH 28/72] feat: add plugin system --- plugins.md | 207 ++++++++++++++++++ src/SEQTA.ts | 15 +- .../built-in/notificationCollector/index.ts | 98 +++++++++ src/plugins/built-in/timetable/index.ts | 196 +++++++++++++++++ src/plugins/core/createAPI.ts | 116 ++++++++++ src/plugins/core/manager.ts | 142 ++++++++++++ src/plugins/core/types.ts | 90 ++++++++ src/plugins/index.ts | 24 +- src/plugins/monofile.ts | 196 +---------------- 9 files changed, 885 insertions(+), 199 deletions(-) create mode 100644 plugins.md create mode 100644 src/plugins/built-in/notificationCollector/index.ts create mode 100644 src/plugins/built-in/timetable/index.ts create mode 100644 src/plugins/core/createAPI.ts create mode 100644 src/plugins/core/manager.ts create mode 100644 src/plugins/core/types.ts diff --git a/plugins.md b/plugins.md new file mode 100644 index 00000000..01ce9cf6 --- /dev/null +++ b/plugins.md @@ -0,0 +1,207 @@ +# BetterSEQTA+ Plugin System + +## Overview +The BetterSEQTA+ plugin system is designed to provide a clean, type-safe, and developer-friendly way to extend the functionality of BetterSEQTA+. While initially focused on built-in plugins, the architecture is designed to potentially support external plugins in the future. + +## Core Concepts + +### Plugin Structure +Each plugin is a simple object that contains metadata and a run function: + +```typescript +const examplePlugin = { + id: 'example', + name: 'Example Plugin', + description: 'Does something cool', + version: '1.0.0', + settings: { + enabled: { type: 'boolean', default: true }, + color: { type: 'string', default: '#ff0000' } + }, + + run: (api) => { + // Plugin logic here + } +}; +``` + +### Plugin API +Plugins receive a powerful API object that provides access to: + +- **Settings**: Type-safe settings management with direct property access +- **SEQTA Integration**: React component mounting and state management +- **Storage**: Persistent storage capabilities +- **Events**: Communication system + +### Settings System +Settings are defined with TypeScript types for safety and accessed like regular properties: + +```typescript +// In your plugin +api.settings.myOption = true; +const value = api.settings.myOption; + +// Watch for changes +api.settings.onChange('myOption', (newValue) => { + console.log('Option changed:', newValue); +}); +``` + +### SEQTA Integration +Plugins can interact with SEQTA's React components: + +```typescript +// Listen for component mounting +api.seqta.onMount('.timetable-view', (element) => { + // Access the DOM element directly + console.log('Timetable mounted:', element); + + // If you need React access, use getFiber + const fiber = api.seqta.getFiber('.timetable-view'); + fiber.setState(prevState => ({ + ...prevState, + someValue: true + })); +}); + +// Get specific component +const fiber = api.seqta.getFiber('.timetable-cell'); +const props = await fiber.getProps(); + +// Listen for page changes +api.seqta.onPageChange((page) => { + if (page === 'timetable') { + // Handle timetable page + } +}); +``` + +## Implementation Status + +### Phase 1: Core Infrastructure ✅ +- [x] Create basic plugin type definitions +- [x] Implement plugin manager +- [x] Set up basic API structure +- [x] Create plugin loading system + +### Phase 2: Settings System ✅ +- [x] Design settings storage structure +- [x] Implement settings proxy system +- [x] Add settings change notifications +- [x] Create settings validation + +### Phase 3: SEQTA Integration ✅ +- [x] Implement component mount detection +- [x] Create ReactFiber wrapper +- [x] Add page change detection +- [x] Create component state utilities + +### Phase 4: Plugin API Features ✅ +- [x] Storage system +- [x] Event system +- [x] Error handling +- [ ] Plugin lifecycle hooks + +### Phase 5: Migration & Testing 🚧 +- [ ] Convert existing features to plugins +- [ ] Create plugin testing utilities +- [ ] Add plugin documentation +- [ ] Create example plugins + +### Phase 6: Future Enhancements 📝 +- [ ] Plugin dependencies system +- [ ] Plugin hot-reloading +- [ ] External plugin support +- [ ] Plugin marketplace infrastructure + +## Plugin Example + +```typescript +const timetablePlugin = { + id: 'timetable', + name: 'Timetable Enhancer', + description: 'Adds extra features to the timetable view', + version: '1.0.0', + settings: { + showWeekends: { + type: 'boolean', + default: false, + description: 'Show weekend days in the timetable' + }, + theme: { + type: 'select', + options: ['light', 'dark', 'auto'], + default: 'auto', + description: 'Timetable theme' + } + }, + + run: async (api) => { + // Listen for timetable mount + api.seqta.onMount('.timetable-view', (element) => { + // Get React access since we need to modify state + const fiber = api.seqta.getFiber('.timetable-view'); + + // Apply settings + if (api.settings.showWeekends) { + fiber.setState(prevState => ({ + ...prevState, + showWeekends: true + })); + } + }); + + // Watch for settings changes + api.settings.onChange('theme', async (newTheme) => { + const timetable = api.seqta.getFiber('.timetable-view'); + if (newTheme !== 'auto') { + await timetable.setProp('theme', newTheme); + } + }); + } +}; +``` + +## Directory Structure +``` +src/ + plugins/ + core/ + types.ts # Core type definitions + createAPI.ts # API implementation + manager.ts # Plugin manager + built-in/ # Built-in plugins + timetable/ + assessments/ + etc... +``` + +## API Type Definitions + +```typescript +interface BSAPI { + seqta: { + onMount: (selector: string, callback: (fiber: ReactFiber) => void) => void; + getFiber: (selector: string) => ReactFiber; + getCurrentPage: () => string; + onPageChange: (callback: (page: string) => void) => void; + }; + + settings: TSettings & { + onChange: ( + key: K, + callback: (value: TSettings[K]) => void + ) => void; + }; + + storage: { + get: (key: string) => Promise; + set: (key: string, value: any) => Promise; + }; + + events: { + on: (event: string, callback: (...args: any[]) => void) => void; + emit: (event: string, ...args: any[]) => void; + }; +} +``` \ No newline at end of file diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 66fd570f..7565dbbe 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -1,4 +1,3 @@ - import { settingsState, } from "@/seqta/utils/listeners/SettingsState" @@ -41,10 +40,18 @@ async function init() { await main() if (settingsState.onoff) { - Object.values(plugins).forEach(plugin => { - plugin(); - }) + // Initialize legacy plugins + const legacyPlugins = [plugins.Monofile, plugins.Themes]; + legacyPlugins.forEach(plugin => { + if (typeof plugin === 'function') { + plugin(); + } + }); + + // Initialize new plugin system + await plugins.initializePlugins(); } + console.info( "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", ) diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts new file mode 100644 index 00000000..cec71a87 --- /dev/null +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -0,0 +1,98 @@ +import { settingsState } from '@/seqta/utils/listeners/SettingsState'; +import type { Plugin, PluginSettings } from '../../core/types'; + +interface NotificationCollectorSettings extends PluginSettings { + enabled: { + type: 'boolean'; + default: boolean; + title: string; + description: string; + }; +} + +const notificationCollectorPlugin: Plugin = { + id: 'notificationCollector', + name: 'Notification Collector', + description: 'Collects and displays SEQTA notifications', + version: '1.0.0', + settings: { + enabled: { + type: 'boolean', + default: true, + title: 'Notification Collector', + description: 'Uncaps the 9+ limit for notifications, showing the real number.', + } + }, + + run: async (api) => { + let pollInterval: number | null = null; + + const checkNotifications = async () => { + try { + const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + timestamp: "1970-01-01 00:00:00.0", + hash: "#?page=/home", + }) + }); + + const data = await response.json(); + const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement; + + if (alertDiv) { + alertDiv.textContent = data.payload.notifications.length.toString(); + } else { + console.info("[BetterSEQTA+] No notifications currently"); + } + } catch (error) { + console.error("[BetterSEQTA+] Error fetching notifications:", error); + } + }; + + // Start polling when enabled + const startPolling = () => { + if (pollInterval) return; // Already polling + checkNotifications(); + pollInterval = window.setInterval(checkNotifications, 30000); + }; + + // Stop polling when disabled + const stopPolling = () => { + if (pollInterval) { + window.clearInterval(pollInterval); + pollInterval = null; + } + }; + + // Start/stop based on initial enabled state + if (settingsState.notificationcollector) { + api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { + startPolling(); + }); + } + + // Store callbacks for cleanup + const enabledCallback = (enabled: boolean) => { + if (enabled) { + startPolling(); + } else { + stopPolling(); + } + }; + + // Handle settings changes + api.settings.onChange('enabled', enabledCallback); + + // Return cleanup function + return () => { + stopPolling(); + api.settings.offChange('enabled', enabledCallback); + }; + } +}; + +export default notificationCollectorPlugin; \ No newline at end of file diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts new file mode 100644 index 00000000..2ba4f4f4 --- /dev/null +++ b/src/plugins/built-in/timetable/index.ts @@ -0,0 +1,196 @@ +import { settingsState } from '@/seqta/utils/listeners/SettingsState'; +import type { Plugin } from '../../core/types'; +import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; +import { waitForElm } from '@/seqta/utils/waitForElm'; + +const timetablePlugin: Plugin = { + id: 'timetable', + name: 'Timetable Enhancer', + description: 'Adds extra features to the timetable view', + version: '1.0.0', + settings: {}, + + run: async (api) => { + api.seqta.onMount('.timetablepage', handleTimetable) + } +}; + +async function handleTimetable(): Promise { + await waitForElm(".time", true, 10) + + // Store original heights when timetable loads + const lessons = document.querySelectorAll(".dailycal .lesson") + lessons.forEach((lesson: Element) => { + const lessonEl = lesson as HTMLElement + lessonEl.setAttribute( + "data-original-height", + lessonEl.offsetHeight.toString(), + ) + }) + + // Existing time format code + if (settingsState.timeFormat == "12") { + const times = document.querySelectorAll(".timetablepage .times .time") + for (const time of times) { + if (!time.textContent) continue + time.textContent = convertTo12HourFormat(time.textContent, true) + } + } + + handleTimetableZoom() + handleTimetableAssessmentHide() +} + +function handleTimetableZoom(): void { + console.log("Initializing timetable zoom controls") + + // Lazy initialize state variables only when function is first called + let timetableZoomLevel = 1 + let baseContainerHeight: number | null = null + const originalEntryPositions = new Map< + Element, + { topRatio: number; heightRatio: number } + >() + + // Create zoom controls + const zoomControls = document.createElement("div") + zoomControls.className = "timetable-zoom-controls" + + const zoomIn = document.createElement("button") + zoomIn.className = "uiButton timetable-zoom iconFamily" + zoomIn.innerHTML = "" // Using unicode for zoom in icon + + const zoomOut = document.createElement("button") + zoomOut.className = "uiButton timetable-zoom iconFamily" + zoomOut.innerHTML = "" // Using unicode for zoom out icon + + zoomControls.appendChild(zoomOut) + zoomControls.appendChild(zoomIn) + + const toolbar = document.getElementById("toolbar") + toolbar?.appendChild(zoomControls) + + const initializePositions = () => { + // Get the base container height from the first TD + const firstDayColumn = document.querySelector( + ".dailycal .content .days td", + ) as HTMLElement + if (!firstDayColumn) return false + + baseContainerHeight = + parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight + + // Store original ratios + const entries = document.querySelectorAll(".entriesWrapper .entry") + entries.forEach((entry: Element) => { + const entryEl = entry as HTMLElement + + // Calculate ratios relative to detected base height + if (baseContainerHeight === null) return + const topRatio = parseInt(entryEl.style.top) / baseContainerHeight + const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight + + originalEntryPositions.set(entry, { topRatio, heightRatio }) + }) + + return true + } + + const updateZoom = () => { + // Initialize positions if not already done + if (baseContainerHeight === null && !initializePositions()) { + console.error("Failed to initialize positions") + return + } + + console.debug(`Updating zoom level to: ${timetableZoomLevel}`) + + // Calculate new container height + if (baseContainerHeight === null) return + const newContainerHeight = baseContainerHeight * timetableZoomLevel + + // Update all day columns (TDs) + const dayColumns = document.querySelectorAll(".dailycal .content .days td") + dayColumns.forEach((td: Element) => { + (td as HTMLElement).style.height = `${newContainerHeight}px` + }) + + // Update all entries using stored ratios + const entries = document.querySelectorAll(".entriesWrapper .entry") + entries.forEach((entry: Element) => { + const entryEl = entry as HTMLElement + const originalRatios = originalEntryPositions.get(entry) + + if (originalRatios) { + // Calculate new positions from original ratios + const newTop = originalRatios.topRatio * newContainerHeight + const newHeight = originalRatios.heightRatio * newContainerHeight + + // Apply new values + entryEl.style.top = `${Math.round(newTop)}px` + entryEl.style.height = `${Math.round(newHeight)}px` + } + }) + + // Update time column to match + const timeColumn = document.querySelector(".times") + if (timeColumn) { + const times = timeColumn.querySelectorAll(".time") + const timeHeight = newContainerHeight / times.length + times.forEach((time: Element) => { + (time as HTMLElement).style.height = `${timeHeight}px` + }) + } + + entries[Math.round((entries.length - 1) / 2)].scrollIntoView({ + behavior: "instant", + block: "center", + }) + } + + zoomIn.addEventListener("click", () => { + if (timetableZoomLevel < 2) { + timetableZoomLevel += 0.2 + updateZoom() + } + }) + + zoomOut.addEventListener("click", () => { + if (timetableZoomLevel > 0.6) { + timetableZoomLevel -= 0.2 + updateZoom() + } + }) +} + +function handleTimetableAssessmentHide(): void { + const hideControls = document.createElement("div") + hideControls.className = "timetable-hide-controls" + + const hideOn = document.createElement("button") + hideOn.className = "uiButton timetable-hide iconFamily" + hideOn.innerHTML = "👁" + + hideControls.appendChild(hideOn) + + const toolbar = document.getElementById("toolbar") + toolbar?.appendChild(hideControls) + + function hideElements(): void { + const entries = document.querySelectorAll(".entry") + entries.forEach((entry: Element) => { + const entryEl = entry as HTMLElement + if (!entryEl.classList.contains("assessment") && !(entryEl.style.opacity === "0.3")) { + entryEl.style.opacity = "0.3" + } else { + entryEl.style.opacity = "1" + } + }) + } + + hideOn.addEventListener("click", () => { + hideElements() + }) +} + +export default timetablePlugin; diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts new file mode 100644 index 00000000..22451223 --- /dev/null +++ b/src/plugins/core/createAPI.ts @@ -0,0 +1,116 @@ +import type { Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, StorageAPI, EventsAPI } from './types'; +import { eventManager } from '@/seqta/utils/listeners/EventManager'; +import ReactFiber from '@/seqta/utils/ReactFiber'; +import browser from 'webextension-polyfill'; + +function createSEQTAAPI(): SEQTAAPI { + return { + onMount: (selector, callback) => { + eventManager.register( + `${selector}Added`, + { + customCheck: (element) => element.matches(selector), + }, + callback + ); + }, + getFiber: (selector) => { + return ReactFiber.find(selector); + }, + getCurrentPage: () => { + const path = window.location.hash.split('?page=/')[1] || ''; + return path.split('/')[0]; + }, + onPageChange: (callback) => { + window.addEventListener('hashchange', () => { + const page = window.location.hash.split('?page=/')[1] || ''; + callback(page.split('/')[0]); + }); + }, + }; +} + +function createSettingsAPI(plugin: Plugin): SettingsAPI { + const storageKey = `plugin.${plugin.id}.settings`; + const listeners = new Map void>>(); + let settings: { [K in keyof T]: T[K]['default'] }; + + // Initialize settings with defaults + settings = Object.entries(plugin.settings).reduce((acc, [key, setting]) => { + acc[key as keyof T] = setting.default; + return acc; + }, {} as { [K in keyof T]: T[K]['default'] }); + + // Load saved settings + browser.storage.local.get(storageKey).then((stored) => { + if (stored[storageKey]) { + Object.assign(settings, stored[storageKey]); + } + }); + + // Create a proxy to handle direct property access + const proxy = new Proxy(settings, { + get(target, prop: string) { + if (prop === 'onChange') { + return (key: keyof T, callback: (value: any) => void) => { + if (!listeners.has(key)) { + listeners.set(key, new Set()); + } + listeners.get(key)!.add(callback); + }; + } + return target[prop as keyof T]; + }, + set(target, prop: string, value: any) { + if (prop === 'onChange') return false; + target[prop as keyof T] = value; + browser.storage.local.set({ [storageKey]: target }); + listeners.get(prop as keyof T)?.forEach(callback => callback(value)); + return true; + }, + }) as SettingsAPI; + + return proxy; +} + +function createStorageAPI(pluginId: string): StorageAPI { + const prefix = `plugin.${pluginId}.storage.`; + + return { + get: async (key: string) => { + const result = await browser.storage.local.get(prefix + key); + return result[prefix + key] as T || null; + }, + set: async (key: string, value: T) => { + await browser.storage.local.set({ [prefix + key]: value }); + }, + }; +} + +function createEventsAPI(pluginId: string): EventsAPI { + const prefix = `plugin.${pluginId}.`; + + return { + on: (event, callback) => { + document.addEventListener(prefix + event, ((e: CustomEvent) => { + callback(...(e.detail || [])); + }) as EventListener); + }, + emit: (event, ...args) => { + document.dispatchEvent( + new CustomEvent(prefix + event, { + detail: args.length > 0 ? args : null + }) + ); + }, + }; +} + +export function createPluginAPI(plugin: Plugin): PluginAPI { + return { + seqta: createSEQTAAPI(), + settings: createSettingsAPI(plugin), + storage: createStorageAPI(plugin.id), + events: createEventsAPI(plugin.id), + }; +} \ No newline at end of file diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts new file mode 100644 index 00000000..19946f8e --- /dev/null +++ b/src/plugins/core/manager.ts @@ -0,0 +1,142 @@ +import type { Plugin, PluginSettings } from './types'; +import { createPluginAPI } from './createAPI'; + +export class PluginManager { + private static instance: PluginManager; + private plugins: Map> = new Map(); + private runningPlugins: Map = new Map(); + private eventBacklog: Map = new Map(); + private cleanupFunctions: Map void> = new Map(); + private listeners: Map void>> = new Map(); + + private constructor() {} + + public static getInstance(): PluginManager { + if (!PluginManager.instance) { + PluginManager.instance = new PluginManager(); + } + return PluginManager.instance; + } + + public dispatchPluginEvent(pluginId: string, event: string, args?: any) { + const fullEventName = `plugin.${pluginId}.${event}`; + + if (this.runningPlugins.get(pluginId)) { + // If plugin is running, dispatch immediately + document.dispatchEvent(new CustomEvent(fullEventName, { detail: args })); + } else { + // Otherwise queue it + const key = `${pluginId}:${event}`; + if (!this.eventBacklog.has(key)) { + this.eventBacklog.set(key, []); + } + this.eventBacklog.get(key)!.push(args); + } + } + + private async processBackloggedEvents(pluginId: string) { + for (const [key, argsList] of this.eventBacklog.entries()) { + const [eventPluginId, event] = key.split(':'); + if (eventPluginId === pluginId) { + for (const args of argsList) { + this.dispatchPluginEvent(pluginId, event, args); + } + this.eventBacklog.delete(key); + } + } + } + + public registerPlugin(plugin: Plugin): void { + if (this.plugins.has(plugin.id)) { + throw new Error(`Plugin with id "${plugin.id}" is already registered`); + } + this.plugins.set(plugin.id, plugin); + } + + public async startPlugin(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin) { + throw new Error(`Plugin "${pluginId}" not found`); + } + + if (this.runningPlugins.get(pluginId)) { + console.warn(`Plugin "${pluginId}" is already running`); + return; + } + + try { + const api = createPluginAPI(plugin); + const result = await plugin.run(api); + if (typeof result === 'function') { + this.cleanupFunctions.set(plugin.id, result); + } + this.runningPlugins.set(pluginId, true); + console.info(`Plugin "${pluginId}" started successfully`); + + // Process any backlogged events + await this.processBackloggedEvents(pluginId); + } catch (error) { + console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error); + throw error; + } + } + + public async startAllPlugins(): Promise { + const startPromises = Array.from(this.plugins.keys()).map(id => + this.startPlugin(id).catch(error => { + console.error(`Failed to start plugin "${id}":`, error); + return Promise.reject(error); + }) + ); + + await Promise.allSettled(startPromises); + } + + public async stopPlugin(pluginId: string): Promise { + const cleanup = this.cleanupFunctions.get(pluginId); + if (cleanup) { + cleanup(); + this.cleanupFunctions.delete(pluginId); + } + this.runningPlugins.set(pluginId, false); + console.info(`Plugin "${pluginId}" stopped`); + this.emit('plugin.stopped', pluginId); + } + + public stopAllPlugins(): void { + Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id)); + } + + public getPlugin(pluginId: string): Plugin | undefined { + return this.plugins.get(pluginId); + } + + public getAllPlugins(): Plugin[] { + return Array.from(this.plugins.values()); + } + + public isPluginRunning(pluginId: string): boolean { + return this.runningPlugins.get(pluginId) || false; + } + + private emit(event: string, ...args: any[]): void { + const listeners = this.listeners.get(event); + if (listeners) { + listeners.forEach(listener => listener(...args)); + } + } + + public on(event: string, callback: (...args: any[]) => void): void { + if (!this.listeners.has(event)) { + this.listeners.set(event, new Set()); + } + this.listeners.get(event)!.add(callback); + } + + public off(event: string, callback: (...args: any[]) => void): void { + const listeners = this.listeners.get(event); + if (listeners) { + listeners.delete(callback); + } + } +} \ No newline at end of file diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts new file mode 100644 index 00000000..fa9c700b --- /dev/null +++ b/src/plugins/core/types.ts @@ -0,0 +1,90 @@ +import ReactFiber from '@/seqta/utils/ReactFiber'; + +interface BooleanSetting { + type: 'boolean'; + default: boolean; + title: string; + description?: string; +} + +interface StringSetting { + type: 'string'; + default: string; + title: string; + description?: string; +} + +interface NumberSetting { + type: 'number'; + default: number; + title: string; + description?: string; +} + +interface SelectSetting { + type: 'select'; + options: readonly T[]; + default: T; + title: string; + description?: string; +} + +type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting; + +// Plugin settings configuration +export type PluginSettings = { + [key: string]: PluginSetting; +} + +// Helper type to extract the actual value type from a setting +type SettingValue = T extends BooleanSetting ? boolean : + T extends StringSetting ? string : + T extends NumberSetting ? number : + T extends SelectSetting ? O : + never; + +// Settings API interface +export type SettingsAPI = { + [K in keyof T]: SettingValue; +} & { + onChange: (key: K, callback: (value: SettingValue) => void) => void; + offChange: (key: K, callback: (value: SettingValue) => void) => void; +} + +// SEQTA API interface +export interface SEQTAAPI { + onMount: (selector: string, callback: (element: Element) => void) => void; + getFiber: (selector: string) => ReactFiber; + getCurrentPage: () => string; + onPageChange: (callback: (page: string) => void) => void; +} + +// Storage API interface +export interface StorageAPI { + get: (key: string) => Promise; + set: (key: string, value: T) => Promise; +} + +// Events API interface +export interface EventsAPI { + on: (event: string, callback: (...args: any[]) => void) => void; + emit: (event: string, ...args: any[]) => void; +} + +// Complete Plugin API interface +export interface PluginAPI { + seqta: SEQTAAPI; + settings: SettingsAPI; + storage: StorageAPI; + events: EventsAPI; +} + +// Plugin interface +export interface Plugin { + id: string; + name: string; + description: string; + version: string; + settings: T; + run: (api: PluginAPI) => void | Promise | (() => void) | Promise<() => void>; +} \ No newline at end of file diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 0e84ec2e..ff167e3d 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,2 +1,22 @@ -export { init as Monofile } from './monofile' -export { init as Themes } from './themes' \ No newline at end of file +import { PluginManager } from './core/manager'; +import timetablePlugin from './built-in/timetable'; +import notificationCollectorPlugin from './built-in/notificationCollector'; + +// Initialize plugin manager +const pluginManager = PluginManager.getInstance(); + +// Register built-in plugins +pluginManager.registerPlugin(timetablePlugin); +pluginManager.registerPlugin(notificationCollectorPlugin); + +// Legacy plugin exports +export { init as Monofile } from './monofile'; +export { init as Themes } from './themes'; + +// New plugin system initialization +export async function initializePlugins(): Promise { + await pluginManager.startAllPlugins(); +} + +// Re-export plugin manager for direct access if needed +export { pluginManager }; \ No newline at end of file diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 59eba8bd..75a92dd2 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -13,11 +13,9 @@ import { settingsState, } from "@/seqta/utils/listeners/SettingsState" import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges" -import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat" import { eventManager } from "@/seqta/utils/listeners/EventManager" // UI and theme management -import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners" import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements" import { updateAllColors } from "@/seqta/ui/colors/Manager" @@ -240,14 +238,14 @@ async function LoadPageElements(): Promise { handleReports, ) - eventManager.register( + /* eventManager.register( "timetableAdded", { elementType: "div", className: "timetablepage", }, handleTimetable, - ) + ) */ eventManager.register( "noticesAdded", @@ -274,159 +272,6 @@ async function LoadPageElements(): Promise { await handleSublink(sublink) } -function handleTimetableZoom(): void { - console.log("Initializing timetable zoom controls") - - // Lazy initialize state variables only when function is first called - let timetableZoomLevel = 1 - let baseContainerHeight: number | null = null - const originalEntryPositions = new Map< - Element, - { topRatio: number; heightRatio: number } - >() - - // Create zoom controls - const zoomControls = document.createElement("div") - zoomControls.className = "timetable-zoom-controls" - - const zoomIn = document.createElement("button") - zoomIn.className = "uiButton timetable-zoom iconFamily" - zoomIn.innerHTML = "" // Using unicode for zoom in icon - - const zoomOut = document.createElement("button") - zoomOut.className = "uiButton timetable-zoom iconFamily" - zoomOut.innerHTML = "" // Using unicode for zoom out icon - - zoomControls.appendChild(zoomOut) - zoomControls.appendChild(zoomIn) - - const toolbar = document.getElementById("toolbar") - toolbar?.appendChild(zoomControls) - - const initializePositions = () => { - // Get the base container height from the first TD - const firstDayColumn = document.querySelector( - ".dailycal .content .days td", - ) as HTMLElement - if (!firstDayColumn) return false - - baseContainerHeight = - parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight - - // Store original ratios - const entries = document.querySelectorAll(".entriesWrapper .entry") - entries.forEach((entry: Element) => { - const entryEl = entry as HTMLElement - - // Calculate ratios relative to detected base height - if (baseContainerHeight === null) return - const topRatio = parseInt(entryEl.style.top) / baseContainerHeight - const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight - - originalEntryPositions.set(entry, { topRatio, heightRatio }) - }) - - return true - } - - const updateZoom = () => { - // Initialize positions if not already done - if (baseContainerHeight === null && !initializePositions()) { - console.error("Failed to initialize positions") - return - } - - console.debug(`Updating zoom level to: ${timetableZoomLevel}`) - - // Calculate new container height - if (baseContainerHeight === null) return - const newContainerHeight = baseContainerHeight * timetableZoomLevel - - // Update all day columns (TDs) - const dayColumns = document.querySelectorAll(".dailycal .content .days td") - dayColumns.forEach((td: Element) => { - (td as HTMLElement).style.height = `${newContainerHeight}px` - }) - - // Update all entries using stored ratios - const entries = document.querySelectorAll(".entriesWrapper .entry") - entries.forEach((entry: Element) => { - const entryEl = entry as HTMLElement - const originalRatios = originalEntryPositions.get(entry) - - if (originalRatios) { - // Calculate new positions from original ratios - const newTop = originalRatios.topRatio * newContainerHeight - const newHeight = originalRatios.heightRatio * newContainerHeight - - // Apply new values - entryEl.style.top = `${Math.round(newTop)}px` - entryEl.style.height = `${Math.round(newHeight)}px` - } - }) - - // Update time column to match - const timeColumn = document.querySelector(".times") - if (timeColumn) { - const times = timeColumn.querySelectorAll(".time") - const timeHeight = newContainerHeight / times.length - times.forEach((time: Element) => { - (time as HTMLElement).style.height = `${timeHeight}px` - }) - } - - entries[Math.round((entries.length - 1) / 2)].scrollIntoView({ - behavior: "instant", - block: "center", - }) - } - - zoomIn.addEventListener("click", () => { - if (timetableZoomLevel < 2) { - timetableZoomLevel += 0.2 - updateZoom() - } - }) - - zoomOut.addEventListener("click", () => { - if (timetableZoomLevel > 0.6) { - timetableZoomLevel -= 0.2 - updateZoom() - } - }) -} - -function handleTimetableAssessmentHide(): void { - const hideControls = document.createElement("div") // Creates the div element which houses the eye icon - hideControls.className = "timetable-hide-controls" - - const hideOn = document.createElement("button") // Creates the actual button which is clicked - hideOn.className = "uiButton timetable-hide iconFamily" - hideOn.innerHTML = "👁" // Using unicode for hide icon - - hideControls.appendChild(hideOn) - - const toolbar = document.getElementById("toolbar") // Appends the new button to the toolbar - toolbar?.appendChild(hideControls) - - function hideElements(): void { - const entries = document.querySelectorAll(".entry") // Gets all the timetables entries on the page, and loops through - entries.forEach((entry: Element) => { - const entryEl = entry as HTMLElement - if (!entryEl.classList.contains("assessment") && !(entryEl.style.opacity === "0.3")) { // If the entry is not an assessment, and hasn't already been hidden, hide it. - entryEl.style.opacity = "0.3" - } else { // Otherwise, it should be shown. - entryEl.style.opacity = "1" - } - }) - } - - hideOn.addEventListener("click", () => { // Listen for when the button is pressed - hideElements() - }) - -} - async function handleNotices(node: Element): Promise { if (!(node instanceof HTMLElement)) return if (!settingsState.animations) return @@ -454,11 +299,8 @@ async function handleSublink(sublink: string | undefined): Promise { await handleNewsPage() break case undefined: - window.location.replace( - `${location.origin}/#?page=/${settingsState.defaultPage}`, - ) + window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`) if (settingsState.defaultPage === "home") loadHomePage() - if (settingsState.defaultPage === "timetable") handleTimetable() if (settingsState.defaultPage === "documents") handleDocuments(document.querySelector(".documents")!) if (settingsState.defaultPage === "reports") @@ -481,48 +323,16 @@ async function handleSublink(sublink: string | undefined): Promise { } } -async function handleTimetable(): Promise { - await waitForElm(".time", true, 10) - - // Store original heights when timetable loads - const lessons = document.querySelectorAll(".dailycal .lesson") - lessons.forEach((lesson: Element) => { - const lessonEl = lesson as HTMLElement - lessonEl.setAttribute( - "data-original-height", - lessonEl.offsetHeight.toString(), - ) - }) - - // Existing time format code - if (settingsState.timeFormat == "12") { - const times = document.querySelectorAll(".timetablepage .times .time") - for (const time of times) { - if (!time.textContent) continue - time.textContent = convertTo12HourFormat(time.textContent, true) - } - } - - handleTimetableZoom() - handleTimetableAssessmentHide() -} - async function handleNewsPage(): Promise { console.info("[BetterSEQTA+] Started Init") if (settingsState.onoff) { SendNewsPage() - if (settingsState.notificationcollector) { - enableNotificationCollector() - } finishLoad() } } async function handleDefault(): Promise { finishLoad() - if (settingsState.notificationcollector) { - enableNotificationCollector() - } } async function handleMessages(node: Element): Promise { From e6f36edabf37242b194acf8378173c314ad54774 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Mon, 17 Mar 2025 20:52:33 +0000 Subject: [PATCH 29/72] [CodeFactor] Apply fixes to commit 587aa5e --- src/plugins/core/createAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts index 22451223..4c257148 100644 --- a/src/plugins/core/createAPI.ts +++ b/src/plugins/core/createAPI.ts @@ -1,4 +1,4 @@ -import type { Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, StorageAPI, EventsAPI } from './types'; +import type { EventsAPI, Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, StorageAPI } from './types'; import { eventManager } from '@/seqta/utils/listeners/EventManager'; import ReactFiber from '@/seqta/utils/ReactFiber'; import browser from 'webextension-polyfill'; From ea46ab41ce9c057d1ec508b43a20e6f6d4beca09 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Tue, 18 Mar 2025 07:53:46 +1100 Subject: [PATCH 30/72] fix: update types --- src/seqta/utils/Loaders/LoadHomePage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index e49c5caf..167f1a28 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -362,7 +362,7 @@ export async function loadHomePage() { func: T, wait: number, ): (...args: Parameters) => void { - let timeout: NodeJS.Timeout + let timeout: any return (...args: Parameters) => { clearTimeout(timeout) timeout = setTimeout(() => func(...args), wait) From f4ae9098d83ab75e17c3d67b4ff81f9d056dea84 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 18 Mar 2025 14:59:32 +0800 Subject: [PATCH 31/72] bug: change theme export to json to avoid accidental execution --- src/seqta/ui/themes/shareTheme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seqta/ui/themes/shareTheme.ts b/src/seqta/ui/themes/shareTheme.ts index eebb11b6..4518ee60 100644 --- a/src/seqta/ui/themes/shareTheme.ts +++ b/src/seqta/ui/themes/shareTheme.ts @@ -6,7 +6,7 @@ const saveThemeFile = (data: object, fileName: string) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${fileName}.json.theme`; + a.download = `${fileName}.theme.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); From 9a71a5241a7c87106f30ae2bb426f4bea7916ec0 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:23:04 +0800 Subject: [PATCH 32/72] vuln-fix: removed image urls, relying on blobs now --- src/interface/pages/themeCreator.svelte | 10 ++++------ src/interface/utils/themeImageHandlers.ts | 4 ++-- src/types/CustomThemes.ts | 2 -- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index d200bd45..382a1948 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -73,10 +73,8 @@ const loadedTheme = { ...tempTheme, CustomImages: tempTheme.CustomImages.map(image => ({ - ...image, - url: image.blob ? URL.createObjectURL(image.blob) : null - })), - coverImageUrl: tempTheme.coverImage ? URL.createObjectURL(tempTheme.coverImage) : undefined + ...image + })) } if (tempTheme) { @@ -210,7 +208,7 @@ {#each theme.CustomImages as image (image.id)}
- {image.variableName} + {image.variableName}
- Cover + Cover {/if}
diff --git a/src/interface/utils/themeImageHandlers.ts b/src/interface/utils/themeImageHandlers.ts index 977d02ff..254ca567 100644 --- a/src/interface/utils/themeImageHandlers.ts +++ b/src/interface/utils/themeImageHandlers.ts @@ -17,7 +17,7 @@ export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promi const variableName = `custom-image-${theme.CustomImages.length}`; resolve({ ...theme, - CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: URL.createObjectURL(imageBlob) }], + CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: null }], }); }; reader.readAsDataURL(file); @@ -51,7 +51,7 @@ export function handleCoverImageUpload(event: Event, theme: LoadedCustomTheme): const reader = new FileReader(); reader.onload = async () => { const imageBlob = await fetch(reader.result as string).then(res => res.blob()); - resolve({ ...theme, coverImage: imageBlob, coverImageUrl: URL.createObjectURL(imageBlob) }); + resolve({ ...theme, coverImage: imageBlob }); }; reader.readAsDataURL(file); }); diff --git a/src/types/CustomThemes.ts b/src/types/CustomThemes.ts index 79990f56..00202ede 100644 --- a/src/types/CustomThemes.ts +++ b/src/types/CustomThemes.ts @@ -20,9 +20,7 @@ export type LoadedCustomTheme = CustomTheme & { id: string; blob: Blob; variableName: string; - url: string | null; }[]; - coverImageUrl?: string; }; export type DownloadedTheme = CustomTheme & { From d06356101a78058c870cfdbcaa881cfafe7a6dc0 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Tue, 18 Mar 2025 18:30:26 +1100 Subject: [PATCH 33/72] feat: display plugin settings in interface --- src/interface/pages/settings/general.svelte | 113 ++++++++++++-- .../built-in/notificationCollector/index.ts | 13 +- src/plugins/built-in/timetable/index.ts | 139 ++++++++++++++--- src/plugins/core/createAPI.ts | 145 ++++++++++++++++-- src/plugins/core/manager.ts | 42 ++++- src/plugins/core/types.ts | 12 +- src/plugins/index.ts | 8 +- src/seqta/utils/listeners/SettingsState.ts | 1 + 8 files changed, 405 insertions(+), 68 deletions(-) diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 7b90e277..09574d16 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -5,12 +5,111 @@ import Select from "@/interface/components/Select.svelte" import browser from "webextension-polyfill" - + import type { SettingsList } from "@/interface/types/SettingsProps" import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" import PickerSwatch from "@/interface/components/PickerSwatch.svelte" import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent" + import { getAllPluginSettings } from "@/plugins" + + interface PluginSetting { + id: string; + title: string; + description?: string; + type: string; + default: any; + options?: Array<{value: string, label: string}>; + } + + interface Plugin { + pluginId: string; + name: string; + settings: Record; + } + + const pluginSettings = getAllPluginSettings() as Plugin[]; + const pluginSettingsValues = $state>>({}); + let nextPluginSettingId = 1000; + const pluginSettingMap = new Map(); + + function getPluginSettingId(pluginId: string, settingKey: string): number { + const id = nextPluginSettingId++; + pluginSettingMap.set(id, {pluginId, settingKey}); + return id; + } + + async function loadPluginSettings() { + for (const plugin of pluginSettings) { + if (Object.keys(plugin.settings).length === 0) continue; + + const storageKey = `plugin.${plugin.pluginId}.settings`; + const stored = await browser.storage.local.get(storageKey); + + pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {}; + + for (const [key, setting] of Object.entries(plugin.settings)) { + if (pluginSettingsValues[plugin.pluginId][key] === undefined) { + pluginSettingsValues[plugin.pluginId][key] = setting.default; + } + } + } + } + + async function updatePluginSetting(pluginId: string, key: string, value: any) { + const storageKey = `plugin.${pluginId}.settings`; + + if (!pluginSettingsValues[pluginId]) { + pluginSettingsValues[pluginId] = {}; + } + pluginSettingsValues[pluginId][key] = value; + + const stored = await browser.storage.local.get(storageKey); + const currentSettings = (stored[storageKey] || {}) as Record; + + currentSettings[key] = value; + + await browser.storage.local.set({ [storageKey]: currentSettings }); + } + + function getPluginSettingEntries() { + const entries: any[] = []; + + pluginSettings.forEach(plugin => { + if (Object.keys(plugin.settings).length === 0) return; + + Object.entries(plugin.settings).forEach(([key, setting]) => { + const id = getPluginSettingId(plugin.pluginId, key); + + entries.push({ + title: setting.title || key, + description: setting.description || '', + id, + Component: setting.type === 'boolean' ? Switch : + setting.type === 'select' ? Select : + setting.type === 'number' ? Slider : + setting.type === 'string' ? (setting.options ? Select : null) : Switch, + props: { + state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default, + onChange: (value: any) => { + if (setting.type === 'number' && typeof value === 'string') { + value = parseFloat(value); + } + updatePluginSetting(plugin.pluginId, key, value); + }, + options: setting.options + } + }); + }); + }); + + return entries; + } + + $effect(() => { + loadPluginSettings(); + }) + const { showColourPicker } = $props<{ showColourPicker: () => void }>(); @@ -28,7 +127,6 @@
{#each [ - { title: "Transparency Effects", description: "Enables transparency effects on certain elements such as blur. (May impact battery life)", @@ -88,16 +186,6 @@ onChange: (isOn: boolean) => settingsState.animations = isOn } }, - { - title: "Notification Collector", - description: "Uncaps the 9+ limit for notifications, showing the real number.", - id: 7, - Component: Switch, - props: { - state: $settingsState.notificationcollector, - onChange: (isOn: boolean) => settingsState.notificationcollector = isOn - } - }, { title: "Assessment Average", description: "Shows your subject average for assessments.", @@ -179,6 +267,7 @@ ] } }, + ...getPluginSettingEntries(), { title: "BetterSEQTA+", description: "Enables BetterSEQTA+ features", diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts index cec71a87..7c26b41c 100644 --- a/src/plugins/built-in/notificationCollector/index.ts +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -1,4 +1,3 @@ -import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import type { Plugin, PluginSettings } from '../../core/types'; interface NotificationCollectorSettings extends PluginSettings { @@ -53,29 +52,29 @@ const notificationCollectorPlugin: Plugin = { } }; - // Start polling when enabled const startPolling = () => { if (pollInterval) return; // Already polling checkNotifications(); pollInterval = window.setInterval(checkNotifications, 30000); }; - // Stop polling when disabled const stopPolling = () => { if (pollInterval) { window.clearInterval(pollInterval); pollInterval = null; + const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement; + if (alertDiv) { + alertDiv.textContent = "9+"; + } } }; - // Start/stop based on initial enabled state - if (settingsState.notificationcollector) { + if (api.settings.enabled) { api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { startPolling(); }); } - // Store callbacks for cleanup const enabledCallback = (enabled: boolean) => { if (enabled) { startPolling(); @@ -84,10 +83,8 @@ const notificationCollectorPlugin: Plugin = { } }; - // Handle settings changes api.settings.onChange('enabled', enabledCallback); - // Return cleanup function return () => { stopPolling(); api.settings.offChange('enabled', enabledCallback); diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts index 2ba4f4f4..d2476546 100644 --- a/src/plugins/built-in/timetable/index.ts +++ b/src/plugins/built-in/timetable/index.ts @@ -1,20 +1,112 @@ import { settingsState } from '@/seqta/utils/listeners/SettingsState'; -import type { Plugin } from '../../core/types'; +import type { Plugin, PluginSettings } from '../../core/types'; import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { waitForElm } from '@/seqta/utils/waitForElm'; -const timetablePlugin: Plugin = { +interface TimetableSettings extends PluginSettings { + enabled: { + type: 'boolean'; + default: boolean; + title: string; + description: string; + }; +} + +const timetablePlugin: Plugin = { id: 'timetable', name: 'Timetable Enhancer', description: 'Adds extra features to the timetable view', version: '1.0.0', - settings: {}, + settings: { + enabled: { + type: 'boolean', + default: true, + title: 'Timetable Enhancer', + description: 'Adds extra features to the timetable view.', + } + }, run: async (api) => { - api.seqta.onMount('.timetablepage', handleTimetable) + if (api.settings.enabled) { + api.seqta.onMount('.timetablepage', handleTimetable) + } + + const enabledCallback = (enabled: boolean) => { + if (enabled) { + api.seqta.onMount('.timetablepage', handleTimetable) + } else { + const timetablePage = document.querySelector('.timetablepage') + if (timetablePage) { + const zoomControls = document.querySelector('.timetable-zoom-controls') + if (zoomControls) zoomControls.remove() + + const hideControls = document.querySelector('.timetable-hide-controls') + if (hideControls) hideControls.remove() + + resetTimetableStyles() + } + } + } + + api.settings.onChange('enabled', enabledCallback) + + return () => { + api.settings.offChange('enabled', enabledCallback) + } } }; +// Store event handlers globally for cleanup +const zoomHandlers = new WeakMap void; zoomOut: () => void }>() + +function resetTimetableStyles(): void { + const firstDayColumn = document.querySelector(".dailycal .content .days td") as HTMLElement + if (!firstDayColumn) return + + const baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight + + const dayColumns = document.querySelectorAll(".dailycal .content .days td") + dayColumns.forEach((td: Element) => { + (td as HTMLElement).style.height = `${baseContainerHeight}px` + }) + + const timeColumn = document.querySelector(".times") + if (timeColumn) { + const times = timeColumn.querySelectorAll(".time") + const timeHeight = baseContainerHeight / times.length + times.forEach((time: Element) => { + (time as HTMLElement).style.height = `${timeHeight}px` + }) + } + + const lessons = document.querySelectorAll(".dailycal .lesson") + lessons.forEach((lesson: Element) => { + const lessonEl = lesson as HTMLElement + const originalHeight = lessonEl.getAttribute('data-original-height') + if (originalHeight) { + lessonEl.style.height = `${originalHeight}px` + } + }) + + const entries = document.querySelectorAll(".entry") + entries.forEach((entry: Element) => { + const entryEl = entry as HTMLElement + entryEl.style.opacity = '1' + }) + + const zoomControls = document.querySelector('.timetable-zoom-controls') + if (zoomControls) { + const handlers = zoomHandlers.get(zoomControls) + if (handlers) { + const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)') + const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)') + if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn) + if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut) + zoomHandlers.delete(zoomControls) + } + } +} + async function handleTimetable(): Promise { await waitForElm(".time", true, 10) @@ -58,11 +150,11 @@ function handleTimetableZoom(): void { const zoomIn = document.createElement("button") zoomIn.className = "uiButton timetable-zoom iconFamily" - zoomIn.innerHTML = "" // Using unicode for zoom in icon + zoomIn.innerHTML = "" // Unicode for zoom in icon (custom iconfamily) const zoomOut = document.createElement("button") zoomOut.className = "uiButton timetable-zoom iconFamily" - zoomOut.innerHTML = "" // Using unicode for zoom out icon + zoomOut.innerHTML = "" // Unicode for zoom out icon (custom iconfamily) zoomControls.appendChild(zoomOut) zoomControls.appendChild(zoomIn) @@ -70,6 +162,27 @@ function handleTimetableZoom(): void { const toolbar = document.getElementById("toolbar") toolbar?.appendChild(zoomControls) + // Store event listener references + const zoomInHandler = () => { + if (timetableZoomLevel < 2) { + timetableZoomLevel += 0.2 + updateZoom() + } + } + + const zoomOutHandler = () => { + if (timetableZoomLevel > 0.6) { + timetableZoomLevel -= 0.2 + updateZoom() + } + } + + zoomIn.addEventListener("click", zoomInHandler) + zoomOut.addEventListener("click", zoomOutHandler) + + // Store references for cleanup + zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler }) + const initializePositions = () => { // Get the base container height from the first TD const firstDayColumn = document.querySelector( @@ -147,20 +260,6 @@ function handleTimetableZoom(): void { block: "center", }) } - - zoomIn.addEventListener("click", () => { - if (timetableZoomLevel < 2) { - timetableZoomLevel += 0.2 - updateZoom() - } - }) - - zoomOut.addEventListener("click", () => { - if (timetableZoomLevel > 0.6) { - timetableZoomLevel -= 0.2 - updateZoom() - } - }) } function handleTimetableAssessmentHide(): void { diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts index 4c257148..9c406c80 100644 --- a/src/plugins/core/createAPI.ts +++ b/src/plugins/core/createAPI.ts @@ -30,7 +30,7 @@ function createSEQTAAPI(): SEQTAAPI { }; } -function createSettingsAPI(plugin: Plugin): SettingsAPI { +function createSettingsAPI(plugin: Plugin): SettingsAPI & { loaded: Promise } { const storageKey = `plugin.${plugin.id}.settings`; const listeners = new Map void>>(); let settings: { [K in keyof T]: T[K]['default'] }; @@ -41,10 +41,35 @@ function createSettingsAPI(plugin: Plugin): Setting return acc; }, {} as { [K in keyof T]: T[K]['default'] }); - // Load saved settings - browser.storage.local.get(storageKey).then((stored) => { - if (stored[storageKey]) { - Object.assign(settings, stored[storageKey]); + // Create a promise that resolves when settings are loaded + const loaded = (async () => { + try { + const stored = await browser.storage.local.get(storageKey); + if (stored[storageKey]) { + Object.entries(stored[storageKey]).forEach(([key, value]) => { + if (key in settings) { + settings[key as keyof T] = value as any; + // Notify any listeners that might have been registered already + listeners.get(key as keyof T)?.forEach(callback => callback(value)); + } + }); + } + } catch (error) { + console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error); + } + })(); + + // Listen for storage changes + browser.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && changes[storageKey]) { + const newValue = changes[storageKey].newValue; + if (newValue) { + // Update settings and notify listeners + Object.entries(newValue).forEach(([key, value]) => { + settings[key as keyof T] = value as any; + listeners.get(key as keyof T)?.forEach(callback => callback(value)); + }); + } } }); @@ -59,32 +84,120 @@ function createSettingsAPI(plugin: Plugin): Setting listeners.get(key)!.add(callback); }; } + if (prop === 'offChange') { + return (key: keyof T, callback: (value: any) => void) => { + listeners.get(key)?.delete(callback); + }; + } + if (prop === 'loaded') { + return loaded; + } return target[prop as keyof T]; }, set(target, prop: string, value: any) { - if (prop === 'onChange') return false; + if (prop === 'onChange' || prop === 'offChange' || prop === 'loaded') return false; target[prop as keyof T] = value; - browser.storage.local.set({ [storageKey]: target }); + + // Store all settings under the plugin's settings key + browser.storage.local.set({ + [storageKey]: target + }); + + // Notify listeners listeners.get(prop as keyof T)?.forEach(callback => callback(value)); return true; }, - }) as SettingsAPI; + }) as SettingsAPI & { loaded: Promise }; return proxy; } function createStorageAPI(pluginId: string): StorageAPI { const prefix = `plugin.${pluginId}.storage.`; + const cache: Record = {}; + const listeners = new Map void>>(); - return { - get: async (key: string) => { - const result = await browser.storage.local.get(prefix + key); - return result[prefix + key] as T || null; + // Load all existing storage values for this plugin + const loadStoragePromise = (async () => { + try { + const allStorage = await browser.storage.local.get(null); + + // Filter for this plugin's storage keys and populate cache + Object.entries(allStorage).forEach(([key, value]) => { + if (key.startsWith(prefix)) { + const shortKey = key.slice(prefix.length); + cache[shortKey] = value; + } + }); + } catch (error) { + console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error); + } + })(); + + // Listen for storage changes + browser.storage.onChanged.addListener((changes, area) => { + if (area === 'local') { + Object.entries(changes).forEach(([key, change]) => { + if (key.startsWith(prefix)) { + const shortKey = key.slice(prefix.length); + cache[shortKey] = change.newValue; + + // Notify listeners + listeners.get(shortKey)?.forEach(callback => callback(change.newValue)); + } + }); + } + }); + + // Create the proxy for direct property access + return new Proxy(cache, { + get(target, prop: string) { + if (prop === 'get') { + return async (key: string) => { + return target[key] as T || null; + }; + } + if (prop === 'set') { + return async (key: string, value: T) => { + target[key] = value; + await browser.storage.local.set({ [prefix + key]: value }); + }; + } + if (prop === 'onChange') { + return (key: string, callback: (value: any) => void) => { + if (!listeners.has(key)) { + listeners.set(key, new Set()); + } + listeners.get(key)!.add(callback); + }; + } + if (prop === 'offChange') { + return (key: string, callback: (value: any) => void) => { + listeners.get(key)?.delete(callback); + }; + } + if (prop === 'loaded') { + return loadStoragePromise; + } + + // Direct property access + return target[prop]; }, - set: async (key: string, value: T) => { - await browser.storage.local.set({ [prefix + key]: value }); - }, - }; + set(target, prop: string, value: any) { + if (['get', 'set', 'onChange', 'offChange', 'loaded'].includes(prop)) { + return false; + } + + // Update cache and store in browser storage + target[prop] = value; + browser.storage.local.set({ [prefix + prop]: value }); + + // Notify listeners + listeners.get(prop)?.forEach(callback => callback(value)); + + return true; + } + }) as StorageAPI; } function createEventsAPI(pluginId: string): EventsAPI { diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index 19946f8e..f7caafb3 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -21,11 +21,10 @@ export class PluginManager { public dispatchPluginEvent(pluginId: string, event: string, args?: any) { const fullEventName = `plugin.${pluginId}.${event}`; + // Dispatch plugin event if it's running otherwise queue it if (this.runningPlugins.get(pluginId)) { - // If plugin is running, dispatch immediately document.dispatchEvent(new CustomEvent(fullEventName, { detail: args })); } else { - // Otherwise queue it const key = `${pluginId}:${event}`; if (!this.eventBacklog.has(key)) { this.eventBacklog.set(key, []); @@ -66,6 +65,13 @@ export class PluginManager { try { const api = createPluginAPI(plugin); + + // Wait for both settings and storage to be loaded before starting the plugin + await Promise.all([ + (api.settings as any).loaded, + api.storage.loaded + ]); + const result = await plugin.run(api); if (typeof result === 'function') { this.cleanupFunctions.set(plugin.id, result); @@ -115,6 +121,38 @@ export class PluginManager { return Array.from(this.plugins.values()); } + public getAllPluginSettings(): Array<{ + pluginId: string; + name: string; + settings: { + [key: string]: { + id: string; + title: string; + description?: string; + type: string; + default: any; + } + } + }> { + return Array.from(this.plugins.entries()).map(([id, plugin]) => { + const settingsEntries = Object.entries(plugin.settings).map(([key, setting]) => { + return [key, { + id: key, + title: (setting as any).title || key, + description: (setting as any).description || '', + type: (setting as any).type, + default: (setting as any).default + }]; + }); + + return { + pluginId: id, + name: plugin.name, + settings: Object.fromEntries(settingsEntries) + }; + }); + } + public isPluginRunning(pluginId: string): boolean { return this.runningPlugins.get(pluginId) || false; } diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts index fa9c700b..657238e3 100644 --- a/src/plugins/core/types.ts +++ b/src/plugins/core/types.ts @@ -31,7 +31,6 @@ interface SelectSetting { type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting; -// Plugin settings configuration export type PluginSettings = { [key: string]: PluginSetting; } @@ -43,15 +42,14 @@ type SettingValue = T extends BooleanSetting ? boolean T extends SelectSetting ? O : never; -// Settings API interface export type SettingsAPI = { [K in keyof T]: SettingValue; } & { onChange: (key: K, callback: (value: SettingValue) => void) => void; offChange: (key: K, callback: (value: SettingValue) => void) => void; + loaded: Promise; // Promise that resolves when settings are loaded } -// SEQTA API interface export interface SEQTAAPI { onMount: (selector: string, callback: (element: Element) => void) => void; getFiber: (selector: string) => ReactFiber; @@ -59,19 +57,20 @@ export interface SEQTAAPI { onPageChange: (callback: (page: string) => void) => void; } -// Storage API interface export interface StorageAPI { get: (key: string) => Promise; set: (key: string, value: T) => Promise; + onChange: (key: string, callback: (value: any) => void) => void; + offChange: (key: string, callback: (value: any) => void) => void; + loaded: Promise; + [key: string]: any; } -// Events API interface export interface EventsAPI { on: (event: string, callback: (...args: any[]) => void) => void; emit: (event: string, ...args: any[]) => void; } -// Complete Plugin API interface export interface PluginAPI { seqta: SEQTAAPI; settings: SettingsAPI; @@ -79,7 +78,6 @@ export interface PluginAPI { events: EventsAPI; } -// Plugin interface export interface Plugin { id: string; name: string; diff --git a/src/plugins/index.ts b/src/plugins/index.ts index ff167e3d..9de2c950 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -13,10 +13,12 @@ pluginManager.registerPlugin(notificationCollectorPlugin); export { init as Monofile } from './monofile'; export { init as Themes } from './themes'; -// New plugin system initialization export async function initializePlugins(): Promise { await pluginManager.startAllPlugins(); } -// Re-export plugin manager for direct access if needed -export { pluginManager }; \ No newline at end of file +export { pluginManager }; + +export function getAllPluginSettings() { + return pluginManager.getAllPluginSettings(); +} \ No newline at end of file diff --git a/src/seqta/utils/listeners/SettingsState.ts b/src/seqta/utils/listeners/SettingsState.ts index a3280c42..a2f6968f 100644 --- a/src/seqta/utils/listeners/SettingsState.ts +++ b/src/seqta/utils/listeners/SettingsState.ts @@ -74,6 +74,7 @@ class StorageManager { } private async saveToStorage(): Promise { + // @ts-expect-error await browser.storage.local.set(this.data); this.notifySubscribers(); } From b644dbbbc7be79a93b7aab9c9c03a42291a4c292 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:37:12 +0800 Subject: [PATCH 34/72] feat: convert base64 in browser to url reference --- src/interface/pages/themeCreator.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index 382a1948..367e1dbe 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -208,7 +208,7 @@ {#each theme.CustomImages as image (image.id)}
- {image.variableName} + {image.variableName}
- Cover + Cover {/if}
From 74e92ddb53988056b7d3d2a060ef839f4e4ac546 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Tue, 18 Mar 2025 20:45:25 +1100 Subject: [PATCH 35/72] feat: add types to storage api --- .../built-in/notificationCollector/index.ts | 26 +++++++++++++-- src/plugins/core/createAPI.ts | 33 +++++++------------ src/plugins/core/manager.ts | 4 +-- src/plugins/core/types.ts | 32 ++++++++++++------ 4 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts index 7c26b41c..fe9644af 100644 --- a/src/plugins/built-in/notificationCollector/index.ts +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -9,7 +9,12 @@ interface NotificationCollectorSettings extends PluginSettings { }; } -const notificationCollectorPlugin: Plugin = { +interface NotificationCollectorStorage { + lastNotificationCount: number; + lastCheckedTime: string; +} + +const notificationCollectorPlugin: Plugin = { id: 'notificationCollector', name: 'Notification Collector', description: 'Collects and displays SEQTA notifications', @@ -26,8 +31,19 @@ const notificationCollectorPlugin: Plugin = { run: async (api) => { let pollInterval: number | null = null; + // Store last notification count in storage + if (!api.storage.lastNotificationCount) { + api.storage.lastNotificationCount = 0; + } + const checkNotifications = async () => { try { + const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement; + + if (api.storage.lastNotificationCount !== 0) { + alertDiv.textContent = api.storage.lastNotificationCount.toString(); + } + const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, { method: 'POST', headers: { @@ -40,10 +56,14 @@ const notificationCollectorPlugin: Plugin = { }); const data = await response.json(); - const alertDiv = document.querySelector(".notifications__bubble___1EkSQ") as HTMLElement; + + // Store notification count for history + const notificationCount = data.payload.notifications.length; + api.storage.lastNotificationCount = notificationCount; + api.storage.lastCheckedTime = new Date().toISOString(); if (alertDiv) { - alertDiv.textContent = data.payload.notifications.length.toString(); + alertDiv.textContent = notificationCount.toString(); } else { console.info("[BetterSEQTA+] No notifications currently"); } diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts index 9c406c80..b1e1cf7d 100644 --- a/src/plugins/core/createAPI.ts +++ b/src/plugins/core/createAPI.ts @@ -112,7 +112,7 @@ function createSettingsAPI(plugin: Plugin): Setting return proxy; } -function createStorageAPI(pluginId: string): StorageAPI { +function createStorageAPI(pluginId: string): StorageAPI & { [K in keyof T]: T[K] } { const prefix = `plugin.${pluginId}.storage.`; const cache: Record = {}; const listeners = new Map void>>(); @@ -152,28 +152,17 @@ function createStorageAPI(pluginId: string): StorageAPI { // Create the proxy for direct property access return new Proxy(cache, { get(target, prop: string) { - if (prop === 'get') { - return async (key: string) => { - return target[key] as T || null; - }; - } - if (prop === 'set') { - return async (key: string, value: T) => { - target[key] = value; - await browser.storage.local.set({ [prefix + key]: value }); - }; - } if (prop === 'onChange') { - return (key: string, callback: (value: any) => void) => { - if (!listeners.has(key)) { - listeners.set(key, new Set()); + return (key: keyof T, callback: (value: T[keyof T]) => void) => { + if (!listeners.has(key as string)) { + listeners.set(key as string, new Set()); } - listeners.get(key)!.add(callback); + listeners.get(key as string)!.add(callback); }; } if (prop === 'offChange') { - return (key: string, callback: (value: any) => void) => { - listeners.get(key)?.delete(callback); + return (key: keyof T, callback: (value: T[keyof T]) => void) => { + listeners.get(key as string)?.delete(callback); }; } if (prop === 'loaded') { @@ -184,7 +173,7 @@ function createStorageAPI(pluginId: string): StorageAPI { return target[prop]; }, set(target, prop: string, value: any) { - if (['get', 'set', 'onChange', 'offChange', 'loaded'].includes(prop)) { + if (['onChange', 'offChange', 'loaded'].includes(prop)) { return false; } @@ -197,7 +186,7 @@ function createStorageAPI(pluginId: string): StorageAPI { return true; } - }) as StorageAPI; + }) as StorageAPI & { [K in keyof T]: T[K] }; } function createEventsAPI(pluginId: string): EventsAPI { @@ -219,11 +208,11 @@ function createEventsAPI(pluginId: string): EventsAPI { }; } -export function createPluginAPI(plugin: Plugin): PluginAPI { +export function createPluginAPI(plugin: Plugin): PluginAPI { return { seqta: createSEQTAAPI(), settings: createSettingsAPI(plugin), - storage: createStorageAPI(plugin.id), + storage: createStorageAPI(plugin.id), events: createEventsAPI(plugin.id), }; } \ No newline at end of file diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index f7caafb3..78017c90 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -3,7 +3,7 @@ import { createPluginAPI } from './createAPI'; export class PluginManager { private static instance: PluginManager; - private plugins: Map> = new Map(); + private plugins: Map> = new Map(); private runningPlugins: Map = new Map(); private eventBacklog: Map = new Map(); private cleanupFunctions: Map void> = new Map(); @@ -45,7 +45,7 @@ export class PluginManager { } } - public registerPlugin(plugin: Plugin): void { + public registerPlugin(plugin: Plugin): void { if (this.plugins.has(plugin.id)) { throw new Error(`Plugin with id "${plugin.id}" is already registered`); } diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts index 657238e3..fc3c9145 100644 --- a/src/plugins/core/types.ts +++ b/src/plugins/core/types.ts @@ -57,13 +57,25 @@ export interface SEQTAAPI { onPageChange: (callback: (page: string) => void) => void; } -export interface StorageAPI { - get: (key: string) => Promise; - set: (key: string, value: T) => Promise; - onChange: (key: string, callback: (value: any) => void) => void; - offChange: (key: string, callback: (value: any) => void) => void; +export interface StorageAPI { + /** + * Register a callback to be called when a storage value changes + */ + onChange: (key: K, callback: (value: T[K]) => void) => void; + + /** + * Remove a previously registered callback + */ + offChange: (key: K, callback: (value: T[K]) => void) => void; + + /** + * Promise that resolves when storage values are loaded + */ loaded: Promise; - [key: string]: any; +} + +export type TypedStorageAPI = StorageAPI & { + [K in keyof T]: T[K]; } export interface EventsAPI { @@ -71,18 +83,18 @@ export interface EventsAPI { emit: (event: string, ...args: any[]) => void; } -export interface PluginAPI { +export interface PluginAPI { seqta: SEQTAAPI; settings: SettingsAPI; - storage: StorageAPI; + storage: TypedStorageAPI; events: EventsAPI; } -export interface Plugin { +export interface Plugin { id: string; name: string; description: string; version: string; settings: T; - run: (api: PluginAPI) => void | Promise | (() => void) | Promise<() => void>; + run: (api: PluginAPI) => void | Promise | (() => void) | Promise<() => void>; } \ No newline at end of file From 9fc24767ecd63a4aea5ebdc19e52271fa915477a Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:37:34 +0800 Subject: [PATCH 36/72] bugfix: theme defaultColor being overridden at all times by default betterseqta+ colour --- src/interface/components/themes/ThemeSelector.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index 9d490729..ab7217f5 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -11,6 +11,7 @@ import { OpenStorePage } from '@/seqta/ui/renderStore' import { themeUpdates } from '@/interface/hooks/ThemeUpdates' import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup' + import { settingsState } from '@/seqta/utils/listeners/SettingsState' let themes = $state(null); let { isEditMode } = $props<{ isEditMode: boolean }>(); @@ -22,8 +23,10 @@ if (theme.id === themes?.selectedTheme) { await disableTheme(); themes.selectedTheme = ''; + settingsState.selectedColor = settingsState.originalSelectedColor; } else { await setTheme(theme.id); + settingsState.selectedColor = theme.defaultColour; if (!themes) return; themes.selectedTheme = theme.id; } From 8e34db4a6725907d724db908ca71fde90bef3765 Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:45:09 +0800 Subject: [PATCH 37/72] feat: synchronise settingstate and theme properly --- src/interface/components/themes/ThemeSelector.svelte | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index ab7217f5..c43696d2 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -23,10 +23,9 @@ if (theme.id === themes?.selectedTheme) { await disableTheme(); themes.selectedTheme = ''; - settingsState.selectedColor = settingsState.originalSelectedColor; } else { await setTheme(theme.id); - settingsState.selectedColor = theme.defaultColour; + settingsState.selectedColor = theme.defaultColour; // settingsState and the theme need to be synchronised. if (!themes) return; themes.selectedTheme = theme.id; } From 7a76d3f4eb77419bedea5828c29f54b3111dbe5b Mon Sep 17 00:00:00 2001 From: Alphons Joseph <93847055+Crazypersonalph@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:03:23 +0800 Subject: [PATCH 38/72] bugfix: Finally fix theme application --- src/interface/components/themes/ThemeSelector.svelte | 2 -- src/seqta/ui/themes/UpdateThemePreview.ts | 8 ++++---- src/seqta/ui/themes/disableTheme.ts | 1 + src/seqta/ui/themes/setTheme.ts | 5 +++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index c43696d2..9d490729 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -11,7 +11,6 @@ import { OpenStorePage } from '@/seqta/ui/renderStore' import { themeUpdates } from '@/interface/hooks/ThemeUpdates' import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup' - import { settingsState } from '@/seqta/utils/listeners/SettingsState' let themes = $state(null); let { isEditMode } = $props<{ isEditMode: boolean }>(); @@ -25,7 +24,6 @@ themes.selectedTheme = ''; } else { await setTheme(theme.id); - settingsState.selectedColor = theme.defaultColour; // settingsState and the theme need to be synchronised. if (!themes) return; themes.selectedTheme = theme.id; } diff --git a/src/seqta/ui/themes/UpdateThemePreview.ts b/src/seqta/ui/themes/UpdateThemePreview.ts index 288ded87..cebc8a28 100644 --- a/src/seqta/ui/themes/UpdateThemePreview.ts +++ b/src/seqta/ui/themes/UpdateThemePreview.ts @@ -42,7 +42,7 @@ export const UpdateThemePreview = async (updatedTheme: LoadedCustomTheme) => { // Apply default color if (defaultColour) { // Store the original color if it hasn't been stored yet - if (originalColor === null) { + if (originalColor == null) { originalColor = settingsState.selectedColor; } settingsState.selectedColor = defaultColour; @@ -62,13 +62,13 @@ export const ClearThemePreview = () => { } // Reset the color to the original value - if (originalColor !== null) { + if (originalColor != null) { settingsState.selectedColor = originalColor; - originalColor = null; + originalColor = settingsState.originalSelectedColor; } // Reset the theme (dark/light mode) to the original value - if (originalTheme !== null) { + if (originalTheme != null) { settingsState.DarkMode = originalTheme; originalTheme = null; } diff --git a/src/seqta/ui/themes/disableTheme.ts b/src/seqta/ui/themes/disableTheme.ts index f033571f..b8e57025 100644 --- a/src/seqta/ui/themes/disableTheme.ts +++ b/src/seqta/ui/themes/disableTheme.ts @@ -27,6 +27,7 @@ export const disableTheme = async () => { } settingsState.selectedTheme = '' + settingsState.selectedColor = settingsState.originalSelectedColor; } catch (error) { console.error('Error disabling theme:', error); } finally { diff --git a/src/seqta/ui/themes/setTheme.ts b/src/seqta/ui/themes/setTheme.ts index 6e560cea..e2b9f8cd 100644 --- a/src/seqta/ui/themes/setTheme.ts +++ b/src/seqta/ui/themes/setTheme.ts @@ -29,8 +29,9 @@ export const setTheme = async (themeId: string) => { await applyTheme(theme); settingsState.selectedTheme = themeId - settingsState.selectedColor = theme.selectedColor ? theme.selectedColor : (theme.defaultColour !== '' ? theme.defaultColour : '#007bff') - settingsState.originalSelectedColor = originalSelectedColor.selectedColor + //settingsState.selectedColor = theme.selectedColor ? theme.selectedColor : (theme.defaultColour !== '' ? theme.defaultColour : '#007bff') + settingsState.originalSelectedColor = settingsState.selectedColor; + settingsState.selectedColor = theme.defaultColour; } catch (error) { console.error('Error setting theme:', error); } From 1c63c06b72d9fa2f8be74d5ea9da8b30dad59339 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Tue, 18 Mar 2025 22:15:40 +1100 Subject: [PATCH 39/72] feat: add docs and dev plugins --- docs/README.md | 60 ++ docs/advanced/plugin-api.md | 421 +++++++++++++ docs/advanced/storage-api.md | 583 ++++++++++++++++++ docs/advanced/third-party-plugins.md | 550 +++++++++++++++++ docs/contributing.md | 262 ++++++++ docs/installation.md | 182 ++++++ docs/plugins/README.md | 155 +++++ docs/plugins/creating-plugins.md | 269 ++++++++ docs/settings/README.md | 301 +++++++++ docs/settings/creating-plugins.md | 335 ++++++++++ docs/settings/custom-ui-components.md | 541 ++++++++++++++++ .../built-in/notificationCollector/index.ts | 40 +- src/plugins/built-in/test/index.ts | 31 + src/plugins/built-in/timetable/index.ts | 48 +- src/plugins/core/settings.ts | 108 ++++ src/plugins/core/types.ts | 15 +- src/plugins/index.ts | 4 + tsconfig.json | 4 + 18 files changed, 3855 insertions(+), 54 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/advanced/plugin-api.md create mode 100644 docs/advanced/storage-api.md create mode 100644 docs/advanced/third-party-plugins.md create mode 100644 docs/contributing.md create mode 100644 docs/installation.md create mode 100644 docs/plugins/README.md create mode 100644 docs/plugins/creating-plugins.md create mode 100644 docs/settings/README.md create mode 100644 docs/settings/creating-plugins.md create mode 100644 docs/settings/custom-ui-components.md create mode 100644 src/plugins/built-in/test/index.ts create mode 100644 src/plugins/core/settings.ts diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..87302c62 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,60 @@ +# BetterSEQTA+ Documentation + +🚧 DOCS UNDER CONSTRUCTION! 🚧 + +Welcome to the BetterSEQTA+ documentation! This documentation will help you understand how BetterSEQTA+ works and how to extend it with plugins and new features. + +## Table of Contents + +### Getting Started +- [Project Overview](./README.md) - This file +- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+ +- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+ + +### Plugin System +- [Plugin System Overview](./plugins/README.md) - Overview of the plugin system +- [Creating Your First Plugin](./plugins/creating-plugins.md) - Guide to creating a simple plugin + +### Settings System +- [Settings System Overview](./settings/README.md) - How the type-safe settings system works +- [Creating Plugins with Settings](./settings/creating-plugins.md) - How to use the decorator-based settings in plugins +- [Creating Custom UI Components](./settings/custom-ui-components.md) - How to create custom UI components for settings + +### Advanced Topics +- [TypeScript Type System](./advanced/typescript.md) - How BetterSEQTA+ leverages TypeScript for type safety +- [Plugin API Reference](./advanced/plugin-api.md) - Detailed reference for the Plugin API +- [Storage API Reference](./advanced/storage-api.md) - Detailed reference for the Storage API + +## Core Concepts + +BetterSEQTA+ is built around several core concepts: + +1. **Plugin System**: BetterSEQTA+ uses a plugin system to extend SEQTA with new features. Plugins are self-contained pieces of code that can be enabled or disabled by the user. + +2. **Type-Safe Settings**: Each plugin can define settings that are type-safe and automatically rendered in the settings UI. The settings system uses TypeScript decorators to make it easy to define settings with proper typing. + +3. **Storage API**: Plugins can use the Storage API to persist data between sessions. The Storage API is also type-safe, ensuring that plugins can only access their own data. + +4. **SEQTA Integration**: BetterSEQTA+ integrates with SEQTA Learn by injecting code into the page. This allows plugins to modify the SEQTA UI and add new features. + +## Getting Help + +If you need help with BetterSEQTA+, you can: + +- [Open an Issue](https://github.com/SeqtaLearning/betterseqta-plus/issues) - Report bugs or request features +- [Join the Discord](https://discord.gg/YzmbnCDkat) - Chat with the community +- [Email the Maintainers](mailto:betterseqta@example.com) - Contact the maintainers directly + +## Contributing to the Documentation + +We welcome contributions to the documentation! If you find something unclear or missing, please open an issue or submit a pull request. + +To contribute to the documentation: + +1. Fork the repository +2. Make your changes to the documentation files +3. Submit a pull request with a clear description of your changes + +## License + +BetterSEQTA+ is licensed under the [MIT License](../LICENSE). \ No newline at end of file diff --git a/docs/advanced/plugin-api.md b/docs/advanced/plugin-api.md new file mode 100644 index 00000000..30c4d547 --- /dev/null +++ b/docs/advanced/plugin-api.md @@ -0,0 +1,421 @@ +# Plugin API Reference + +This document provides a comprehensive reference for the BetterSEQTA+ Plugin API. The Plugin API is the primary interface through which plugins interact with BetterSEQTA+ and SEQTA Learn. + +## Overview + +The Plugin API consists of several sub-APIs: + +```typescript +export interface PluginAPI { + seqta: SEQTAAPI; + settings: SettingsAPI; + storage: TypedStorageAPI; + events: EventsAPI; +} +``` + +Each plugin receives an instance of this API when it is initialized, with the appropriate generic types for its settings and storage. + +## SEQTA API + +The SEQTA API provides methods for interacting with the SEQTA Learn interface. + +```typescript +export interface SEQTAAPI { + onPageLoad(path: string, callback: PageLoadCallback): () => void; + getCurrentPath(): string; + waitForElement(selector: string, options?: WaitForElementOptions): Promise; + createStyleElement(css: string): HTMLStyleElement; +} +``` + +### `onPageLoad(path: string, callback: PageLoadCallback): () => void` + +Registers a callback to be called when a specific page is loaded in SEQTA Learn. + +**Parameters:** +- `path`: The URL path to match (e.g., `/timetable`, `/assessments`). Can be a string or a regular expression. +- `callback`: A function to be called when the page is loaded. + +**Returns:** A function that, when called, will remove the page load listener. + +**Example:** +```typescript +const removeListener = api.seqta.onPageLoad('/timetable', () => { + console.log('Timetable page loaded!'); +}); + +// Later, to remove the listener +removeListener(); +``` + +### `getCurrentPath(): string` + +Gets the current URL path in SEQTA Learn. + +**Returns:** The current URL path as a string. + +**Example:** +```typescript +const currentPath = api.seqta.getCurrentPath(); +console.log(`Current path: ${currentPath}`); +``` + +### `waitForElement(selector: string, options?: WaitForElementOptions): Promise` + +Waits for an element matching the given selector to appear in the DOM. + +**Parameters:** +- `selector`: A CSS selector to match the element. +- `options`: (Optional) An object with the following properties: + - `timeout`: The maximum time to wait for the element, in milliseconds. Default: 5000. + - `interval`: The interval between checks, in milliseconds. Default: 100. + +**Returns:** A Promise that resolves to the matched element, or rejects if the timeout is reached. + +**Example:** +```typescript +try { + const timetableElement = await api.seqta.waitForElement('.timetable'); + console.log('Timetable element found:', timetableElement); +} catch (error) { + console.error('Timetable element not found:', error); +} +``` + +### `createStyleElement(css: string): HTMLStyleElement` + +Creates a style element with the given CSS and adds it to the document head. + +**Parameters:** +- `css`: The CSS to add to the style element. + +**Returns:** The created style element. + +**Example:** +```typescript +const styleElement = api.seqta.createStyleElement(` + .timetable { + background-color: #f5f5f5; + } + .timetable-cell { + border: 1px solid #ccc; + } +`); + +// Later, to remove the style +styleElement.remove(); +``` + +## Settings API + +The Settings API provides type-safe access to plugin settings. + +```typescript +export interface SettingsAPI { + get(key: K): SettingValue; + set(key: K, value: SettingValue): void; + onChange(key: K, callback: (value: SettingValue) => void): () => void; + getAll(): { [K in keyof T]: SettingValue }; +} +``` + +### `get(key: K): SettingValue` + +Gets the value of a setting. + +**Parameters:** +- `key`: The key of the setting to get. + +**Returns:** The value of the setting. + +**Example:** +```typescript +const isEnabled = api.settings.get('enabled'); +console.log(`Plugin enabled: ${isEnabled}`); +``` + +### `set(key: K, value: SettingValue): void` + +Sets the value of a setting. + +**Parameters:** +- `key`: The key of the setting to set. +- `value`: The new value for the setting. + +**Example:** +```typescript +api.settings.set('enabled', true); +console.log('Plugin enabled!'); +``` + +### `onChange(key: K, callback: (value: SettingValue) => void): () => void` + +Registers a callback to be called when a setting changes. + +**Parameters:** +- `key`: The key of the setting to watch. +- `callback`: A function to be called when the setting changes. + +**Returns:** A function that, when called, will remove the change listener. + +**Example:** +```typescript +const removeListener = api.settings.onChange('enabled', (newValue) => { + console.log(`Plugin enabled changed to: ${newValue}`); + if (newValue) { + // Enable functionality + } else { + // Disable functionality + } +}); + +// Later, to remove the listener +removeListener(); +``` + +### `getAll(): { [K in keyof T]: SettingValue }` + +Gets all settings as an object. + +**Returns:** An object containing all settings. + +**Example:** +```typescript +const allSettings = api.settings.getAll(); +console.log('All settings:', allSettings); +``` + +## Storage API + +The Storage API provides type-safe persistent storage for plugin data. + +```typescript +export interface TypedStorageAPI { + get(key: K): S[K] | undefined; + set(key: K, value: S[K]): void; + onChange(key: K, callback: (value: S[K]) => void): () => void; + getAll(): Partial; + clear(): void; +} +``` + +### `get(key: K): S[K] | undefined` + +Gets a value from storage. + +**Parameters:** +- `key`: The key of the value to get. + +**Returns:** The stored value, or `undefined` if it doesn't exist. + +**Example:** +```typescript +const lastRun = api.storage.get('lastRun'); +console.log(`Last run: ${lastRun || 'Never'}`); +``` + +### `set(key: K, value: S[K]): void` + +Sets a value in storage. + +**Parameters:** +- `key`: The key of the value to set. +- `value`: The new value to store. + +**Example:** +```typescript +api.storage.set('lastRun', new Date().toISOString()); +console.log('Last run updated!'); +``` + +### `onChange(key: K, callback: (value: S[K]) => void): () => void` + +Registers a callback to be called when a stored value changes. + +**Parameters:** +- `key`: The key of the value to watch. +- `callback`: A function to be called when the value changes. + +**Returns:** A function that, when called, will remove the change listener. + +**Example:** +```typescript +const removeListener = api.storage.onChange('lastRun', (newValue) => { + console.log(`Last run updated to: ${newValue}`); +}); + +// Later, to remove the listener +removeListener(); +``` + +### `getAll(): Partial` + +Gets all stored values as an object. + +**Returns:** An object containing all stored values. + +**Example:** +```typescript +const allStoredValues = api.storage.getAll(); +console.log('All stored values:', allStoredValues); +``` + +### `clear(): void` + +Clears all stored values. + +**Example:** +```typescript +api.storage.clear(); +console.log('All stored values cleared!'); +``` + +## Events API + +The Events API allows plugins to emit and listen for events. + +```typescript +export interface EventsAPI { + on(event: string, callback: (data: T) => void): () => void; + emit(event: string, data: T): void; +} +``` + +### `on(event: string, callback: (data: T) => void): () => void` + +Registers a callback to be called when an event is emitted. + +**Parameters:** +- `event`: The name of the event to listen for. +- `callback`: A function to be called when the event is emitted. + +**Returns:** A function that, when called, will remove the event listener. + +**Example:** +```typescript +const removeListener = api.events.on('assessmentLoaded', (data) => { + console.log('Assessment loaded:', data); +}); + +// Later, to remove the listener +removeListener(); +``` + +### `emit(event: string, data: T): void` + +Emits an event with the given data. + +**Parameters:** +- `event`: The name of the event to emit. +- `data`: The data to include with the event. + +**Example:** +```typescript +api.events.emit('myPluginEvent', { message: 'Hello from My Plugin!' }); +``` + +## Using the Plugin API in Practice + +### Combining APIs for Complex Functionality + +The true power of the Plugin API comes from combining the different sub-APIs to create complex functionality. Here's an example of a plugin that enhances the timetable view: + +```typescript +run: (api) => { + if (!api.settings.get('enabled')) { + return; + } + + // Initialize storage if needed + if (api.storage.get('zoomLevel') === undefined) { + api.storage.set('zoomLevel', 1); + } + + // Add styles based on current zoom level + const updateStyles = () => { + const zoomLevel = api.storage.get('zoomLevel'); + const styleElement = api.seqta.createStyleElement(` + .timetable-cell { + transform: scale(${zoomLevel}); + } + `); + return styleElement; + }; + + let currentStyleElement = updateStyles(); + + // Listen for storage changes + const removeStorageListener = api.storage.onChange('zoomLevel', () => { + // Remove old styles and add new ones + currentStyleElement.remove(); + currentStyleElement = updateStyles(); + }); + + // Add UI controls + const removePageListener = api.seqta.onPageLoad('/timetable', async () => { + try { + const timetableElement = await api.seqta.waitForElement('.timetable'); + + // Create controls + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'my-plugin-controls'; + controlsDiv.innerHTML = ''; + timetableElement.appendChild(controlsDiv); + + // Add event listeners + const zoomInButton = controlsDiv.querySelector('button:first-child'); + const zoomOutButton = controlsDiv.querySelector('button:last-child'); + + zoomInButton.addEventListener('click', () => { + const currentZoom = api.storage.get('zoomLevel'); + api.storage.set('zoomLevel', Math.min(currentZoom + 0.1, 2)); + }); + + zoomOutButton.addEventListener('click', () => { + const currentZoom = api.storage.get('zoomLevel'); + api.storage.set('zoomLevel', Math.max(currentZoom - 0.1, 0.5)); + }); + + // Emit an event + api.events.emit('timetableEnhanced', { zoomLevel: api.storage.get('zoomLevel') }); + } catch (error) { + console.error('Error enhancing timetable:', error); + } + }); + + // Return cleanup function + return () => { + removeStorageListener(); + removePageListener(); + currentStyleElement.remove(); + }; +} +``` + +### Error Handling + +Always handle errors gracefully to prevent your plugin from crashing: + +```typescript +try { + // Your code +} catch (error) { + console.error('Plugin error:', error); +} +``` + +### Performance Considerations + +Be mindful of performance when using the Plugin API: + +1. Use `onPageLoad` efficiently to avoid unnecessary work. +2. Clean up event listeners and DOM elements when they're no longer needed. +3. Use `waitForElement` with appropriate timeouts to avoid hanging indefinitely. + +## Next Steps + +- [Explore the Storage API in Detail](./storage-api.md) +- [Learn About Third-Party Plugins](./third-party-plugins.md) +- [Contribute to BetterSEQTA+](../contributing.md) \ No newline at end of file diff --git a/docs/advanced/storage-api.md b/docs/advanced/storage-api.md new file mode 100644 index 00000000..9fc62de5 --- /dev/null +++ b/docs/advanced/storage-api.md @@ -0,0 +1,583 @@ +# Storage API Guide + +The Storage API is a powerful component of BetterSEQTA+ that allows plugins to store and retrieve data persistently. This guide covers the TypedStorageAPI in detail, including advanced usage patterns and best practices. + +## Overview + +The Storage API provides a type-safe, persistent storage mechanism for plugins. Each plugin has its own storage namespace, ensuring that plugins cannot interfere with each other's data. + +The Storage API is generic, allowing plugins to define their own storage structure through TypeScript interfaces: + +```typescript +export interface TypedStorageAPI { + get(key: K): S[K] | undefined; + set(key: K, value: S[K]): void; + onChange(key: K, callback: (value: S[K]) => void): () => void; + getAll(): Partial; + clear(): void; +} +``` + +## Defining Your Storage Structure + +Before using the Storage API, you should define the structure of your plugin's storage using a TypeScript interface: + +```typescript +interface MyPluginStorage { + lastRun: string; + userPreferences: { + theme: 'light' | 'dark'; + fontSize: number; + }; + savedItems: string[]; +} +``` + +Then, when creating your plugin, specify this interface as the second generic parameter to the `Plugin` interface: + +```typescript +const myPlugin: Plugin = { + // Plugin implementation +}; +``` + +## Using the Storage API + +### Getting and Setting Values + +The most basic operations are getting and setting values: + +```typescript +// Get a value (returns undefined if not set) +const lastRun = api.storage.get('lastRun'); + +// Set a value +api.storage.set('lastRun', new Date().toISOString()); + +// Get a nested value +const theme = api.storage.get('userPreferences')?.theme; + +// Set a nested value (make sure to preserve existing properties) +const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 }; +api.storage.set('userPreferences', { ...preferences, theme: 'dark' }); +``` + +### Working with Complex Objects + +When working with complex objects, it's important to remember that the Storage API works with references. To update a property of a complex object, you need to create a new object with the updated property: + +```typescript +// Get the current preferences +const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 }; + +// Update a property (wrong way - changes won't be detected) +// preferences.theme = 'dark'; +// api.storage.set('userPreferences', preferences); + +// Update a property (correct way) +api.storage.set('userPreferences', { ...preferences, theme: 'dark' }); +``` + +### Working with Arrays + +Similarly, when working with arrays, you need to create a new array to trigger change detection: + +```typescript +// Get the current items +const items = api.storage.get('savedItems') || []; + +// Add an item (wrong way - changes won't be detected) +// items.push('new item'); +// api.storage.set('savedItems', items); + +// Add an item (correct way) +api.storage.set('savedItems', [...items, 'new item']); + +// Remove an item +api.storage.set('savedItems', items.filter(item => item !== 'item to remove')); +``` + +### Handling Default Values + +When getting a value that might not exist yet, you should provide a default value: + +```typescript +const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 }; +``` + +Or, as part of your plugin initialization: + +```typescript +run: (api) => { + // Initialize storage with default values + if (api.storage.get('lastRun') === undefined) { + api.storage.set('lastRun', new Date().toISOString()); + } + + if (api.storage.get('userPreferences') === undefined) { + api.storage.set('userPreferences', { theme: 'light', fontSize: 14 }); + } + + if (api.storage.get('savedItems') === undefined) { + api.storage.set('savedItems', []); + } + + // Rest of plugin logic +}; +``` + +## Advanced Usage + +### Reacting to Storage Changes + +The Storage API allows you to register callbacks that will be called when a value changes: + +```typescript +const removeListener = api.storage.onChange('userPreferences', (newPreferences) => { + console.log('User preferences changed:', newPreferences); + + // Update UI based on new preferences + if (newPreferences?.theme === 'dark') { + document.body.classList.add('dark-theme'); + } else { + document.body.classList.remove('dark-theme'); + } +}); + +// Later, to remove the listener +removeListener(); +``` + +This is particularly useful for updating the UI in response to storage changes, whether those changes were made by your plugin or by the user through a settings panel. + +### Synchronizing with Settings + +In some cases, you might want to synchronize certain storage values with settings. For example, you might want to save the user's preferences as settings: + +```typescript +// When user preferences change +api.storage.onChange('userPreferences', (newPreferences) => { + // Update the settings + api.settings.set('theme', newPreferences?.theme || 'light'); + api.settings.set('fontSize', newPreferences?.fontSize || 14); +}); + +// When settings change +api.settings.onChange('theme', (newTheme) => { + // Update the storage + const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 }; + api.storage.set('userPreferences', { ...preferences, theme: newTheme }); +}); +``` + +### Clearing Storage + +You can clear all stored values for your plugin: + +```typescript +api.storage.clear(); +``` + +This is useful when you want to reset your plugin to its default state. + +### Getting All Stored Values + +You can get all stored values as an object: + +```typescript +const allStoredValues = api.storage.getAll(); +console.log('All stored values:', allStoredValues); +``` + +This is useful for debugging or for implementing a "reset to defaults" feature. + +## Storage Persistence + +The Storage API persists data using browser storage mechanisms (e.g., `localStorage`). This means that the data will be available across page refreshes and browser restarts, but will not be shared across different devices or browsers. + +The persistence is handled automatically by BetterSEQTA+, so you don't need to worry about saving or loading data explicitly. + +## Type Safety Considerations + +The TypedStorageAPI is designed to be type-safe, but there are a few things to keep in mind: + +1. **Keys Must Exist in Interface**: You can only use keys that are defined in your storage interface. + +2. **Values Must Match Type**: The values you set must match the types defined in your interface. + +3. **Default Values for Complex Types**: When getting a value that might not exist, make sure to provide a default value with the correct type. + +## Best Practices + +### 1. Define a Clear Storage Structure + +Define a clear and well-documented storage structure using a TypeScript interface. This makes it easier to understand what data your plugin is storing and how it's organized. + +```typescript +interface MyPluginStorage { + /** + * The timestamp of the last time the plugin was run. + * Format: ISO 8601 string + */ + lastRun: string; + + /** + * User-specific preferences for the plugin. + */ + userPreferences: { + /** + * The user's preferred theme. + */ + theme: 'light' | 'dark'; + + /** + * The user's preferred font size in pixels. + */ + fontSize: number; + }; + + /** + * A list of items saved by the user. + */ + savedItems: string[]; +} +``` + +### 2. Initialize Storage Early + +Initialize your storage with default values as early as possible, ideally at the beginning of your plugin's `run` method. This ensures that the values are available throughout your plugin. + +```typescript +run: (api) => { + // Initialize storage with default values + const initializeStorage = () => { + if (api.storage.get('lastRun') === undefined) { + api.storage.set('lastRun', new Date().toISOString()); + } + + if (api.storage.get('userPreferences') === undefined) { + api.storage.set('userPreferences', { theme: 'light', fontSize: 14 }); + } + + if (api.storage.get('savedItems') === undefined) { + api.storage.set('savedItems', []); + } + }; + + initializeStorage(); + + // Rest of plugin logic +}; +``` + +### 3. Handle Missing Values Gracefully + +Always handle the case where a value might not exist yet. This can happen if the user is running your plugin for the first time, or if there was an issue with storage. + +```typescript +const preferences = api.storage.get('userPreferences'); +const theme = preferences?.theme || 'light'; +const fontSize = preferences?.fontSize || 14; +``` + +### 4. Clean Up Listeners + +If you register change listeners, make sure to clean them up when your plugin is stopped. This prevents memory leaks and ensures that the listeners are not called after your plugin is disabled. + +```typescript +run: (api) => { + // Register listeners + const listeners = [ + api.storage.onChange('userPreferences', handlePreferencesChange), + api.storage.onChange('savedItems', handleSavedItemsChange), + ]; + + // Return cleanup function + return () => { + // Clean up listeners + listeners.forEach(removeListener => removeListener()); + }; +}; +``` + +### 5. Batch Updates When Possible + +If you need to update multiple values, consider batching them to reduce the number of storage operations: + +```typescript +// Instead of this: +api.storage.set('userPreferences', { ...preferences, theme: 'dark' }); +api.storage.set('lastRun', new Date().toISOString()); +api.storage.set('savedItems', [...items, 'new item']); + +// Consider using a helper function: +const batchUpdate = () => { + const preferences = api.storage.get('userPreferences') || { theme: 'light', fontSize: 14 }; + const items = api.storage.get('savedItems') || []; + + api.storage.set('userPreferences', { ...preferences, theme: 'dark' }); + api.storage.set('lastRun', new Date().toISOString()); + api.storage.set('savedItems', [...items, 'new item']); +}; + +batchUpdate(); +``` + +## Example: A Complete Plugin with Storage + +Here's a complete example of a plugin that uses the Storage API effectively: + +```typescript +interface NotesPluginStorage { + notes: { + id: string; + title: string; + content: string; + createdAt: string; + updatedAt: string; + }[]; + activeNoteId: string | null; + view: 'list' | 'detail'; +} + +const notesPlugin: Plugin = { + id: 'notes', + name: 'Notes', + description: 'A simple notes plugin for BetterSEQTA+', + version: '1.0.0', + settings: { + enabled: { + type: 'boolean', + default: true, + title: 'Enable Notes', + description: 'Turn the notes plugin on or off', + }, + autoSave: { + type: 'boolean', + default: true, + title: 'Auto Save', + description: 'Automatically save notes as you type', + }, + }, + run: (api) => { + if (!api.settings.get('enabled')) { + return; + } + + // Initialize storage with default values + if (api.storage.get('notes') === undefined) { + api.storage.set('notes', []); + } + + if (api.storage.get('activeNoteId') === undefined) { + api.storage.set('activeNoteId', null); + } + + if (api.storage.get('view') === undefined) { + api.storage.set('view', 'list'); + } + + // Create and render the UI + let notesContainer: HTMLElement | null = null; + let removePageListener: () => void; + + const renderUI = async () => { + const pageContainer = await api.seqta.waitForElement('#page-container'); + + if (!notesContainer) { + notesContainer = document.createElement('div'); + notesContainer.className = 'notes-plugin-container'; + pageContainer.appendChild(notesContainer); + } + + renderNotes(); + }; + + const renderNotes = () => { + if (!notesContainer) return; + + const notes = api.storage.get('notes') || []; + const activeNoteId = api.storage.get('activeNoteId'); + const view = api.storage.get('view'); + + if (view === 'list') { + notesContainer.innerHTML = ` +
+

Notes

+ +
+
+ ${notes.length === 0 + ? '

No notes yet. Click "Add Note" to create one.

' + : notes.map(note => ` +
+

${note.title}

+

${note.content.substring(0, 50)}${note.content.length > 50 ? '...' : ''}

+
+ + +
+
+ `).join('')} +
+ `; + + // Add event listeners + notesContainer.querySelector('.add-note-btn')?.addEventListener('click', addNote); + notesContainer.querySelectorAll('.view-note-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = (e.target as HTMLElement).getAttribute('data-id'); + if (id) viewNote(id); + }); + }); + notesContainer.querySelectorAll('.delete-note-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const id = (e.target as HTMLElement).getAttribute('data-id'); + if (id) deleteNote(id); + }); + }); + } else if (view === 'detail') { + const activeNote = notes.find(note => note.id === activeNoteId); + + if (!activeNote) { + api.storage.set('view', 'list'); + renderNotes(); + return; + } + + notesContainer.innerHTML = ` +
+ +

Editing Note

+
+
+ + +
+ +
+
+ `; + + // Add event listeners + notesContainer.querySelector('.back-btn')?.addEventListener('click', () => { + api.storage.set('view', 'list'); + renderNotes(); + }); + + const titleInput = notesContainer.querySelector('.note-title') as HTMLInputElement; + const contentTextarea = notesContainer.querySelector('.note-content') as HTMLTextAreaElement; + + if (api.settings.get('autoSave')) { + let timeout: number; + + const autoSave = () => { + clearTimeout(timeout); + timeout = setTimeout(() => { + updateNote(activeNoteId, titleInput.value, contentTextarea.value); + }, 500) as unknown as number; + }; + + titleInput.addEventListener('input', autoSave); + contentTextarea.addEventListener('input', autoSave); + } + + notesContainer.querySelector('.save-note-btn')?.addEventListener('click', () => { + updateNote(activeNoteId, titleInput.value, contentTextarea.value); + api.storage.set('view', 'list'); + renderNotes(); + }); + } + }; + + const addNote = () => { + const notes = api.storage.get('notes') || []; + const newNote = { + id: Date.now().toString(), + title: 'New Note', + content: '', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + api.storage.set('notes', [...notes, newNote]); + api.storage.set('activeNoteId', newNote.id); + api.storage.set('view', 'detail'); + renderNotes(); + }; + + const viewNote = (id: string) => { + api.storage.set('activeNoteId', id); + api.storage.set('view', 'detail'); + renderNotes(); + }; + + const updateNote = (id: string, title: string, content: string) => { + const notes = api.storage.get('notes') || []; + const updatedNotes = notes.map(note => + note.id === id + ? { ...note, title, content, updatedAt: new Date().toISOString() } + : note + ); + + api.storage.set('notes', updatedNotes); + }; + + const deleteNote = (id: string) => { + const notes = api.storage.get('notes') || []; + const updatedNotes = notes.filter(note => note.id !== id); + + api.storage.set('notes', updatedNotes); + + if (api.storage.get('activeNoteId') === id) { + api.storage.set('activeNoteId', null); + } + + renderNotes(); + }; + + // Register listeners + const storageListeners = [ + api.storage.onChange('notes', renderNotes), + api.storage.onChange('activeNoteId', renderNotes), + api.storage.onChange('view', renderNotes), + ]; + + // Set up page load listener + removePageListener = api.seqta.onPageLoad('*', renderUI); + + // Return cleanup function + return () => { + // Remove event listeners + storageListeners.forEach(removeListener => removeListener()); + removePageListener(); + + // Remove UI + notesContainer?.remove(); + notesContainer = null; + }; + }, +}; + +export default notesPlugin; +``` + +## Summary + +The Storage API is a powerful tool for maintaining state in your BetterSEQTA+ plugins. By following the best practices outlined in this guide, you can create robust and reliable plugins that provide a great user experience. + +Key takeaways: + +1. Define a clear storage structure using TypeScript interfaces +2. Initialize storage early with default values +3. Handle missing values gracefully +4. Clean up listeners when your plugin is stopped +5. Use the onChange method to react to storage changes + +With these principles in mind, you can leverage the full power of the Storage API in your plugins. + +## Next Steps + +- [Explore the Plugin API](./plugin-api.md) +- [Learn About Third-Party Plugins](./third-party-plugins.md) +- [Contribute to BetterSEQTA+](../contributing.md) \ No newline at end of file diff --git a/docs/advanced/third-party-plugins.md b/docs/advanced/third-party-plugins.md new file mode 100644 index 00000000..ae006c0e --- /dev/null +++ b/docs/advanced/third-party-plugins.md @@ -0,0 +1,550 @@ +# Developing Third-Party Plugins + +BetterSEQTA+ supports third-party plugins, allowing developers to extend its functionality beyond what's provided by the built-in plugins. This guide covers everything you need to know about developing, distributing, and installing third-party plugins. + +## Introduction to Third-Party Plugins + +Third-party plugins are plugins developed outside of the main BetterSEQTA+ codebase. They can be created by anyone and distributed to users who want to extend their BetterSEQTA+ experience. + +Unlike built-in plugins, which are included with BetterSEQTA+, third-party plugins must be installed separately by users. This allows for a wide range of extensions without bloating the core application. + +## Plugin Structure + +A third-party plugin is a JavaScript or TypeScript module that exports a plugin object conforming to the `Plugin` interface. It can be distributed as a single file or as a package with multiple files. + +### Basic Structure + +```typescript +// my-awesome-plugin.ts +import { Plugin, PluginAPI, PluginSettings } from 'betterseqta-plugin-api'; + +export interface MyAwesomePluginSettings extends PluginSettings { + enabled: { + type: 'boolean'; + default: true; + title: 'Enable My Awesome Plugin'; + description: 'Turn my awesome plugin on or off'; + }; + // Add more settings as needed +} + +export interface MyAwesomePluginStorage { + lastRun: string; + // Add more storage fields as needed +} + +const myAwesomePlugin: Plugin = { + id: 'my-awesome-plugin', + name: 'My Awesome Plugin', + description: 'A simple plugin for BetterSEQTA+', + version: '1.0.0', + author: 'Your Name', + license: 'MIT', + settings: { + enabled: { + type: 'boolean', + default: true, + title: 'Enable My Awesome Plugin', + description: 'Turn my awesome plugin on or off', + }, + // Initialize your settings here + }, + run: (api) => { + // Your plugin logic goes here + console.log('My Awesome Plugin is running!'); + + // Return a cleanup function (optional but recommended) + return () => { + console.log('My Awesome Plugin is cleaning up!'); + // Cleanup logic goes here + }; + }, +}; + +export default myAwesomePlugin; +``` + +### Plugin Manifest + +For plugins that consist of multiple files or that need additional resources, a manifest file is recommended. This file provides metadata about the plugin and points to the main plugin file. + +```json +// plugin.json +{ + "id": "my-awesome-plugin", + "name": "My Awesome Plugin", + "description": "A simple plugin for BetterSEQTA+", + "version": "1.0.0", + "author": "Your Name", + "license": "MIT", + "main": "index.js", + "dependencies": { + "betterseqta-plus": "^1.0.0" + } +} +``` + +## Development Environment + +### Setting Up Your Development Environment + +1. Clone the BetterSEQTA+ repository or create a new project: + ```bash + git clone https://github.com/yourusername/betterseqta-plus-plugin.git + cd betterseqta-plus-plugin + ``` + +2. Initialize a new npm project: + ```bash + npm init -y + ``` + +3. Install the necessary dependencies: + ```bash + npm install --save-dev typescript webpack webpack-cli @types/node + npm install --save betterseqta-plugin-api + ``` + +4. Set up TypeScript configuration: + ```json + // tsconfig.json + { + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "declaration": true, + "outDir": "dist", + "lib": ["es2020", "dom"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] + } + ``` + +5. Set up webpack configuration: + ```javascript + // webpack.config.js + const path = require('path'); + + module.exports = { + entry: './src/index.ts', + mode: 'production', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js'], + }, + output: { + filename: 'index.js', + path: path.resolve(__dirname, 'dist'), + library: { + type: 'umd', + name: 'MyAwesomePlugin', + }, + globalObject: 'this', + }, + externals: { + 'betterseqta-plugin-api': 'betterseqta-plugin-api', + }, + }; + ``` + +6. Create your plugin in the `src` directory: + ```bash + mkdir -p src + touch src/index.ts + ``` + +7. Add build scripts to your `package.json`: + ```json + "scripts": { + "build": "webpack", + "dev": "webpack --watch" + } + ``` + +### Developing Your Plugin + +1. Implement your plugin in `src/index.ts` following the structure shown above. + +2. Build your plugin: + ```bash + npm run build + ``` + +3. For development, you can use the watch mode: + ```bash + npm run dev + ``` + +### Testing Your Plugin + +There are several ways to test your plugin during development: + +#### Method 1: Plugin Development Mode + +BetterSEQTA+ provides a development mode for testing plugins: + +1. Open BetterSEQTA+ settings +2. Navigate to the "Developer" section +3. Enable "Plugin Development Mode" +4. Click "Load Local Plugin" and select your plugin's directory or main file + +#### Method 2: Manual Installation + +You can manually install your plugin in a development environment: + +1. Build your plugin +2. Copy the output file to the BetterSEQTA+ plugins directory: + ```bash + cp dist/index.js ~/.betterseqta/plugins/my-awesome-plugin/ + ``` +3. Reload BetterSEQTA+ + +## Packaging and Distribution + +### Creating a Plugin Package + +A plugin package should include: + +1. **The plugin code**: Compiled JavaScript file(s) +2. **A manifest file**: `plugin.json` with metadata +3. **Documentation**: README.md and other documentation +4. **License**: A license file + +Example file structure: +``` +my-awesome-plugin/ +├── index.js # Compiled plugin code +├── plugin.json # Plugin manifest +├── README.md # Documentation +└── LICENSE # License file +``` + +### Publishing Your Plugin + +You can publish your plugin in several ways: + +#### 1. GitHub Repository + +Host your plugin on GitHub: + +1. Create a new repository +2. Push your plugin code +3. Create releases for new versions +4. Users can install it using the GitHub URL + +#### 2. npm Package + +Publish your plugin as an npm package: + +1. Prepare your package: + ```json + // package.json + { + "name": "betterseqta-plugin-my-awesome", + "version": "1.0.0", + "description": "An awesome plugin for BetterSEQTA+", + "main": "dist/index.js", + "files": [ + "dist", + "plugin.json", + "README.md", + "LICENSE" + ], + "keywords": [ + "betterseqta", + "plugin" + ], + "author": "Your Name", + "license": "MIT" + } + ``` + +2. Build your plugin: + ```bash + npm run build + ``` + +3. Publish to npm: + ```bash + npm publish + ``` + +#### 3. BetterSEQTA+ Plugin Directory + +Submit your plugin to the official BetterSEQTA+ plugin directory: + +1. Ensure your plugin follows all guidelines +2. Create a pull request to add your plugin to the directory +3. Once approved, your plugin will be available in the BetterSEQTA+ plugin browser + +### Creating a Plugin Listing + +Your plugin listing should include: + +1. **Name and Description**: Clear, concise name and description +2. **Screenshots**: Showcase your plugin in action +3. **Features**: List of key features +4. **Installation Instructions**: How to install your plugin +5. **Configuration**: How to configure your plugin +6. **Support Information**: Where users can get help + +## Plugin Installation Guide + +Include instructions for users to install your plugin: + +### Method 1: Using the Plugin Browser + +1. Open BetterSEQTA+ +2. Go to Settings → Plugins → Browse +3. Search for "My Awesome Plugin" +4. Click "Install" + +### Method 2: Manual Installation + +1. Download the plugin files +2. Create a folder in the BetterSEQTA+ plugins directory: + ```bash + mkdir -p ~/.betterseqta/plugins/my-awesome-plugin + ``` +3. Copy the plugin files to the folder: + ```bash + cp -r * ~/.betterseqta/plugins/my-awesome-plugin/ + ``` +4. Restart BetterSEQTA+ + +### Method 3: Using npm + +If your plugin is published on npm: + +```bash +npm install -g betterseqta-plugin-my-awesome +``` + +## Best Practices for Plugin Development + +### Security Considerations + +1. **Respect User Privacy**: Don't collect unnecessary data +2. **Secure Data Handling**: Encrypt sensitive data +3. **Minimize Permissions**: Only request the permissions you need +4. **Code Review**: Get others to review your code for security issues + +### Performance Optimization + +1. **Minimize DOM Operations**: Batch DOM operations when possible +2. **Use Event Delegation**: Instead of adding many individual event listeners +3. **Lazy Loading**: Load resources only when needed +4. **Throttle and Debounce**: Limit frequent events like scroll or resize + +### User Experience + +1. **Clear UI**: Keep your UI simple and intuitive +2. **Consistent Design**: Follow SEQTA's design language +3. **Responsive Feedback**: Provide feedback for user actions +4. **Error Handling**: Gracefully handle errors and inform the user + +### Accessibility + +1. **Keyboard Navigation**: Ensure all features are accessible via keyboard +2. **Screen Reader Support**: Use appropriate ARIA attributes +3. **Color Contrast**: Ensure sufficient contrast for text +4. **Font Size**: Allow for text resizing + +### Maintenance + +1. **Version Control**: Use semantic versioning +2. **Changelog**: Maintain a changelog +3. **Documentation**: Keep documentation up to date +4. **Issue Tracking**: Set up an issue tracker for bug reports and feature requests + +## Advanced Topics + +### Plugin Communication + +Plugins can communicate with each other using the Events API: + +```typescript +// Plugin A: Emit an event +api.events.emit('pluginA:dataUpdated', { data: 'some data' }); + +// Plugin B: Listen for the event +api.events.on('pluginA:dataUpdated', (data) => { + console.log('Data from Plugin A:', data); +}); +``` + +### Plugin Dependencies + +If your plugin depends on other plugins, you should specify this in your manifest: + +```json +// plugin.json +{ + "id": "my-awesome-plugin", + "name": "My Awesome Plugin", + "dependencies": { + "another-plugin": "^1.0.0" + } +} +``` + +Your plugin's `run` method should check if the dependencies are available: + +```typescript +run: (api) => { + // Check if dependencies are available + if (!window.betterseqta.plugins.isPluginLoaded('another-plugin')) { + console.error('My Awesome Plugin requires Another Plugin to be installed and enabled'); + return; + } + + // Plugin logic +} +``` + +### Plugin Configuration UI + +For complex plugins, you might want to provide a custom settings UI beyond what the automatic settings generation provides: + +```typescript +settings: { + enabled: { + type: 'boolean', + default: true, + title: 'Enable My Awesome Plugin', + description: 'Turn my awesome plugin on or off', + }, + customUI: { + type: 'custom', + render: (container, value, onChange) => { + // Create a custom UI + const div = document.createElement('div'); + div.innerHTML = ` +

Custom Settings

+

This is a custom settings UI.

+ + `; + + // Add event listeners + div.querySelector('button').addEventListener('click', () => { + // Do something + onChange({ clicked: true }); + }); + + // Append to container + container.appendChild(div); + + // Return a cleanup function + return () => { + // Clean up event listeners + div.querySelector('button').removeEventListener('click', handleClick); + }; + } + } +} +``` + +### Internationalization + +For plugins with international users, consider adding support for multiple languages: + +```typescript +// Define translations +const translations = { + en: { + title: 'My Awesome Plugin', + description: 'A simple plugin for BetterSEQTA+', + button: 'Click Me', + }, + fr: { + title: 'Mon Plugin Génial', + description: 'Un plugin simple pour BetterSEQTA+', + button: 'Cliquez-moi', + }, +}; + +// Get the current language +const language = navigator.language.split('-')[0]; +const t = translations[language] || translations.en; + +// Use translations +console.log(t.title); +``` + +## Troubleshooting and FAQ + +### Common Issues + +#### "Plugin not found" error + +- Make sure your plugin is installed in the correct directory +- Check that the plugin ID in your code matches the one in the manifest + +#### "Plugin failed to load" error + +- Check the console for error messages +- Ensure your plugin's code is compatible with the current version of BetterSEQTA+ + +#### "Settings not saving" issue + +- Make sure you're using the Settings API correctly +- Check that your settings have the correct types + +### FAQ + +#### Q: Can I use external libraries in my plugin? +A: Yes, you can include external libraries. However, be mindful of the size and performance impact. + +#### Q: How do I update my plugin? +A: Update the code, increment the version number, and publish the new version. Users will be notified of the update. + +#### Q: Can I monetize my plugin? +A: There's no built-in payment system, but you can offer premium versions or accept donations. + +#### Q: How do I debug my plugin? +A: Use the browser's developer tools to debug your plugin. BetterSEQTA+ also provides debugging tools in the developer settings. + +## Contributing to the Plugin Ecosystem + +### Reporting Issues + +If you find a bug in the plugin API, report it on the BetterSEQTA+ GitHub repository: + +1. Go to the Issues tab +2. Click "New Issue" +3. Select "Plugin API Bug" +4. Fill in the details + +### Contributing Documentation + +Improvements to the plugin documentation are always welcome: + +1. Fork the repository +2. Make your changes +3. Submit a pull request + +### Sharing Your Plugins + +Share your plugins with the community: + +1. Announce your plugin on the BetterSEQTA+ forum +2. Create a GitHub repository for your plugin +3. Submit your plugin to the plugin directory + +## Conclusion + +Developing third-party plugins for BetterSEQTA+ is a rewarding way to customize and extend the platform. By following these guidelines, you can create high-quality plugins that enhance the experience for yourself and other users. + +Remember that the plugin ecosystem thrives on community contributions. Share your plugins, collaborate with other developers, and help make BetterSEQTA+ even better for everyone! \ No newline at end of file diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..8a624a9e --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,262 @@ +# Contributing to BetterSEQTA+ + +Thank you for your interest in contributing to BetterSEQTA+! This document provides guidelines and instructions for contributing to the project. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) + - [Setting Up Your Development Environment](#setting-up-your-development-environment) + - [Project Structure](#project-structure) +- [Contributing Code](#contributing-code) + - [Branching Strategy](#branching-strategy) + - [Pull Request Process](#pull-request-process) + - [Coding Standards](#coding-standards) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Features](#suggesting-features) +- [Writing Documentation](#writing-documentation) +- [Community](#community) + +## Code of Conduct + +BetterSEQTA+ is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to adhere to our Code of Conduct, which promotes respectful and harassment-free interaction. + +Key points: +- Be respectful and inclusive +- Focus on what is best for the community +- Show empathy towards other community members +- Be open to constructive feedback + +## Getting Started + +### Setting Up Your Development Environment + +1. **Fork the Repository** + + Start by forking the BetterSEQTA+ repository to your GitHub account. + +2. **Clone Your Fork** + + ```bash + git clone https://github.com/yourusername/betterseqta-plus.git + cd betterseqta-plus + ``` + +3. **Install Dependencies** + + ```bash + npm install + ``` + +4. **Set Up Development Environment** + + ```bash + npm run dev + ``` + +5. **Install in Chrome/Firefox** + + Follow the [installation instructions](./installation.md#development-installation) to load the development version into your browser. + +### Project Structure + +Understanding the project structure will help you navigate the codebase: + +``` +betterseqta-plus/ +├── src/ # Source code +│ ├── plugins/ # Plugin system +│ │ ├── built-in/ # Built-in plugins +│ │ ├── core/ # Plugin core functionality +│ ├── settings/ # Settings system +│ ├── utils/ # Utility functions +│ ├── extension/ # Browser extension code +├── docs/ # Documentation +├── test/ # Test files +├── dist/ # Build output (generated) +├── package.json # Project dependencies +├── tsconfig.json # TypeScript configuration +└── README.md # Project README +``` + +## Contributing Code + +### Branching Strategy + +We follow a simple branching strategy: + +- `main` - The main development branch +- `feature/*` - Feature branches +- `bugfix/*` - Bug fix branches +- `docs/*` - Documentation branches + +Always create a new branch for your changes: + +```bash +git checkout -b feature/my-new-feature +``` + +### Pull Request Process + +1. **Keep PRs Focused** + + Each pull request should address a single concern. If you're working on multiple features, create separate PRs for each. + +2. **Write Clear Commit Messages** + + Follow the conventional commits format: + ``` + feat: add new feature + fix: resolve bug with timetable + docs: update installation instructions + ``` + +3. **Update Documentation** + + If your changes require documentation updates, include them in the same PR. + +4. **Run Tests** + + Make sure all tests pass before submitting your PR: + ```bash + npm test + ``` + +5. **Submit Your PR** + + When you're ready, push your branch and create a pull request on GitHub. + +6. **Code Review** + + All PRs will be reviewed by maintainers. Be responsive to feedback and make requested changes. + +7. **Merge** + + Once approved, a maintainer will merge your PR. + +### Coding Standards + +We follow TypeScript best practices and have a consistent code style: + +1. **Use TypeScript** + + All new code should be written in TypeScript with proper typing. + +2. **Follow Existing Patterns** + + Match the coding style of the existing codebase. + +3. **Write Tests** + + Add tests for new features and bug fixes. + +4. **Document Your Code** + + Add comments for complex logic and JSDoc comments for functions. + +5. **Use Linters** + + We use ESLint and Prettier. Run them before submitting your PR: + ```bash + npm run lint + npm run format + ``` + +## Reporting Bugs + +If you find a bug, please report it by creating an issue on GitHub: + +1. **Search Existing Issues** + + Check if the bug has already been reported. + +2. **Use the Bug Report Template** + + Fill in all sections of the bug report template: + - Description + - Steps to reproduce + - Expected behavior + - Actual behavior + - Screenshots (if applicable) + - Environment (browser, OS, etc.) + +3. **Be Specific** + + The more details you provide, the easier it will be to fix the bug. + +## Suggesting Features + +We welcome feature suggestions! To suggest a new feature: + +1. **Search Existing Suggestions** + + Check if your idea has already been suggested. + +2. **Use the Feature Request Template** + + Fill in all sections of the feature request template: + - Description + - Use case + - Potential implementation + - Alternatives considered + +3. **Be Patient** + + Feature requests are evaluated based on alignment with project goals, feasibility, and maintainer bandwidth. + +## Writing Documentation + +Good documentation is crucial for the project. To contribute to documentation: + +1. **Identify Gaps** + + Look for areas where documentation is missing or unclear. + +2. **Follow Documentation Style** + + Maintain a consistent style and format. + +3. **Use Clear Language** + + Write in simple, clear English. Avoid jargon when possible. + +4. **Include Examples** + + Code examples and screenshots help users understand. + +5. **Submit a PR** + + Follow the same process as code contributions, but create a branch with a `docs/` prefix. + +## Community + +Join our community channels to discuss the project, get help, and connect with other contributors: + +- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta) +- **GitHub Discussions**: For longer-form conversations +- **GitHub Issues**: For bug reports and feature requests + +## Creating Plugins + +If you're interested in creating plugins for BetterSEQTA+, check out our plugin development guides: + +- [Creating Your First Plugin](./plugins/creating-plugins.md) +- [Plugin API Reference](./advanced/plugin-api.md) + +## Recognition + +Contributors are recognized in several ways: + +1. **CONTRIBUTORS.md**: All contributors are listed in this file +2. **Release Notes**: Significant contributions are highlighted in release notes +3. **Community Recognition**: Regular shout-outs in community channels + +## Questions? + +If you have any questions about contributing, please: + +1. Check the documentation +2. Ask in the Discord server +3. Open a GitHub Discussion + +Thank you for contributing to BetterSEQTA+! Your efforts help make SEQTA better for students and teachers everywhere. \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..a3342a35 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,182 @@ +# Installing BetterSEQTA+ + +This guide will walk you through the process of installing and setting up BetterSEQTA+ for development or usage. + +## Prerequisites + +Before you begin, make sure you have the following installed: + +- [Node.js](https://nodejs.org/) (v16 or higher) +- [npm](https://www.npmjs.com/) (v7 or higher) or [Bun](https://bun.sh/) (recommended) +- A modern web browser (Chrome, Firefox, or Edge) + +## Installation Methods + +There are two ways to install BetterSEQTA+: + +1. **For Users**: Install the browser extension +2. **For Developers**: Clone the repository and set up the development environment + +## For Users: Installing the Browser Extension + +BetterSEQTA+ is available as a browser extension for Chrome, Firefox, and Edge. + +### Chrome/Edge + +1. Visit the [Chrome Web Store page for BetterSEQTA+](https://chrome.google.com/webstore/detail/betterseqta) +2. Click the "Add to Chrome" button +3. Confirm the installation when prompted +4. The extension will be installed and ready to use + +### Firefox + +1. Visit the [Firefox Add-ons page for BetterSEQTA+](https://addons.mozilla.org/en-US/firefox/addon/betterseqta) +2. Click the "Add to Firefox" button +3. Confirm the installation when prompted +4. The extension will be installed and ready to use + +## For Developers: Setting Up the Development Environment + +If you want to develop for BetterSEQTA+ or modify the code, follow these steps: + +### 1. Clone the Repository + +```bash +git clone https://github.com/SeqtaLearning/betterseqta-plus.git +cd betterseqta-plus +``` + +### 2. Install Dependencies + +Using npm: + +```bash +npm install +``` + +Using Bun (recommended): + +```bash +bun install +``` + +### 3. Set Up Environment Variables + +Copy the example environment file: + +```bash +cp .env.submit.example .env +``` + +Edit the `.env` file with your SEQTA credentials and settings. + +### 4. Start the Development Server + +Using npm: + +```bash +npm run dev +``` + +Using Bun: + +```bash +bun run dev +``` + +This will start a development server and build the extension in watch mode. + +### 5. Load the Extension in Your Browser + +#### Chrome/Edge + +1. Open Chrome/Edge and navigate to `chrome://extensions` or `edge://extensions` +2. Enable "Developer mode" using the toggle in the top right +3. Click "Load unpacked" and select the `dist` folder in your BetterSEQTA+ directory +4. The extension should now appear in your extensions list + +#### Firefox + +1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox` +2. Click "Load Temporary Add-on..." +3. Select the `manifest.json` file in the `dist` folder +4. The extension should now appear in your add-ons list + +### 6. Test Your Changes + +After making changes to the code, the development server will automatically rebuild the extension. However, you may need to reload the extension in your browser to see the changes: + +1. Go to the extensions page in your browser +2. Find BetterSEQTA+ and click the reload icon +3. Refresh any SEQTA Learn pages you have open + +## Troubleshooting Installation + +### Common Issues + +#### "Cannot find module" errors + +If you see errors about missing modules, try: + +```bash +rm -rf node_modules +npm install +``` + +Or with Bun: + +```bash +rm -rf node_modules +bun install +``` + +#### Extension not appearing in SEQTA + +Make sure: +- You're visiting a SEQTA Learn page +- The extension is enabled +- You've refreshed the page after installing the extension + +#### Development build not updating + +Try: +1. Stopping the development server +2. Clearing your browser cache +3. Removing the extension from your browser +4. Rebuilding the extension +5. Loading it again + +## Updating BetterSEQTA+ + +### For Users + +Browser extensions update automatically, but you can manually check for updates: + +- **Chrome/Edge**: Go to `chrome://extensions` or `edge://extensions`, enable Developer mode, and click "Update" +- **Firefox**: Go to `about:addons`, click the gear icon, and select "Check for Updates" + +### For Developers + +If you're working on the code, pull the latest changes and reinstall dependencies: + +```bash +git pull +npm install +npm run dev +``` + +Or with Bun: + +```bash +git pull +bun install +bun run dev +``` + +## Next Steps + +Now that you have BetterSEQTA+ installed, you can: + +- [Configure your settings](./settings/README.md) +- [Create your own plugins](./plugins/creating-plugins.md) +- [Contribute to the project](../CONTRIBUTING.md) \ No newline at end of file diff --git a/docs/plugins/README.md b/docs/plugins/README.md new file mode 100644 index 00000000..ba10f260 --- /dev/null +++ b/docs/plugins/README.md @@ -0,0 +1,155 @@ +# BetterSEQTA+ Plugin System + +BetterSEQTA+ features a powerful plugin system that allows developers to extend and customize the functionality of SEQTA Learn. This document provides an overview of how the plugin system works and how to get started with creating your own plugins. + +## What is a Plugin? + +A plugin is a self-contained piece of code that adds functionality to BetterSEQTA+. Plugins can: + +- Add new UI elements to SEQTA Learn +- Modify existing UI elements +- Add new features to SEQTA Learn +- Modify or extend existing features +- Store and retrieve user data +- Respond to events in SEQTA Learn + +Each plugin is isolated from other plugins, with its own settings, storage, and lifecycle. This ensures that plugins can be enabled, disabled, or removed without affecting other parts of the system. + +## Plugin Architecture + +The BetterSEQTA+ plugin system consists of several key components: + +### 1. Plugin Interface + +All plugins implement the `Plugin` interface, which defines the structure and lifecycle methods of a plugin: + +```typescript +export interface Plugin { + id: string; + name: string; + description: string; + version: string; + settings: T; + run: (api: PluginAPI) => void | Promise | (() => void) | Promise<(() => void)>; +} +``` + +### 2. Plugin API + +When a plugin is run, it receives an instance of the `PluginAPI`, which provides access to various services and utilities: + +```typescript +export interface PluginAPI { + seqta: SEQTAAPI; + settings: SettingsAPI; + storage: TypedStorageAPI; + events: EventsAPI; +} +``` + +- **SEQTA API**: Provides methods for interacting with the SEQTA Learn UI +- **Settings API**: Provides type-safe access to plugin settings +- **Storage API**: Provides type-safe persistent storage for plugin data +- **Events API**: Allows plugins to emit and listen for events + +### 3. Plugin Manager + +The Plugin Manager is responsible for loading, starting, stopping, and managing plugins. It handles the lifecycle of each plugin and ensures that plugins have access to the resources they need. + +### 4. Plugin Registry + +The Plugin Registry is a central repository of all available plugins. Built-in plugins are automatically registered, and additional plugins can be registered dynamically. + +## Plugin Lifecycle + +Plugins follow a simple lifecycle: + +1. **Registration**: The plugin is registered with the Plugin Manager +2. **Loading**: The plugin's settings and storage are loaded +3. **Running**: The plugin's `run` method is called with the Plugin API +4. **Cleanup**: If the plugin returns a cleanup function, it is called when the plugin is stopped + +## Creating a Plugin + +Creating a plugin for BetterSEQTA+ involves a few simple steps: + +1. Define your plugin's interface +2. Implement the Plugin interface +3. Register your plugin with the Plugin Manager + +For a detailed guide on creating plugins, see [Creating Your First Plugin](./creating-plugins.md). + +## Built-in Plugins + +BetterSEQTA+ comes with several built-in plugins that provide core functionality: + +- **Timetable**: Enhances the SEQTA timetable view +- **Notification Collector**: Improves the notification system +- **Theme Customizer**: Allows customization of the SEQTA theme +- **Assessment Enhancer**: Adds features to the assessment view + +These plugins serve as good examples of how to use the plugin system effectively. + +## Type-Safe Settings and Storage + +One of the key features of the BetterSEQTA+ plugin system is its type-safe settings and storage. Using TypeScript generics, plugins can define the structure of their settings and storage, ensuring that they are used correctly throughout the codebase. + +### Settings Example + +```typescript +interface MyPluginSettings extends PluginSettings { + enabled: { + type: 'boolean'; + default: boolean; + title: string; + description: string; + }; + refreshInterval: { + type: 'number'; + default: number; + title: string; + description: string; + min: number; + max: number; + }; +} +``` + +### Storage Example + +```typescript +interface MyPluginStorage { + lastRefresh: string; + savedItems: string[]; + userPreferences: { + theme: 'light' | 'dark'; + fontSize: number; + }; +} +``` + +## Decorator-Based Settings + +BetterSEQTA+ also offers a more modern, decorator-based approach to defining settings. For more information, see [Creating Plugins with Settings](../settings/creating-plugins.md). + +## Plugin API Reference + +The Plugin API provides a rich set of features for interacting with SEQTA Learn. For a complete reference, see [Plugin API Reference](../advanced/plugin-api.md). + +## Best Practices + +When creating plugins for BetterSEQTA+, consider these best practices: + +1. **Use TypeScript**: Take advantage of TypeScript's type system to ensure type safety in your plugins. +2. **Keep Plugins Focused**: Each plugin should do one thing well. +3. **Handle Cleanup**: Always return a cleanup function from your plugin's `run` method to ensure proper resource management. +4. **Document Your Code**: Add clear documentation to your code, especially for public APIs. +5. **Test Thoroughly**: Test your plugins in different environments and with different configurations. +6. **Follow UI Guidelines**: When adding UI elements, follow the SEQTA Learn UI guidelines to maintain a consistent experience. +7. **Optimize Performance**: Be mindful of performance impact, especially for plugins that run on every page. + +## Next Steps + +- [Creating Your First Plugin](./creating-plugins.md) +- [Plugin API Reference](../advanced/plugin-api.md) +- [Typed Storage API](../advanced/storage-api.md) \ No newline at end of file diff --git a/docs/plugins/creating-plugins.md b/docs/plugins/creating-plugins.md new file mode 100644 index 00000000..71dfd93b --- /dev/null +++ b/docs/plugins/creating-plugins.md @@ -0,0 +1,269 @@ +# Creating Your First Plugin + +This guide will walk you through the process of creating a plugin for BetterSEQTA+, from setup to implementation to testing. + +## Prerequisites + +Before you start creating a plugin, make sure you have: + +- Basic knowledge of TypeScript +- Familiarity with the BetterSEQTA+ codebase +- A development environment set up according to the [Installation Guide](../installation.md) + +## Plugin Structure + +A typical BetterSEQTA+ plugin consists of: + +1. **Plugin Definition**: A TypeScript file that defines the plugin's metadata and functionality +2. **Settings Interface**: (Optional) A TypeScript interface that defines the plugin's settings +3. **Storage Interface**: (Optional) A TypeScript interface that defines the plugin's storage structure + +## Step 1: Planning Your Plugin + +Before you start coding, take some time to plan your plugin: + +1. **Identify the Problem**: What issue or need does your plugin address? +2. **Define the Scope**: What specific features will your plugin include? +3. **Consider the User Experience**: How will users interact with your plugin? + +## Step 2: Creating the Plugin File + +Create a new TypeScript file for your plugin. The convention is to place it in the `src/plugins/` directory, either in the `built-in` folder or a new folder if it's a third-party plugin. + +```typescript +// src/plugins/my-plugin/index.ts + +import { Plugin, PluginAPI, PluginSettings } from '../../core/types'; + +export interface MyPluginSettings extends PluginSettings { + enabled: { + type: 'boolean'; + default: true; + title: 'Enable My Plugin'; + description: 'Turn my plugin on or off'; + }; + // Add more settings as needed +} + +export interface MyPluginStorage { + lastRun: string; + // Add more storage fields as needed +} + +const myPlugin: Plugin = { + id: 'my-plugin', + name: 'My Plugin', + description: 'A simple plugin for BetterSEQTA+', + version: '1.0.0', + settings: { + enabled: { + type: 'boolean', + default: true, + title: 'Enable My Plugin', + description: 'Turn my plugin on or off', + }, + // Initialize your settings here + }, + run: (api) => { + if (!api.settings.get('enabled')) { + return; + } + + // Initialize storage with default values if needed + if (api.storage.get('lastRun') === undefined) { + api.storage.set('lastRun', new Date().toISOString()); + } + + // Your plugin logic goes here + console.log('My Plugin is running!'); + + // Access the SEQTA API + api.seqta.onPageLoad('/timetable', () => { + // Code to run when the timetable page loads + }); + + // Return a cleanup function (optional but recommended) + return () => { + console.log('My Plugin is cleaning up!'); + // Cleanup logic goes here + }; + }, +}; + +export default myPlugin; +``` + +## Step 3: Registering Your Plugin + +To make your plugin available to BetterSEQTA+, you need to register it with the Plugin Manager. For built-in plugins, you can add your plugin to the `src/plugins/built-in/index.ts` file: + +```typescript +// src/plugins/built-in/index.ts + +import myPlugin from './my-plugin'; +// Other imports... + +export const builtInPlugins = [ + myPlugin, + // Other plugins... +]; +``` + +For third-party plugins, you'll need to follow a different approach, as detailed in [Third-Party Plugins](../advanced/third-party-plugins.md). + +## Step 4: Implementing Your Plugin Logic + +The main functionality of your plugin goes in the `run` method. Here are some common patterns: + +### Responding to Page Loads + +```typescript +api.seqta.onPageLoad('/timetable', () => { + // Code to run when the timetable page loads +}); +``` + +### Modifying the UI + +```typescript +api.seqta.onPageLoad('/timetable', () => { + const timetableElement = document.querySelector('.timetable'); + if (timetableElement) { + // Modify the timetable element + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'my-plugin-controls'; + controlsDiv.innerHTML = ''; + timetableElement.appendChild(controlsDiv); + + // Add event listeners + controlsDiv.querySelector('button:first-child').addEventListener('click', () => { + // Zoom in logic + }); + } +}); +``` + +### Working with Settings + +```typescript +// Get a setting value +const isEnabled = api.settings.get('enabled'); + +// Listen for settings changes +api.settings.onChange('enabled', (newValue) => { + if (newValue) { + // Enable functionality + } else { + // Disable functionality + } +}); +``` + +### Working with Storage + +```typescript +// Get a stored value +const lastRun = api.storage.get('lastRun'); + +// Set a stored value +api.storage.set('lastRun', new Date().toISOString()); + +// Listen for storage changes +api.storage.onChange('lastRun', (newValue) => { + console.log(`Last run updated to: ${newValue}`); +}); +``` + +### Working with Events + +```typescript +// Listen for events +api.events.on('assessmentLoaded', (data) => { + console.log(`Assessment loaded: ${data.id}`); +}); + +// Emit an event +api.events.emit('myPluginEvent', { message: 'Hello from My Plugin!' }); +``` + +## Step 5: Testing Your Plugin + +To test your plugin: + +1. Run the development server: + ``` + npm run dev + ``` + +2. Open SEQTA Learn in your browser with BetterSEQTA+ enabled. + +3. Check the console for any error messages. + +4. Verify that your plugin works as expected. + +## Step 6: Adding Plugin Settings UI + +If your plugin has settings, they will automatically appear in the BetterSEQTA+ settings panel. The UI is generated based on the settings interface you defined. + +For more control over the settings UI, you can use the decorator-based settings system. See [Creating Plugins with Settings](../settings/creating-plugins.md) for more information. + +## Best Practices for Plugin Development + +1. **Follow TypeScript Best Practices**: Use proper typing for all variables and functions. + +2. **Handle Errors Gracefully**: Wrap your code in try-catch blocks to prevent crashes. + ```typescript + try { + // Your code + } catch (error) { + console.error('My Plugin Error:', error); + } + ``` + +3. **Clean Up After Yourself**: Always return a cleanup function from your plugin's `run` method. + ```typescript + const cleanup = () => { + // Remove event listeners, DOM elements, etc. + }; + return cleanup; + ``` + +4. **Document Your Code**: Add comments to explain complex logic or unusual patterns. + +5. **Keep It Simple**: Start with a simple plugin and add features incrementally. + +## Example Plugins + +For inspiration, check out these example plugins in the BetterSEQTA+ codebase: + +1. **Timetable Plugin**: Enhances the SEQTA timetable view with zoom controls and filtering options. + - Location: `src/plugins/built-in/timetable/index.ts` + +2. **Notification Collector**: Improves the notification system in SEQTA Learn. + - Location: `src/plugins/built-in/notification-collector/index.ts` + +## Troubleshooting + +### Plugin Not Loading + +- Check that your plugin is properly registered +- Verify that there are no TypeScript errors +- Look for error messages in the console + +### Plugin Not Working as Expected + +- Ensure that your plugin's `enabled` setting is true +- Check that your selectors match the SEQTA DOM structure +- Use `console.log` statements to debug your code + +### TypeScript Errors + +- Make sure your interfaces are properly defined +- Check that you're using the correct types for the plugin API +- Verify that your plugin implements the `Plugin` interface correctly + +## Next Steps + +- [Learn About Type-Safe Settings](../settings/creating-plugins.md) +- [Explore the Plugin API](../advanced/plugin-api.md) +- [Contribute to BetterSEQTA+](../contributing.md) \ No newline at end of file diff --git a/docs/settings/README.md b/docs/settings/README.md new file mode 100644 index 00000000..86032113 --- /dev/null +++ b/docs/settings/README.md @@ -0,0 +1,301 @@ +# BetterSEQTA+ Settings System + +BetterSEQTA+ includes a powerful, type-safe settings system that uses TypeScript decorators to create a seamless API for plugin developers. This document explains how the settings system works and how to extend it. + +## Table of Contents + +- [Overview](#overview) +- [Existing Setting Types](#existing-setting-types) +- [Using Settings in Plugins](#using-settings-in-plugins) +- [Adding New Setting Types](#adding-new-setting-types) +- [Rendering in the UI](#rendering-in-the-ui) + +## Overview + +The settings system is built around TypeScript decorators and uses TypeScript's type system to provide type safety for plugin settings. The system consists of a few key components: + +1. **Setting Type Interfaces** in `src/plugins/core/types.ts` - Define the structure of the setting +2. **Setting Decorator Options** in `src/plugins/core/settings.ts` - Define the options for the decorator +3. **Setting Decorators** in `src/plugins/core/settings.ts` - Register the setting in the plugin +4. **BasePlugin Class** in `src/plugins/core/settings.ts` - Base class that handles the settings + +## Existing Setting Types + +BetterSEQTA+ currently supports the following setting types: + +- **Boolean Settings** - Simple on/off toggle +- **String Settings** - Text input with optional validation +- **Number Settings** - Numeric input with optional min/max/step +- **Select Settings** - Dropdown selection from predefined options + +Each setting type has a corresponding interface, options interface, and decorator. + +## Using Settings in Plugins + +Here's how to use the settings system in a plugin: + +```typescript +import { BasePlugin, BooleanSetting, StringSetting } from '../../core/settings'; + +// Define the plugin settings class +class MyPluginClass extends BasePlugin { + @BooleanSetting({ + default: true, + title: "Enable Feature", + description: "Enables the awesome feature." + }) + enabled!: boolean; + + @StringSetting({ + default: "Default Value", + title: "Custom Text", + description: "Enter your custom text here.", + maxLength: 100 + }) + customText!: string; +} + +// Create an instance to extract settings +const settingsInstance = new MyPluginClass(); + +// Use in plugin definition +const myPlugin = { + id: 'my-plugin', + name: 'My Plugin', + description: 'Does awesome things', + version: '1.0.0', + settings: settingsInstance.settings, + run: async (api) => { + // Access settings via api.settings + if (api.settings.enabled) { + console.log(api.settings.customText); + } + + // Listen for settings changes + api.settings.onChange('enabled', (value) => { + console.log(`Enabled changed to: ${value}`); + }); + } +}; +``` + +## Adding New Setting Types + +To add a new setting type, you need to follow these steps: + +### 1. Define the Setting Interface in `src/plugins/core/types.ts` + +```typescript +export interface ColorSetting { + type: 'color'; + default: string; // HEX color code + title: string; + description?: string; + presets?: string[]; // Optional color presets +} + +// Update the PluginSetting type to include the new setting type +export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | + SelectSetting | ColorSetting; + +// Update the SettingValue type helper +type SettingValue = T extends BooleanSetting ? boolean : + T extends StringSetting ? string : + T extends NumberSetting ? number : + T extends SelectSetting ? O : + T extends ColorSetting ? string : // Add this line + never; +``` + +### 2. Define the Options Interface in `src/plugins/core/settings.ts` + +```typescript +interface ColorSettingOptions extends BaseSettingOptions { + default: string; // HEX color + presets?: string[]; +} +``` + +### 3. Create the Decorator Function in `src/plugins/core/settings.ts` + +```typescript +export function ColorSetting(options: ColorSettingOptions): PropertyDecorator { + return (target: Object, propertyKey: string | symbol) => { + // Ensure the settings property exists on the constructor's prototype + const proto = target.constructor.prototype; + if (!proto.hasOwnProperty('settings')) { + proto.settings = {}; + } + + // Add the setting to the prototype's settings object + proto.settings[propertyKey] = { + type: 'color', + ...options + }; + }; +} +``` + +### 4. Create a Corresponding UI Component (if needed) + +If your setting type needs a custom UI component, create one in the `src/interface/components` directory. + +For example, you might create a `ColorPicker.svelte` component. + +### 5. Update the Settings UI in `src/interface/pages/settings/general.svelte` + +Update the `getPluginSettingEntries` function to handle your new setting type: + +```javascript +entries.push({ + title: setting.title || key, + description: setting.description || '', + id, + Component: setting.type === 'boolean' ? Switch : + setting.type === 'select' ? Select : + setting.type === 'number' ? Slider : + setting.type === 'color' ? ColorPicker : // Add this line + setting.type === 'string' ? (setting.options ? Select : null) : Switch, + props: { + state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default, + onChange: (value: any) => { + updatePluginSetting(plugin.pluginId, key, value); + }, + options: setting.options, + presets: setting.presets // Add this line if needed for your component + } +}); +``` + +## Rendering in the UI + +The settings UI is handled in `src/interface/pages/settings/general.svelte`. This file does a few key things: + +1. Loads settings for all plugins from storage +2. Maps setting types to UI components +3. Handles updating settings when users interact with the UI + +For most setting types, you'll need to ensure there's a corresponding Svelte component in the `src/interface/components` directory that can render and edit the setting value. + +## Example: Adding a Color Setting + +Here's a complete example of adding a color setting type: + +1. Define the setting interface in `types.ts`: + +```typescript +export interface ColorSetting { + type: 'color'; + default: string; + title: string; + description?: string; + presets?: string[]; +} + +export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | + SelectSetting | ColorSetting; + +type SettingValue = T extends BooleanSetting ? boolean : + T extends StringSetting ? string : + T extends NumberSetting ? number : + T extends SelectSetting ? O : + T extends ColorSetting ? string : + never; +``` + +2. Create the options interface and decorator in `settings.ts`: + +```typescript +interface ColorSettingOptions extends BaseSettingOptions { + default: string; + presets?: string[]; +} + +export function ColorSetting(options: ColorSettingOptions): PropertyDecorator { + return (target: Object, propertyKey: string | symbol) => { + const proto = target.constructor.prototype; + if (!proto.hasOwnProperty('settings')) { + proto.settings = {}; + } + + proto.settings[propertyKey] = { + type: 'color', + ...options + }; + }; +} +``` + +3. Create a ColorPicker component in `src/interface/components/ColorPicker.svelte`: + +```html + + +
+ onChange(e.currentTarget.value)} + /> +
+ {#each presets as preset} + + {/each} +
+
+ + +``` + +4. Update the UI renderer in `general.svelte`: + +```javascript +Component: setting.type === 'boolean' ? Switch : + setting.type === 'select' ? Select : + setting.type === 'number' ? Slider : + setting.type === 'color' ? ColorPicker : + setting.type === 'string' ? (setting.options ? Select : null) : Switch, +``` + +5. Use the new setting type in a plugin: + +```typescript +class ThemePlugin extends BasePlugin { + @ColorSetting({ + default: "#4285f4", + title: "Primary Color", + description: "The main color for the theme", + presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"] + }) + primaryColor!: string; +} +``` + +With these steps, you've added a completely new setting type to the BetterSEQTA+ plugin system! \ No newline at end of file diff --git a/docs/settings/creating-plugins.md b/docs/settings/creating-plugins.md new file mode 100644 index 00000000..b4625ae8 --- /dev/null +++ b/docs/settings/creating-plugins.md @@ -0,0 +1,335 @@ +# Creating Plugins with Decorator-Based Settings + +This guide will walk you through creating a BetterSEQTA+ plugin using the new decorator-based settings system. + +## Prerequisites + +- Understand basic TypeScript concepts (classes, interfaces, decorators) +- Familiarity with the BetterSEQTA+ plugin system + +## Plugin Structure + +A typical plugin consists of: + +1. A settings class that defines the plugin's settings using decorators +2. The plugin definition object +3. The actual plugin functionality + +## Step by Step Guide + +### 1. Create a Plugin File + +Start by creating a new file in the `src/plugins/built-in` directory. For example, `myFeature/index.ts`. + +### 2. Define Storage Type (Optional) + +If your plugin needs to store data, define a storage interface: + +```typescript +interface MyFeatureStorage { + lastUsed: string; + favoriteItems: string[]; +} +``` + +### 3. Create a Settings Class + +Create a class that extends `BasePlugin` and use decorators to define settings: + +```typescript +import { BasePlugin, BooleanSetting, StringSetting, NumberSetting, SelectSetting } from '../../core/settings'; + +class MyFeaturePluginClass extends BasePlugin { + @BooleanSetting({ + default: true, + title: "Enable My Feature", + description: "Enables the awesome new feature." + }) + enabled!: boolean; + + @StringSetting({ + default: "Default text", + title: "Custom Message", + description: "Sets a custom message for the feature", + maxLength: 100 + }) + message!: string; + + @NumberSetting({ + default: 5, + title: "Refresh Interval", + description: "How often to refresh the data (in seconds)", + min: 1, + max: 60, + step: 1 + }) + refreshInterval!: number; + + @SelectSetting({ + default: "small", + options: ["small", "medium", "large"] as const, + title: "Display Size", + description: "Control how large the feature appears" + }) + displaySize!: "small" | "medium" | "large"; +} +``` + +### 4. Create a Plugin Instance + +Create an instance of your settings class and define the plugin object: + +```typescript +// Create an instance to extract settings +const settingsInstance = new MyFeaturePluginClass(); + +const myFeaturePlugin: Plugin = { + id: 'myFeature', + name: 'My Awesome Feature', + description: 'Adds an awesome new feature to SEQTA', + version: '1.0.0', + settings: settingsInstance.settings, + run: async (api) => { + // Plugin implementation goes here + } +}; + +export default myFeaturePlugin; +``` + +### 5. Implement Plugin Functionality + +Implement your plugin's functionality in the `run` function: + +```typescript +run: async (api) => { + // Initialize storage with defaults if needed + if (api.storage.lastUsed === undefined) { + api.storage.lastUsed = new Date().toISOString(); + } + + if (api.storage.favoriteItems === undefined) { + api.storage.favoriteItems = []; + } + + // Only run if enabled + if (!api.settings.enabled) return; + + // Main plugin logic + const initializeFeature = () => { + console.log(`Initializing feature with message: ${api.settings.message}`); + console.log(`Using display size: ${api.settings.displaySize}`); + + // Set up refreshing + const intervalId = setInterval(() => { + refreshData(); + }, api.settings.refreshInterval * 1000); + + // Clean up function returned here + return () => { + clearInterval(intervalId); + console.log('Feature cleaned up'); + }; + }; + + const refreshData = () => { + console.log('Refreshing data...'); + api.storage.lastUsed = new Date().toISOString(); + }; + + // Listen for elements we need + api.seqta.onMount('.some-element', (element) => { + // Do something when element appears + }); + + // Listen for settings changes + api.settings.onChange('refreshInterval', (newValue) => { + console.log(`Refresh interval changed to ${newValue} seconds`); + }); + + // Return cleanup function + return initializeFeature(); +} +``` + +### 6. Register the Plugin + +Make sure your plugin is registered in the plugin system. In the `src/plugins/index.ts` file, add your plugin to the list of built-in plugins: + +```typescript +import myFeaturePlugin from './built-in/myFeature'; + +// Add your plugin to this array +const builtInPlugins = [ + // ... other plugins + myFeaturePlugin, +]; +``` + +## Advanced Features + +### Reacting to Settings Changes + +You can listen for settings changes with the `onChange` method: + +```typescript +api.settings.onChange('enabled', (value) => { + if (value) { + // Setting was turned on + initialize(); + } else { + // Setting was turned off + cleanup(); + } +}); +``` + +### Using Storage + +The storage API lets you persist data between sessions: + +```typescript +// Read from storage +const favorites = api.storage.favoriteItems; + +// Write to storage +api.storage.favoriteItems = [...favorites, 'new item']; + +// Listen for storage changes +api.storage.onChange('favoriteItems', (newValue) => { + console.log('Favorites updated:', newValue); +}); +``` + +### Cleaning Up + +Always return a cleanup function from your plugin's `run` method if you have any resources to clean up: + +```typescript +run: async (api) => { + // Set up resources + const intervalId = setInterval(() => { + // Do something + }, 1000); + + // Return cleanup function + return () => { + clearInterval(intervalId); + // Clean up any other resources + }; +} +``` + +## Best Practices + +1. **Initialize Storage Values**: Always check if storage values are undefined and set defaults +2. **Handle Enabled State**: Check if your plugin is enabled before running main functionality +3. **Use TypeScript**: Take advantage of TypeScript's type system to ensure type safety +4. **Clean Up Resources**: Always clean up resources when a plugin is disabled +5. **Document Settings**: Use clear titles and descriptions for your settings + +## Complete Example + +Here's a complete example of a simple plugin that changes the color of elements: + +```typescript +import { BasePlugin, BooleanSetting, ColorSetting } from '../../core/settings'; +import type { Plugin } from '../../core/types'; + +interface ColorChangerStorage { + lastApplied: string; +} + +class ColorChangerPluginClass extends BasePlugin { + @BooleanSetting({ + default: true, + title: "Enable Color Changer", + description: "Applies custom colors to elements on the page." + }) + enabled!: boolean; + + @ColorSetting({ + default: "#4285f4", + title: "Heading Color", + description: "Color for headings on the page", + presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"] + }) + headingColor!: string; + + @ColorSetting({ + default: "#34a853", + title: "Button Color", + description: "Color for buttons on the page", + presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"] + }) + buttonColor!: string; +} + +const settingsInstance = new ColorChangerPluginClass(); + +const colorChangerPlugin: Plugin = { + id: 'colorChanger', + name: 'Color Changer', + description: 'Changes colors of various elements on the page', + version: '1.0.0', + settings: settingsInstance.settings, + run: async (api) => { + if (api.storage.lastApplied === undefined) { + api.storage.lastApplied = new Date().toISOString(); + } + + const applyColors = () => { + if (!api.settings.enabled) return; + + // Apply heading color + document.querySelectorAll('h1, h2, h3').forEach(heading => { + (heading as HTMLElement).style.color = api.settings.headingColor; + }); + + // Apply button color + document.querySelectorAll('button').forEach(button => { + (button as HTMLElement).style.backgroundColor = api.settings.buttonColor; + }); + + api.storage.lastApplied = new Date().toISOString(); + }; + + // Apply colors initially + applyColors(); + + // Apply colors when DOM changes + api.seqta.onMount('h1, h2, h3, button', applyColors); + + // Listen for color changes + api.settings.onChange('headingColor', applyColors); + api.settings.onChange('buttonColor', applyColors); + api.settings.onChange('enabled', (enabled) => { + if (enabled) { + applyColors(); + } else { + // Reset colors + document.querySelectorAll('h1, h2, h3').forEach(heading => { + (heading as HTMLElement).style.color = ''; + }); + + document.querySelectorAll('button').forEach(button => { + (button as HTMLElement).style.backgroundColor = ''; + }); + } + }); + + // No cleanup needed for this plugin + return () => {}; + } +}; + +export default colorChangerPlugin; +``` + +This plugin demonstrates: +- Using multiple setting types including a custom color setting +- Handling the enabled state +- Initializing storage +- Listening for setting changes +- Applying and resetting styles based on settings +- Proper cleanup when disabled \ No newline at end of file diff --git a/docs/settings/custom-ui-components.md b/docs/settings/custom-ui-components.md new file mode 100644 index 00000000..d5405144 --- /dev/null +++ b/docs/settings/custom-ui-components.md @@ -0,0 +1,541 @@ +# Creating Custom UI Components for Settings + +When adding new setting types to BetterSEQTA+, you'll often need to create custom UI components to render and edit these settings. This guide covers how to create Svelte components for the settings UI and how to integrate them with the settings system. + +## Understanding the Settings UI + +Settings in BetterSEQTA+ are rendered by the `src/interface/pages/settings/general.svelte` component. This component: + +1. Loads settings from all plugins +2. Maps setting types to appropriate UI components +3. Renders the settings UI +4. Handles updates when settings are changed + +## Basic Component Requirements + +Every setting UI component should follow these conventions: + +1. **Accept a `state` prop** for the current value +2. **Accept an `onChange` prop** for updating the value +3. **Accept any additional props** specific to the setting type (e.g., `options`, `min`, `max`) +4. **Handle user input** and call `onChange` with the new value + +## Creating a Basic Component + +Here's an example of a basic Svelte component for a custom setting type: + +```svelte + + + +
+ +
+ + +``` + +## Example: Slider Component + +BetterSEQTA+ includes a Slider component for number settings: + +```svelte + + + +
+ + {stringValue} +
+``` + +## Example: Color Picker Component + +Here's a more complex example of a color picker component: + +```svelte + + + +
+ + + {#if isOpen} +
+ + +
+ {#each presets as preset} + + {/each} +
+
+ {/if} +
+ + +``` + +## Integrating with the Settings System + +Once you've created your component, you need to update `general.svelte` to use it for your custom setting type. + +### 1. Import Your Component + +At the top of `src/interface/pages/settings/general.svelte`, add an import for your component: + +```typescript +import ColorPicker from "../../components/ColorPicker.svelte" +``` + +### 2. Update Component Mapping + +Find the `getPluginSettingEntries` function in `general.svelte` and update the component mapping: + +```typescript +function getPluginSettingEntries() { + const entries: any[] = []; + + pluginSettings.forEach(plugin => { + if (Object.keys(plugin.settings).length === 0) return; + + Object.entries(plugin.settings).forEach(([key, setting]) => { + const id = getPluginSettingId(plugin.pluginId, key); + + entries.push({ + title: setting.title || key, + description: setting.description || '', + id, + Component: setting.type === 'boolean' ? Switch : + setting.type === 'select' ? Select : + setting.type === 'number' ? Slider : + setting.type === 'color' ? ColorPicker : // Add your component here + setting.type === 'string' ? (setting.options ? Select : null) : Switch, + props: { + state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default, + onChange: (value: any) => { + updatePluginSetting(plugin.pluginId, key, value); + }, + options: setting.options, + // Add any additional props your component needs + presets: setting.presets, + min: setting.min, + max: setting.max, + step: setting.step + } + }); + }); + }); + + return entries; +} +``` + +## Handling Different UI Needs + +Different setting types may have different UI needs: + +### Toggle Switches + +For boolean settings, a toggle switch is usually appropriate: + +```svelte + + + + + +``` + +### Text Inputs + +For string settings, a text input with validation: + +```svelte + + +
+ + {#if error} +
{error}
+ {/if} +
+ + +``` + +### Complex Interactive Components + +For more complex settings, you may need more interactive components with dropdowns, modals, or other features. Consider using additional Svelte features like: + +- `{#if}...{/if}` blocks for conditional rendering +- Svelte transitions for animations +- Svelte actions for DOM interactions +- Svelte stores for shared state + +## Best Practices + +1. **Keep Components Focused**: Each component should do one thing well +2. **Use TypeScript**: Define proper types for your props +3. **Handle Errors**: Validate input and show meaningful error messages +4. **Use Clear UI**: Make it obvious how to interact with the component +5. **Add Accessibility**: Include proper ARIA attributes and keyboard handling +6. **Support Theming**: Use CSS variables or design system tokens for consistent styling +7. **Test Edge Cases**: Ensure your component handles all possible inputs + +## Complete Example + +Here's a complete example of a custom file picker component: + +```svelte + + + +
+
+ + {fileName} + {#if state} + + {/if} +
+ + + + {#if error} +
{error}
+ {/if} +
+ + +``` + +To use this in the settings system, you would: + +1. Define a `FileSetting` interface in `types.ts` +2. Create a `FileSetting` decorator in `settings.ts` +3. Update the `getPluginSettingEntries` function in `general.svelte` + +This component demonstrates: +- Handling file input (a more complex input type) +- Input validation +- Error handling +- Multiple interactive elements +- Binding to DOM elements +- Clean UI that follows platform conventions \ No newline at end of file diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts index fe9644af..0fd297dc 100644 --- a/src/plugins/built-in/notificationCollector/index.ts +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -1,33 +1,29 @@ -import type { Plugin, PluginSettings } from '../../core/types'; - -interface NotificationCollectorSettings extends PluginSettings { - enabled: { - type: 'boolean'; - default: boolean; - title: string; - description: string; - }; -} +import type { Plugin } from '../../core/types'; +import { BasePlugin, BooleanSetting } from '../../core/settings'; interface NotificationCollectorStorage { lastNotificationCount: number; lastCheckedTime: string; } -const notificationCollectorPlugin: Plugin = { +class NotificationCollectorPluginClass extends BasePlugin { + @BooleanSetting({ + default: true, + title: "Notification Collector", + description: "Uncaps the 9+ limit for notifications, showing the real number.", + }) + enabled!: boolean; +} + +// Create an instance to extract settings +const settingsInstance = new NotificationCollectorPluginClass(); + +const notificationCollectorPlugin: Plugin = { id: 'notificationCollector', name: 'Notification Collector', description: 'Collects and displays SEQTA notifications', version: '1.0.0', - settings: { - enabled: { - type: 'boolean', - default: true, - title: 'Notification Collector', - description: 'Uncaps the 9+ limit for notifications, showing the real number.', - } - }, - + settings: settingsInstance.settings, run: async (api) => { let pollInterval: number | null = null; @@ -95,8 +91,8 @@ const notificationCollectorPlugin: Plugin { - if (enabled) { + const enabledCallback = (value: any) => { + if (value) { startPolling(); } else { stopPolling(); diff --git a/src/plugins/built-in/test/index.ts b/src/plugins/built-in/test/index.ts new file mode 100644 index 00000000..2788ac12 --- /dev/null +++ b/src/plugins/built-in/test/index.ts @@ -0,0 +1,31 @@ +import type { Plugin } from '../../core/types'; +import { BasePlugin, BooleanSetting } from '../../core/settings'; + +class TestPluginClass extends BasePlugin { + @BooleanSetting({ + default: true, + title: "Test Plugin", + description: "A test plugin for BetterSEQTA+", + }) + enabled!: boolean; +} + +const settingsInstance = new TestPluginClass(); + +const testPlugin: Plugin = { + id: 'test', + name: 'Test Plugin', + description: 'A test plugin for BetterSEQTA+', + version: '1.0.0', + settings: settingsInstance.settings, + + run: async (api) => { + console.log('Test plugin running'); + + api.seqta.onPageChange((page) => { + console.log('Page changed to', page); + }); + } +}; + +export default testPlugin; \ No newline at end of file diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts index d2476546..e3254d8a 100644 --- a/src/plugins/built-in/timetable/index.ts +++ b/src/plugins/built-in/timetable/index.ts @@ -1,38 +1,35 @@ import { settingsState } from '@/seqta/utils/listeners/SettingsState'; -import type { Plugin, PluginSettings } from '../../core/types'; +import type { Plugin } from '../../core/types'; import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { waitForElm } from '@/seqta/utils/waitForElm'; +import { BasePlugin, BooleanSetting } from '../../core/settings'; -interface TimetableSettings extends PluginSettings { - enabled: { - type: 'boolean'; - default: boolean; - title: string; - description: string; - }; +// Define only the typed settings - no need for redundant interface +class TimetablePluginClass extends BasePlugin { + @BooleanSetting({ + default: true, + title: "Timetable Enhancer", + description: "Adds extra features to the timetable view." + }) + enabled!: boolean; } -const timetablePlugin: Plugin = { +// Create an instance to extract settings +const settingsInstance = new TimetablePluginClass(); + +const timetablePlugin: Plugin = { id: 'timetable', name: 'Timetable Enhancer', description: 'Adds extra features to the timetable view', version: '1.0.0', - settings: { - enabled: { - type: 'boolean', - default: true, - title: 'Timetable Enhancer', - description: 'Adds extra features to the timetable view.', - } - }, - + settings: settingsInstance.settings, run: async (api) => { if (api.settings.enabled) { api.seqta.onMount('.timetablepage', handleTimetable) } - const enabledCallback = (enabled: boolean) => { - if (enabled) { + const enabledCallback = (value: any) => { + if (value) { api.seqta.onMount('.timetablepage', handleTimetable) } else { const timetablePage = document.querySelector('.timetablepage') @@ -277,19 +274,16 @@ function handleTimetableAssessmentHide(): void { function hideElements(): void { const entries = document.querySelectorAll(".entry") + entries.forEach((entry: Element) => { const entryEl = entry as HTMLElement - if (!entryEl.classList.contains("assessment") && !(entryEl.style.opacity === "0.3")) { - entryEl.style.opacity = "0.3" - } else { - entryEl.style.opacity = "1" + if (!entryEl.classList.contains("assessment")) { + entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3" } }) } - hideOn.addEventListener("click", () => { - hideElements() - }) + hideOn.addEventListener("click", hideElements) } export default timetablePlugin; diff --git a/src/plugins/core/settings.ts b/src/plugins/core/settings.ts new file mode 100644 index 00000000..8bac3fba --- /dev/null +++ b/src/plugins/core/settings.ts @@ -0,0 +1,108 @@ +import type { PluginSettings } from './types'; + +// Base interfaces for our settings +interface BaseSettingOptions { + title: string; + description?: string; +} + +interface BooleanSettingOptions extends BaseSettingOptions { + default: boolean; +} + +interface StringSettingOptions extends BaseSettingOptions { + default: string; + maxLength?: number; + pattern?: string; +} + +interface NumberSettingOptions extends BaseSettingOptions { + default: number; + min?: number; + max?: number; + step?: number; +} + +interface SelectSettingOptions extends BaseSettingOptions { + default: T; + options: readonly T[]; +} + +// The actual decorators +export function BooleanSetting(options: BooleanSettingOptions): PropertyDecorator { + return (target: Object, propertyKey: string | symbol) => { + // Ensure the settings property exists on the constructor's prototype + const proto = target.constructor.prototype; + if (!proto.hasOwnProperty('settings')) { + proto.settings = {}; + } + + // Add the setting to the prototype's settings object + proto.settings[propertyKey] = { + type: 'boolean', + ...options + }; + }; +} + +export function StringSetting(options: StringSettingOptions): PropertyDecorator { + return (target: Object, propertyKey: string | symbol) => { + // Ensure the settings property exists on the constructor's prototype + const proto = target.constructor.prototype; + if (!proto.hasOwnProperty('settings')) { + proto.settings = {}; + } + + // Add the setting to the prototype's settings object + proto.settings[propertyKey] = { + type: 'string', + ...options + }; + }; +} + +export function NumberSetting(options: NumberSettingOptions): PropertyDecorator { + return (target: Object, propertyKey: string | symbol) => { + // Ensure the settings property exists on the constructor's prototype + const proto = target.constructor.prototype; + if (!proto.hasOwnProperty('settings')) { + proto.settings = {}; + } + + // Add the setting to the prototype's settings object + proto.settings[propertyKey] = { + type: 'number', + ...options + }; + }; +} + +export function SelectSetting(options: SelectSettingOptions): PropertyDecorator { + return (target: Object, propertyKey: string | symbol) => { + // Ensure the settings property exists on the constructor's prototype + const proto = target.constructor.prototype; + if (!proto.hasOwnProperty('settings')) { + proto.settings = {}; + } + + // Add the setting to the prototype's settings object + proto.settings[propertyKey] = { + type: 'select', + ...options + }; + }; +} + +// Base plugin class that handles settings +export abstract class BasePlugin { + // The settings property will be populated by decorators + settings: T = {} as T; + + constructor() { + // Copy settings from the prototype to the instance + // This ensures that each instance has its own settings object + if (this.constructor.prototype.settings) { + this.settings = { ...this.constructor.prototype.settings }; + } + } +} \ No newline at end of file diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts index fc3c9145..269df35b 100644 --- a/src/plugins/core/types.ts +++ b/src/plugins/core/types.ts @@ -1,27 +1,32 @@ import ReactFiber from '@/seqta/utils/ReactFiber'; -interface BooleanSetting { +export interface BooleanSetting { type: 'boolean'; default: boolean; title: string; description?: string; } -interface StringSetting { +export interface StringSetting { type: 'string'; default: string; title: string; description?: string; + maxLength?: number; + pattern?: string; } -interface NumberSetting { +export interface NumberSetting { type: 'number'; default: number; title: string; description?: string; + min?: number; + max?: number; + step?: number; } -interface SelectSetting { +export interface SelectSetting { type: 'select'; options: readonly T[]; default: T; @@ -29,7 +34,7 @@ interface SelectSetting { description?: string; } -type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting; +export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting; export type PluginSettings = { [key: string]: PluginSetting; diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 9de2c950..41d26a76 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -1,6 +1,9 @@ import { PluginManager } from './core/manager'; + +// plugins import timetablePlugin from './built-in/timetable'; import notificationCollectorPlugin from './built-in/notificationCollector'; +import testPlugin from './built-in/test'; // Initialize plugin manager const pluginManager = PluginManager.getInstance(); @@ -8,6 +11,7 @@ const pluginManager = PluginManager.getInstance(); // Register built-in plugins pluginManager.registerPlugin(timetablePlugin); pluginManager.registerPlugin(notificationCollectorPlugin); +pluginManager.registerPlugin(testPlugin); // Legacy plugin exports export { init as Monofile } from './monofile'; diff --git a/tsconfig.json b/tsconfig.json index e5f763d8..eb905118 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,10 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, + /* Decorators */ + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { "@/*": ["./src/*"] }, From 620d168d280b927cf391381d831129134c03fdb9 Mon Sep 17 00:00:00 2001 From: Seth Burkart <108050083+SethBurkart123@users.noreply.github.com> Date: Tue, 18 Mar 2025 22:19:32 +1100 Subject: [PATCH 40/72] Update creating-plugins.md --- docs/plugins/creating-plugins.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/plugins/creating-plugins.md b/docs/plugins/creating-plugins.md index 71dfd93b..a1ce9a6a 100644 --- a/docs/plugins/creating-plugins.md +++ b/docs/plugins/creating-plugins.md @@ -65,13 +65,13 @@ const myPlugin: Plugin = { // Initialize your settings here }, run: (api) => { - if (!api.settings.get('enabled')) { + if (!api.settings.enabled) { return; } // Initialize storage with default values if needed - if (api.storage.get('lastRun') === undefined) { - api.storage.set('lastRun', new Date().toISOString()); + if (api.storage.lastRun === undefined) { + api.storage.lastRun = new Date().toISOString(); } // Your plugin logic goes here @@ -147,7 +147,7 @@ api.seqta.onPageLoad('/timetable', () => { ```typescript // Get a setting value -const isEnabled = api.settings.get('enabled'); +const isEnabled = api.settings.enabled; // Listen for settings changes api.settings.onChange('enabled', (newValue) => { @@ -163,10 +163,10 @@ api.settings.onChange('enabled', (newValue) => { ```typescript // Get a stored value -const lastRun = api.storage.get('lastRun'); +const lastRun = api.storage.lastRun; // Set a stored value -api.storage.set('lastRun', new Date().toISOString()); +api.storage.lastRun = new Date().toISOString(); // Listen for storage changes api.storage.onChange('lastRun', (newValue) => { @@ -266,4 +266,4 @@ For inspiration, check out these example plugins in the BetterSEQTA+ codebase: - [Learn About Type-Safe Settings](../settings/creating-plugins.md) - [Explore the Plugin API](../advanced/plugin-api.md) -- [Contribute to BetterSEQTA+](../contributing.md) \ No newline at end of file +- [Contribute to BetterSEQTA+](../contributing.md) From 68159ddd0efdf1b20ee4640bf90ec005c23d97d1 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 21 Mar 2025 17:59:28 +1100 Subject: [PATCH 41/72] chore: hide test plugin --- src/interface/pages/settings/general.svelte | 10 ---------- src/plugins/index.ts | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index 09574d16..faa95790 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -206,16 +206,6 @@ onChange: (isOn: boolean) => settingsState.lettergrade = isOn } }, - { - title: "Lesson Alerts", - description: "Sends a native browser notification ~5 minutes prior to lessons.", - id: 8, - Component: Switch, - props: { - state: $settingsState.lessonalert, - onChange: (isOn: boolean) => settingsState.lessonalert = isOn - } - }, { title: "12 Hour Time", description: "Prefer 12 hour time format for SEQTA", diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 41d26a76..8c4566e9 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -3,7 +3,6 @@ import { PluginManager } from './core/manager'; // plugins import timetablePlugin from './built-in/timetable'; import notificationCollectorPlugin from './built-in/notificationCollector'; -import testPlugin from './built-in/test'; // Initialize plugin manager const pluginManager = PluginManager.getInstance(); @@ -11,7 +10,7 @@ const pluginManager = PluginManager.getInstance(); // Register built-in plugins pluginManager.registerPlugin(timetablePlugin); pluginManager.registerPlugin(notificationCollectorPlugin); -pluginManager.registerPlugin(testPlugin); +//pluginManager.registerPlugin(testPlugin); // Legacy plugin exports export { init as Monofile } from './monofile'; From f2b594a13b4d07707604eecd0d4d180153757dbb Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Wed, 26 Mar 2025 17:00:58 +1100 Subject: [PATCH 42/72] fix: crxjs plugin issues --- docs/README.md | 2 +- lib/patchPackage.ts | 1 - lib/shadowDom.ts | 27 +++++++++ lib/touchGlobalCSS.ts | 17 ++++++ src/background.ts | 43 -------------- .../components/themes/ThemeSelector.svelte | 8 ++- src/interface/main.ts | 8 +-- src/shadowDomUtils.ts | 59 +++++++++++++++++++ vite.config.ts | 9 ++- 9 files changed, 119 insertions(+), 55 deletions(-) create mode 100644 lib/shadowDom.ts create mode 100644 lib/touchGlobalCSS.ts create mode 100644 src/shadowDomUtils.ts diff --git a/docs/README.md b/docs/README.md index 87302c62..c98ac4f9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -43,7 +43,7 @@ If you need help with BetterSEQTA+, you can: - [Open an Issue](https://github.com/SeqtaLearning/betterseqta-plus/issues) - Report bugs or request features - [Join the Discord](https://discord.gg/YzmbnCDkat) - Chat with the community -- [Email the Maintainers](mailto:betterseqta@example.com) - Contact the maintainers directly +- [Email the Maintainers](mailto:betterseqta.plus@gmail.com) - Contact the maintainers directly ## Contributing to the Documentation diff --git a/lib/patchPackage.ts b/lib/patchPackage.ts index df0ede8a..08dec9bb 100644 --- a/lib/patchPackage.ts +++ b/lib/patchPackage.ts @@ -34,7 +34,6 @@ export function updateManifestPlugin(): PluginOption { } fs.watchFile(manifestPath, () => { - console.log('** watchFile **'); try { const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); if (manifestContents.web_accessible_resources?.some((resource: any) => resource.use_dynamic_url)) { diff --git a/lib/shadowDom.ts b/lib/shadowDom.ts new file mode 100644 index 00000000..9eff7eea --- /dev/null +++ b/lib/shadowDom.ts @@ -0,0 +1,27 @@ +import { Plugin } from "vite"; + +export default function shadowDom(): Plugin { + return { + name: 'merge-css-shadow-dom', + enforce: 'post', + apply: 'serve', + transform(src, id) { + if (/\.(css).*$/.test(id)) { + const fn = + "import { updateStyle, removeStyle } from '@/shadowDomUtils.ts';\n"; + let updatedSrc = fn + src; + updatedSrc = updatedSrc.replace( + '__vite__updateStyle(', + 'updateStyle(', + ); + updatedSrc = updatedSrc.replace( + '__vite__removeStyle(', + 'removeStyle(', + ); + return { + code: updatedSrc, + }; + } + } + } +} \ No newline at end of file diff --git a/lib/touchGlobalCSS.ts b/lib/touchGlobalCSS.ts new file mode 100644 index 00000000..d01db686 --- /dev/null +++ b/lib/touchGlobalCSS.ts @@ -0,0 +1,17 @@ +import fs from 'fs'; + +export default function touchGlobalCSSPlugin() { + return { + name: 'touch-global-css', + handleHotUpdate({ modules }) { + // log all of the staticImportedUrls + const importers = modules[0]._clientModule.importers + importers.forEach((importer) => { + if (importer.file.includes('.css')) { + console.log("touching", importer.file) + fs.utimesSync(importer.file, new Date(), new Date()) + } + }) + } + }; +} diff --git a/src/background.ts b/src/background.ts index f88e2369..3c3949f6 100644 --- a/src/background.ts +++ b/src/background.ts @@ -154,53 +154,10 @@ function SetStorageValue(object: any) { } } -async function UpdateCurrentValues() { - try { - const items = await browser.storage.local.get(); - const CurrentValues = items; - - const NewValue = Object.assign({}, DefaultValues, CurrentValues); - - function CheckInnerElement(element: any) { - for (let i in element) { - if (typeof element[i] === 'object') { - // @ts-expect-error - if (!Array.isArray(DefaultValues[i])) { - // @ts-expect-error - NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]); - } else { - // @ts-expect-error - const length = DefaultValues[i].length; - // @ts-expect-error - NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]); - let NewArray = []; - for (let j = 0; j < length; j++) { - NewArray.push(NewValue[i][j]); - } - NewValue[i] = NewArray; - } - } - } - } - - CheckInnerElement(DefaultValues); - - if (items['customshortcuts']) { - NewValue['customshortcuts'] = items['customshortcuts']; - } - - SetStorageValue(NewValue); - console.log('[BetterSEQTA+] Values updated successfully'); - } catch (error) { - console.error('[BetterSEQTA+] Error updating values:', error); - } -} - browser.runtime.onInstalled.addListener(function (event) { browser.storage.local.remove(['justupdated']); browser.storage.local.remove(['data']); - UpdateCurrentValues(); if ( event.reason == 'install', event.reason == 'update' ) { browser.storage.local.set({ justupdated: true }); } diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index 9d490729..33d61b27 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -75,6 +75,7 @@ await InstallTheme(result); await fetchThemes(); } catch (error) { + console.error('Error parsing file:', error); alert('Error parsing file. Please upload a valid JSON theme file.'); } tempTheme = null; @@ -95,6 +96,11 @@ onDestroy(() => { themeUpdates.removeListener(fetchThemes); }) + + $effect(() => { + if (!themes) return; + console.log(themes.selectedTheme); + })
handleThemeClick(theme)} > {#if isEditMode} diff --git a/src/interface/main.ts b/src/interface/main.ts index 63ec7ce1..ac5ab2e5 100644 --- a/src/interface/main.ts +++ b/src/interface/main.ts @@ -1,6 +1,7 @@ -import styles from "./index.css?inline" +//import styles from "./index.css?inline" import { mount } from "svelte" import type { ComponentType } from "svelte" +import './index.css' export default function renderSvelte( Component: ComponentType | any, @@ -15,10 +16,5 @@ export default function renderSvelte( }, }) - const style = document.createElement("style") - style.setAttribute("type", "text/css") - style.innerHTML = styles - mountPoint.appendChild(style) - return app } diff --git a/src/shadowDomUtils.ts b/src/shadowDomUtils.ts new file mode 100644 index 00000000..5ab6339e --- /dev/null +++ b/src/shadowDomUtils.ts @@ -0,0 +1,59 @@ +const sheetsMap = new Map(); +export function updateStyle(id: string, content: string) { + let style = sheetsMap.get(id); + { + if (style && !(style instanceof HTMLStyleElement)) { + removeStyle(id); + style = undefined; + } + if (!style) { + style = document.createElement('style'); + style.setAttribute('type', 'text/css'); + style.innerHTML = content; + if (window.location.href.includes('chrome-extension://')) { + document.head.appendChild(style); + } else { + const root = document.getElementById('ExtensionPopup'); + + // if no root try again in a second + if (!root) { + setTimeout(() => updateStyle(id, content), 1000); + return; + } + const shadowEl = root?.shadowRoot; + shadowEl?.appendChild(style); + } + } else { + style.innerHTML = content; + } + } + sheetsMap.set(id, style); +} + +export function removeStyle(id: string) { + const style = sheetsMap.get(id); + if (style) { + if (window.location.href.includes('chrome-extension://')) { + if (style instanceof CSSStyleSheet) { + (document as any).adoptedStyleSheets = ( + document as any + ).adoptedStyleSheets.filter((s: any) => s !== style); + } else { + document.head.removeChild(style); + } + } else { + const root = document.getElementById('ExtensionPopup'); + const shadowEl: any = root?.shadowRoot; + if (style instanceof CSSStyleSheet) { + if (shadowEl) { + shadowEl.adoptedStyleSheets = shadowEl.adoptedStyleSheets.filter( + (s: any) => s !== style, + ); + } + } else if (shadowEl) { + shadowEl.removeChild(style); + } + } + sheetsMap.delete(id); + } +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 90ec170a..81e28270 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,6 @@ import { defineConfig } from 'vite'; -import { join, resolve } from 'path'; - +import path, { join, resolve } from 'path'; +import fs from 'fs'; import { updateManifestPlugin } from './lib/patchPackage'; import { base64Loader } from './lib/base64loader'; import type { BuildTarget } from './lib/types'; @@ -19,7 +19,8 @@ import { firefox } from './src/manifests/firefox'; import { opera } from './src/manifests/opera'; import { safari } from './src/manifests/safari'; import { crx } from '@crxjs/vite-plugin'; - +import shadowDom from './lib/shadowDom'; +import touchGlobalCSSPlugin from './lib/touchGlobalCSS'; const targets: BuildTarget[] = [ chrome, brave, edge, firefox, opera, safari ] @@ -41,6 +42,8 @@ export default defineConfig(({ command }) => ({ browser: mode.toLowerCase() === "firefox" ? "firefox" : "chrome" }), updateManifestPlugin(), + shadowDom(), + touchGlobalCSSPlugin(), ...(command === 'build' ? [ClosePlugin()] : []) ], root: resolve(__dirname, './src'), From 7196a85f7d80cf7145caf3c0794ebb16830859da Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Wed, 26 Mar 2025 17:35:35 +1100 Subject: [PATCH 43/72] fix: downgrade to tailwindcss v3 because of issues --- package.json | 6 +-- .../components/TabbedContainer.svelte | 4 +- .../components/store/Backgrounds.svelte | 2 +- .../components/store/CoverSwiper.svelte | 2 +- .../components/store/FilterPanel.svelte | 8 ++-- src/interface/components/store/Header.svelte | 2 +- .../components/themes/ThemeSelector.svelte | 2 +- src/interface/index.css | 48 ++----------------- src/interface/pages/themeCreator.svelte | 6 +-- src/postcss.config.cjs | 7 +++ tailwind.config.js | 45 +++++++++++++++++ vite.config.ts | 2 - 12 files changed, 71 insertions(+), 63 deletions(-) create mode 100644 src/postcss.config.cjs create mode 100644 tailwind.config.js diff --git a/package.json b/package.json index f8225af7..3cd068bf 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "sass": "^1.85.1", "sass-loader": "^16.0.5", "semver": "^7.7.1", - "tailwindcss": "^4.0.13", + "tailwindcss": "3", "url": "^0.11.4" }, "dependencies": { @@ -63,8 +63,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/view": "^6.36.4", "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@tailwindcss/forms": "^0.5.9", - "@tailwindcss/vite": "^4.0.12", + "@tailwindcss/forms": "^0.5.10", "@tsconfig/svelte": "^5.0.4", "@types/chrome": "^0.0.308", "@types/color": "^4.2.0", @@ -87,6 +86,7 @@ "lodash": "^4.17.21", "million": "^3.1.11", "motion": "^12.4.12", + "postcss": "^8.5.3", "react": "17", "react-best-gradient-color-picker": "3.0.11", "react-dom": "17", diff --git a/src/interface/components/TabbedContainer.svelte b/src/interface/components/TabbedContainer.svelte index c5866562..080f4b3a 100644 --- a/src/interface/components/TabbedContainer.svelte +++ b/src/interface/components/TabbedContainer.svelte @@ -49,7 +49,7 @@ /> {#each tabs as { title }, index}
diff --git a/src/postcss.config.cjs b/src/postcss.config.cjs new file mode 100644 index 00000000..34c1412a --- /dev/null +++ b/src/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + } +} + \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 00000000..a7738abc --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,45 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./src/**/*.{js,ts,jsx,tsx,html,svelte}", + ], + darkMode: "class", + theme: { + fontSize: { + "xs": ".65rem", + "sm": ".775rem", + "base": "0.65rem", + "md": "0.65rem", + "lg": "1rem", + "xl": "1.25rem", + "2xl": "1.5rem", + "3xl": "1.875rem", + "4xl": "2.25rem", + "5xl": "3rem", + "6xl": "4rem", + "7xl": "5rem", + "8xl": "6rem", + "9xl": "8rem", + "10xl": "10rem", + "11xl": "12rem", + "12xl": "14rem", + "13xl": "16rem", + "14xl": "18rem", + }, + extend: { + fontFamily: { + "IconFamily": "IconFamily" + }, + animation: { + 'spin-fast': 'spin 0.4s linear infinite', + }, + aspectRatio: { + "theme": "5 / 1" + } + } + }, + plugins: [ + require('@tailwindcss/forms'), + ], +}; + \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 81e28270..6ac8c430 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,7 +10,6 @@ import million from "million/compiler"; //import MillionLint from '@million/lint'; import { svelte } from '@sveltejs/vite-plugin-svelte' -import tailwindcss from '@tailwindcss/vite'; import { chrome } from './src/manifests/chrome'; import { brave } from './src/manifests/brave'; @@ -31,7 +30,6 @@ const mode = process.env.MODE || 'chrome'; // Check the environment variable to export default defineConfig(({ command }) => ({ plugins: [ base64Loader, - tailwindcss(), svelte({ emitCss: false }), From 64bf1d88e88d0e26ad8e85ae55bb4665ab2a3481 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Wed, 26 Mar 2025 17:38:19 +1100 Subject: [PATCH 44/72] fix: background type error --- src/background.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/background.ts b/src/background.ts index 3c3949f6..638a952b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -49,13 +49,14 @@ browser.runtime.onMessage.addListener((request: any, _: any, sendResponse: (resp break; case 'sendNews': - fetchNews(request.source ?? 'australia', sendResponse); return true; default: console.log('Unknown request type'); } + + return true; }); const DefaultValues: SettingsState = { From f0c5b1dace47a5ca7f53bb573f12d7509f487dcc Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Thu, 27 Mar 2025 21:31:41 +1100 Subject: [PATCH 45/72] feat: build themes into a centralised plugin --- plugins.md | 207 ------ .../components/store/Backgrounds.svelte | 6 +- .../components/themes/ThemeSelector.svelte | 26 +- src/interface/pages/store.svelte | 17 +- src/interface/pages/themeCreator.svelte | 28 +- src/plugins/built-in/themes/index.ts | 54 ++ src/plugins/built-in/themes/theme-manager.ts | 644 ++++++++++++++++++ src/plugins/index.ts | 2 + src/seqta/utils/listeners/MessageListener.ts | 34 +- themes.md | 123 ++++ 10 files changed, 877 insertions(+), 264 deletions(-) delete mode 100644 plugins.md create mode 100644 src/plugins/built-in/themes/index.ts create mode 100644 src/plugins/built-in/themes/theme-manager.ts create mode 100644 themes.md diff --git a/plugins.md b/plugins.md deleted file mode 100644 index 01ce9cf6..00000000 --- a/plugins.md +++ /dev/null @@ -1,207 +0,0 @@ -# BetterSEQTA+ Plugin System - -## Overview -The BetterSEQTA+ plugin system is designed to provide a clean, type-safe, and developer-friendly way to extend the functionality of BetterSEQTA+. While initially focused on built-in plugins, the architecture is designed to potentially support external plugins in the future. - -## Core Concepts - -### Plugin Structure -Each plugin is a simple object that contains metadata and a run function: - -```typescript -const examplePlugin = { - id: 'example', - name: 'Example Plugin', - description: 'Does something cool', - version: '1.0.0', - settings: { - enabled: { type: 'boolean', default: true }, - color: { type: 'string', default: '#ff0000' } - }, - - run: (api) => { - // Plugin logic here - } -}; -``` - -### Plugin API -Plugins receive a powerful API object that provides access to: - -- **Settings**: Type-safe settings management with direct property access -- **SEQTA Integration**: React component mounting and state management -- **Storage**: Persistent storage capabilities -- **Events**: Communication system - -### Settings System -Settings are defined with TypeScript types for safety and accessed like regular properties: - -```typescript -// In your plugin -api.settings.myOption = true; -const value = api.settings.myOption; - -// Watch for changes -api.settings.onChange('myOption', (newValue) => { - console.log('Option changed:', newValue); -}); -``` - -### SEQTA Integration -Plugins can interact with SEQTA's React components: - -```typescript -// Listen for component mounting -api.seqta.onMount('.timetable-view', (element) => { - // Access the DOM element directly - console.log('Timetable mounted:', element); - - // If you need React access, use getFiber - const fiber = api.seqta.getFiber('.timetable-view'); - fiber.setState(prevState => ({ - ...prevState, - someValue: true - })); -}); - -// Get specific component -const fiber = api.seqta.getFiber('.timetable-cell'); -const props = await fiber.getProps(); - -// Listen for page changes -api.seqta.onPageChange((page) => { - if (page === 'timetable') { - // Handle timetable page - } -}); -``` - -## Implementation Status - -### Phase 1: Core Infrastructure ✅ -- [x] Create basic plugin type definitions -- [x] Implement plugin manager -- [x] Set up basic API structure -- [x] Create plugin loading system - -### Phase 2: Settings System ✅ -- [x] Design settings storage structure -- [x] Implement settings proxy system -- [x] Add settings change notifications -- [x] Create settings validation - -### Phase 3: SEQTA Integration ✅ -- [x] Implement component mount detection -- [x] Create ReactFiber wrapper -- [x] Add page change detection -- [x] Create component state utilities - -### Phase 4: Plugin API Features ✅ -- [x] Storage system -- [x] Event system -- [x] Error handling -- [ ] Plugin lifecycle hooks - -### Phase 5: Migration & Testing 🚧 -- [ ] Convert existing features to plugins -- [ ] Create plugin testing utilities -- [ ] Add plugin documentation -- [ ] Create example plugins - -### Phase 6: Future Enhancements 📝 -- [ ] Plugin dependencies system -- [ ] Plugin hot-reloading -- [ ] External plugin support -- [ ] Plugin marketplace infrastructure - -## Plugin Example - -```typescript -const timetablePlugin = { - id: 'timetable', - name: 'Timetable Enhancer', - description: 'Adds extra features to the timetable view', - version: '1.0.0', - settings: { - showWeekends: { - type: 'boolean', - default: false, - description: 'Show weekend days in the timetable' - }, - theme: { - type: 'select', - options: ['light', 'dark', 'auto'], - default: 'auto', - description: 'Timetable theme' - } - }, - - run: async (api) => { - // Listen for timetable mount - api.seqta.onMount('.timetable-view', (element) => { - // Get React access since we need to modify state - const fiber = api.seqta.getFiber('.timetable-view'); - - // Apply settings - if (api.settings.showWeekends) { - fiber.setState(prevState => ({ - ...prevState, - showWeekends: true - })); - } - }); - - // Watch for settings changes - api.settings.onChange('theme', async (newTheme) => { - const timetable = api.seqta.getFiber('.timetable-view'); - if (newTheme !== 'auto') { - await timetable.setProp('theme', newTheme); - } - }); - } -}; -``` - -## Directory Structure -``` -src/ - plugins/ - core/ - types.ts # Core type definitions - createAPI.ts # API implementation - manager.ts # Plugin manager - built-in/ # Built-in plugins - timetable/ - assessments/ - etc... -``` - -## API Type Definitions - -```typescript -interface BSAPI { - seqta: { - onMount: (selector: string, callback: (fiber: ReactFiber) => void) => void; - getFiber: (selector: string) => ReactFiber; - getCurrentPage: () => string; - onPageChange: (callback: (page: string) => void) => void; - }; - - settings: TSettings & { - onChange: ( - key: K, - callback: (value: TSettings[K]) => void - ) => void; - }; - - storage: { - get: (key: string) => Promise; - set: (key: string, value: any) => Promise; - }; - - events: { - on: (event: string, callback: (...args: any[]) => void) => void; - emit: (event: string, ...args: any[]) => void; - }; -} -``` \ No newline at end of file diff --git a/src/interface/components/store/Backgrounds.svelte b/src/interface/components/store/Backgrounds.svelte index 09650525..307af84a 100644 --- a/src/interface/components/store/Backgrounds.svelte +++ b/src/interface/components/store/Backgrounds.svelte @@ -1,10 +1,12 @@ diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index f68e0105..88e983e6 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -1,16 +1,13 @@ - \ No newline at end of file diff --git a/src/interface/index.ts b/src/interface/index.ts index af9715be..e044d2dd 100644 --- a/src/interface/index.ts +++ b/src/interface/index.ts @@ -1,25 +1,8 @@ import "./index.css" -import { mount } from "svelte" -import type { ComponentType } from "svelte" import Settings from "./pages/settings.svelte" import IconFamily from '@/resources/fonts/IconFamily.woff' import browser from "webextension-polyfill" - -export default function renderSvelte( - Component: ComponentType | any, - mountPoint: ShadowRoot | HTMLElement, - props: Record = {}, -) { - const app = mount(Component, { - target: mountPoint, - props: { - standalone: true, - ...props, - }, - }) - - return app -} +import renderSvelte from "./main" function InjectCustomIcons() { console.info('[BetterSEQTA+] Injecting Icons') diff --git a/src/interface/main.ts b/src/interface/main.ts index ac5ab2e5..c7d1e02d 100644 --- a/src/interface/main.ts +++ b/src/interface/main.ts @@ -1,7 +1,6 @@ -//import styles from "./index.css?inline" import { mount } from "svelte" import type { ComponentType } from "svelte" -import './index.css' +import style from './index.css?inline' export default function renderSvelte( Component: ComponentType | any, @@ -16,5 +15,9 @@ export default function renderSvelte( }, }) + const styleElement = document.createElement('style') + styleElement.textContent = style + mountPoint.appendChild(styleElement) + return app } diff --git a/src/shadowDomUtils.ts b/src/shadowDomUtils.ts deleted file mode 100644 index 5ab6339e..00000000 --- a/src/shadowDomUtils.ts +++ /dev/null @@ -1,59 +0,0 @@ -const sheetsMap = new Map(); -export function updateStyle(id: string, content: string) { - let style = sheetsMap.get(id); - { - if (style && !(style instanceof HTMLStyleElement)) { - removeStyle(id); - style = undefined; - } - if (!style) { - style = document.createElement('style'); - style.setAttribute('type', 'text/css'); - style.innerHTML = content; - if (window.location.href.includes('chrome-extension://')) { - document.head.appendChild(style); - } else { - const root = document.getElementById('ExtensionPopup'); - - // if no root try again in a second - if (!root) { - setTimeout(() => updateStyle(id, content), 1000); - return; - } - const shadowEl = root?.shadowRoot; - shadowEl?.appendChild(style); - } - } else { - style.innerHTML = content; - } - } - sheetsMap.set(id, style); -} - -export function removeStyle(id: string) { - const style = sheetsMap.get(id); - if (style) { - if (window.location.href.includes('chrome-extension://')) { - if (style instanceof CSSStyleSheet) { - (document as any).adoptedStyleSheets = ( - document as any - ).adoptedStyleSheets.filter((s: any) => s !== style); - } else { - document.head.removeChild(style); - } - } else { - const root = document.getElementById('ExtensionPopup'); - const shadowEl: any = root?.shadowRoot; - if (style instanceof CSSStyleSheet) { - if (shadowEl) { - shadowEl.adoptedStyleSheets = shadowEl.adoptedStyleSheets.filter( - (s: any) => s !== style, - ); - } - } else if (shadowEl) { - shadowEl.removeChild(style); - } - } - sheetsMap.delete(id); - } -} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 6ac8c430..9254e6d8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,7 +18,7 @@ import { firefox } from './src/manifests/firefox'; import { opera } from './src/manifests/opera'; import { safari } from './src/manifests/safari'; import { crx } from '@crxjs/vite-plugin'; -import shadowDom from './lib/shadowDom'; + import touchGlobalCSSPlugin from './lib/touchGlobalCSS'; const targets: BuildTarget[] = [ chrome, brave, edge, firefox, opera, safari @@ -40,7 +40,6 @@ export default defineConfig(({ command }) => ({ browser: mode.toLowerCase() === "firefox" ? "firefox" : "chrome" }), updateManifestPlugin(), - shadowDom(), touchGlobalCSSPlugin(), ...(command === 'build' ? [ClosePlugin()] : []) ], From ad2ad4d456261f419613826d83da143b7f351073 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 28 Mar 2025 00:14:29 +1100 Subject: [PATCH 47/72] feat: debounce creator + general improvements --- src/interface/pages/themeCreator.svelte | 4 ++-- .../ui => plugins/built-in/themes}/ThemeCreator.ts | 5 +++-- src/plugins/built-in/themes/theme-manager.ts | 9 +++++++++ src/seqta/utils/debounce.ts | 7 +++++++ src/seqta/utils/listeners/MessageListener.ts | 2 +- 5 files changed, 22 insertions(+), 5 deletions(-) rename src/{seqta/ui => plugins/built-in/themes}/ThemeCreator.ts (95%) create mode 100644 src/seqta/utils/debounce.ts diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index fb219c3f..32b57c28 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -21,7 +21,7 @@ handleImageVariableChange, handleCoverImageUpload } from '../utils/themeImageHandlers'; - import { CloseThemeCreator } from '@/seqta/ui/ThemeCreator' + import { CloseThemeCreator } from '@/plugins/built-in/themes/ThemeCreator' import { themeUpdates } from '../hooks/ThemeUpdates' import { ThemeManager } from '@/plugins/built-in/themes/theme-manager' @@ -121,7 +121,7 @@ $effect(() => { if (themeLoaded) { - void themeManager.updatePreview(theme); + void themeManager.updatePreviewDebounced(theme); } }); diff --git a/src/seqta/ui/ThemeCreator.ts b/src/plugins/built-in/themes/ThemeCreator.ts similarity index 95% rename from src/seqta/ui/ThemeCreator.ts rename to src/plugins/built-in/themes/ThemeCreator.ts index 963d008d..dd23ca1f 100644 --- a/src/seqta/ui/ThemeCreator.ts +++ b/src/plugins/built-in/themes/ThemeCreator.ts @@ -1,9 +1,10 @@ import renderSvelte from "@/interface/main" import themeCreator from "@/interface/pages/themeCreator.svelte" import { unmount } from "svelte" -import { ClearThemePreview } from "./themes/UpdateThemePreview" +import { ThemeManager } from "@/plugins/built-in/themes/theme-manager" let themeCreatorSvelteApp: any = null +const themeManager = ThemeManager.getInstance(); /** * Open the Theme Creator sidebar, it is an embedded page loaded similar to the extension popup @@ -33,7 +34,7 @@ export function OpenThemeCreator(themeID: string = "") { closeButton.textContent = "×" closeButton.addEventListener("click", () => { CloseThemeCreator() - ClearThemePreview() + themeManager.clearPreview() }) document.body.appendChild(closeButton) diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index 9358bfe9..01130b9b 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -1,6 +1,7 @@ import localforage from 'localforage'; import type { CustomTheme, LoadedCustomTheme } from '@/types/CustomThemes'; import { settingsState } from '@/seqta/utils/listeners/SettingsState'; +import debounce from '@/seqta/utils/debounce'; type ThemeContent = { id: string; @@ -517,6 +518,14 @@ export class ThemeManager { } } + /** + * Update the preview of a theme (debounced) + * @param theme - The theme to update the preview of + */ + public updatePreviewDebounced = debounce((theme: Partial): void => { + this.updatePreview(theme); + }, 2); + /** * Clear theme preview */ diff --git a/src/seqta/utils/debounce.ts b/src/seqta/utils/debounce.ts new file mode 100644 index 00000000..916e8933 --- /dev/null +++ b/src/seqta/utils/debounce.ts @@ -0,0 +1,7 @@ +export default function debounce void>(fn: T, delay: number): (...args: Parameters) => void { + let timeout: ReturnType; + return function(this: ThisParameterType, ...args: Parameters) { + clearTimeout(timeout); + timeout = setTimeout(() => fn.apply(this, args), delay); + }; +} diff --git a/src/seqta/utils/listeners/MessageListener.ts b/src/seqta/utils/listeners/MessageListener.ts index 2745caa4..673ca435 100644 --- a/src/seqta/utils/listeners/MessageListener.ts +++ b/src/seqta/utils/listeners/MessageListener.ts @@ -3,7 +3,7 @@ import browser from 'webextension-polyfill' import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup" import { MenuOptionsOpen, OpenMenuOptions } from "@/seqta/utils/Openers/OpenMenuOptions" -import { CloseThemeCreator, OpenThemeCreator } from '@/seqta/ui/ThemeCreator'; +import { CloseThemeCreator, OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'; import sendThemeUpdate from '@/seqta/utils/sendThemeUpdate'; import hideSensitiveContent from '@/seqta/ui/dev/hideSensitiveContent'; import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'; From dc4499e8a23ec2f0e5c9cc6c21807e14088b2a7e Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 28 Mar 2025 00:19:40 +1100 Subject: [PATCH 48/72] refactor: remove legacy theme handling and streamline plugin initialization --- src/SEQTA.ts | 2 +- .../components/themes/ThemeSelector.svelte | 2 +- src/plugins/index.ts | 1 - src/plugins/themes.ts | 5 -- src/seqta/ui/themes/Themes.ts | 15 ---- src/seqta/ui/themes/UpdateThemePreview.ts | 75 ------------------ src/seqta/ui/themes/applyTheme.ts | 26 ------ src/seqta/ui/themes/deleteTheme.ts | 23 ------ src/seqta/ui/themes/disableTheme.ts | 37 --------- src/seqta/ui/themes/downloadTheme.ts | 79 ------------------- src/seqta/ui/themes/enableCurrent.ts | 14 ---- src/seqta/ui/themes/getAvailableThemes.ts | 29 ------- src/seqta/ui/themes/getTheme.ts | 14 ---- src/seqta/ui/themes/removeTheme.ts | 36 --------- src/seqta/ui/themes/saveTheme.ts | 30 ------- src/seqta/ui/themes/setTheme.ts | 38 --------- src/seqta/ui/themes/shareTheme.ts | 74 ----------------- 17 files changed, 2 insertions(+), 498 deletions(-) delete mode 100644 src/plugins/themes.ts delete mode 100644 src/seqta/ui/themes/Themes.ts delete mode 100644 src/seqta/ui/themes/UpdateThemePreview.ts delete mode 100644 src/seqta/ui/themes/applyTheme.ts delete mode 100644 src/seqta/ui/themes/deleteTheme.ts delete mode 100644 src/seqta/ui/themes/disableTheme.ts delete mode 100644 src/seqta/ui/themes/downloadTheme.ts delete mode 100644 src/seqta/ui/themes/enableCurrent.ts delete mode 100644 src/seqta/ui/themes/getAvailableThemes.ts delete mode 100644 src/seqta/ui/themes/getTheme.ts delete mode 100644 src/seqta/ui/themes/removeTheme.ts delete mode 100644 src/seqta/ui/themes/saveTheme.ts delete mode 100644 src/seqta/ui/themes/setTheme.ts delete mode 100644 src/seqta/ui/themes/shareTheme.ts diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 7565dbbe..bff719c0 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -41,7 +41,7 @@ async function init() { if (settingsState.onoff) { // Initialize legacy plugins - const legacyPlugins = [plugins.Monofile, plugins.Themes]; + const legacyPlugins = [plugins.Monofile]; legacyPlugins.forEach(plugin => { if (typeof plugin === 'function') { plugin(); diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte index 88e983e6..d6b6ce56 100644 --- a/src/interface/components/themes/ThemeSelector.svelte +++ b/src/interface/components/themes/ThemeSelector.svelte @@ -1,7 +1,7 @@
): Promise { console.debug('[ThemeManager] Updating theme preview'); try { - // Store original settings if not already stored - if (this.originalPreviewColor === null) { - this.originalPreviewColor = settingsState.selectedColor; - } - if (this.originalPreviewTheme === null) { - this.originalPreviewTheme = settingsState.DarkMode; + // Only store original settings if this is a new theme (not editing) + // We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded) + if (!theme.webURL) { + if (this.originalPreviewColor === null) { + this.originalPreviewColor = settingsState.selectedColor; + } + if (this.originalPreviewTheme === null) { + this.originalPreviewTheme = settingsState.DarkMode; + } } // Apply CSS if changed @@ -548,12 +555,14 @@ export class ThemeManager { this.previousImageVariableNames = newImageVariableNames; } - // Update theme settings - if (theme.forceDark !== undefined) { - settingsState.DarkMode = theme.forceDark; - } - if (theme.defaultColour) { - settingsState.selectedColor = theme.defaultColour; + // Update theme settings only if this is a new theme + if (!theme.webURL) { + if (theme.forceDark !== undefined) { + settingsState.DarkMode = theme.forceDark; + } + if (theme.defaultColour) { + settingsState.selectedColor = theme.defaultColour; + } } } catch (error) { console.error('[ThemeManager] Error updating theme preview:', error); From 7af6acaf383e69406be26a82aeaaa6c20fb009ac Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 28 Mar 2025 12:17:37 +1100 Subject: [PATCH 52/72] fix: themes custom colour not being completely applied --- src/interface/pages/themeCreator.svelte | 6 +-- src/plugins/built-in/themes/theme-manager.ts | 48 ++++++++++++-------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index 32b57c28..9b68ca4c 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -75,10 +75,8 @@ })) } - if (tempTheme) { - theme = loadedTheme - themeLoaded = true - } + theme = loadedTheme + themeLoaded = true } else { themeLoaded = true } diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index 94f10e32..052f380f 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -145,6 +145,7 @@ export class ThemeManager { // Remove current theme if exists if (this.currentTheme) { console.debug('[ThemeManager] Removing current theme'); + await this.removeTheme(this.currentTheme); } @@ -185,8 +186,12 @@ export class ThemeManager { settingsState.DarkMode = theme.forceDark; } - if (theme.defaultColour) { - console.debug('[ThemeManager] Setting color:', theme.defaultColour); + // Use the stored selected color if available, otherwise use the default + if (theme.selectedColor) { + console.debug('[ThemeManager] Restoring saved color:', theme.selectedColor); + settingsState.selectedColor = theme.selectedColor; + } else if (theme.defaultColour) { + console.debug('[ThemeManager] Using default color:', theme.defaultColour); settingsState.selectedColor = theme.defaultColour; } @@ -220,6 +225,14 @@ export class ThemeManager { }); } + if (this.currentTheme) { + // Store the current color with the theme before removing it + await localforage.setItem(this.currentTheme.id, { + ...this.currentTheme, + selectedColor: settingsState.selectedColor + }); + } + // Restore original settings if (settingsState.originalSelectedColor) { console.debug('[ThemeManager] Restoring original color:', settingsState.originalSelectedColor); @@ -489,14 +502,13 @@ export class ThemeManager { // Update previousImageVariableNames this.previousImageVariableNames = newImageVariableNames; - // Apply theme settings only if this is a new theme - if (!theme.webURL) { - if (forceDark !== undefined) { - settingsState.DarkMode = forceDark; - } - if (defaultColour) { - settingsState.selectedColor = defaultColour; - } + // Apply theme settings + if (forceDark !== undefined) { + settingsState.DarkMode = forceDark; + } + + if (defaultColour) { + settingsState.selectedColor = defaultColour; } } catch (error) { console.error('[ThemeManager] Error previewing theme:', error); @@ -555,14 +567,14 @@ export class ThemeManager { this.previousImageVariableNames = newImageVariableNames; } - // Update theme settings only if this is a new theme - if (!theme.webURL) { - if (theme.forceDark !== undefined) { - settingsState.DarkMode = theme.forceDark; - } - if (theme.defaultColour) { - settingsState.selectedColor = theme.defaultColour; - } + // Always apply dark mode setting + if (theme.forceDark !== undefined) { + settingsState.DarkMode = theme.forceDark; + } + + // Only apply color if this is a new theme + if (!theme.webURL && theme.defaultColour) { + settingsState.selectedColor = theme.defaultColour; } } catch (error) { console.error('[ThemeManager] Error updating theme preview:', error); From d19f5730936e2486d69908797bc38eeaeb82f9c9 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 28 Mar 2025 12:29:26 +1100 Subject: [PATCH 53/72] fix: theme creator fullscreen view not working --- src/interface/pages/themeCreator.svelte | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/interface/pages/themeCreator.svelte b/src/interface/pages/themeCreator.svelte index 9b68ca4c..ec6b74f5 100644 --- a/src/interface/pages/themeCreator.svelte +++ b/src/interface/pages/themeCreator.svelte @@ -51,7 +51,12 @@ codeEditorFullscreen = !codeEditorFullscreen; } - function toggleAccordion(title: string) { + function toggleAccordion(title: string, e: MouseEvent | KeyboardEvent) { + // if the target is the fullscreen button return + if (e.target instanceof HTMLButtonElement && e.target.classList.contains('fullscreen-toggle')) { + return; + } + if (closedAccordions.includes(title)) { closedAccordions = closedAccordions.filter(t => t !== title); } else { @@ -160,8 +165,8 @@
{ item.direction === 'vertical' && toggleAccordion(item.title) }} - onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title) }} + onclick={(e) => { item.direction === 'vertical' && toggleAccordion(item.title, e) }} + onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title, e) }} class="flex justify-between pr-4 {item.direction === 'vertical' ? 'cursor-pointer w-full select-none' : ''}">
@@ -173,7 +178,7 @@
{#if item.type === 'codeEditor'} - {/if} @@ -251,7 +256,7 @@
{#if codeEditorFullscreen} -
+

Custom CSS

From 9542cb13f5e123db6f1ad2c51064b7d4f69c653f Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Fri, 28 Mar 2025 17:18:23 +1100 Subject: [PATCH 54/72] fix: initial install not loading seqta --- src/SEQTA.ts | 21 +++++++++++---------- src/background/news.ts | 1 + src/plugins/index.ts | 1 - src/plugins/monofile.ts | 4 ---- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/SEQTA.ts b/src/SEQTA.ts index bff719c0..edfeba65 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -2,8 +2,12 @@ import { settingsState, } from "@/seqta/utils/listeners/SettingsState" import documentLoadCSS from "@/css/documentload.scss?inline" - import icon48 from "@/resources/icons/icon-48.png?base64" +import browser from "webextension-polyfill" + +import * as plugins from "@/plugins" +import { main } from "@/seqta/main" + export let MenuOptionsOpen = false @@ -19,9 +23,6 @@ if (document.childNodes[1]) { init() } -import * as plugins from "@/plugins" -import { main } from "@/seqta/main" - async function init() { const hasSEQTATitle = document.title.includes("SEQTA Learn") @@ -37,16 +38,16 @@ async function init() { icon.href = icon48 // Change the icon try { + if (typeof settingsState.onoff === "undefined") { + browser.runtime.sendMessage({ type: "setDefaultStorage" }) + } + await main() if (settingsState.onoff) { // Initialize legacy plugins - const legacyPlugins = [plugins.Monofile]; - legacyPlugins.forEach(plugin => { - if (typeof plugin === 'function') { - plugin(); - } - }); + console.log('init legacy plugins') + plugins.Monofile() // Initialize new plugin system await plugins.initializePlugins(); diff --git a/src/background/news.ts b/src/background/news.ts index a480f3f4..3bb52f9e 100644 --- a/src/background/news.ts +++ b/src/background/news.ts @@ -55,6 +55,7 @@ const rssFeedsByCountry: Record = { export async function fetchNews(source: string, sendResponse: any) { const parser = new Parser(); let feeds: string[]; + console.log('fetchNews', source) if (source === "australia") { const date = new Date(); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 57dddbc5..f9e4f908 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -14,7 +14,6 @@ pluginManager.registerPlugin(notificationCollectorPlugin); pluginManager.registerPlugin(themesPlugin); //pluginManager.registerPlugin(testPlugin); -// Legacy plugin exports export { init as Monofile } from './monofile'; export async function initializePlugins(): Promise { diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index 75a92dd2..09fdcd3e 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -585,10 +585,6 @@ export function showConflictPopup() { } export function init() { - if (typeof settingsState.onoff === "undefined") { - browser.runtime.sendMessage({ type: "setDefaultStorage" }) - } - const handleDisabled = () => { waitForElm(".code", true, 50).then(AppendElementsToDisabledPage) } From 09855c9ef51b5288dfb9cc87f5503ff246d39a94 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sat, 29 Mar 2025 21:56:46 +1100 Subject: [PATCH 55/72] fix: themes randomly disabling after quick succesive page loads --- src/plugins/built-in/themes/theme-manager.ts | 23 +++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/plugins/built-in/themes/theme-manager.ts b/src/plugins/built-in/themes/theme-manager.ts index 052f380f..0afca3a8 100644 --- a/src/plugins/built-in/themes/theme-manager.ts +++ b/src/plugins/built-in/themes/theme-manager.ts @@ -116,7 +116,7 @@ export class ThemeManager { console.debug('[ThemeManager] Cleaning up resources'); try { if (this.currentTheme) { - await this.removeTheme(this.currentTheme); + await this.removeTheme(this.currentTheme, false); } } catch (error) { console.error('[ThemeManager] Error during cleanup:', error); @@ -203,7 +203,7 @@ export class ThemeManager { /** * Remove theme and restore original settings */ - private async removeTheme(theme: CustomTheme): Promise { + private async removeTheme(theme: CustomTheme, clearSelectedTheme: boolean = true): Promise { console.debug('[ThemeManager] Removing theme:', theme.name); try { // Remove custom CSS @@ -246,7 +246,9 @@ export class ThemeManager { } this.currentTheme = null; - settingsState.selectedTheme = ''; + if (clearSelectedTheme) { + settingsState.selectedTheme = ''; + } } catch (error) { console.error('[ThemeManager] Error removing theme:', error); @@ -433,7 +435,16 @@ export class ThemeManager { return; } - const { CustomImages = [], coverImage, ...themeWithoutImages } = theme; + // Extract only the fields we want to share + const { + CustomImages = [], + coverImage, + webURL, + isEditable, + selectedColor, + allowBackgrounds, + ...themeBasics + } = theme; // Convert images to base64 const finalImages = await Promise.all(CustomImages.map(async (image) => ({ @@ -445,9 +456,9 @@ export class ThemeManager { // Convert cover image to base64 const coverImageBase64 = coverImage ? await this.blobToBase64(coverImage) : null; - // Create shareable theme data + // Create shareable theme data with only necessary fields const shareableTheme = { - ...themeWithoutImages, + ...themeBasics, images: finalImages, coverImage: coverImageBase64 }; From 6147e96cc95eaf91e6188fbafbbe2401c1b0d0be Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sat, 29 Mar 2025 22:33:06 +1100 Subject: [PATCH 56/72] feat: remove theme toggle --- src/plugins/built-in/themes/index.ts | 47 +++------------------------- 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/src/plugins/built-in/themes/index.ts b/src/plugins/built-in/themes/index.ts index 9373582a..9230e4ce 100644 --- a/src/plugins/built-in/themes/index.ts +++ b/src/plugins/built-in/themes/index.ts @@ -1,53 +1,16 @@ import type { Plugin } from '../../core/types'; -import { BasePlugin, BooleanSetting } from '../../core/settings'; import { ThemeManager } from './theme-manager'; -// Define only the typed settings - no need for redundant interface -class ThemePluginClass extends BasePlugin { - @BooleanSetting({ - default: true, - title: "Themes", - description: "Adds a theme selector to the settings page" - }) - enabled!: boolean; -} - -// Create an instance to extract settings -const settingsInstance = new ThemePluginClass(); - -const themesPlugin: Plugin = { +const themesPlugin: Plugin = { id: 'themes', name: 'Themes', description: 'Adds a theme selector to the settings page', version: '1.0.0', - settings: settingsInstance.settings, - run: async (api) => { - console.debug('[ThemesPlugin] Starting plugin'); - const themeManager = ThemeManager.getInstance(); + settings: {}, - if (api.settings.enabled) { - console.debug('[ThemesPlugin] Plugin enabled, initializing theme manager'); - await themeManager.initialize(); - } - - const enabledCallback = (value: string | number | boolean) => { - console.debug('[ThemesPlugin] Enabled setting changed:', value); - if (value === true) { - console.debug('[ThemesPlugin] Plugin enabled, initializing theme manager'); - void themeManager.initialize(); - } else if (value === false) { - console.debug('[ThemesPlugin] Plugin disabled, cleaning up theme manager'); - void themeManager.cleanup(); - } - } - - api.settings.onChange('enabled', enabledCallback); - - return () => { - console.debug('[ThemesPlugin] Plugin cleanup'); - api.settings.offChange('enabled', enabledCallback); - void themeManager.cleanup(); - } + run: async (_) => { + const themeManager = ThemeManager.getInstance(); + await themeManager.initialize(); } }; From 3ecd7205ed499124fa2fbb2f677c9f6c66490fd9 Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sun, 30 Mar 2025 08:49:13 +1100 Subject: [PATCH 57/72] feat: add global theme toggle --- src/background.ts | 1 - src/interface/index.html | 4 +- src/interface/pages/settings/general.svelte | 18 +++++ .../built-in/notificationCollector/index.ts | 44 ++++-------- src/plugins/built-in/test/index.ts | 11 ++- src/plugins/built-in/timetable/index.ts | 59 +++++----------- src/plugins/core/createAPI.ts | 17 +++-- src/plugins/core/manager.ts | 70 ++++++++++++++++++- src/plugins/core/types.ts | 7 +- .../EnableNotificationCollector.ts | 24 ------- .../DisableNotificationCollector.ts | 13 ---- src/seqta/utils/Loaders/LoadHomePage.ts | 6 -- src/seqta/utils/listeners/StorageChanges.ts | 11 --- src/types/storage.ts | 1 - 14 files changed, 145 insertions(+), 141 deletions(-) delete mode 100644 src/seqta/utils/CreateEnable/EnableNotificationCollector.ts delete mode 100644 src/seqta/utils/DisableRemove/DisableNotificationCollector.ts diff --git a/src/background.ts b/src/background.ts index 638a952b..4360540b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -65,7 +65,6 @@ const DefaultValues: SettingsState = { bksliderinput: "50", transparencyEffects: false, lessonalert: true, - notificationcollector: true, defaultmenuorder: [], menuitems: { assessments: { toggle: true }, diff --git a/src/interface/index.html b/src/interface/index.html index b324d8bb..0a3f1b8a 100644 --- a/src/interface/index.html +++ b/src/interface/index.html @@ -5,8 +5,8 @@ BetterSEQTA+ Settings - -
+ +
\ No newline at end of file diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index faa95790..61db53ab 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -25,6 +25,7 @@ interface Plugin { pluginId: string; name: string; + description: string; settings: Record; } @@ -78,6 +79,23 @@ pluginSettings.forEach(plugin => { if (Object.keys(plugin.settings).length === 0) return; + // Add enable/disable toggle if plugin has disableToggle set + if ((plugin as any).disableToggle) { + entries.push({ + title: `Enable ${plugin.name}`, + description: `${plugin.description}`, + id: getPluginSettingId(plugin.pluginId, 'enabled'), + Component: Switch, + props: { + state: pluginSettingsValues[plugin.pluginId]?.enabled ?? true, + onChange: (value: boolean) => { + updatePluginSetting(plugin.pluginId, 'enabled', value); + // The plugin manager will handle the actual enabling/disabling + } + } + }); + } + Object.entries(plugin.settings).forEach(([key, setting]) => { const id = getPluginSettingId(plugin.pluginId, key); diff --git a/src/plugins/built-in/notificationCollector/index.ts b/src/plugins/built-in/notificationCollector/index.ts index 0fd297dc..880cb03a 100644 --- a/src/plugins/built-in/notificationCollector/index.ts +++ b/src/plugins/built-in/notificationCollector/index.ts @@ -1,29 +1,18 @@ import type { Plugin } from '../../core/types'; -import { BasePlugin, BooleanSetting } from '../../core/settings'; interface NotificationCollectorStorage { lastNotificationCount: number; lastCheckedTime: string; } -class NotificationCollectorPluginClass extends BasePlugin { - @BooleanSetting({ - default: true, - title: "Notification Collector", - description: "Uncaps the 9+ limit for notifications, showing the real number.", - }) - enabled!: boolean; -} - -// Create an instance to extract settings -const settingsInstance = new NotificationCollectorPluginClass(); - -const notificationCollectorPlugin: Plugin = { +const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { id: 'notificationCollector', name: 'Notification Collector', description: 'Collects and displays SEQTA notifications', version: '1.0.0', - settings: settingsInstance.settings, + settings: {}, + disableToggle: true, + run: async (api) => { let pollInterval: number | null = null; @@ -80,30 +69,21 @@ const notificationCollectorPlugin: Plugin 9) { + alertDiv.textContent = "9+"; + } else { + alertDiv.textContent = api.storage.lastNotificationCount.toString(); + } } } }; - if (api.settings.enabled) { - api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { - startPolling(); - }); - } - - const enabledCallback = (value: any) => { - if (value) { - startPolling(); - } else { - stopPolling(); - } - }; - - api.settings.onChange('enabled', enabledCallback); + api.seqta.onMount(".notifications__bubble___1EkSQ", (_) => { + startPolling(); + }); return () => { stopPolling(); - api.settings.offChange('enabled', enabledCallback); }; } }; diff --git a/src/plugins/built-in/test/index.ts b/src/plugins/built-in/test/index.ts index 2788ac12..d77bc5c1 100644 --- a/src/plugins/built-in/test/index.ts +++ b/src/plugins/built-in/test/index.ts @@ -5,9 +5,9 @@ class TestPluginClass extends BasePlugin { @BooleanSetting({ default: true, title: "Test Plugin", - description: "A test plugin for BetterSEQTA+", + description: "Some random setting", }) - enabled!: boolean; + someSetting!: boolean; } const settingsInstance = new TestPluginClass(); @@ -22,9 +22,14 @@ const testPlugin: Plugin = { run: async (api) => { console.log('Test plugin running'); - api.seqta.onPageChange((page) => { + const { unregister } = api.seqta.onPageChange((page) => { console.log('Page changed to', page); }); + + return () => { + console.log('Test plugin stopped'); + unregister(); + } } }; diff --git a/src/plugins/built-in/timetable/index.ts b/src/plugins/built-in/timetable/index.ts index e3254d8a..af25fb85 100644 --- a/src/plugins/built-in/timetable/index.ts +++ b/src/plugins/built-in/timetable/index.ts @@ -2,53 +2,32 @@ import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import type { Plugin } from '../../core/types'; import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { waitForElm } from '@/seqta/utils/waitForElm'; -import { BasePlugin, BooleanSetting } from '../../core/settings'; -// Define only the typed settings - no need for redundant interface -class TimetablePluginClass extends BasePlugin { - @BooleanSetting({ - default: true, - title: "Timetable Enhancer", - description: "Adds extra features to the timetable view." - }) - enabled!: boolean; -} - -// Create an instance to extract settings -const settingsInstance = new TimetablePluginClass(); - -const timetablePlugin: Plugin = { +const timetablePlugin: Plugin<{}, {}> = { id: 'timetable', name: 'Timetable Enhancer', description: 'Adds extra features to the timetable view', version: '1.0.0', - settings: settingsInstance.settings, + settings: {}, + disableToggle: true, + run: async (api) => { - if (api.settings.enabled) { - api.seqta.onMount('.timetablepage', handleTimetable) - } - - const enabledCallback = (value: any) => { - if (value) { - api.seqta.onMount('.timetablepage', handleTimetable) - } else { - const timetablePage = document.querySelector('.timetablepage') - if (timetablePage) { - const zoomControls = document.querySelector('.timetable-zoom-controls') - if (zoomControls) zoomControls.remove() - - const hideControls = document.querySelector('.timetable-hide-controls') - if (hideControls) hideControls.remove() - - resetTimetableStyles() - } - } - } - - api.settings.onChange('enabled', enabledCallback) - + const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable) + return () => { - api.settings.offChange('enabled', enabledCallback) + // Call the unregister function to remove the mount listener + unregister(); + + const timetablePage = document.querySelector('.timetablepage') + if (timetablePage) { + const zoomControls = document.querySelector('.timetable-zoom-controls') + if (zoomControls) zoomControls.remove() + + const hideControls = document.querySelector('.timetable-hide-controls') + if (hideControls) hideControls.remove() + + resetTimetableStyles() + } } } }; diff --git a/src/plugins/core/createAPI.ts b/src/plugins/core/createAPI.ts index b1e1cf7d..d24df593 100644 --- a/src/plugins/core/createAPI.ts +++ b/src/plugins/core/createAPI.ts @@ -6,7 +6,7 @@ import browser from 'webextension-polyfill'; function createSEQTAAPI(): SEQTAAPI { return { onMount: (selector, callback) => { - eventManager.register( + return eventManager.register( `${selector}Added`, { customCheck: (element) => element.matches(selector), @@ -22,11 +22,20 @@ function createSEQTAAPI(): SEQTAAPI { return path.split('/')[0]; }, onPageChange: (callback) => { - window.addEventListener('hashchange', () => { + const handler = () => { const page = window.location.hash.split('?page=/')[1] || ''; callback(page.split('/')[0]); - }); - }, + }; + + window.addEventListener('hashchange', handler); + + // Return an unregister function + return { + unregister: () => { + window.removeEventListener('hashchange', handler); + } + }; + } }; } diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index 78017c90..b329f8f3 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -1,5 +1,16 @@ import type { Plugin, PluginSettings } from './types'; import { createPluginAPI } from './createAPI'; +import browser from 'webextension-polyfill'; + +interface PluginSettingsStorage { + enabled?: boolean; + [key: string]: any; +} + +interface StorageChange { + oldValue?: T; + newValue?: T; +} export class PluginManager { private static instance: PluginManager; @@ -9,7 +20,9 @@ export class PluginManager { private cleanupFunctions: Map void> = new Map(); private listeners: Map void>> = new Map(); - private constructor() {} + private constructor() { + this.setupPluginStateListener(); + } public static getInstance(): PluginManager { if (!PluginManager.instance) { @@ -66,6 +79,17 @@ export class PluginManager { try { const api = createPluginAPI(plugin); + // Check if plugin is enabled before starting + if (plugin.disableToggle) { + const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`); + const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined; + const enabled = pluginSettings?.enabled ?? true; + if (!enabled) { + console.info(`Plugin "${pluginId}" is disabled, skipping initialization`); + return; + } + } + // Wait for both settings and storage to be loaded before starting the plugin await Promise.all([ (api.settings as any).loaded, @@ -145,9 +169,21 @@ export class PluginManager { }]; }); + if (plugin.disableToggle) { + settingsEntries.push([ + 'enabled', { + id: 'enabled', + title: plugin.name, + description: plugin.description, + type: 'boolean', + default: true + } + ]) + } return { pluginId: id, name: plugin.name, + description: plugin.description, settings: Object.fromEntries(settingsEntries) }; }); @@ -177,4 +213,36 @@ export class PluginManager { listeners.delete(callback); } } + + // Add handler for plugin enable/disable state changes + private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise { + if (enabled) { + await this.startPlugin(pluginId); + } else { + await this.stopPlugin(pluginId); + } + } + + // Add listener for plugin settings changes + private setupPluginStateListener(): void { + browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => { + if (area !== 'local') return; + + for (const [key, change] of Object.entries(changes)) { + const match = key.match(/^plugin\.(.+)\.settings$/); + if (!match) continue; + + const pluginId = match[1]; + const plugin = this.plugins.get(pluginId); + if (!plugin?.disableToggle) continue; + + const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true; + const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? true; + + if (enabled !== wasEnabled) { + this.handlePluginStateChange(pluginId, enabled); + } + } + }); + } } \ No newline at end of file diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts index 269df35b..097d232f 100644 --- a/src/plugins/core/types.ts +++ b/src/plugins/core/types.ts @@ -56,10 +56,10 @@ export type SettingsAPI = { } export interface SEQTAAPI { - onMount: (selector: string, callback: (element: Element) => void) => void; + onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void }; getFiber: (selector: string) => ReactFiber; getCurrentPage: () => string; - onPageChange: (callback: (page: string) => void) => void; + onPageChange: (callback: (page: string) => void) => { unregister: () => void }; } export interface StorageAPI { @@ -101,5 +101,6 @@ export interface Plugin { description: string; version: string; settings: T; - run: (api: PluginAPI) => void | Promise | (() => void) | Promise<() => void>; + disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings + run: (api: PluginAPI) => void | Promise | (() => void) | Promise<(() => void)>; } \ No newline at end of file diff --git a/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts b/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts deleted file mode 100644 index 8477d668..00000000 --- a/src/seqta/utils/CreateEnable/EnableNotificationCollector.ts +++ /dev/null @@ -1,24 +0,0 @@ -export function enableNotificationCollector() { - var xhr3 = new XMLHttpRequest() - xhr3.open("POST", `${location.origin}/seqta/student/heartbeat?`, true) - xhr3.setRequestHeader("Content-Type", "application/json; charset=utf-8") - xhr3.onreadystatechange = function () { - if (xhr3.readyState === 4) { - var Notifications = JSON.parse(xhr3.response) - var alertdiv = document.getElementsByClassName( - "notifications__bubble___1EkSQ", - )[0] - if (typeof alertdiv == "undefined") { - console.info("[BetterSEQTA+] No notifications currently") - } else { - alertdiv.textContent = Notifications.payload.notifications.length - } - } - } - xhr3.send( - JSON.stringify({ - timestamp: "1970-01-01 00:00:00.0", - hash: "#?page=/home", - }), - ) -} \ No newline at end of file diff --git a/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts b/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts deleted file mode 100644 index 10edf229..00000000 --- a/src/seqta/utils/DisableRemove/DisableNotificationCollector.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function disableNotificationCollector() { - var alertdiv = document.getElementsByClassName( - "notifications__bubble___1EkSQ", - )[0] - if (typeof alertdiv != "undefined") { - var currentNumber = parseInt(alertdiv.textContent!) - if (currentNumber < 9) { - alertdiv.textContent = currentNumber.toString() - } else { - alertdiv.textContent = "9+" - } - } -} \ No newline at end of file diff --git a/src/seqta/utils/Loaders/LoadHomePage.ts b/src/seqta/utils/Loaders/LoadHomePage.ts index 167f1a28..c3664fe1 100644 --- a/src/seqta/utils/Loaders/LoadHomePage.ts +++ b/src/seqta/utils/Loaders/LoadHomePage.ts @@ -19,8 +19,6 @@ import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement" import { convertTo12HourFormat } from "../convertTo12HourFormat" -import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector" - let LessonInterval: any let currentSelectedDate = new Date() @@ -249,10 +247,6 @@ export async function loadHomePage() { } } - if (settingsState.notificationcollector) { - enableNotificationCollector() - } - return cleanup } diff --git a/src/seqta/utils/listeners/StorageChanges.ts b/src/seqta/utils/listeners/StorageChanges.ts index c61485fe..5ccf07ca 100644 --- a/src/seqta/utils/listeners/StorageChanges.ts +++ b/src/seqta/utils/listeners/StorageChanges.ts @@ -5,8 +5,6 @@ import { updateAllColors } from '@/seqta/ui/colors/Manager'; import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts"; import { CreateBackground } from "@/seqta/utils/CreateEnable/CreateBackground"; import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv"; -import { disableNotificationCollector } from "@/seqta/utils/DisableRemove/DisableNotificationCollector"; -import { enableNotificationCollector } from "@/seqta/utils/CreateEnable/EnableNotificationCollector"; import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments"; import { RemoveBackground } from "@/seqta/utils/DisableRemove/RemoveBackground"; import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv"; @@ -27,7 +25,6 @@ export class StorageChangeHandler { settingsState.register('onoff', this.handleOnOffChange.bind(this)); settingsState.register('shortcuts', this.handleShortcutsChange.bind(this)); settingsState.register('customshortcuts', this.handleCustomShortcutsChange.bind(this)); - settingsState.register('notificationcollector', this.handleNotificationCollectorChange.bind(this)); settingsState.register('bksliderinput', updateBgDurations.bind(this)); settingsState.register('animatedbk', this.handleAnimatedBkChange.bind(this)); settingsState.register('transparencyEffects', this.handleTransparencyEffectsChange.bind(this)); @@ -43,14 +40,6 @@ export class StorageChangeHandler { browser.runtime.sendMessage({ type: 'reloadTabs' }); } - private handleNotificationCollectorChange(newValue: boolean) { - if (newValue) { - enableNotificationCollector(); - } else { - disableNotificationCollector(); - } - } - private handleCustomShortcutsChange(newValue: CustomShortcut[], oldValue: CustomShortcut[]) { if (newValue) { if (newValue.length > oldValue.length) { diff --git a/src/types/storage.ts b/src/types/storage.ts index ee42560d..fd3a7ffb 100644 --- a/src/types/storage.ts +++ b/src/types/storage.ts @@ -25,7 +25,6 @@ export interface SettingsState { welcome: ToggleItem; }; menuorder: any[]; - notificationcollector: boolean; onoff: boolean; selectedColor: string; originalSelectedColor: string; From 600456f28e1ced9c0fd7d9b687c0500eaea7b8ca Mon Sep 17 00:00:00 2001 From: SethBurkart123 Date: Sun, 30 Mar 2025 09:09:55 +1100 Subject: [PATCH 58/72] chore: remove dev themes file --- themes.md | 123 ------------------------------------------------------ 1 file changed, 123 deletions(-) delete mode 100644 themes.md diff --git a/themes.md b/themes.md deleted file mode 100644 index 7c02ede4..00000000 --- a/themes.md +++ /dev/null @@ -1,123 +0,0 @@ -# BetterSEQTA+ Theme System Documentation - -## Overview -The BetterSEQTA+ theme system allows users to customize their SEQTA interface with custom CSS, colors, and images. Themes are stored locally using `localforage` and can be shared, downloaded, and modified. - -## Theme Storage -Themes are stored using `localforage` in two main ways: -1. A list of theme IDs is stored under the key 'customThemes' -2. Individual themes are stored using their unique ID as the key - -## Theme Structure -A theme consists of the following components: - -```typescript -type CustomTheme = { - id: string; // Unique identifier for the theme - name: string; // Display name - description: string; // Theme description - defaultColour: string; // Default accent color - CanChangeColour: boolean; // Whether users can change the accent color - allowBackgrounds: boolean; // Whether background customization is allowed - CustomCSS: string; // Custom CSS styles - CustomImages: CustomImage[]; // Array of custom images used in the theme - coverImage: Blob | null; // Theme preview image - isEditable: boolean; // Whether the theme can be edited - hideThemeName: boolean; // Whether to hide the theme name in UI - webURL?: string; // Optional URL for web-downloaded themes - selectedColor?: string; // Currently selected accent color - forceDark?: boolean; // Force dark mode when theme is active -} -``` - -## Theme Management Functions - -### Core Functions -1. `setTheme(themeId)`: Activates a theme - - Removes currently active theme - - Applies new theme's CSS and images - - Updates color settings - -2. `applyTheme(theme)`: Applies theme components - - Applies custom CSS - - Sets up custom images - - Handles dark mode settings - -3. `removeTheme(theme)`: Cleans up theme components - - Removes custom CSS - - Cleans up image URLs - - Restores original settings - -### Theme Storage Operations -1. `saveTheme(theme)`: Saves/updates a theme - - Stores theme data in localforage - - Updates theme list if new - - Triggers theme update notifications - -2. `deleteTheme(themeId)`: Removes a theme - - Removes theme data - - Updates theme list - - Cleans up theme components - -### Theme Sharing -1. `shareTheme(themeId)`: Exports theme for sharing - - Converts blobs to base64 - - Packages theme data - - Creates downloadable JSON file - -2. `downloadTheme(theme)`: Installs shared theme - - Converts base64 to blobs - - Stores theme data - - Updates theme list - -## State Management -The theme system uses a `settingsState` object to track: -- Currently selected theme (`selectedTheme`) -- Original and current color settings (`originalSelectedColor`, `selectedColor`) -- Dark mode state (`DarkMode`, `originalDarkMode`) - -## Known Issues and Considerations - -### Image Handling -1. Images are stored as Blobs and converted to URLs for display -2. Need to properly revoke object URLs to prevent memory leaks -3. Image variable names must be unique across themes - -### Color Management -1. Theme colors can override user preferences -2. Need to properly restore original colors when disabling themes -3. Color change permissions (`CanChangeColour`) may need better enforcement - -### CSS Application -1. CSS is applied through a single `