diff --git a/.eslintrc.json b/.eslintrc.json
index b9fb23bc..f6cc10d1 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -3,12 +3,12 @@
"browser": true,
"commonjs": true,
"es2021": true,
- "node": true // add this line to allow Node.js-specific globals
+ "node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
- "sourceType": "module" // add this line to allow 'import' and 'export' statements
+ "sourceType": "module"
},
"rules": {
// allow importing ts extensions
diff --git a/README.md b/README.md
index f9afa194..d85958e3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,8 @@
+# SVELTE BUILD - NOT STABLE
+Please don't use this in a production environment - it is quite buggy and is not fully completed as of yet.
+
+#
+
diff --git a/package.json b/package.json
index aadca2d2..417ca1bd 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,16 @@
{
"name": "betterseqtaplus",
- "version": "3.3.2",
+ "version": "3.4.0",
"type": "module",
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development, while incorporating a plethora of new and improved features!",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
- "dev": "MODE=chrome vite dev",
- "dev:firefox": "MODE=firefox vite build --watch",
- "build": "MODE=chrome vite build && MODE=firefox vite build",
- "build:chrome": "MODE=chrome vite build",
- "build:firefox": "MODE=firefox vite build",
- "build:safari": "MODE=safari vite build",
+ "dev": "cross-env MODE=chrome vite dev",
+ "dev:firefox": "cross-env MODE=firefox vite build --watch",
+ "build": "cross-env MODE=chrome vite build && cross-env MODE=firefox vite build",
+ "build:chrome": "cross-env MODE=chrome vite build",
+ "build:firefox": "cross-env MODE=firefox vite build",
+ "build:safari": "cross-env MODE=safari vite build",
"convert:safari": "xcrun safari-web-extension-converter dist/safari --project-location . --app-name $npm_package_name-safari",
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
"publish": "bun lib/publish.js --b",
@@ -31,66 +31,66 @@
},
"license": "MIT",
"devDependencies": {
- "@crxjs/vite-plugin": "^2.0.0-beta.23",
+ "@crxjs/vite-plugin": "2.0.0-beta.25",
"@types/mime-types": "^2.1.4",
- "@vitejs/plugin-react-swc": "^3.6.0",
- "eslint": "^8.56.0",
+ "@vitejs/plugin-react-swc": "^3.7.0",
+ "cross-env": "^7.0.3",
+ "eslint": "^8.57.0",
"glob": "^11.0.0",
"mime-types": "^2.1.35",
- "parcel": "^2.11.0",
- "prettier": "^3.2.5",
+ "prettier": "^3.3.3",
"process": "^0.11.10",
- "sass": "^1.70.0",
+ "sass": "^1.78.0",
"sass-loader": "^13.3.3",
"semver": "^7.6.3",
- "url": "^0.11.3"
+ "url": "^0.11.4"
},
"dependencies": {
"@bedframe/cli": "^0.0.85",
- "@blocknote/core": "^0.14.1",
- "@blocknote/mantine": "^0.14.1",
- "@blocknote/react": "^0.14.1",
+ "@codemirror/lang-css": "^6.3.0",
"@codemirror/lang-less": "^6.0.2",
- "@heroicons/react": "^2.1.3",
- "@million/lint": "latest",
- "@tailwindcss/forms": "^0.5.7",
+ "@codemirror/theme-one-dark": "^6.1.2",
+ "@sveltejs/vite-plugin-svelte": "^4.0.0",
+ "@tailwindcss/forms": "^0.5.9",
+ "@tsconfig/svelte": "^5.0.4",
+ "@types/chrome": "^0.0.270",
"@types/color": "^3.0.6",
"@types/dompurify": "^3.0.5",
- "@types/lodash": "^4.17.4",
- "@types/node": "^20.11.30",
- "@types/react": "^18.2.55",
- "@types/react-dom": "^18.2.19",
- "@types/sortablejs": "^1.15.7",
+ "@types/lodash": "^4.17.7",
+ "@types/node": "^20.16.5",
+ "@types/react": "17",
+ "@types/react-dom": "17",
+ "@types/sortablejs": "^1.15.8",
"@types/uuid": "^9.0.8",
"@types/webextension-polyfill": "^0.10.7",
- "@uiw/codemirror-extensions-color": "^4.21.25",
- "@uiw/codemirror-theme-github": "^4.21.25",
- "@uiw/react-codemirror": "^4.21.25",
- "autoprefixer": "^10.4.17",
+ "@uiw/codemirror-extensions-color": "^4.23.3",
+ "@uiw/codemirror-theme-github": "^4.23.3",
+ "@vitejs/plugin-react": "^4.3.1",
+ "autoprefixer": "^10.4.20",
"classnames": "^2.5.1",
+ "codemirror": "^6.0.1",
"color": "^4.2.3",
- "dompurify": "^3.0.8",
- "framer-motion": "^11.0.25",
+ "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",
"kolorist": "^1.8.0",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
- "million": "latest",
- "motion": "^10.17.0",
+ "million": "^3.1.11",
+ "motion": "^10.18.0",
+ "postcss": "^8.4.45",
"publish-browser-extension": "^2.2.1",
- "react": "^18.2.0",
- "react-best-gradient-color-picker": "3.0.5",
- "react-dom": "^18.2.0",
- "react-error-boundary": "^4.0.13",
- "react-router-dom": "^6.22.0",
- "react-toastify": "^10.0.5",
- "rimraf": "^5.0.5",
- "sortablejs": "^1.15.2",
- "swiper": "latest",
- "tailwindcss": "^3.4.1",
- "ts-loader": "^9.5.1",
- "typescript": "^5.3.3",
+ "react": "17",
+ "react-best-gradient-color-picker": "^3.0.10",
+ "react-dom": "17",
+ "sortablejs": "^1.15.3",
+ "svelte": "^5.1.9",
+ "tailwindcss": "^3.4.11",
+ "typescript": "^5.6.2",
"uuid": "^9.0.1",
- "vite": "^5.4.2",
+ "vite": "^5.4.4",
"webextension-polyfill": "^0.10.0"
}
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 00000000..2e7af2b7
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/src/SEQTA.ts b/src/SEQTA.ts
index 4fbc9251..b4f84912 100644
--- a/src/SEQTA.ts
+++ b/src/SEQTA.ts
@@ -35,6 +35,10 @@ import coursesicon from '@/seqta/icons/coursesIcon'
import iframeCSS from '@/css/iframe.scss?raw'
import injectedCSS from '@/css/injected.scss?inline'
import documentLoadCSS from '@/css/documentload.scss?inline'
+import renderSvelte from '@/interface/main'
+import Settings from '@/interface/pages/settings.svelte'
+import { settingsPopup } from './interface/hooks/SettingsPopup'
+import { migrateBackgrounds } from './seqta/utils/migrateBackgrounds'
let SettingsClicked = false
export let MenuOptionsOpen = false
@@ -42,10 +46,13 @@ let currentSelectedDate = new Date()
let LessonInterval: any
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)
-const hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software')
-init()
+if (document.childNodes[1]) {
+ hasSEQTAText = document.childNodes[1].textContent?.includes('Copyright (c) SEQTA Software') ?? false
+ init()
+}
async function init() {
CheckForMenuList()
@@ -153,6 +160,15 @@ export function OpenWhatsNewPopup() {
let text = stringToHTML(
/* html */ `
+
3.4.0 - Major Performance Update
+
Completely rebuilt the extension popup using Svelte for dramatically improved performance
+
Added a brand new background store with search functionality and downloadable backgrounds
+
Significant code cleanup and optimization across the extension
+
Improved overall responsiveness and load times
+
Smoother animations and improved scrolling
+
Fixed Firefox compatibility issues
+
Other minor bug fixes and under the hood improvements
+
3.3.1 - Hot Fix
Fixed assessments not loading when no notices are available
@@ -342,6 +358,102 @@ export function OpenWhatsNewPopup() {
})
}
+export function OpenAboutPage() {
+ const background = document.createElement('div')
+ background.id = 'whatsnewbk'
+ background.classList.add('whatsnewBackground')
+
+ const container = document.createElement('div')
+ container.classList.add('whatsnewContainer')
+
+ var header: any = stringToHTML(
+ /* html */
+ ``
+ ).firstChild
+
+ let text = stringToHTML(
+ /* html */ `
+
+
+
+
BetterSEQTA+ is a fork of BetterSEQTA which was originally developed by Nulkem, which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features.
+
We are currently working on fixing bugs and adding good features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below).
+
Credits
+
Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph and SethBurkart123.
+
+ `,
+ ).firstChild
+
+ let footer = stringToHTML(
+ /* html */ `
+
+ `).firstChild
+
+ let exitbutton = document.createElement('div')
+ exitbutton.id = 'whatsnewclosebutton'
+
+ container.append(header)
+ container.append(text as ChildNode)
+ container.append(footer as ChildNode)
+ container.append(exitbutton)
+
+ background.append(container)
+
+ document.getElementById('container')!.append(background)
+
+ let bkelement = document.getElementById('whatsnewbk')
+ let popup = document.getElementsByClassName('whatsnewContainer')[0]
+
+ if (settingsState.animations) {
+ animate(
+ [popup, bkelement as HTMLElement],
+ { scale: [0, 1], opacity: [0, 1] },
+ { easing: spring({ stiffness: 220, damping: 18 }) }
+ )
+
+ animate(
+ '.whatsnewTextContainer *',
+ { opacity: [0, 1], y: [10, 0] },
+ {
+ delay: stagger(0.05, { start: 0.1 }),
+ duration: 0.5,
+ easing: [.22, .03, .26, 1]
+ }
+ )
+ }
+
+ delete settingsState.justupdated
+
+ bkelement!.addEventListener('click', function (event) {
+ // Check if the click event originated from the element itself and not any of its children
+ if (event.target === bkelement) {
+ DeleteWhatsNew()
+ }
+ });
+
+ var closeelement = document.getElementById('whatsnewclosebutton')
+ closeelement!.addEventListener('click', function () {
+ DeleteWhatsNew()
+ })
+}
+
export async function finishLoad() {
try {
document.querySelector('.legacy-root')?.classList.remove('hidden');
@@ -356,7 +468,10 @@ export async function finishLoad() {
if (settingsState.justupdated && !document.getElementById('whatsnewbk')) {
OpenWhatsNewPopup();
+
+ /* Background Migration script */
}
+ migrateBackgrounds();
}
async function DeleteWhatsNew() {
@@ -522,12 +637,9 @@ function applyDarkModeToIframe(iframe: HTMLIFrameElement, cssLink: HTMLStyleElem
const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return;
- if (iframeDocument.readyState !== 'complete') {
- iframe.onload = () => {
- applyDarkModeToIframe(iframe, cssLink);
- };
- return;
- }
+ iframe.onload = () => {
+ applyDarkModeToIframe(iframe, cssLink);
+ };
if (settingsState.DarkMode) {
iframeDocument.documentElement.classList.add('dark')
@@ -935,30 +1047,22 @@ export function AppendElementsToDisabledPage() {
document.head.append(settingsStyle)
}
-export function closeSettings() {
- const ExtensionSettings = document.getElementById('ExtensionPopup')!
- const ExtensionIframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement
+export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
+ if (!extensionPopup) extensionPopup = document.getElementById('ExtensionPopup')!
- if (SettingsClicked == true) {
- ExtensionSettings!.classList.add('hide')
- if (settingsState.animations) {
- animate(
- '#ExtensionPopup',
- { opacity: [1, 0], scale: [1, 0] },
- { easing: spring({ stiffness: 220, damping: 18 }) }
- )
- } else {
- ExtensionSettings.style.opacity = '0'
- ExtensionSettings.style.transform = 'scale(0)'
- }
- SettingsClicked = false
-
- if (ExtensionIframe.contentWindow) {
- ExtensionIframe.contentWindow.postMessage('popupClosed', '*')
- }
+ extensionPopup.classList.add('hide')
+ if (settingsState.animations) {
+ animate((progress) => {
+ extensionPopup.style.opacity = Math.max(0, 1 - progress).toString()
+ extensionPopup.style.transform = `scale(${Math.max(0, 1 - progress)})`
+ }, { easing: spring({ stiffness: 520, damping: 20 }) })
+ } else {
+ extensionPopup.style.opacity = '0'
+ extensionPopup.style.transform = 'scale(0)'
}
-
- ExtensionSettings!.classList.add('hide')
+
+ settingsPopup.triggerClose()
+ SettingsClicked = false
}
export function addExtensionSettings() {
@@ -969,42 +1073,23 @@ export function addExtensionSettings() {
const extensionContainer = document.querySelector('#container') as HTMLDivElement
if (extensionContainer) extensionContainer.appendChild(extensionPopup)
- const extensionIframe: HTMLIFrameElement = document.createElement('iframe')
- extensionIframe.src = `${browser.runtime.getURL('interface/index.html')}#settings/embedded`
- extensionIframe.id = 'ExtensionIframe'
- extensionIframe.setAttribute('allowTransparency', 'true')
- extensionIframe.setAttribute('excludeDarkCheck', 'true')
- extensionIframe.style.width = '384px'
- extensionIframe.style.height = '100%'
- extensionIframe.style.border = 'none'
- extensionPopup.appendChild(extensionIframe)
+ // create shadow dom and render svelte app
+ try {
+ const shadow = extensionPopup.attachShadow({ mode: 'open' });
+ requestIdleCallback(() => renderSvelte(Settings, shadow));
+ } catch (err) {
+ console.error(err)
+ }
const container = document.getElementById('container')
new SettingsResizer();
-
- const closeExtensionPopup = () => {
- const ExtensionIframe = document.getElementById('ExtensionIframe') as HTMLIFrameElement
-
- extensionPopup.classList.add('hide')
- if (settingsState.animations) {
- animate(
- '#ExtensionPopup',
- { opacity: [1, 0], scale: [1, 0] },
- { easing: spring({ stiffness: 220, damping: 18 }) }
- )
- } else {
- extensionPopup.style.opacity = '0'
- extensionPopup.style.transform = 'scale(0)'
- }
- if (ExtensionIframe.contentWindow) {
- ExtensionIframe.contentWindow.postMessage('popupClosed', '*')
- }
- SettingsClicked = false
- }
container!.onclick = (event) => {
- if ((event.target as HTMLElement).closest('#AddedSettings') == null && SettingsClicked) {
+ if (!SettingsClicked) return;
+
+ if (!(event.target as HTMLElement).closest('#AddedSettings')) {
+ if (event.target == extensionPopup) return;
closeExtensionPopup()
}
}
@@ -1239,21 +1324,17 @@ export function setupSettingsButton() {
var AddedSettings = document.getElementById('AddedSettings');
var extensionPopup = document.getElementById('ExtensionPopup');
- AddedSettings!.addEventListener('click', function () {
+ AddedSettings!.addEventListener('click', async () => {
if (SettingsClicked) {
- extensionPopup!.classList.add('hide');
- if (settingsState.animations) {
- animate('#ExtensionPopup', { opacity: [1, 0], scale: [1, 0] }, { easing: spring({ stiffness: 220, damping: 18 }) });
- } else {
- extensionPopup!.style.opacity = '0'
- extensionPopup!.style.transform = 'scale(0)'
- }
- (document.getElementById('ExtensionIframe')! as HTMLIFrameElement).contentWindow!.postMessage('popupClosed', '*');
- SettingsClicked = false;
+ closeExtensionPopup(extensionPopup as HTMLElement);
} else {
- extensionPopup!.classList.remove('hide');
if (settingsState.animations) {
- animate('#ExtensionPopup', { opacity: [0, 1], scale: [0, 1] }, { easing: spring({ stiffness: 260, damping: 24 }) });
+ animate((progress) => {
+ extensionPopup!.style.opacity = progress.toString()
+ extensionPopup!.style.transform = `scale(${progress})`
+ }, { easing: spring({ stiffness: 280, damping: 20 }) });
+
+ extensionPopup!.classList.remove('hide');
} else {
extensionPopup!.style.opacity = '1'
extensionPopup!.style.transform = 'scale(1)'
diff --git a/src/background.ts b/src/background.ts
index 4c41dffe..e95640ce 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -1,5 +1,5 @@
import browser from 'webextension-polyfill'
-import { SettingsState } from "@/types/storage";
+import type { SettingsState } from "@/types/storage";
export const openDB = () => {
return new Promise((resolve, reject) => {
diff --git a/src/css/injected.scss b/src/css/injected.scss
index 6153625c..3dca2557 100644
--- a/src/css/injected.scss
+++ b/src/css/injected.scss
@@ -1,9 +1,9 @@
@charset "UTF-8";
-@import url('https://fonts.googleapis.com/css?family=Rubik:300,400,500,600');
+@import url("https://fonts.googleapis.com/css?family=Rubik:300,400,500,600");
-@import './injected/sidebar-animation.scss';
-@import './injected/theme.scss';
-@import './injected/transparency.scss';
+@import "./injected/sidebar-animation.scss";
+@import "./injected/theme.scss";
+@import "./injected/transparency.scss";
:root {
background: var(--better-main) !important;
@@ -16,7 +16,12 @@
}
body,
-.legacy-root input, .legacy-root textarea, .legacy-root button, .legacy-root select, .legacy-root option, .legacy-root .input,
+.legacy-root input,
+.legacy-root textarea,
+.legacy-root button,
+.legacy-root select,
+.legacy-root option,
+.legacy-root .input,
html {
font-family: Rubik, sans-serif !important;
}
@@ -26,10 +31,6 @@ html {
}
* {
--theme-fg-parts: white;
-
- transition:
- background-color 200ms ease-in-out,
- backdrop-filter 200ms ease-in-out;
}
.extension-editor {
background: var(--background-primary);
@@ -39,7 +40,8 @@ html {
height: 100%;
visibility: visible !important;
}
-#themeCreatorIframe {
+
+#themeCreator {
position: fixed;
right: 0;
height: 100%;
@@ -62,6 +64,40 @@ html {
}
}
+#store {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 9998;
+ animation: fadeIn 500ms forwards;
+ animation-delay: 200ms;
+ opacity: 0;
+
+ &.hide {
+ animation: fadeOut 500ms forwards;
+ }
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes fadeOut {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
.dark .resizeBar {
background-color: rgb(28 28 30);
}
@@ -169,7 +205,8 @@ html {
background: unset;
}
-.legacy-root button:active, .legacy-root a:active:not(.cke_combo_button) {
+.legacy-root button:active,
+.legacy-root a:active:not(.cke_combo_button) {
background-image: unset !important;
}
@@ -179,7 +216,8 @@ html {
}
.dark .dashboard section {
- input, select {
+ input,
+ select {
background: rgba(255, 255, 255, 0.1);
}
}
@@ -201,7 +239,7 @@ html {
input,
select {
border: transparent;
- background: rgba(0, 0, 0, .1);
+ background: rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
@@ -243,7 +281,7 @@ html {
color: var(--text-primary);
}
ul.magicDelete > li:hover {
- background: rgba(0, 0, 0, .1);
+ background: rgba(0, 0, 0, 0.1);
}
.dashlet-notes > .editor {
background: unset;
@@ -261,7 +299,17 @@ ul.magicDelete > li.deleting {
background: transparent !important;
color: var(--text-color) !important;
}
+#media-container {
+ width: 100%;
+ height: 100%;
+}
+#media-container video,
+#media-container img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
#menu li,
#menu section {
margin: 8px auto !important;
@@ -292,7 +340,6 @@ ul.magicDelete > li.deleting {
#menu {
width: 270px;
z-index: 19;
- transition-duration: 0.4s;
background: var(--better-main) !important;
color: var(--text-color);
border-right: none;
@@ -338,14 +385,16 @@ ul.magicDelete > li.deleting {
position: relative;
&::before {
- content: '';
+ content: "";
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 3px;
background: var(--item-colour, transparent);
- transition: width 100ms, transform 0.3s ease;
+ transition:
+ width 100ms,
+ transform 0.3s ease;
border-radius: 8px 0 0 8px;
}
@@ -447,7 +496,7 @@ ol:has(.MessageList__avatar___2wxyb svg) {
.quickbar .actions [title="Choose a colour"] > svg {
scale: 0.9;
}
-.quickbar[data-yiq='light'] .actions {
+.quickbar[data-yiq="light"] .actions {
color: white !important;
}
.singleSelect > li {
@@ -474,7 +523,7 @@ ol:has(.MessageList__avatar___2wxyb svg) {
}
#main .timetablepage .quickbar {
border: none;
- box-shadow: 0 4px 8px rgba(0,0,0,0.5);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5);
border-radius: 16px;
}
.quickbar .actions {
@@ -505,7 +554,7 @@ ol:has(.MessageList__avatar___2wxyb svg) {
margin: 0 0 0 -12px;
background-color: rgba(255, 255, 255, 0.2);
clip-path: polygon(50% 40%, 0 0, 100% 0);
- border: 12px solid rgba(255,255,255,0);
+ border: 12px solid rgba(255, 255, 255, 0);
border-top-color: transparent;
}
#main > .timetablepage > .quickbar.above::before {
@@ -1294,7 +1343,9 @@ div > ol:has(.uiFileHandlerWrapper) {
height: 25px;
width: 24px;
}
-.notifications__notifications___3mmLY > button > .notifications__bubble___1EkSQ {
+.notifications__notifications___3mmLY
+ > button
+ > .notifications__bubble___1EkSQ {
background: var(--better-alert-highlight);
width: 25px;
height: 25px;
@@ -1396,7 +1447,7 @@ div > ol:has(.uiFileHandlerWrapper) {
font-size: 12px;
margin-right: 4px;
font-family: "IconFamily";
- pointer-events: none;
+ pointer-events: none;
}
.MessageList__MessageList___3DxoC ol .Button__Button___3SRFo {
@@ -1500,7 +1551,9 @@ iframe.userHTML {
.Collapsible__Collapsible___3O8P3 > .Collapsible__header___-Afvq {
background: none;
}
-.AssessmentList__AssessmentList___1GdCl > .AssessmentList__searchFilter___3N70o + .AssessmentList__items___3LcmQ {
+.AssessmentList__AssessmentList___1GdCl
+ > .AssessmentList__searchFilter___3N70o
+ + .AssessmentList__items___3LcmQ {
color: var(--text-primary);
}
.Thermoscore__Thermoscore___2tWMi {
@@ -1550,19 +1603,10 @@ iframe.userHTML {
#main > .course > .content > .resources > h2 {
color: var(--text-primary);
}
-::-webkit-scrollbar {
- width: 10px;
- height: 10px;
- transition: 1s;
- border-radius: 16px;
- //width: 0px !important;
- //background: none;
-}
-::-webkit-scrollbar-track {
- background-color: transparent;
-}
-::-webkit-scrollbar-corner {
- background: none;
+
+/* set button(top and bottom of the scrollbar) */
+body::-webkit-scrollbar-button {
+ display: none !important;
}
:root,
html,
@@ -1571,8 +1615,19 @@ div,
ol,
ul {
scrollbar-width: thin !important;
- scrollbar-color: var(--better-light) var(--better-sub);
+ scrollbar-color: #babac0 #fff !important;
}
+
+.dark {
+ body,
+ div,
+ ol,
+ ul {
+ scrollbar-width: thin !important;
+ scrollbar-color: #333 #111 !important;
+ }
+}
+
.connectedNotificationsWrapper > div > button {
color: var(--text-primary) !important;
height: 45px;
@@ -1596,10 +1651,13 @@ ul {
> .SelectedAssessment__clearBtn___21D85 {
background: var(--better-main);
}
-.SelectedAssessment__SelectedAssessment___3Bu5D > .SelectedAssessment__meta___1gq_y {
+.SelectedAssessment__SelectedAssessment___3Bu5D
+ > .SelectedAssessment__meta___1gq_y {
border-bottom: 1px solid var(--better-main);
}
-.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk > li.TabSet__selected___1psfF {
+.TabSet__TabSet___Vo-SZ
+ > ol.TabSet__tabs___1RRZk
+ > li.TabSet__selected___1psfF {
border-bottom-color: var(--better-main);
}
.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk {
@@ -1724,7 +1782,15 @@ div.entry.class[style*="left: 46.5%"] {
div.entry.class[style*="width: 46.5%"] {
width: 50% !important;
}
-.timetablepage .dailycal > .content > .wrapper > .days > tbody > tr > td > .entriesWrapper {
+.timetablepage
+ .dailycal
+ > .content
+ > .wrapper
+ > .days
+ > tbody
+ > tr
+ > td
+ > .entriesWrapper {
min-width: 0;
width: auto !important;
}
@@ -1918,7 +1984,6 @@ div.bar.flat {
.cke_toolbox > .cke_toolbar .cke_button_on {
background-color: #3d3d3e !important;
}
-
}
.legacy-root input.singleSelect:focus {
background: var(--auto-background);
@@ -1962,7 +2027,15 @@ body {
.forumView .assessment {
background: var(--better-main);
}
-.dailycal > .content > .wrapper > .days > tbody > tr > td > .entriesWrapper > .entry {
+.dailycal
+ > .content
+ > .wrapper
+ > .days
+ > tbody
+ > tr
+ > td
+ > .entriesWrapper
+ > .entry {
padding: 3px;
}
.Viewer__Viewer___32BH- {
@@ -2001,7 +2074,9 @@ li.MessageList__unread___3imtO {
border-radius: 1600px;
}
-.MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz.MessageList__unread___3imtO {
+.MessageList__MessageList___3DxoC
+ > ol
+ > li.MessageList__selected___1SJNz.MessageList__unread___3imtO {
box-shadow: none;
}
@@ -2021,7 +2096,9 @@ li.MessageList__unread___3imtO {
transition: width 0.1s;
}
-.MessageList__MessageList___3DxoC > ol > li.MessageList__unread___3imtO::before {
+.MessageList__MessageList___3DxoC
+ > ol
+ > li.MessageList__unread___3imtO::before {
width: 3px;
}
.connectedNotificationsWrapper > div > button {
@@ -2095,7 +2172,10 @@ li.MessageList__unread___3imtO {
cursor: pointer;
}
-.dark .MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz {
+.dark
+ .MessageList__MessageList___3DxoC
+ > ol
+ > li.MessageList__selected___1SJNz {
background: var(--background-secondary);
}
@@ -2513,20 +2593,21 @@ li.MessageList__unread___3imtO {
border-radius: 5px;
color: var(--text-color);
}
-/* On mouse-over, add a grey background color */
+
.upcoming-checkbox-container:hover input ~ .upcoming-checkmark {
filter: brightness(0.8);
}
-/* When the checkbox is checked, add a blue background */
+
.upcoming-checkbox-container input:checked ~ .upcoming-checkmark {
background: var(--item-colour);
}
-/* Create the checkmark/indicator (hidden when not checked) */
+
.upcoming-checkmark:after {
content: "";
position: absolute;
display: none;
}
+
/* Show the checkmark when checked */
.upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after {
display: block;
@@ -2585,7 +2666,9 @@ li.MessageList__unread___3imtO {
width: 100%;
display: flex;
flex-direction: column;
- box-shadow: inset 0px 6px 0 var(--item-colour, transparent), inset 0px 40px 50px -40px rgba(179, 179, 179, 0.9);
+ box-shadow:
+ inset 0px 6px 0 var(--item-colour, transparent),
+ inset 0px 40px 50px -40px rgba(179, 179, 179, 0.9);
transition: 200ms;
position: relative;
height: 15em;
@@ -2594,7 +2677,9 @@ li.MessageList__unread___3imtO {
font-family: Rubik, sans-serif;
}
.dark .day {
- box-shadow: inset 0px 6px 0 var(--item-colour, transparent), inset 0px 40px 50px -40px rgba(0,0,0,0.9);
+ box-shadow:
+ inset 0px 6px 0 var(--item-colour, transparent),
+ inset 0px 40px 50px -40px rgba(0, 0, 0, 0.9);
}
.clickable {
cursor: pointer;
@@ -2619,6 +2704,157 @@ li.MessageList__unread___3imtO {
margin: 0;
}
+.upcoming-items {
+ background: var(--background-primary);
+ width: 100%;
+ max-height: 55em;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ color: var(--text-primary);
+ transition: 200ms;
+ border-radius: 16px;
+}
+.dark .upcoming-items {
+ box-shadow: inset 0px 40px 80px -40px rgba(0, 0, 0, 0.6);
+}
+.upcoming-assessment-title {
+ color: var(--text-primary);
+ transition: 200ms;
+ font-size: 10px;
+ margin: 0;
+}
+.upcoming-assessment {
+ border: 3px solid var(--item-colour);
+ margin: 5px 50px;
+ height: 6em;
+ padding: 0px;
+ border-radius: 10px;
+}
+.upcoming-assessment {
+ display: flex;
+}
+.upcoming-date-container {
+ margin-bottom: 20px;
+}
+.upcoming-date-title h5 {
+ margin: 0;
+ font-weight: 500;
+}
+.upcoming-details {
+ width: 60%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0px 12px;
+}
+.upcoming-details h5 {
+ text-transform: uppercase;
+ color: #aaaaaa;
+ padding: 0px 4px;
+ margin: 0;
+}
+.upcoming-details p {
+ font-size: 15px;
+ padding: 4px;
+}
+.upcoming-special-day {
+ font-size: 20px;
+}
+.upcoming-blank {
+ display: flex;
+ border-bottom: 2px solid #bebebe;
+ margin: 5px 50px;
+ height: 2em;
+ padding: 0px;
+}
+.upcoming-blank p {
+ padding: 0;
+ margin: 0;
+}
+.upcoming-title {
+ display: flex;
+ align-content: space-between;
+}
+.upcoming-filters {
+ display: flex;
+ height: 26px;
+ width: 65%;
+ align-self: center;
+ align-items: center;
+ color: var(--text-color);
+ padding: 5px;
+ overflow-x: scroll;
+ overflow-y: hidden;
+}
+.upcoming-checkbox-container {
+ position: relative;
+ padding: none !important;
+ padding-left: 25px !important;
+ padding-right: 10px !important;
+ cursor: pointer;
+ font-size: 12px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ height: 20px;
+ align-items: center;
+ display: flex;
+}
+/* Hide the browser's default checkbox */
+.upcoming-checkbox-container input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+ padding: 0;
+}
+/* Create a custom checkbox */
+.upcoming-checkmark {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 15px;
+ width: 15px;
+ border: 3px solid var(--item-colour);
+ border-radius: 5px;
+ color: var(--text-color);
+}
+/* On mouse-over, add a grey background color */
+.upcoming-checkbox-container:hover input ~ .upcoming-checkmark {
+ filter: brightness(0.8);
+}
+/* When the checkbox is checked, add a blue background */
+.upcoming-checkbox-container input:checked ~ .upcoming-checkmark {
+ background: var(--item-colour);
+}
+/* Create the checkmark/indicator (hidden when not checked) */
+.upcoming-checkmark:after {
+ content: "";
+ position: absolute;
+ display: none;
+}
+/* Show the checkmark when checked */
+.upcoming-checkbox-container input:checked ~ .upcoming-checkmark:after {
+ display: block;
+}
+/* Style the checkmark/indicator */
+.upcoming-checkbox-container .upcoming-checkmark:after {
+ left: 3.5px;
+ top: 0px;
+ width: 5px;
+ height: 10px;
+ border: solid white;
+ border-width: 0 3px 3px 0;
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+.upcoming-hiddenassessment {
+ color: #797979;
+}
.titlebar {
align-items: center;
transition: 200ms;
@@ -2760,7 +2996,6 @@ li.MessageList__unread___3imtO {
width: 90%;
border-radius: 16px;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
-
}
.whatsnewTextContainer {
display: flex;
@@ -2830,9 +3065,20 @@ li.MessageList__unread___3imtO {
}
.whatsnewTextContainer h1:not(.whatsnewTextHeader) {
position: sticky;
+ font-size: 1.2em;
width: 100%;
top: 0;
background: var(--background-primary) !important;
z-index: 1;
- padding: 10px;
+ padding: 12px;
+ padding-left: 0px;
+ padding-bottom: 8px;
}
+
+.whatsnewTextContainer img {
+ width: 100%;
+ border-radius: 12px;
+ aspect-ratio: 16/9;
+ object-fit: cover;
+ margin-bottom: 12px;
+}
\ No newline at end of file
diff --git a/src/css/injected/popup.scss b/src/css/injected/popup.scss
index f2ae0ef5..2f040bfe 100644
--- a/src/css/injected/popup.scss
+++ b/src/css/injected/popup.scss
@@ -36,4 +36,5 @@
transform-origin: 70% 0;
will-change: opacity, transform;
transform: translateZ(0); // promotes GPU rendering
+ transition: opacity 0.05s, transform 0.05s;
}
\ No newline at end of file
diff --git a/src/interface/SettingsContext.tsx b/src/interface/SettingsContext.tsx
deleted file mode 100644
index ae78ce7d..00000000
--- a/src/interface/SettingsContext.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { createContext, ReactNode, useContext, useState } from 'react';
-import { SettingsState } from './types/AppProps';
-import useSettingsState from './hooks/settingsState';
-
-// Create a context with an initial state
-const SettingsContext = createContext<{
- settingsState: SettingsState;
- setSettingsState: React.Dispatch
>;
- showPicker: boolean;
- setShowPicker: React.Dispatch>;
- standalone: boolean;
- setStandalone: React.Dispatch>;
-} | undefined>(undefined);
-
-export const SettingsContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
- const [settingsState, setSettingsState] = useState({
- notificationCollector: false,
- lessonAlerts: false,
- animatedBackground: false,
- animatedBackgroundSpeed: "0",
- customThemeColor: "rgba(219, 105, 105, 1)",
- betterSEQTAPlus: true,
- shortcuts: [],
- customshortcuts: [],
- transparencyEffects: false,
- selectedTheme: '',
- animations: true,
- defaultPage: 'home',
- devMode: false
- });
-
- const [showPicker, setShowPicker] = useState(false);
- const [standalone, setStandalone] = useState(false);
-
- useSettingsState({ settingsState, setSettingsState });
-
- return (
-
- {children}
-
- );
-};
-
-// eslint-disable-next-line
-export const useSettingsContext = () => {
- const context = useContext(SettingsContext);
- if (!context) {
- throw new Error('useSettingsContext must be used within a SettingsContextProvider');
- }
- return context;
-};
diff --git a/src/interface/assets/presetBackgrounds.tsx b/src/interface/assets/presetBackgrounds.tsx
deleted file mode 100644
index d6258c0d..00000000
--- a/src/interface/assets/presetBackgrounds.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-const presetBackgrounds = [
- // Images
- {
- id: 'image-preset-1',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-1.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-1-thumb.jpg',
- isPreset: true
- },
- {
- id: 'image-preset-2',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-2.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-2-thumb.jpg',
- isPreset: true
- },
- {
- id: 'image-preset-3',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-3.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-3-thumb.jpg',
- isPreset: true
- },
- {
- id: 'image-preset-4',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-4.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-4-thumb.jpg',
- isPreset: true
- },
- {
- id: 'image-preset-5',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-5.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-5-thumb.jpg',
- isPreset: true
- },
- {
- id: 'image-preset-6',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-6.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-6-thumb.jpg',
- isPreset: true
- },
- {
- id: 'image-preset-7',
- type: 'image',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-7.jpg',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/images/background-7-thumb.jpg',
- isPreset: true
- },
-
- // Videos
- {
- id: 'video-preset-1',
- type: 'video',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animated-1.mp4',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animation-1-thumb.mp4',
- isPreset: true
- },
- {
- id: 'video-preset-2',
- type: 'video',
- url: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animation-2.mp4',
- previewUrl: 'https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/backgrounds/videos/animation-2-thumb.mp4',
- isPreset: true
- }
-];
-
-export default presetBackgrounds;
\ No newline at end of file
diff --git a/src/interface/assets/react.svg b/src/interface/assets/react.svg
deleted file mode 100644
index 6c87de9b..00000000
--- a/src/interface/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/interface/components/Accordian.tsx b/src/interface/components/Accordian.tsx
deleted file mode 100644
index 0fef18e7..00000000
--- a/src/interface/components/Accordian.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-import { ChevronDownIcon } from '@heroicons/react/24/outline';
-
-const Accordion = ({ children, title, defaultOpened }: { children: React.ReactNode, title: string, defaultOpened?: boolean }) => {
- const ref = useRef(null);
- const [shown, setShown] = useState(false);
-
- useEffect(() => {
- const show = async () => {
- if (defaultOpened) {
- await new Promise(resolve => setTimeout(resolve, 100));
- setShown(true);
- }
- };
-
- show();
- }, [])
-
- return (
-
-
setShown(!shown)} className='flex items-center justify-between text-[15px] w-full'>
- { title }
-
-
-
- {children}
-
-
- );
-};
-
-export default Accordion;
diff --git a/src/interface/components/BackgroundSelector.css b/src/interface/components/BackgroundSelector.css
deleted file mode 100644
index 58a82a77..00000000
--- a/src/interface/components/BackgroundSelector.css
+++ /dev/null
@@ -1,21 +0,0 @@
-@keyframes shake {
- 0% {
- transform: rotate(0);
- }
- 25% {
- transform: rotate(-1deg);
- }
- 50% {
- transform: rotate(1deg);
- }
- 75% {
- transform: rotate(-1deg);
- }
- 100% {
- transform: rotate(0);
- }
-}
-
-.animate-shake {
- animation: shake 0.5s linear infinite;
-}
diff --git a/src/interface/components/BackgroundSelector.tsx b/src/interface/components/BackgroundSelector.tsx
deleted file mode 100644
index aaeb3607..00000000
--- a/src/interface/components/BackgroundSelector.tsx
+++ /dev/null
@@ -1,252 +0,0 @@
-import { ChangeEvent, memo, useEffect, useState } from "react";
-import { downloadPresetBackground, openDB, readAllData, writeData } from "../hooks/BackgroundDataLoader";
-import presetBackgrounds from "../assets/presetBackgrounds";
-import "./BackgroundSelector.css";
-
-export interface Background {
- id: string;
- type: string;
- blob: Blob;
- url?: string;
- previewUrl?: string;
- isPreset?: boolean;
- isDownloaded?: boolean;
-}
-
-interface BackgroundSelectorProps {
- isEditMode: boolean;
- disableTheme: () => void;
-}
-
-async function GetTheme() {
- return localStorage.getItem('selectedBackground');
-}
-
-async function SetTheme(theme: string) {
- localStorage.setItem('selectedBackground', theme);
- //await browser.storage.local.set({ theme });
-}
-
-function BackgroundSelector({ isEditMode, disableTheme }: BackgroundSelectorProps) {
- const [backgrounds, setBackgrounds] = useState([]);
- const [selectedBackground, setSelectedBackground] = useState();
- const [downloadedPresetIds, setDownloadedPresetIds] = useState([]);
- const [downloadProgress, setDownloadProgress] = useState>({});
-
- const [BackgroundsBlocked, setBackgroundsBlocked] = useState(false);
-
- useEffect(() => {
- GetTheme().then((theme) => {
- setSelectedBackground(theme);
- });
- }, []);
-
- const handleFileChange = async (e: ChangeEvent): Promise => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- const fileId = `${Date.now()}-${file.name}`;
- const fileType = file.type.split('/')[0];
- const blob = new Blob([file], { type: file.type });
-
- await writeData(fileId, fileType, blob);
- setBackgrounds(prev => [...prev, { id: fileId, type: fileType, blob, url: URL.createObjectURL(blob) }]);
- };
-
- const loadBackgrounds = async (): Promise => {
- const data = await readAllData();
- const dataWithUrls = data.map(bg => ({ ...bg, url: URL.createObjectURL(bg.blob) }));
-
- // Update downloaded preset IDs
- setDownloadedPresetIds(data.map(bg => bg.id));
-
- setBackgrounds(dataWithUrls);
- };
-
- const handlePresetClick = async (bg: Background): Promise => {
- if (bg.isPreset) {
- // Check if indexed DB is accessible or whether cross site cookies blocks it
- try {
- await openDB();
- } catch (error) {
- // @ts-expect-error - Brave is not in the navigator type (unless you are actually using brave browser)
- if (navigator.brave && await navigator.brave.isBrave() || false) {
- console.error('[BetterSEQTA+] Brave browser is blocking access to IndexedDB. Please disable the "Cross-site cookies blocked" setting in the Shields panel. (or you can just disable brave shields for SEQTA)');
- setBackgroundsBlocked(true);
- return;
- }
- alert("[BetterSEQTA+] IndexedDB is not accessible. Please check your browser settings (It's probably cross-site cookies that are blocked).");
- return;
- }
-
- // Check if already exists in IndexedDB or is currently being downloaded
- const existingBackgrounds = await readAllData();
- const alreadyExists = existingBackgrounds.some(ebg => ebg.id === bg.id) || downloadProgress[bg.id] !== undefined;
-
- if (!alreadyExists) {
- setDownloadProgress(prev => ({ ...prev, [bg.id]: 0 }));
- const downloadedBg = await downloadPresetBackground(bg, progress => {
- setDownloadProgress(prev => ({ ...prev, [bg.id]: progress }));
- });
- setDownloadProgress(prev => {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { [bg.id]: _, ...rest } = prev;
- return rest;
- });
- await writeData(downloadedBg.id, downloadedBg.type, downloadedBg.blob);
- setBackgrounds(prev => [...prev, downloadedBg]);
- setDownloadedPresetIds(prev => [...prev, downloadedBg.id]);
- }
- selectBackground(bg.id);
- }
- };
-
- const selectBackground = (fileId: string): void => {
- if (selectedBackground == fileId) {
- selectNoBackground();
- return;
- }
-
- setSelectedBackground(fileId);
- SetTheme(fileId);
- };
-
- const deleteBackground = async (fileId: string): Promise => {
- const db = await openDB();
- const tx = db.transaction('backgrounds', 'readwrite');
- const store = tx.objectStore('backgrounds');
- store.delete(fileId);
- setBackgrounds(prev => prev.filter(bg => bg.id !== fileId));
-
- // Check if the background being deleted is currently selected
- if (fileId === selectedBackground) {
- selectNoBackground(); // Disable the current background
- }
- };
-
- const selectNoBackground = (): void => {
- setSelectedBackground(null);
- SetTheme('');
- };
-
- const calcCircumference = (radius: number) => 2 * Math.PI * radius;
-
- useEffect(() => {
- loadBackgrounds();
- }, []);
-
- return (
- <>
- { disableTheme(), selectNoBackground() }}>
- {selectedBackground == null ? 'No Theme' : 'Remove Theme'}
-
-
- {BackgroundsBlocked && (
-
-
File Storage Blocked
-
Brave browser is blocking access to IndexedDB. Please disable the "Cross-site cookies blocked" setting in the Shields panel. (or you can just disable brave shields for SEQTA)
-
-
- )}
-
-
-
Background Images
-
- { isEditMode ? <>> :
-
-
- {/* Plus icon */}
-
-
-
-
}
- {backgrounds.filter(bg => bg.type === 'image').map(bg => (
-
selectBackground(bg.id)}
- className={`relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 ${isEditMode ? 'animate-shake' : ''} ${selectedBackground === bg.id ? 'dark:ring-2 ring-4' : 'ring-0'}`}>
- {isEditMode && (
-
deleteBackground(bg.id)}>
-
-
- )}
-
-
- ))}
- {backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'image' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => (
-
handlePresetClick(bg)}
- className={`relative w-16 h-16 transition cursor-pointer rounded-xl duration-300 ${ isEditMode ? 'opacity-0 pointer-events-none hidden' : 'opacity-100'}`}>
- {bg.isPreset && downloadProgress[bg.id] !== undefined && (
-
-
-
-
-
-
- )}
-
-
- {downloadProgress[bg.id] === undefined ? '' : ''}
-
-
-
-
- ))}
-
-
-
Background Videos
-
- { isEditMode ? <>> :
-
-
- {/* Plus icon */}
-
-
-
-
- }
- {backgrounds.filter(bg => bg.type === 'video').map(bg => (
-
selectBackground(bg.id)} className={`relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 ${isEditMode ? 'animate-shake' : ''} ${selectedBackground === bg.id ? 'dark:ring-2 ring-4' : 'ring-0'}`}>
- {isEditMode && (
-
deleteBackground(bg.id)}>
-
-
- )}
-
-
- ))}
- {backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'video' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => (
-
handlePresetClick(bg)}
- className={`relative w-16 h-16 transition cursor-pointer rounded-xl duration-300 ${ isEditMode ? 'opacity-0 pointer-events-none hidden' : 'opacity-100'}`}>
- {bg.isPreset && downloadProgress[bg.id] !== undefined && (
-
-
-
-
-
-
- )}
-
-
- {downloadProgress[bg.id] === undefined ? '' : ''}
-
-
-
-
- ))}
-
-
- >
- );
-}
-
-export default memo(BackgroundSelector);
\ No newline at end of file
diff --git a/src/interface/components/Button.svelte b/src/interface/components/Button.svelte
new file mode 100644
index 00000000..97f07499
--- /dev/null
+++ b/src/interface/components/Button.svelte
@@ -0,0 +1,7 @@
+
+
+
+ {text}
+
\ No newline at end of file
diff --git a/src/interface/components/Checkbox.tsx b/src/interface/components/Checkbox.tsx
deleted file mode 100644
index 421f4640..00000000
--- a/src/interface/components/Checkbox.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-
-type CheckboxProps = {
- value: boolean;
- onChange: (e: React.ChangeEvent) => void;
-};
-
-const Checkbox: React.FC = ({ value, onChange }) => {
- return (
-
-
-
-
- {value && (
-
-
-
- )}
-
-
- );
-};
-
-export default Checkbox;
\ No newline at end of file
diff --git a/src/interface/components/CodeEditor.css b/src/interface/components/CodeEditor.css
deleted file mode 100644
index 321236b7..00000000
--- a/src/interface/components/CodeEditor.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.cm-editor {
- border-radius: 8px;
-}
-
-body:not(.dark) .cm-editor {
- @apply bg-zinc-200;
-}
-
-.cm-editor.cm-focused {
- outline: none;
-}
\ No newline at end of file
diff --git a/src/interface/components/CodeEditor.svelte b/src/interface/components/CodeEditor.svelte
new file mode 100644
index 00000000..af4cb5ba
--- /dev/null
+++ b/src/interface/components/CodeEditor.svelte
@@ -0,0 +1,94 @@
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/CodeEditor.tsx b/src/interface/components/CodeEditor.tsx
deleted file mode 100644
index 603dfd17..00000000
--- a/src/interface/components/CodeEditor.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import CodeMirror, { ViewUpdate } from '@uiw/react-codemirror'
-import { githubDark, githubLight } from '@uiw/codemirror-theme-github'
-import { color } from '@uiw/codemirror-extensions-color';
-import { less } from '@codemirror/lang-less'
-import { useCallback, useEffect, useState } from 'react';
-import './CodeEditor.css'
-
-export default function CodeEditor({
- className = '',
- height = '100%',
- value,
- setValue
-}: {
- className?: string;
- height?: string;
- value: string;
- setValue: (value: string) => void;
-}) {
- const [darkMode, setDarkMode] = useState(false)
-
- useEffect(() => {
- if (document.documentElement.classList.contains('dark')) {
- setDarkMode(true)
- }
- }, [])
-
- const onChange = useCallback((value: string, _: ViewUpdate) => {
- setValue(value)
- }, [])
-
- return(
-
- )
-}
\ No newline at end of file
diff --git a/src/interface/components/ColourPicker.css b/src/interface/components/ColourPicker.css
new file mode 100644
index 00000000..84533f88
--- /dev/null
+++ b/src/interface/components/ColourPicker.css
@@ -0,0 +1,86 @@
+div:has(> #rbgcp-wrapper) {
+ background: transparent !important;
+}
+
+.dark {
+ #rbgcp-wrapper {
+ div[style="padding-top: 11px; position: relative;"] div {
+ color: white !important;
+ }
+
+ div:has(> #rbgcp-solid-btn),
+ div:has(> #rbgcp-advanced-btn),
+ #rbgcp-color-model-btn > div,
+ #rbgcp-gradient-controls-wrap {
+ background-color: #37373b !important;
+ color: white !important;
+
+ svg {
+ circle {
+ fill: white !important;
+ }
+
+ polyline,
+ line,
+ g,
+ path {
+ stroke: white !important;
+ }
+ }
+
+ #rbgcp-radial-btn,
+ #rbgcp-linear-btn {
+ &[style*="background: white;"] {
+ background-color: #28282b !important;
+ }
+
+ svg {
+ path,
+ g,
+ polyline,
+ circle {
+ stroke: white !important;
+ fill: transparent !important;
+ }
+ }
+ }
+
+ div:has(> #rbgcp-stop-input) svg {
+ path {
+ stroke: unset !important;
+ fill: white !important;
+ }
+ }
+
+ #rbgcp-comparibles-btn svg path {
+ fill: white !important;
+ }
+
+ > div {
+ color: white !important;
+
+ &[style*="background: white;"] {
+ background: #28282b !important;
+ }
+ }
+ }
+
+ div:has(> #rbgcp-degree-input) {
+ width: 70px !important;
+ }
+
+ #rbgcp-degree-input {
+ width: 50px !important;
+ }
+
+ #rbgcp-degree-input,
+ #rbgcp-stop-input {
+ color: white !important;
+ }
+
+ #rbgcp-gradient-controls-wrap > div,
+ #rbgcp-gradient-controls-wrap > div > div:not([role="button"]) {
+ background-color: #37373b !important;
+ }
+ }
+}
diff --git a/src/interface/components/ColourPicker.svelte b/src/interface/components/ColourPicker.svelte
new file mode 100644
index 00000000..02db3683
--- /dev/null
+++ b/src/interface/components/ColourPicker.svelte
@@ -0,0 +1,94 @@
+
+
+{#if standalone}
+
+
+
+{:else}
+
+ { e.key === 'Enter' && handleBackgroundClick }}
+ >
+
+
+
+
+{/if}
diff --git a/src/interface/components/ColourPicker.tsx b/src/interface/components/ColourPicker.tsx
new file mode 100644
index 00000000..c21c3fd1
--- /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/src/interface/components/LoadingSpinner.tsx b/src/interface/components/LoadingSpinner.tsx
deleted file mode 100644
index 249415d8..00000000
--- a/src/interface/components/LoadingSpinner.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-const SpinnerIcon = ({ className }: { className: string }) => (
-
-
-
-
-);
-
-export default SpinnerIcon;
\ No newline at end of file
diff --git a/src/interface/components/MotionDiv.svelte b/src/interface/components/MotionDiv.svelte
new file mode 100644
index 00000000..b9d6802f
--- /dev/null
+++ b/src/interface/components/MotionDiv.svelte
@@ -0,0 +1,83 @@
+
+
+
+ {#if children}
+ {@render children()}
+ {/if}
+
diff --git a/src/interface/components/Picker.css b/src/interface/components/Picker.css
deleted file mode 100644
index e5ac10ed..00000000
--- a/src/interface/components/Picker.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.dark [class*="rbgcpColorModelDropdown"],
-.dark [class*="rbgcpControlBtnWrapper"],
-.dark #rbgcp-gradient-controls-wrap {
- background-color: #37373b !important;
- color: white !important;
-}
-
-.dark [class*="rbgcpControlBtn"][class*="rbgcpControlBtnSelected"] {
- color: #568cf5 !important;
-}
-
-.dark [class*="rbgcpControlBtn"] {
- color: #CDCEC9 !important;
-}
-
-.dark [class*="rbgcpControlBtnSelected"] svg {
- filter: none !important;
-}
-
-.dark [class*="rbgcpControlBtnSelected"] {
- background-color: #28282b !important;
-}
-
-.dark [class*="rbgcpComparibleLabel"] {
- color: #CDCEC9 !important;
-}
-
-.dark #rbgcp-stop-input,
-.dark #rbgcp-degree-input,
-.dark [class*="rbgcpControlBtnWrapper"] svg {
- filter: invert();
-}
\ No newline at end of file
diff --git a/src/interface/components/Picker.tsx b/src/interface/components/Picker.tsx
deleted file mode 100644
index 24c18752..00000000
--- a/src/interface/components/Picker.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import ColorPicker from 'react-best-gradient-color-picker';
-import { useSettingsContext } from '../SettingsContext';
-import { motion } from "framer-motion";
-
-import "./Picker.css";
-import { memo, useEffect, useState } from 'react';
-
-function Picker() {
- const { settingsState, setSettingsState, showPicker, setShowPicker } = useSettingsContext();
-
- 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)'
- ];
- const [presets, setPresets] = useState(() => {
- const savedPresets = localStorage.getItem('colorPickerPresets');
- return savedPresets ? JSON.parse(savedPresets) : defaultPresets;
- });
-
- const handleMessage = (event: MessageEvent) => {
- if (event.data === "popupClosed") {
- setShowPicker(false);
- }
- };
-
- useEffect(() => {
- // Add event listener for 'message' event
- window.addEventListener("message", handleMessage);
-
- // Cleanup
- return () => {
- window.removeEventListener("message", handleMessage);
- };
- }, []);
-
- useEffect(() => {
- // Watch for changes in showPicker and update the presets
- if (!showPicker) {
- // Check if the selected color is already in the presets
- const existingIndex = presets.indexOf(settingsState.customThemeColor);
-
- let updatedPresets;
- if (existingIndex > -1) {
- // If the color exists, move it to the front
- updatedPresets = [
- settingsState.customThemeColor,
- ...presets.slice(0, existingIndex),
- ...presets.slice(existingIndex + 1)
- ];
- } else {
- // If the color is new, add it to the front and slice the array
- updatedPresets = [settingsState.customThemeColor, ...presets].slice(0, 18);
- }
-
- setPresets(updatedPresets);
- localStorage.setItem('colorPickerPresets', JSON.stringify(updatedPresets));
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [showPicker]);
-
- const colorChange = (color: string) => {
- setSettingsState({
- ...settingsState,
- customThemeColor: color,
- });
- };
-
- // Define animation variants
- const backgroundVariants = {
- hidden: { opacity: 0 },
- visible: { opacity: 1 },
- exit: { opacity: 0 }
- };
-
- const scaleVariants = {
- hidden: { scale: 0.3 },
- visible: { scale: 1 },
- exit: { scale: 0.4 } // Adding exit animation
- };
-
- return (
- // Apply fade-in animation to background
- setShowPicker(false)}
- className={`absolute top-0 left-0 z-50 flex justify-center w-full h-full pt-4 bg-black/20 ${!showPicker ? 'pointer-events-none' : ''}`}
- >
-
- {/* Apply springy scale animation */}
- e.stopPropagation()}
- className="h-auto p-4 bg-white border rounded-lg shadow-lg dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
- >
-
-
-
-
- );
-}
-
-export default memo(Picker);
diff --git a/src/interface/components/PickerSwatch.svelte b/src/interface/components/PickerSwatch.svelte
new file mode 100644
index 00000000..643f5f46
--- /dev/null
+++ b/src/interface/components/PickerSwatch.svelte
@@ -0,0 +1,11 @@
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/PickerSwatch.tsx b/src/interface/components/PickerSwatch.tsx
deleted file mode 100644
index 74277e0c..00000000
--- a/src/interface/components/PickerSwatch.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { memo } from 'react';
-import { useSettingsContext } from '../SettingsContext';
-
-const PickerSwatch = () => {
- const { setShowPicker, settingsState } = useSettingsContext();
-
- const enablePicker = () => {
- setShowPicker(true);
- };
-
- return (
-
- );
-};
-
-export default memo(PickerSwatch);
diff --git a/src/interface/components/Select.svelte b/src/interface/components/Select.svelte
new file mode 100644
index 00000000..38f37280
--- /dev/null
+++ b/src/interface/components/Select.svelte
@@ -0,0 +1,22 @@
+
+
+ onChange(select.value)}
+ class="px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md w-full"
+>
+ {#each options as option}
+
+ {option.label}
+
+ {/each}
+
diff --git a/src/interface/components/Select.tsx b/src/interface/components/Select.tsx
deleted file mode 100644
index 1ac6fdfa..00000000
--- a/src/interface/components/Select.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Select({ state, onChange, options }: { state: string, onChange: (value: string) => void, options: { value: string, label: string }[] }) {
- return (
- onChange(e.target.value)}>
- {options.map((option) => {option.label} )}
-
- )
-}
\ No newline at end of file
diff --git a/src/interface/components/SkeletonLoader.svelte b/src/interface/components/SkeletonLoader.svelte
new file mode 100644
index 00000000..17e23ade
--- /dev/null
+++ b/src/interface/components/SkeletonLoader.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/interface/components/Slider.css b/src/interface/components/Slider.css
deleted file mode 100644
index f200958c..00000000
--- a/src/interface/components/Slider.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/* Slider Thumb */
-.slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 24px;
- height: 24px;
- box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
- background: white;
- cursor: pointer;
- border-radius: 50%;
-}
-
-.slider::-moz-range-thumb {
- width: 24px;
- height: 24px;
- box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
- background: white;
- cursor: pointer;
- border-radius: 50%;
-}
\ No newline at end of file
diff --git a/src/interface/components/Slider.svelte b/src/interface/components/Slider.svelte
new file mode 100644
index 00000000..00459c70
--- /dev/null
+++ b/src/interface/components/Slider.svelte
@@ -0,0 +1,37 @@
+
+
+
+ onChange(Number(e.currentTarget.value))}
+ class="w-full h-1 rounded-full appearance-none cursor-pointer dark:bg-[#38373D] bg-[#DDDDDD] slider"
+ />
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/Slider.tsx b/src/interface/components/Slider.tsx
deleted file mode 100644
index a6c268cf..00000000
--- a/src/interface/components/Slider.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { memo } from "react";
-import "./Slider.css";
-
-interface SliderProps {
- state: number;
- onChange: (value: number) => void;
-}
-
-const Slider: React.FC = ({ state, onChange }) => {
-
- return (
-
- onChange(Number(e.target.value))}
- className="w-full h-1 rounded-full appearance-none cursor-pointer slider dark:bg-[#38373D] bg-[#DDDDDD]"
- />
-
- );
-};
-
-export default memo(Slider);
\ No newline at end of file
diff --git a/src/interface/components/Spinner.svelte b/src/interface/components/Spinner.svelte
new file mode 100644
index 00000000..a578633a
--- /dev/null
+++ b/src/interface/components/Spinner.svelte
@@ -0,0 +1,34 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/Switch.svelte b/src/interface/components/Switch.svelte
new file mode 100644
index 00000000..645c63ff
--- /dev/null
+++ b/src/interface/components/Switch.svelte
@@ -0,0 +1,49 @@
+
+
+ onChange(!state)}
+ onkeydown={(e) => e.key === "Enter" && onChange(!state)}
+ role="switch"
+ aria-checked={state}
+ tabindex="0"
+>
+
+
+
+
diff --git a/src/interface/components/Switch.tsx b/src/interface/components/Switch.tsx
deleted file mode 100644
index a62007b8..00000000
--- a/src/interface/components/Switch.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { motion } from "framer-motion";
-import "./Switch.css";
-import type { SwitchProps } from "../types/SwitchProps";
-import { memo } from "react";
-
-function Switch(props: SwitchProps) {
- const toggleSwitch = () => {
- const newIsOn = !props.state;
- props.onChange(newIsOn);
- };
-
- return (
-
-
-
- );
-}
-
-const spring = {
- type: "spring",
- stiffness: 700,
- damping: 30
-};
-
-export default memo(Switch);
\ No newline at end of file
diff --git a/src/interface/components/TabbedContainer.css b/src/interface/components/TabbedContainer.css
new file mode 100644
index 00000000..a0ce8c97
--- /dev/null
+++ b/src/interface/components/TabbedContainer.css
@@ -0,0 +1,3 @@
+.tab-width {
+ width: var(--tab-width);
+}
\ No newline at end of file
diff --git a/src/interface/components/TabbedContainer.svelte b/src/interface/components/TabbedContainer.svelte
new file mode 100644
index 00000000..c06d2804
--- /dev/null
+++ b/src/interface/components/TabbedContainer.svelte
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+ {#each tabs as { title }, index}
+ activeTab = index}
+ onmouseenter={() => hoveredTab = index}
+ onmouseleave={() => hoveredTab = null}
+ >
+ {title}
+
+ {/each}
+
+
+
+
+
+ {#each tabs as { Content, props }, index}
+
+
+
+ {/each}
+
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/TabbedContainer.tsx b/src/interface/components/TabbedContainer.tsx
deleted file mode 100644
index 8b1f4e10..00000000
--- a/src/interface/components/TabbedContainer.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import React, { memo, useEffect, useRef, useState } from 'react';
-import { motion } from 'framer-motion';
-import type { TabbedContainerProps } from '../types/TabbedContainerProps';
-import { useSettingsContext } from '../SettingsContext';
-
-const TabbedContainer: React.FC = ({ tabs }) => {
- const { settingsState } = useSettingsContext();
- const [activeTab, setActiveTab] = useState(0);
- const [hoveredTab, setHoveredTab] = useState(null);
- const [tabWidth, setTabWidth] = useState(0);
- const [position, setPosition] = useState(0);
- const positionRef = useRef(position);
-
- // Function to handle message
- const handleMessage = (event: MessageEvent) => {
- if (event.data === "popupClosed") {
- setActiveTab(0);
- }
- };
-
- useEffect(() => {
- // Add event listener for 'message' event
- window.addEventListener("message", handleMessage);
-
- // Cleanup
- return () => {
- window.removeEventListener("message", handleMessage);
- };
- }, []);
-
- useEffect(() => {
- const newPosition = -activeTab * 100;
- setPosition(newPosition);
- positionRef.current = newPosition;
- }, [activeTab]);
-
- const containerRef = useRef(null);
-
- const springTransition = settingsState.animations ? { type: 'spring', stiffness: 250, damping: 25 } : { duration: 0 };
-
- useEffect(() => {
- if (containerRef.current) {
- // @ts-expect-error for some reason its giving an error in TS but it works...
- const width = containerRef.current.getBoundingClientRect().width;
- setTabWidth(width / tabs.length);
- }
- }, [tabs.length]);
-
- const calcXPos = (index: number | null) => {
- if (index !== null) {
- return tabWidth * index;
- }
- return tabWidth * activeTab;
- };
-
- return (
- <>
-
-
-
- {tabs.map((tab, index) => (
- setActiveTab(index)}
- onMouseEnter={() => setHoveredTab(index)}
- onMouseLeave={() => setHoveredTab(null)}
- >
- {tab.title}
-
- ))}
-
-
-
-
- {tabs.map((tab, index) => (
-
- {tab.content}
-
- ))}
-
-
- >
- );
-};
-
-export default memo(TabbedContainer);
\ No newline at end of file
diff --git a/src/interface/components/ThemeCover.tsx b/src/interface/components/ThemeCover.tsx
deleted file mode 100644
index d6ae88ae..00000000
--- a/src/interface/components/ThemeCover.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { useState } from 'react';
-import { CustomTheme, DownloadedTheme } from '../types/CustomThemes';
-import browser from 'webextension-polyfill';
-import { ArrowUpOnSquareIcon, PencilIcon } from '@heroicons/react/24/outline';
-import { sendThemeUpdate, setTheme } from '../hooks/ThemeManagment';
-import { DeleteDownloadedTheme } from '../pages/Store';
-
-type ThemeCoverProps = {
- theme: Omit | DownloadedTheme;
- isSelected: boolean;
- isEditMode: boolean;
- downloaded?: boolean;
- onThemeSelect: (themeId: string) => void;
- onThemeDelete: (themeId: string) => void;
-};
-
-export const ThemeCover: React.FC = React.memo(({
- theme,
- downloaded,
- isSelected,
- isEditMode,
- onThemeSelect,
- onThemeDelete,
-}) => {
- const [uploading, setUploading] = useState(false);
- const handleThemeClick = async () => {
- if (isEditMode) return;
- if (downloaded) {
- await sendThemeUpdate(theme as DownloadedTheme, true)
- DeleteDownloadedTheme(theme.id);
- setTheme(theme.id);
- } else {
- onThemeSelect(theme.id);
- }
- };
-
- const handleDeleteClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- onThemeDelete(theme.id);
- };
-
- const handleShareClick = (event: React.MouseEvent) => {
- event?.preventDefault();
- setUploading(true);
- browser.runtime.sendMessage({ type: 'currentTab', info: 'ShareTheme', body: { themeID: theme.id } }).then(() => {
- setUploading(false);
- });
- };
-
- return (
-
- {isEditMode && (
-
- )}
-
- { ( !isEditMode ) && !downloaded /* && !theme.webURL */ ? (
- <>
- { event?.preventDefault(), browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenThemeCreator', body: { themeID: theme.id } }) }}
- >
-
-
-
-
- >
- ) : null}
-
-
- {theme.coverImage &&
-
- }
- {
- theme.hideThemeName ? <>> :
-
{theme.name}
- }
-
-
- );
-});
-
-const LoadingSpinner = ({ size }: { size: number }) => {
- return
;
-};
-
diff --git a/src/interface/components/ThemeSelector.tsx b/src/interface/components/ThemeSelector.tsx
deleted file mode 100644
index 75328881..00000000
--- a/src/interface/components/ThemeSelector.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-import React, { forwardRef, ForwardRefExoticComponent, RefAttributes, useCallback, useEffect, useImperativeHandle, useState } from 'react';
-import { deleteTheme, disableTheme, getDownloadedThemes, listThemes, sendThemeUpdate, setTheme } from '../hooks/ThemeManagment';
-import { DeleteDownloadedTheme } from '../pages/Store';
-import { ThemeCover } from './ThemeCover';
-import browser from 'webextension-polyfill';
-import { CustomTheme, DownloadedTheme } from '../types/CustomThemes';
-import { useSettingsContext } from '../SettingsContext';
-import { SettingsState } from '../types/AppProps';
-import { InstallTheme } from '../../seqta/ui/themes/downloadTheme';
-import SpinnerIcon from './LoadingSpinner';
-import { toast } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import useVisibility from './useVisibility';
-import { debounce } from 'lodash';
-import { Mutex } from '../../seqta/utils/mutex';
-
-interface ThemeSelectorProps {
- isEditMode: boolean;
- ref: React.Ref;
-}
-
-const ThemeSelector: ForwardRefExoticComponent & RefAttributes> = forwardRef(({ isEditMode = false }, ref) => {
- const [themes, setThemes] = useState[]>([]);
- const [isLoading, setIsLoading] = useState(true);
- const [isDragging, setIsDragging] = useState(false);
- const [tempTheme, setTempTheme] = useState(null);
- const { settingsState, setSettingsState } = useSettingsContext();
- const [elementRef, isVisible] = useVisibility({
- root: null, // Use the viewport as the root
- rootMargin: '0px',
- threshold: 0.1, // 10% of the element needs to be visible
- });
-
- const mutex = new Mutex();
-
- const setSelectedTheme = (themeId: string) => {
- setSettingsState((prevState: SettingsState) => ({
- ...prevState,
- selectedTheme: themeId,
- }));
- }
-
- useImperativeHandle(ref, () => ({
- disableTheme: async () => {
- await disableTheme();
- setSelectedTheme('');
- }
- }));
-
- useEffect(() => {
- const handleThemeChange = async () => {
- //await new Promise((resolve) => setTimeout(resolve, 500));
- fetchThemes();
- };
-
- window.addEventListener('message', (message) => {
- if (message.data.type === 'themeChanged') {
- handleThemeChange();
- }
- });
-
- return () => {
- window.removeEventListener('message', (message) => {
- if (message.data.type === 'themeChanged') {
- handleThemeChange();
- }
- });
- };
- }, []);
-
- useEffect(() => {
- let intervalId: any;
- if (isVisible) {
- intervalId = setInterval(fetchThemes, 2000);
- } else {
- clearInterval(intervalId);
- }
-
- return () => {
- clearInterval(intervalId);
- };
- }, [isVisible]);
-
- const fetchThemes = async () => {
- try {
- const { themes, selectedTheme } = await listThemes();
- let tempDownloadedThemes = await getDownloadedThemes();
-
- setThemes(themes);
- setSelectedTheme(selectedTheme ? selectedTheme : '');
-
- const matchingThemes = themes.filter(theme =>
- tempDownloadedThemes.some(downloadedTheme => downloadedTheme.id === theme.id)
- );
-
- if (matchingThemes.length > 0) {
- matchingThemes.forEach((theme) => {
- DeleteDownloadedTheme(theme.id);
- tempDownloadedThemes = tempDownloadedThemes.filter(downloadedTheme => downloadedTheme.id !== theme.id);
- })
- }
-
- tempDownloadedThemes.forEach(async (theme) => {
- await sendThemeUpdate(theme as DownloadedTheme, true, false)
- DeleteDownloadedTheme(theme.id);
- });
- } catch (error) {
- console.error('Error fetching themes:', error);
- } finally {
- setIsLoading(false);
- }
- };
-
- useEffect(() => {
- fetchThemes();
- }, []);
-
- const handleThemeSelect = useCallback(
- async (themeId: string) => {
- const unlock = await mutex.lock();
- try {
- if (themeId === settingsState.selectedTheme) {
- await disableTheme();
- setSelectedTheme('');
- } else {
- const selectedTheme = themes.find((theme) => theme.id === themeId);
- if (selectedTheme) {
- await setTheme(selectedTheme.id);
- setSelectedTheme(themeId);
- }
- }
- } finally {
- unlock();
- }
- },
- [settingsState.selectedTheme, themes]
- );
-
- const handleThemeSelectDebounced = useCallback(
- debounce(handleThemeSelect, 100),
- [handleThemeSelect]
- );
-
- const handleThemeDelete = useCallback(
- async (themeId: string) => {
- try {
- await deleteTheme(themeId);
- setThemes((prevThemes) => prevThemes.filter((theme) => theme.id !== themeId));
- if (themeId === settingsState.selectedTheme) {
- setSelectedTheme('')
- disableTheme();
- }
- } catch (error) {
- console.error('Error deleting theme:', error);
- }
- },
- [settingsState.selectedTheme]
- );
-
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragging(true);
- };
-
- const handleDragLeave = () => {
- setIsDragging(false);
- };
-
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault();
- setIsDragging(false);
- const file: File = e.dataTransfer.files[0];
- const reader: FileReader = new FileReader();
-
- reader.onload = async (event: ProgressEvent) => {
- try {
- const result: any = JSON.parse(event.target!.result as string);
- try {
- setTempTheme(result);
- await InstallTheme(result);
- await fetchThemes();
- setTempTheme(null);
- } catch(error) {
- toast.error('Invalid file type. Please upload a valid theme file.');
- setTempTheme(null);
- }
- } catch (error) {
- toast.error('Error parsing file. Please upload a valid JSON theme file.');
- setTempTheme(null);
- }
- };
-
- reader.readAsText(file);
- };
-
- if (isLoading) {
- return Loading themes...
;
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
Import Theme
-
-
-
-
-
Themes
-
-
- {themes.map((theme) => (
-
- ))}
-
- {tempTheme && (
-
-
-
- )}
-
- { themes.length > 0 &&
}
-
-
- {'\uecc5'}
- Theme Store
-
-
-
browser.runtime.sendMessage({ type: 'currentTab', info: 'OpenThemeCreator' })}
- className="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
- >
- {'\uec60'}
- Create your own
-
-
-
- );
-});
-
-export default ThemeSelector;
\ No newline at end of file
diff --git a/src/interface/components/store/Backgrounds.svelte b/src/interface/components/store/Backgrounds.svelte
new file mode 100644
index 00000000..8dbc4412
--- /dev/null
+++ b/src/interface/components/store/Backgrounds.svelte
@@ -0,0 +1,322 @@
+
+
+
+
+
+
+
Categories
+
+ selectedCategory = 'All'}
+ >
+ All
+
+ selectedCategory = 'Featured'}
+ >
+ Featured
+
+
+
+
+ {#each categories as category}
+ selectedCategory = category}
+ >
+ {category}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
Explore Backgrounds {searchTerm ? `- "${searchTerm}"` : ''}
+
+
+ Newest
+ Name
+
+
+
+
+
+
+ {#each ['All', 'Installed', 'Photos', 'Videos'] as tab}
+ activeTab = tab.toLowerCase() as typeof activeTab}
+ >
+ {tab}
+
+ {/each}
+
+
+
+
+
+ {#if isLoading}
+
+ {#each Array(9) as _}
+
+ {/each}
+
+ {:else if error}
+
+ Error: {error}
+
+ {:else}
+
+ {#each filteredBackgrounds.filter((bg: Background) => {
+ if (activeTab === 'installed') return savedBackgrounds.includes(bg.id);
+ if (activeTab === 'photos') return bg.type === 'image';
+ if (activeTab === 'videos') return bg.type !== 'image';
+ return true;
+ }) as background (background.id)}
+
toggleBackgroundInstallation(background)}
+ onkeydown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ toggleBackgroundInstallation(background);
+ }
+ }}
+ role="button"
+ tabindex="0"
+ >
+ {#if background.type === 'image'}
+
+ {:else}
+
+ {/if}
+
+ {#if installingBackgrounds.has(background.id)}
+
+ {:else if savedBackgrounds.includes(background.id)}
+
+
+ Remove
+
+ {:else}
+
+
+ Install
+
+ {/if}
+
+
+ {/each}
+
+ {/if}
+
+
+
+
+{#if settingsState.devMode}
+
+
Debug Info:
+
{debugInfo}
+
Total backgrounds: {backgrounds.length}
+
Categories: {categories.join(', ') || ''}
+
Active Tab: {activeTab}
+
Selected Category: {selectedCategory}
+
+{/if}
+
diff --git a/src/interface/components/store/CoverSwiper.svelte b/src/interface/components/store/CoverSwiper.svelte
new file mode 100644
index 00000000..634b6e94
--- /dev/null
+++ b/src/interface/components/store/CoverSwiper.svelte
@@ -0,0 +1,70 @@
+
+
+{#if coverThemes.length > 0}
+
+
+
+ {#each coverThemes as theme}
+
{ if (e.key === 'Enter') setDisplayTheme(theme) }}
+ onclick={() => setDisplayTheme(theme)}
+ >
+
+
+
{theme.name}
+
{theme.description}
+
+
+
+ {/each}
+
+
+
+
+
+
+{/if}
diff --git a/src/interface/components/store/FilterPanel.svelte b/src/interface/components/store/FilterPanel.svelte
new file mode 100644
index 00000000..5ca3ad2f
--- /dev/null
+++ b/src/interface/components/store/FilterPanel.svelte
@@ -0,0 +1,63 @@
+
+
+
+
Filters
+
+
+
+
+
+
+ Clear Filters
+
+
\ No newline at end of file
diff --git a/src/interface/components/store/Header.svelte b/src/interface/components/store/Header.svelte
new file mode 100644
index 00000000..49f8d3b4
--- /dev/null
+++ b/src/interface/components/store/Header.svelte
@@ -0,0 +1,71 @@
+
+
+
+
+
{ if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0">
+
+
+
+
+
+
setActiveTab('themes')}
+ >
+ Themes
+
+
setActiveTab('backgrounds')}
+ >
+ Backgrounds
+
+
+
+
+
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" />
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/store/ThemeCard.svelte b/src/interface/components/store/ThemeCard.svelte
new file mode 100644
index 00000000..2bae7ad8
--- /dev/null
+++ b/src/interface/components/store/ThemeCard.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
+ {theme.name}
+
+
+
+
+
+
+
diff --git a/src/interface/components/store/ThemeGrid.svelte b/src/interface/components/store/ThemeGrid.svelte
new file mode 100644
index 00000000..6f56120c
--- /dev/null
+++ b/src/interface/components/store/ThemeGrid.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+ {#if filteredThemes.length === 0}
+
+
That doesn't exist! 😭😭😭
+
Sorry, we couldn't find the theme you're looking for. Maybe... you could create it?
+
+ Show me how!
+
+
+ {/if}
+
\ No newline at end of file
diff --git a/src/interface/components/store/ThemeModal.svelte b/src/interface/components/store/ThemeModal.svelte
new file mode 100644
index 00000000..72f83170
--- /dev/null
+++ b/src/interface/components/store/ThemeModal.svelte
@@ -0,0 +1,118 @@
+
+
+ {
+ if (e.target === e.currentTarget) hideModal();
+ }}
+ onkeydown={(e) => {
+ if (e.target === e.currentTarget) hideModal();
+ }}
+ role="button"
+ tabindex="-1"
+ transition:fade
+>
+
+
+
diff --git a/src/interface/components/store/header.tsx b/src/interface/components/store/header.tsx
deleted file mode 100644
index 7da53301..00000000
--- a/src/interface/components/store/header.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import logo from '../../../resources/icons/betterseqta-dark-full.png';
-import logoDark from '../../../resources/icons/betterseqta-light-full.png';
-
-export default function header({ searchTerm, setSearchTerm }: { searchTerm: string, setSearchTerm: (value: string) => void }) {
- return
-
-
setSearchTerm('')}>
-
-
-
-
-
-
Theme Store
-
-
-
setSearchTerm(e.target.value)}
- className="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" />
-
-
-
-
-
- ;
-}
diff --git a/src/interface/components/themeCreator/divider.svelte b/src/interface/components/themeCreator/divider.svelte
new file mode 100644
index 00000000..adae5a68
--- /dev/null
+++ b/src/interface/components/themeCreator/divider.svelte
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/interface/components/themes/BackgroundItem.svelte b/src/interface/components/themes/BackgroundItem.svelte
new file mode 100644
index 00000000..e03945a0
--- /dev/null
+++ b/src/interface/components/themes/BackgroundItem.svelte
@@ -0,0 +1,38 @@
+
+
+
+ {#if isEditMode}
+
+ {/if}
+ {#if bg.url}
+ {#if bg.type === 'image'}
+
+ {:else if bg.type === 'video'}
+
+ {/if}
+ {/if}
+
\ No newline at end of file
diff --git a/src/interface/components/themes/BackgroundSelector.svelte b/src/interface/components/themes/BackgroundSelector.svelte
new file mode 100644
index 00000000..c745b825
--- /dev/null
+++ b/src/interface/components/themes/BackgroundSelector.svelte
@@ -0,0 +1,235 @@
+
+
+
+ {#if !(imageBackgrounds.length === 0 && isEditMode)}
+
Background Images
+
+ {#if !isEditMode}
+
handleFileChange(e.detail)} />
+ {/if}
+ {#each imageBackgrounds as bg (bg.id)}
+ {#if isVisible && bg.blob}
+ selectBackground(bg.id)}
+ onDelete={() => deleteBackground(bg.id)}/>
+ {:else}
+
+ {/if}
+ {/each}
+
+ {/if}
+
+ {#if !(videoBackgrounds.length === 0 && isEditMode)}
+
Background Videos
+
+ {#if !isEditMode}
+
handleFileChange(e.detail)} />
+ {/if}
+ {#each videoBackgrounds as bg (bg.id)}
+ {#if isVisible && bg.blob}
+ selectBackground(bg.id)}
+ onDelete={() => deleteBackground(bg.id)}
+ />
+ {:else}
+
+ {/if}
+ {/each}
+
+ {/if}
+
\ No newline at end of file
diff --git a/src/interface/components/themes/BackgroundUploader.svelte b/src/interface/components/themes/BackgroundUploader.svelte
new file mode 100644
index 00000000..2b34d995
--- /dev/null
+++ b/src/interface/components/themes/BackgroundUploader.svelte
@@ -0,0 +1,26 @@
+
+
+
\ No newline at end of file
diff --git a/src/interface/components/themes/ThemeSelector.svelte b/src/interface/components/themes/ThemeSelector.svelte
new file mode 100644
index 00000000..7692e965
--- /dev/null
+++ b/src/interface/components/themes/ThemeSelector.svelte
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Import Theme
+
+
+
+
+
Themes
+
+ {#if themes}
+ {#each themes.themes as theme (theme.id)}
+
handleThemeClick(theme)}
+ >
+ {#if isEditMode}
+ { event.stopPropagation(); handleThemeDelete(theme.id) }}
+ onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleThemeDelete(theme.id) }}
+ role="button"
+ tabindex="-1"
+ >
+
+
+ {/if}
+
+ {#if !isEditMode}
+ { event.stopPropagation(); OpenThemeCreator(theme.id); closeExtensionPopup() }}
+ onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') OpenThemeCreator(theme.id); closeExtensionPopup() }}
+ role="button"
+ tabindex="-1"
+ >
+
+
+
+ { event.stopPropagation(); handleShareTheme(theme) }}
+ onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleShareTheme(theme) }}
+ role="button"
+ tabindex="-1"
+ >
+
+
+ {/if}
+
+
+ {#if theme.coverImage}
+
+ {/if}
+ {#if !theme.hideThemeName}
+
{theme.name}
+ {/if}
+
+
+ {/each}
+ {/if}
+
+ {#if tempTheme}
+
+ {/if}
+
+ {#if themes && themes.themes.length > 0}
+
+ {/if}
+
+
OpenStorePage()}
+ class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
+ >
+
+ Theme Store
+
+
+
{ OpenThemeCreator(); closeExtensionPopup() }}
+ class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
+ >
+
+ Create your own
+
+
+
diff --git a/src/interface/components/useVisibility.tsx b/src/interface/components/useVisibility.tsx
deleted file mode 100644
index f09620da..00000000
--- a/src/interface/components/useVisibility.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useEffect, useRef, useState } from 'react';
-
-interface Options {
- root?: Element | null;
- rootMargin?: string;
- threshold?: number | number[];
-}
-
-type UseVisibilityReturnType = [any | null, boolean];
-
-const useVisibility = (options: Options): UseVisibilityReturnType => {
- const [isVisible, setIsVisible] = useState(false);
- const elementRef = useRef(null);
-
- useEffect(() => {
- const observer = new IntersectionObserver(([entry]) => {
- setIsVisible(entry.isIntersecting);
- }, options);
-
- if (elementRef.current) {
- observer.observe(elementRef.current);
- }
-
- return () => {
- if (elementRef.current) {
- observer.unobserve(elementRef.current);
- }
- };
- }, [elementRef, options]);
-
- return [elementRef, isVisible];
-};
-
-export default useVisibility;
\ No newline at end of file
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
diff --git a/src/interface/dark.ts b/src/interface/dark.ts
deleted file mode 100644
index e14b76f2..00000000
--- a/src/interface/dark.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import Browser from "webextension-polyfill";
-
-(async () => {
- const result = await Browser.storage.local.get();
- if (result.DarkMode) {
- document.body.classList.add('dark');
- }
-})();
\ No newline at end of file
diff --git a/src/interface/hooks/BackgroundDataLoader.ts b/src/interface/hooks/BackgroundDataLoader.ts
new file mode 100644
index 00000000..99756252
--- /dev/null
+++ b/src/interface/hooks/BackgroundDataLoader.ts
@@ -0,0 +1,75 @@
+import { type DBSchema, type IDBPDatabase, openDB } from 'idb';
+
+interface BackgroundDB extends DBSchema {
+ backgrounds: {
+ key: string;
+ value: {
+ id: string;
+ type: string;
+ blob: Blob;
+ };
+ };
+}
+
+let db: IDBPDatabase | null = null;
+
+export async function openDatabase(): Promise> {
+ if (db) return db;
+
+ db = await openDB('BackgroundDB', 1, {
+ upgrade(db: IDBPDatabase) {
+ db.createObjectStore('backgrounds', { keyPath: 'id' });
+ },
+ });
+
+ return db;
+}
+
+export async function readAllData(): Promise> {
+ const db = await openDatabase();
+ return db.getAll('backgrounds');
+}
+
+export async function writeData(id: string, type: string, blob: Blob): Promise {
+ const db = await openDatabase();
+ await db.put('backgrounds', { id, type, blob });
+}
+
+export async function deleteData(id: string): Promise {
+ const db = await openDatabase();
+ await db.delete('backgrounds', id);
+}
+
+export async function clearAllData(): Promise {
+ const db = await openDatabase();
+ await db.clear('backgrounds');
+}
+
+export async function getDataById(id: string): Promise<{ id: string; type: string; blob: Blob } | undefined> {
+ const db = await openDatabase();
+ return db.get('backgrounds', id);
+}
+
+export function closeDatabase(): void {
+ if (db) {
+ db.close();
+ db = null;
+ }
+}
+
+// Helper function to check if IndexedDB is supported
+export function isIndexedDBSupported(): boolean {
+ return 'indexedDB' in window;
+}
+
+// Helper function to check if there's enough storage space
+export async function hasEnoughStorageSpace(requiredSpace: number): Promise {
+ if ('storage' in navigator && 'estimate' in navigator.storage) {
+ const { quota, usage } = await navigator.storage.estimate();
+ if (quota !== undefined && usage !== undefined) {
+ return (quota - usage) > requiredSpace;
+ }
+ }
+ // If we can't determine, assume there's enough space
+ return true;
+}
\ No newline at end of file
diff --git a/src/interface/hooks/BackgroundDataLoader.tsx b/src/interface/hooks/BackgroundDataLoader.tsx
deleted file mode 100644
index dc26b266..00000000
--- a/src/interface/hooks/BackgroundDataLoader.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Background } from "../components/BackgroundSelector";
-
-export const downloadPresetBackground = async (background: Background, onProgress: (progress: number) => void): Promise => {
- const response = await fetch(background.url as string);
-
- const totalLength = +response.headers.get('Content-Length')!;
- let receivedLength = 0;
-
- const reader = response.body?.getReader();
- const chunks = [];
-
- // eslint-disable-next-line no-constant-condition
- while (true) {
- const { done, value } = await reader!.read();
-
- if (done) break;
-
- chunks.push(value!);
- receivedLength += value!.length;
-
- onProgress(Math.ceil(receivedLength / totalLength * 100));
- }
-
- const blob = new Blob(chunks);
- await writeData(background.id, background.type, blob);
-
- return {
- id: background.id,
- type: background.type,
- blob,
- url: URL.createObjectURL(blob),
- };
-};
-// IndexedDB utility functions
-export const openDB = () => {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open('MyDatabase', 1);
-
- request.onerror = () => reject(request.error);
- request.onsuccess = () => resolve(request.result);
-
- request.onupgradeneeded = (event) => {
- const db = (event.target as IDBOpenDBRequest).result;
- db.createObjectStore('backgrounds', { keyPath: 'id' });
- };
- });
-};
-export const writeData = async (fileId: string, type: string, blob: Blob) => {
- return new Promise((resolve, reject) => {
- openDB().then(async (db) => {
- const tx = db.transaction('backgrounds', 'readwrite');
- const store = tx.objectStore('backgrounds');
- const request = store.put({ id: fileId, type, blob });
-
- await new Promise((res, rej) => {
- tx.oncomplete = () => res(request.result);
- tx.onerror = () => rej(tx.error);
- }).then(resolve, reject);
-
- }).catch(reject);
- });
-};
-export const readAllData = async (): Promise => {
- const db = await openDB();
- const tx = db.transaction('backgrounds', 'readonly');
- const store = tx.objectStore('backgrounds');
- const request = store.getAll();
-
- return await new Promise((resolve, reject) => {
- request.onsuccess = () => resolve(request.result);
- request.onerror = () => reject(request.error);
- });
-};
diff --git a/src/interface/hooks/BackgroundUpdates.ts b/src/interface/hooks/BackgroundUpdates.ts
new file mode 100644
index 00000000..5f853f65
--- /dev/null
+++ b/src/interface/hooks/BackgroundUpdates.ts
@@ -0,0 +1,29 @@
+type BackgroundUpdateCallback = () => void;
+
+class BackgroundUpdates {
+ private static instance: BackgroundUpdates;
+ private listeners: Set = new Set();
+
+ private constructor() {}
+
+ public static getInstance(): BackgroundUpdates {
+ if (!BackgroundUpdates.instance) {
+ BackgroundUpdates.instance = new BackgroundUpdates();
+ }
+ return BackgroundUpdates.instance;
+ }
+
+ public addListener(callback: BackgroundUpdateCallback): void {
+ this.listeners.add(callback);
+ }
+
+ public removeListener(callback: BackgroundUpdateCallback): void {
+ this.listeners.delete(callback);
+ }
+
+ public triggerUpdate(): void {
+ this.listeners.forEach(callback => callback());
+ }
+}
+
+export const backgroundUpdates = BackgroundUpdates.getInstance();
diff --git a/src/interface/hooks/SettingsPopup.ts b/src/interface/hooks/SettingsPopup.ts
new file mode 100644
index 00000000..cb2b5463
--- /dev/null
+++ b/src/interface/hooks/SettingsPopup.ts
@@ -0,0 +1,37 @@
+type SettingsPopupCallback = () => void;
+
+/**
+ * This is a singleton that triggers an update when the settings popup is closed.
+ * This is used to close the colour picker.
+ * Usage:
+ * settingsPopup.addListener(() => {
+ * console.log('Settings popup closed');
+ * });
+*/
+class SettingsPopup {
+ private static instance: SettingsPopup;
+ private listeners: Set = new Set();
+
+ private constructor() {}
+
+ public static getInstance(): SettingsPopup {
+ if (!SettingsPopup.instance) {
+ SettingsPopup.instance = new SettingsPopup();
+ }
+ return SettingsPopup.instance;
+ }
+
+ public addListener(callback: SettingsPopupCallback): void {
+ this.listeners.add(callback);
+ }
+
+ public removeListener(callback: SettingsPopupCallback): void {
+ this.listeners.delete(callback);
+ }
+
+ public triggerClose(): void {
+ this.listeners.forEach(callback => callback());
+ }
+}
+
+export const settingsPopup = SettingsPopup.getInstance();
diff --git a/src/interface/hooks/ThemeManagment.ts b/src/interface/hooks/ThemeManagment.ts
deleted file mode 100644
index 74d56183..00000000
--- a/src/interface/hooks/ThemeManagment.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import browser from 'webextension-polyfill'
-import { CustomTheme, DownloadedTheme, ThemeList } from '../types/CustomThemes';
-import localforage from 'localforage';
-
-export const setTheme = async (themeID: string) => {
- // send message to the background script
- await browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'SetTheme',
- body: {
- themeID: themeID
- }
- });
-}
-
-export const getDownloadedThemes = async (): Promise => {
- // send message to the background script
- const response: DownloadedTheme[] = await new Promise(async (resolve, reject) => {
- try {
- let availableThemes = await localforage.getItem('availableThemes') as string[];
- availableThemes = Array.from(new Set(availableThemes));
-
- const downloadedThemes: DownloadedTheme[] = [];
- for (let i = 0; i < availableThemes.length; i++) {
- let themeData = await localforage.getItem(availableThemes[i]) as DownloadedTheme;
-
- downloadedThemes.push(themeData);
- }
-
- resolve(downloadedThemes);
- } catch(error) {
- reject(error);
- }
- });
-
- return response;
-}
-
-export const listThemes = async (): Promise => {
- // send message to the background script
- const response: ThemeList = await new Promise((resolve, reject) => {
- browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'ListThemes'
- }).then(async (response) => {
- if (response) {
- // convert the response themes coverImage to a bloburl
- response.themes = await Promise.all(
- response.themes.map(async (theme: Omit) => {
- if (theme.coverImage) {
- const blob = await fetch(theme.coverImage as string).then((res) => res.blob());
- theme.coverImage = URL.createObjectURL(blob);
- }
- return theme;
- })
- );
-
- resolve(response);
- } else {
- reject(new Error('Failed to get response'));
- }
- }).catch((error: any) => {
- reject(error);
- });
- });
-
- return response;
-}
-
-export const disableTheme = async () => {
- await browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'DisableTheme',
- });
-};
-
-export const deleteTheme = async (themeID: string) => {
- await browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'DeleteTheme',
- body: {
- themeID: themeID
- }
- });
-}
-
-export const sendThemeUpdate = async (updatedTheme: CustomTheme | DownloadedTheme, saveTheme?: boolean, updateImages?: boolean, enableTheme?: boolean) => {
- saveTheme = saveTheme || false;
- enableTheme = enableTheme || false;
-
- const imageDataPromises = updatedTheme.CustomImages.map(async (image) => {
- if (saveTheme || updateImages) {
- const base64 = await blobToBase64(image.blob);
- return {
- id: image.id,
- variableName: image.variableName,
- url: base64,
- };
- }
- return {
- id: image.id,
- variableName: image.variableName,
- url: ''
- };
- });
-
- Promise.all(imageDataPromises).then(async (imageData) => {
- const themeData = {
- ...updatedTheme,
- CustomImages: imageData,
- };
-
- if (saveTheme && updatedTheme.coverImage) {
- themeData.coverImage = await blobToBase64(updatedTheme.coverImage as Blob);
- } else {
- themeData.coverImage = null;
- }
-
- browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'UpdateThemePreview',
- body: themeData,
- save: saveTheme,
- enableTheme: enableTheme
- });
-
- if (saveTheme) {
- browser.runtime.sendMessage({ type: 'currentTab', info: 'CloseThemeCreator' });
- }
- });
-};
-
-// Helper function to convert a Blob to base64
-const blobToBase64 = (blob: Blob): Promise => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onloadend = () => {
- const base64 = reader.result as string;
- resolve(base64);
- };
- reader.onerror = (error) => {
- reject(error);
- };
- reader.readAsDataURL(blob);
- });
-};
-
-export const enableCurrentTheme = async () => {
- await browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'EnableCurrentTheme',
- });
-};
-
-export const saveUpdatedTheme = async (updatedTheme: CustomTheme) => {
- await browser.runtime.sendMessage({
- type: 'currentTab',
- info: 'SaveTheme',
- body: updatedTheme,
- });
-};
\ No newline at end of file
diff --git a/src/interface/hooks/ThemeUpdates.ts b/src/interface/hooks/ThemeUpdates.ts
new file mode 100644
index 00000000..548da9cc
--- /dev/null
+++ b/src/interface/hooks/ThemeUpdates.ts
@@ -0,0 +1,29 @@
+type ThemeUpdateCallback = () => void;
+
+class ThemeUpdates {
+ private static instance: ThemeUpdates;
+ private listeners: Set = new Set();
+
+ private constructor() {}
+
+ public static getInstance(): ThemeUpdates {
+ if (!ThemeUpdates.instance) {
+ ThemeUpdates.instance = new ThemeUpdates();
+ }
+ return ThemeUpdates.instance;
+ }
+
+ public addListener(callback: ThemeUpdateCallback): void {
+ this.listeners.add(callback);
+ }
+
+ public removeListener(callback: ThemeUpdateCallback): void {
+ this.listeners.delete(callback);
+ }
+
+ public triggerUpdate(): void {
+ this.listeners.forEach(callback => callback());
+ }
+}
+
+export const themeUpdates = ThemeUpdates.getInstance();
diff --git a/src/interface/hooks/backgroundState.svelte.ts b/src/interface/hooks/backgroundState.svelte.ts
new file mode 100644
index 00000000..3bab4725
--- /dev/null
+++ b/src/interface/hooks/backgroundState.svelte.ts
@@ -0,0 +1 @@
+export let selectedBackground = $state(null);
\ No newline at end of file
diff --git a/src/interface/hooks/settingsState.ts b/src/interface/hooks/settingsState.ts
deleted file mode 100644
index 6506bf0b..00000000
--- a/src/interface/hooks/settingsState.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import browser from 'webextension-polyfill'
-import { useEffect, useMemo } from "react";
-import { SettingsProps } from "../types/SettingsProps";
-import { SettingsState } from "../types/AppProps";
-import { SettingsState as StorageSettingsState } from '../../types/storage';
-
-let RanOnce = false;
-let previousSettingsState: SettingsState
-
-const useSettingsState = ({ settingsState, setSettingsState }: SettingsProps) => {
- useEffect(() => {
- if (RanOnce) return;
- RanOnce = true;
-
- // @ts-expect-error - TODO: Fix this
- browser.storage.local.get().then((result: StorageSettingsState) => {
- setSettingsState({
- notificationCollector: result.notificationcollector,
- lessonAlerts: result.lessonalert,
- animatedBackground: result.animatedbk,
- animatedBackgroundSpeed: result.bksliderinput,
- customThemeColor: result.selectedColor,
- betterSEQTAPlus: result.onoff,
- shortcuts: result.shortcuts,
- customshortcuts: result.customshortcuts,
- transparencyEffects: result.transparencyEffects,
- selectedTheme: result.selectedTheme,
- timeFormat: result.timeFormat,
- animations: result.animations,
- defaultPage: result.defaultPage,
- devMode: result.devMode || false
- });
- });
- });
- const keyToStateMap = useMemo(() => ({
- "notificationcollector": "notificationCollector",
- "lessonalert": "lessonAlerts",
- "animatedbk": "animatedBackground",
- "bksliderinput": "animatedBackgroundSpeed",
- "selectedColor": "customThemeColor",
- "onoff": "betterSEQTAPlus",
- "shortcuts": "shortcuts",
- "customshortcuts": "customshortcuts",
- "transparencyEffects": "transparencyEffects",
- "selectedTheme": "selectedTheme",
- "timeFormat": "timeFormat",
- "animations": "animations",
- "defaultPage": "defaultPage",
- "devMode": "devMode"
- }), []);
-
- const storageChangeListener = (changes: browser.Storage.StorageChange) => {
- for (const [key, { newValue }] of Object.entries(changes)) {
- if (key === "DarkMode") {
- if (key === "DarkMode" && newValue) {
- document.documentElement.classList.add('dark');
- } else {
- document.documentElement.classList.remove('dark');
- }
- }
-
- // @ts-expect-error - TODO: Fix this
- const stateKey = keyToStateMap[key as keyof StorageSettingsState];
- if (stateKey) {
- setSettingsState((prevState: SettingsState) => ({
- ...prevState,
- [stateKey]: newValue
- }));
-
- }
- }
- };
-
- useEffect(() => {
- browser.storage.onChanged.addListener(storageChangeListener);
- return () => {
- browser.storage.onChanged.removeListener(storageChangeListener);
- };
- });
-
- const setStorage = (key: keyof StorageSettingsState, value: any) => {
- browser.storage.local.set({ [key]: value });
- }
-
- useEffect(() => {
- if (previousSettingsState) {
- for (const [key, value] of Object.entries(settingsState)) {
- // @ts-expect-error - TODO: Fix this
- const storageKey = Object.keys(keyToStateMap).find(k => keyToStateMap[k] === key);
- // @ts-expect-error - TODO: Fix this
- if (storageKey && value !== previousSettingsState[key]) {
- setStorage(storageKey as keyof StorageSettingsState, value);
- }
- }
- }
- previousSettingsState = settingsState;
- }, [settingsState, keyToStateMap])
-}
-
-export default useSettingsState;
\ No newline at end of file
diff --git a/src/interface/index.css b/src/interface/index.css
index 04395611..7c2c9299 100644
--- a/src/interface/index.css
+++ b/src/interface/index.css
@@ -1,3 +1,5 @@
+@import './components/ColourPicker.css';
+
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -16,6 +18,35 @@
display: none;
}
-button, a {
- @apply text-[0.875rem];
+.tab-width {
+ width: var(--tab-width);
+}
+
+input {
+ &:focus {
+ box-shadow: unset !important;
+ }
+}
+
+.animate-fade-in {
+ animation: fadeIn 0.5s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.no-scrollbar {
+ scrollbar-width: none !important;
+}
+
+.cm-editor {
+ width: 100%;
+ min-height: 100px;
+ max-height: 400px;
}
\ No newline at end of file
diff --git a/src/interface/index.html b/src/interface/index.html
index 43c86d39..b324d8bb 100644
--- a/src/interface/index.html
+++ b/src/interface/index.html
@@ -5,10 +5,8 @@
BetterSEQTA+ Settings
-
-
-
-
+
+
+
-
-
+