format: run prettify

This commit is contained in:
SethBurkart123
2025-05-05 18:04:10 +10:00
parent 771169348f
commit 0f9f618164
142 changed files with 28768 additions and 20790 deletions
+94 -111
View File
@@ -2,87 +2,83 @@
module.exports = { module.exports = {
forbidden: [ forbidden: [
{ {
name: 'no-circular', name: "no-circular",
severity: 'warn', severity: "warn",
comment: comment:
'This dependency is part of a circular relationship. You might want to revise ' + "This dependency is part of a circular relationship. You might want to revise " +
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ', "your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ",
from: {}, from: {},
to: { to: {
circular: true circular: true,
} },
}, },
{ {
name: 'no-orphans', name: "no-orphans",
comment: comment:
"This is an orphan module - it's likely not used (anymore?). Either use it or " + "This is an orphan module - it's likely not used (anymore?). Either use it or " +
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " + "remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
"add an exception for it in your dependency-cruiser configuration. By default " + "add an exception for it in your dependency-cruiser configuration. By default " +
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " + "this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.", "files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
severity: 'warn', severity: "warn",
from: { from: {
orphan: true, orphan: true,
pathNot: [ pathNot: [
'(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files "(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$", // dot files
'[.]d[.]ts$', // TypeScript declaration files "[.]d[.]ts$", // TypeScript declaration files
'(^|/)tsconfig[.]json$', // TypeScript config "(^|/)tsconfig[.]json$", // TypeScript config
'(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs "(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs
] ],
}, },
to: {}, to: {},
}, },
{ {
name: 'no-deprecated-core', name: "no-deprecated-core",
comment: comment:
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' + "A module depends on a node core module that has been deprecated. Find an alternative - these are " +
"bound to exist - node doesn't deprecate lightly.", "bound to exist - node doesn't deprecate lightly.",
severity: 'warn', severity: "warn",
from: {}, from: {},
to: { to: {
dependencyTypes: [ dependencyTypes: ["core"],
'core'
],
path: [ path: [
'^v8/tools/codemap$', "^v8/tools/codemap$",
'^v8/tools/consarray$', "^v8/tools/consarray$",
'^v8/tools/csvparser$', "^v8/tools/csvparser$",
'^v8/tools/logreader$', "^v8/tools/logreader$",
'^v8/tools/profile_view$', "^v8/tools/profile_view$",
'^v8/tools/profile$', "^v8/tools/profile$",
'^v8/tools/SourceMap$', "^v8/tools/SourceMap$",
'^v8/tools/splaytree$', "^v8/tools/splaytree$",
'^v8/tools/tickprocessor-driver$', "^v8/tools/tickprocessor-driver$",
'^v8/tools/tickprocessor$', "^v8/tools/tickprocessor$",
'^node-inspect/lib/_inspect$', "^node-inspect/lib/_inspect$",
'^node-inspect/lib/internal/inspect_client$', "^node-inspect/lib/internal/inspect_client$",
'^node-inspect/lib/internal/inspect_repl$', "^node-inspect/lib/internal/inspect_repl$",
'^async_hooks$', "^async_hooks$",
'^punycode$', "^punycode$",
'^domain$', "^domain$",
'^constants$', "^constants$",
'^sys$', "^sys$",
'^_linklist$', "^_linklist$",
'^_stream_wrap$' "^_stream_wrap$",
], ],
} },
}, },
{ {
name: 'not-to-deprecated', name: "not-to-deprecated",
comment: comment:
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' + "This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later " +
'version of that module, or find an alternative. Deprecated modules are a security risk.', "version of that module, or find an alternative. Deprecated modules are a security risk.",
severity: 'warn', severity: "warn",
from: {}, from: {},
to: { to: {
dependencyTypes: [ dependencyTypes: ["deprecated"],
'deprecated' },
]
}
}, },
{ {
name: 'no-non-package-json', name: "no-non-package-json",
severity: 'error', severity: "error",
comment: comment:
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " + "This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " + "That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
@@ -90,84 +86,75 @@ module.exports = {
"in your package.json.", "in your package.json.",
from: {}, from: {},
to: { to: {
dependencyTypes: [ dependencyTypes: ["npm-no-pkg", "npm-unknown"],
'npm-no-pkg', },
'npm-unknown'
]
}
}, },
{ {
name: 'not-to-unresolvable', name: "not-to-unresolvable",
comment: comment:
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " + "This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
'module: add it to your package.json. In all other cases you likely already know what to do.', "module: add it to your package.json. In all other cases you likely already know what to do.",
severity: 'error', severity: "error",
from: {}, from: {},
to: { to: {
couldNotResolve: true couldNotResolve: true,
} },
}, },
{ {
name: 'no-duplicate-dep-types', name: "no-duplicate-dep-types",
comment: comment:
"Likely this module depends on an external ('npm') package that occurs more than once " + "Likely this module depends on an external ('npm') package that occurs more than once " +
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " + "in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
"maintenance problems later on.", "maintenance problems later on.",
severity: 'warn', severity: "warn",
from: {}, from: {},
to: { to: {
moreThanOneDependencyType: true, moreThanOneDependencyType: true,
// as it's pretty common to have a type import be a type only import // as it's pretty common to have a type import be a type only import
// _and_ (e.g.) a devDependency - don't consider type-only dependency // _and_ (e.g.) a devDependency - don't consider type-only dependency
// types for this rule // types for this rule
dependencyTypesNot: ["type-only"] dependencyTypesNot: ["type-only"],
} },
}, },
/* rules you might want to tweak for your specific situation: */ /* rules you might want to tweak for your specific situation: */
{ {
name: 'not-to-spec', name: "not-to-spec",
comment: comment:
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' + "This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. " +
"If there's something in a spec that's of use to other modules, it doesn't have that single " + "If there's something in a spec that's of use to other modules, it doesn't have that single " +
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.', "responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.",
severity: 'error', severity: "error",
from: {}, from: {},
to: { to: {
path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' path: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$",
} },
}, },
{ {
name: 'not-to-dev-dep', name: "not-to-dev-dep",
severity: 'error', severity: "error",
comment: comment:
"This module depends on an npm package from the 'devDependencies' section of your " + "This module depends on an npm package from the 'devDependencies' section of your " +
'package.json. It looks like something that ships to production, though. To prevent problems ' + "package.json. It looks like something that ships to production, though. To prevent problems " +
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" + "with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
'section of your package.json. If this module is development only - add it to the ' + "section of your package.json. If this module is development only - add it to the " +
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration', "from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration",
from: { from: {
path: '^(src)', path: "^(src)",
pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$' pathNot: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$",
}, },
to: { to: {
dependencyTypes: [ dependencyTypes: ["npm-dev"],
'npm-dev',
],
// type only dependencies are not a problem as they don't end up in the // type only dependencies are not a problem as they don't end up in the
// production code or are ignored by the runtime. // production code or are ignored by the runtime.
dependencyTypesNot: [ dependencyTypesNot: ["type-only"],
'type-only' pathNot: ["node_modules/@types/"],
], },
pathNot: [
'node_modules/@types/'
]
}
}, },
{ {
name: 'optional-deps-used', name: "optional-deps-used",
severity: 'info', severity: "info",
comment: comment:
"This module depends on an npm package that is declared as an optional dependency " + "This module depends on an npm package that is declared as an optional dependency " +
"in your package.json. As this makes sense in limited situations only, it's flagged here. " + "in your package.json. As this makes sense in limited situations only, it's flagged here. " +
@@ -175,33 +162,28 @@ module.exports = {
"dependency-cruiser configuration.", "dependency-cruiser configuration.",
from: {}, from: {},
to: { to: {
dependencyTypes: [ dependencyTypes: ["npm-optional"],
'npm-optional' },
]
}
}, },
{ {
name: 'peer-deps-used', name: "peer-deps-used",
comment: comment:
"This module depends on an npm package that is declared as a peer dependency " + "This module depends on an npm package that is declared as a peer dependency " +
"in your package.json. This makes sense if your package is e.g. a plugin, but in " + "in your package.json. This makes sense if your package is e.g. a plugin, but in " +
"other cases - maybe not so much. If the use of a peer dependency is intentional " + "other cases - maybe not so much. If the use of a peer dependency is intentional " +
"add an exception to your dependency-cruiser configuration.", "add an exception to your dependency-cruiser configuration.",
severity: 'warn', severity: "warn",
from: {}, from: {},
to: { to: {
dependencyTypes: [ dependencyTypes: ["npm-peer"],
'npm-peer' },
] },
}
}
], ],
options: { options: {
/* Which modules not to follow further when encountered */ /* Which modules not to follow further when encountered */
doNotFollow: { doNotFollow: {
/* path: an array of regular expressions in strings to match against */ /* path: an array of regular expressions in strings to match against */
path: ['node_modules'] path: ["node_modules"],
}, },
/* Which modules to exclude */ /* Which modules to exclude */
@@ -274,7 +256,7 @@ module.exports = {
defaults to './tsconfig.json'. defaults to './tsconfig.json'.
*/ */
tsConfig: { tsConfig: {
fileName: 'tsconfig.json' fileName: "tsconfig.json",
}, },
/* Webpack configuration to use to get resolve options from. /* Webpack configuration to use to get resolve options from.
@@ -364,8 +346,8 @@ module.exports = {
"bun:wrap", "bun:wrap",
"detect-libc", "detect-libc",
"undici", "undici",
"ws" "ws",
] ],
}, },
reporterOptions: { reporterOptions: {
@@ -375,7 +357,7 @@ module.exports = {
collapses everything in node_modules to one folder deep so you see collapses everything in node_modules to one folder deep so you see
the external modules, but their innards. the external modules, but their innards.
*/ */
collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)', collapsePattern: "node_modules/(?:@[^/]+/[^/]+|[^/]+)",
/* Options to tweak the appearance of your graph.See /* Options to tweak the appearance of your graph.See
https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#reporteroptions
@@ -397,7 +379,8 @@ module.exports = {
dependency graph reporter (`archi`) you probably want to tweak dependency graph reporter (`archi`) you probably want to tweak
this collapsePattern to your situation. this collapsePattern to your situation.
*/ */
collapsePattern: '^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)', collapsePattern:
"^(?:packages|src|lib(s?)|app(s?)|bin|test(s?)|spec(s?))/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)",
/* Options to tweak the appearance of your graph. If you don't specify a /* Options to tweak the appearance of your graph. If you don't specify a
theme for 'archi' dependency-cruiser will use the one specified in the theme for 'archi' dependency-cruiser will use the one specified in the
@@ -405,10 +388,10 @@ module.exports = {
*/ */
// theme: { }, // theme: { },
}, },
"text": { text: {
"highlightFocused": true highlightFocused: true,
},
},
}, },
}
}
}; };
// generated: dependency-cruiser@16.10.0 on 2025-02-16T22:32:01.621Z // generated: dependency-cruiser@16.10.0 on 2025-02-16T22:32:01.621Z
+5 -2
View File
@@ -12,12 +12,15 @@
}, },
"rules": { "rules": {
// allow importing ts extensions // allow importing ts extensions
"sort-imports": ["error", { "sort-imports": [
"error",
{
"ignoreCase": true, "ignoreCase": true,
"ignoreDeclarationSort": true, "ignoreDeclarationSort": true,
"ignoreMemberSort": false, "ignoreMemberSort": false,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"] "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
}], }
],
"import/extensions": [ "import/extensions": [
"error", "error",
"ignorePackages", "ignorePackages",
@@ -3,7 +3,6 @@ description: Suggest a new Feature to be added or replaced in BetterSeqtaPLUS
labels: enhancement labels: enhancement
title: "[FR]" title: "[FR]"
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Confirm label: Confirm
@@ -25,7 +24,6 @@ body:
## Feature details ## Feature details
Before you request a feature, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) if it has already been requested. (Make sure to check closed issues as well!) Before you request a feature, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) if it has already been requested. (Make sure to check closed issues as well!)
- type: dropdown - type: dropdown
attributes: attributes:
label: Feature type label: Feature type
@@ -37,7 +35,6 @@ body:
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: Feature Details label: Feature Details
+10 -10
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
* Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
* Focusing on what is best not just for us as individuals, but for the - Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment - Public or private harassment
* Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
+4 -5
View File
@@ -1,4 +1,3 @@
# #
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel"> <a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
@@ -65,8 +64,6 @@ Don't worry- if you get stuck feel free to ask around in the [discord](https://d
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
``` ```
1. Install dependencies 1. Install dependencies
You may install the dependencies like below: You may install the dependencies like below:
@@ -80,15 +77,15 @@ But it is recommended to do it like this:
``` ```
npm install --legacy-peer-deps # Only NPM supported npm install --legacy-peer-deps # Only NPM supported
``` ```
### Running Development ### Running Development
2. Run the dev script (it updates as you save files) 2. Run the dev script (it updates as you save files)
``` ```
npm run dev # or use your perferred package manager npm run dev # or use your perferred package manager
``` ```
### Building for production ### Building for production
2. Run the build script 2. Run the build script
@@ -102,6 +99,7 @@ npm run build # or use your perferred package manager
``` ```
npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your perferred package manager npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your perferred package manager
``` ```
3. Load the extension into chrome 3. Load the extension into chrome
- Go to `chrome://extensions` - Go to `chrome://extensions`
@@ -130,6 +128,7 @@ The folder structure is as follows:
</a> </a>
Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/CONTRIBUTING.md) Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/CONTRIBUTING.md)
## Credits ## Credits
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development by [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development by [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph)
+2 -1
View File
@@ -5,11 +5,12 @@
Below here is the supported versions of BetterSEQTA+. Anything older than this is not supported and contains bugs. Below here is the supported versions of BetterSEQTA+. Anything older than this is not supported and contains bugs.
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | --------- |
| 3.4.3 | ✅ | | 3.4.3 | ✅ |
| < 3.4.3 | :x: | | < 3.4.3 | :x: |
`*` May not work on other devices. `*` May not work on other devices.
## Reporting a Vulnerability ## Reporting a Vulnerability
If you find vulnerabilities, REPORT IT IMMEDIATELY. open the [advisories tab](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories) on the left and click the green "report a vulnerability" button or use [this quick-link](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories/new) to create a new report If you find vulnerabilities, REPORT IT IMMEDIATELY. open the [advisories tab](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories) on the left and click the green "report a vulnerability" button or use [this quick-link](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories/new) to create a new report
+2
View File
@@ -7,11 +7,13 @@ Welcome to the BetterSEQTA+ documentation! This documentation will help you unde
## Table of Contents ## Table of Contents
### Getting Started ### Getting Started
- [Project Overview](./README.md) - This file - [Project Overview](./README.md) - This file
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+ - [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+ - [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
### Plugin System ### Plugin System
- [Creating Your First Plugin](./plugins/README.md) - A comprehensive, beginner-friendly guide to creating plugins - [Creating Your First Plugin](./plugins/README.md) - A comprehensive, beginner-friendly guide to creating plugins
- [Plugin API Reference](./plugins/api-reference.md) - Detailed technical documentation of the plugin APIs - [Plugin API Reference](./plugins/api-reference.md) - Detailed technical documentation of the plugin APIs
+6
View File
@@ -22,6 +22,7 @@ Thank you for your interest in contributing to BetterSEQTA+! This document provi
BetterSEQTA+ is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to adhere to our Code of Conduct, which promotes respectful and harassment-free interaction. BetterSEQTA+ is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to adhere to our Code of Conduct, which promotes respectful and harassment-free interaction.
Key points: Key points:
- Be respectful and inclusive - Be respectful and inclusive
- Focus on what is best for the community - Focus on what is best for the community
- Show empathy towards other community members - Show empathy towards other community members
@@ -105,6 +106,7 @@ git checkout -b feature/my-new-feature
2. **Write Clear Commit Messages** 2. **Write Clear Commit Messages**
Follow the conventional commits format: Follow the conventional commits format:
``` ```
feat: add new feature feat: add new feature
fix: resolve bug with timetable fix: resolve bug with timetable
@@ -118,6 +120,7 @@ git checkout -b feature/my-new-feature
4. **Run Tests** 4. **Run Tests**
Make sure all tests pass before submitting your PR: Make sure all tests pass before submitting your PR:
```bash ```bash
npm test npm test
``` ```
@@ -157,6 +160,7 @@ We follow TypeScript best practices and have a consistent code style:
5. **Use Linters** 5. **Use Linters**
We use ESLint and Prettier. Run them before submitting your PR: We use ESLint and Prettier. Run them before submitting your PR:
```bash ```bash
npm run lint npm run lint
npm run format npm run format
@@ -173,6 +177,7 @@ If you find a bug, please report it by creating an issue on GitHub:
2. **Use the Bug Report Template** 2. **Use the Bug Report Template**
Fill in all sections of the bug report template: Fill in all sections of the bug report template:
- Description - Description
- Steps to reproduce - Steps to reproduce
- Expected behavior - Expected behavior
@@ -195,6 +200,7 @@ We welcome feature suggestions! To suggest a new feature:
2. **Use the Feature Request Template** 2. **Use the Feature Request Template**
Fill in all sections of the feature request template: Fill in all sections of the feature request template:
- Description - Description
- Use case - Use case
- Potential implementation - Potential implementation
+2
View File
@@ -132,6 +132,7 @@ bun install
#### Extension not appearing in SEQTA #### Extension not appearing in SEQTA
Make sure: Make sure:
- You're visiting a SEQTA Learn page - You're visiting a SEQTA Learn page
- The extension is enabled - The extension is enabled
- You've refreshed the page after installing the extension - You've refreshed the page after installing the extension
@@ -139,6 +140,7 @@ Make sure:
#### Development build not updating #### Development build not updating
Try: Try:
1. Stopping the development server 1. Stopping the development server
2. Clearing your browser cache 2. Clearing your browser cache
3. Removing the extension from your browser 3. Removing the extension from your browser
+41 -31
View File
@@ -5,6 +5,7 @@ Hey there! 👋 So you want to create a plugin for BetterSEQTA+? That's awesome!
## What is a Plugin? ## What is a Plugin?
In BetterSEQTA+, a plugin is like a mini-app that adds new features to SEQTA. Think of it as a piece of LEGO that you can snap onto SEQTA to make it do new things. For example, you could create a plugin that: In BetterSEQTA+, a plugin is like a mini-app that adds new features to SEQTA. Think of it as a piece of LEGO that you can snap onto SEQTA to make it do new things. For example, you could create a plugin that:
- Changes how SEQTA looks - Changes how SEQTA looks
- Adds new buttons or features - Adds new buttons or features
- Shows extra information on your timetable - Shows extra information on your timetable
@@ -16,14 +17,14 @@ In BetterSEQTA+, a plugin is like a mini-app that adds new features to SEQTA. Th
Let's create a super simple plugin together. We'll make one that adds a friendly message to the SEQTA homepage. Here's what we'll need: Let's create a super simple plugin together. We'll make one that adds a friendly message to the SEQTA homepage. Here's what we'll need:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
const myFirstPlugin: Plugin = { const myFirstPlugin: Plugin = {
// Every plugin needs these basic details // Every plugin needs these basic details
id: 'my-first-plugin', id: "my-first-plugin",
name: 'My First Plugin', name: "My First Plugin",
description: 'Adds a friendly message to SEQTA', description: "Adds a friendly message to SEQTA",
version: '1.0.0', version: "1.0.0",
// This tells BetterSEQTA+ that users can turn our plugin on/off // This tells BetterSEQTA+ that users can turn our plugin on/off
disableToggle: true, disableToggle: true,
@@ -31,14 +32,14 @@ const myFirstPlugin: Plugin = {
// This is where the magic happens! // This is where the magic happens!
run: async (api) => { run: async (api) => {
// Wait for the homepage to load // Wait for the homepage to load
api.seqta.onMount('.home-page', (homePage) => { api.seqta.onMount(".home-page", (homePage) => {
// Create our message // Create our message
const message = document.createElement('div'); const message = document.createElement("div");
message.textContent = 'Hello from my first plugin! 🎉'; message.textContent = "Hello from my first plugin! 🎉";
message.style.padding = '20px'; message.style.padding = "20px";
message.style.backgroundColor = '#e9f5ff'; message.style.backgroundColor = "#e9f5ff";
message.style.borderRadius = '8px'; message.style.borderRadius = "8px";
message.style.margin = '20px'; message.style.margin = "20px";
// Add it to the page // Add it to the page
homePage.prepend(message); homePage.prepend(message);
@@ -46,10 +47,10 @@ const myFirstPlugin: Plugin = {
// Return a cleanup function that removes our message when the plugin is disabled // Return a cleanup function that removes our message when the plugin is disabled
return () => { return () => {
const message = document.querySelector('.home-page > div'); const message = document.querySelector(".home-page > div");
message?.remove(); message?.remove();
}; };
} },
}; };
export default myFirstPlugin; export default myFirstPlugin;
@@ -79,13 +80,13 @@ This helps you interact with SEQTA's pages:
```typescript ```typescript
// Wait for an element to appear on the page // Wait for an element to appear on the page
api.seqta.onMount('.some-class', (element) => { api.seqta.onMount(".some-class", (element) => {
// Do something with the element // Do something with the element
}); });
// Know when the user changes pages // Know when the user changes pages
api.seqta.onPageChange((page) => { api.seqta.onPageChange((page) => {
console.log('User went to:', page); console.log("User went to:", page);
}); });
// Get the current page // Get the current page
@@ -97,8 +98,12 @@ const currentPage = api.seqta.getCurrentPage();
Want to let users customize your plugin? Use settings! Want to let users customize your plugin? Use settings!
```typescript ```typescript
import { BasePlugin } from '@/plugins/core/settings'; import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers'; import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Define your settings // Define your settings
const settings = defineSettings({ const settings = defineSettings({
@@ -106,7 +111,7 @@ const settings = defineSettings({
default: true, default: true,
title: "Show Welcome Message", title: "Show Welcome Message",
description: "Show a friendly message on the homepage", description: "Show a friendly message on the homepage",
}) }),
}); });
// Create a class for your plugin // Create a class for your plugin
@@ -129,14 +134,14 @@ const myPlugin: Plugin<typeof settings> = {
} }
// Listen for setting changes // Listen for setting changes
api.settings.onChange('showMessage', (newValue) => { api.settings.onChange("showMessage", (newValue) => {
if (newValue) { if (newValue) {
// Show the message // Show the message
} else { } else {
// Hide the message // Hide the message
} }
}); });
} },
}; };
``` ```
@@ -146,14 +151,14 @@ Need to save some data? The storage API has got you covered:
```typescript ```typescript
// Save some data // Save some data
await api.storage.set('lastVisit', new Date().toISOString()); await api.storage.set("lastVisit", new Date().toISOString());
// Get it back later // Get it back later
const lastVisit = await api.storage.get('lastVisit'); const lastVisit = await api.storage.get("lastVisit");
// Listen for changes // Listen for changes
api.storage.onChange('lastVisit', (newValue) => { api.storage.onChange("lastVisit", (newValue) => {
console.log('Last visit updated:', newValue); console.log("Last visit updated:", newValue);
}); });
``` ```
@@ -163,12 +168,12 @@ Want your plugin to be able to interface with other plugins? Then use events!
```typescript ```typescript
// Listen for an event // Listen for an event
api.events.on('myCustomEvent', (data) => { api.events.on("myCustomEvent", (data) => {
console.log('Got event:', data); console.log("Got event:", data);
}); });
// Send an event // Send an event
api.events.emit('myCustomEvent', { some: 'data' }); api.events.emit("myCustomEvent", { some: "data" });
``` ```
## Adding Styles ## Adding Styles
@@ -199,7 +204,7 @@ const myPlugin: Plugin = {
run: async (api) => { run: async (api) => {
// Your plugin code here // Your plugin code here
} },
}; };
``` ```
@@ -208,28 +213,31 @@ const myPlugin: Plugin = {
Here are some tips to make your plugin awesome: Here are some tips to make your plugin awesome:
1. **Always Clean Up**: When your plugin is disabled, clean up any changes you made: 1. **Always Clean Up**: When your plugin is disabled, clean up any changes you made:
```typescript ```typescript
run: async (api) => { run: async (api) => {
// Add stuff to the page // Add stuff to the page
const element = document.createElement('div'); const element = document.createElement("div");
document.body.appendChild(element); document.body.appendChild(element);
// Return a cleanup function // Return a cleanup function
return () => { return () => {
element.remove(); element.remove();
}; };
} };
``` ```
2. **Use TypeScript**: It helps catch errors before they happen and makes your code easier to understand. 2. **Use TypeScript**: It helps catch errors before they happen and makes your code easier to understand.
3. **Test Your Plugin**: Make sure it works in different situations: 3. **Test Your Plugin**: Make sure it works in different situations:
- When SEQTA is loading - When SEQTA is loading
- When the user switches pages - When the user switches pages
- When the plugin is enabled/disabled - When the plugin is enabled/disabled
- When settings are changed - When settings are changed
4. **Keep It Fast**: Don't slow down SEQTA: 4. **Keep It Fast**: Don't slow down SEQTA:
- Use `onMount` instead of intervals or timeouts - Use `onMount` instead of intervals or timeouts
- Clean up event listeners when they're not needed - Clean up event listeners when they're not needed
- Don't do heavy calculations on the main thread - Don't do heavy calculations on the main thread
@@ -242,6 +250,7 @@ Here are some tips to make your plugin awesome:
## Examples ## Examples
Want to see more examples? Check out our built-in plugins: Want to see more examples? Check out our built-in plugins:
- [themes](../../src/plugins/built-in/themes/index.ts): Shows how to change SEQTA's appearance - [themes](../../src/plugins/built-in/themes/index.ts): Shows how to change SEQTA's appearance
- [notificationCollector](../../src/plugins/built-in/notificationCollector/index.ts): Shows how to work with SEQTA's notifications - [notificationCollector](../../src/plugins/built-in/notificationCollector/index.ts): Shows how to work with SEQTA's notifications
- [timetable](../../src/plugins/built-in/timetable/index.ts): Shows how to modify SEQTA's timetable view - [timetable](../../src/plugins/built-in/timetable/index.ts): Shows how to modify SEQTA's timetable view
@@ -250,6 +259,7 @@ Want to see more examples? Check out our built-in plugins:
## Need Help? ## Need Help?
Got stuck? No worries! Here's where you can get help: Got stuck? No worries! Here's where you can get help:
- Join our [Discord server](https://discord.gg/YzmbnCDkat) - Join our [Discord server](https://discord.gg/YzmbnCDkat)
- Check out the built-in plugins in the `src/plugins/built-in` folder - Check out the built-in plugins in the `src/plugins/built-in` folder
- Open an issue on our [GitHub page](https://github.com/betterseqta/betterseqta-plus/issues) - Open an issue on our [GitHub page](https://github.com/betterseqta/betterseqta-plus/issues)
+96 -74
View File
@@ -7,9 +7,13 @@ This document provides detailed technical information about BetterSEQTA+'s plugi
Here's how a plugin is structured: Here's how a plugin is structured:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from '@/plugins/core/settings'; import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers'; import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// First, define your settings // First, define your settings
const settings = defineSettings({ const settings = defineSettings({
@@ -17,7 +21,7 @@ const settings = defineSettings({
default: true, default: true,
title: "Enable Feature", title: "Enable Feature",
description: "Turn this feature on or off", description: "Turn this feature on or off",
}) }),
}); });
// Create a class to handle your settings // Create a class to handle your settings
@@ -31,28 +35,28 @@ const settingsInstance = new MyPluginClass();
// Create your plugin // Create your plugin
const myPlugin: Plugin<typeof settings> = { const myPlugin: Plugin<typeof settings> = {
id: 'my-plugin', id: "my-plugin",
name: 'My Plugin', name: "My Plugin",
description: 'A cool plugin that does things', description: "A cool plugin that does things",
version: '1.0.0', version: "1.0.0",
settings: settingsInstance.settings, settings: settingsInstance.settings,
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
console.log('Plugin is running!'); console.log("Plugin is running!");
// Do stuff when settings change // Do stuff when settings change
api.settings.onChange('enabled', (enabled) => { api.settings.onChange("enabled", (enabled) => {
if (enabled) { if (enabled) {
console.log('Feature enabled!'); console.log("Feature enabled!");
} }
}); });
// Return a cleanup function // Return a cleanup function
return () => { return () => {
console.log('Plugin cleanup'); console.log("Plugin cleanup");
}; };
} },
}; };
export default myPlugin; export default myPlugin;
@@ -63,27 +67,30 @@ export default myPlugin;
The SEQTA API helps you interact with SEQTA's pages: The SEQTA API helps you interact with SEQTA's pages:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
const seqtaPlugin: Plugin<typeof settings> = { const seqtaPlugin: Plugin<typeof settings> = {
id: 'seqta-example', id: "seqta-example",
name: 'SEQTA Example', name: "SEQTA Example",
description: 'Shows how to use the SEQTA API', description: "Shows how to use the SEQTA API",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
// Wait for elements to appear // Wait for elements to appear
const { unregister: timetableUnregister } = api.seqta.onMount('.timetable', (timetable) => { const { unregister: timetableUnregister } = api.seqta.onMount(
const button = document.createElement('button'); ".timetable",
button.textContent = 'Export'; (timetable) => {
const button = document.createElement("button");
button.textContent = "Export";
timetable.appendChild(button); timetable.appendChild(button);
}); },
);
// Track page changes // Track page changes
const { unregister: pageUnregister } = api.seqta.onPageChange((page) => { const { unregister: pageUnregister } = api.seqta.onPageChange((page) => {
console.log('User went to:', page); console.log("User went to:", page);
}); });
// Clean up when disabled // Clean up when disabled
@@ -91,7 +98,7 @@ const seqtaPlugin: Plugin<typeof settings> = {
timetableUnregister(); timetableUnregister();
pageUnregister(); pageUnregister();
}; };
} },
}; };
export default seqtaPlugin; export default seqtaPlugin;
@@ -102,22 +109,29 @@ export default seqtaPlugin;
Here's how to add settings to your plugin: Here's how to add settings to your plugin:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from '@/plugins/core/settings'; import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, stringSetting, numberSetting, selectSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers'; import {
booleanSetting,
stringSetting,
numberSetting,
selectSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Define your settings // Define your settings
const settings = defineSettings({ const settings = defineSettings({
darkMode: booleanSetting({ darkMode: booleanSetting({
default: false, default: false,
title: "Dark Mode", title: "Dark Mode",
description: "Enable dark mode" description: "Enable dark mode",
}), }),
userName: stringSetting({ userName: stringSetting({
default: "", default: "",
title: "User Name", title: "User Name",
description: "Your display name", description: "Your display name",
placeholder: "Enter your name..." placeholder: "Enter your name...",
}), }),
theme: selectSetting({ theme: selectSetting({
default: "light", default: "light",
@@ -125,9 +139,9 @@ const settings = defineSettings({
description: "Choose your theme", description: "Choose your theme",
options: [ options: [
{ value: "light", label: "Light" }, { value: "light", label: "Light" },
{ value: "dark", label: "Dark" } { value: "dark", label: "Dark" },
] ],
}) }),
}); });
// Create your settings class // Create your settings class
@@ -144,29 +158,29 @@ class ThemePluginClass extends BasePlugin<typeof settings> {
// Create the plugin // Create the plugin
const themePlugin: Plugin<typeof settings> = { const themePlugin: Plugin<typeof settings> = {
id: 'theme-example', id: "theme-example",
name: 'Theme Example', name: "Theme Example",
description: 'Shows how to use settings', description: "Shows how to use settings",
version: '1.0.0', version: "1.0.0",
settings: new ThemePluginClass().settings, settings: new ThemePluginClass().settings,
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
// Apply initial settings // Apply initial settings
if (api.settings.darkMode) { if (api.settings.darkMode) {
document.body.classList.add('dark'); document.body.classList.add("dark");
} }
// Listen for changes // Listen for changes
const { unregister } = api.settings.onChange('darkMode', (enabled) => { const { unregister } = api.settings.onChange("darkMode", (enabled) => {
document.body.classList.toggle('dark', enabled); document.body.classList.toggle("dark", enabled);
}); });
return () => { return () => {
unregister(); unregister();
document.body.classList.remove('dark'); document.body.classList.remove("dark");
}; };
} },
}; };
export default themePlugin; export default themePlugin;
@@ -177,13 +191,13 @@ export default themePlugin;
Here's how to use storage in your plugin: Here's how to use storage in your plugin:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
const storagePlugin: Plugin<typeof settings> = { const storagePlugin: Plugin<typeof settings> = {
id: 'storage-example', id: "storage-example",
name: 'Storage Example', name: "Storage Example",
description: 'Shows how to use storage', description: "Shows how to use storage",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
disableToggle: true, disableToggle: true,
@@ -192,21 +206,21 @@ const storagePlugin: Plugin<typeof settings> = {
await api.storage.loaded; await api.storage.loaded;
// Save some data // Save some data
await api.storage.set('lastVisit', new Date().toISOString()); await api.storage.set("lastVisit", new Date().toISOString());
// Get saved data // Get saved data
const lastVisit = await api.storage.get('lastVisit'); const lastVisit = await api.storage.get("lastVisit");
console.log('Last visit:', lastVisit); console.log("Last visit:", lastVisit);
// Listen for changes // Listen for changes
const { unregister } = api.storage.onChange('lastVisit', (newValue) => { const { unregister } = api.storage.onChange("lastVisit", (newValue) => {
console.log('Last visit updated:', newValue); console.log("Last visit updated:", newValue);
}); });
return () => { return () => {
unregister(); unregister();
}; };
} },
}; };
export default storagePlugin; export default storagePlugin;
@@ -217,33 +231,39 @@ export default storagePlugin;
Here's how to use events in your plugin: Here's how to use events in your plugin:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
const eventsPlugin: Plugin<typeof settings> = { const eventsPlugin: Plugin<typeof settings> = {
id: 'events-example', id: "events-example",
name: 'Events Example', name: "Events Example",
description: 'Shows how to use events', description: "Shows how to use events",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
// Listen for theme changes // Listen for theme changes
const { unregister: themeListener } = api.events.on('theme.changed', (theme) => { const { unregister: themeListener } = api.events.on(
console.log('Theme changed to:', theme); "theme.changed",
}); (theme) => {
console.log("Theme changed to:", theme);
},
);
// Listen for notifications // Listen for notifications
const { unregister: notifyListener } = api.events.on('notification.new', (notification) => { const { unregister: notifyListener } = api.events.on(
console.log('New notification:', notification); "notification.new",
}); (notification) => {
console.log("New notification:", notification);
},
);
// Clean up listeners // Clean up listeners
return () => { return () => {
themeListener(); themeListener();
notifyListener(); notifyListener();
}; };
} },
}; };
export default eventsPlugin; export default eventsPlugin;
@@ -254,20 +274,20 @@ export default eventsPlugin;
Here's how to write efficient plugins: Here's how to write efficient plugins:
```typescript ```typescript
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
const efficientPlugin: Plugin<typeof settings> = { const efficientPlugin: Plugin<typeof settings> = {
id: 'efficient-example', id: "efficient-example",
name: 'Efficient Example', name: "Efficient Example",
description: 'Shows performance best practices', description: "Shows performance best practices",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
// ✅ Good: Use onMount // ✅ Good: Use onMount
const { unregister } = api.seqta.onMount('.timetable', (el) => { const { unregister } = api.seqta.onMount(".timetable", (el) => {
el.classList.add('enhanced'); el.classList.add("enhanced");
}); });
// ❌ Bad: Don't use intervals // ❌ Bad: Don't use intervals
@@ -277,7 +297,7 @@ const efficientPlugin: Plugin<typeof settings> = {
// }, 100); // }, 100);
// ✅ Good: Cache DOM elements // ✅ Good: Cache DOM elements
const header = document.querySelector('.header'); const header = document.querySelector(".header");
if (header) { if (header) {
// Reuse header instead of querying again // Reuse header instead of querying again
} }
@@ -285,7 +305,7 @@ const efficientPlugin: Plugin<typeof settings> = {
// ✅ Good: Batch DOM updates // ✅ Good: Batch DOM updates
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const div = document.createElement('div'); const div = document.createElement("div");
fragment.appendChild(div); fragment.appendChild(div);
} }
document.body.appendChild(fragment); document.body.appendChild(fragment);
@@ -294,13 +314,14 @@ const efficientPlugin: Plugin<typeof settings> = {
unregister(); unregister();
// clearInterval(interval); // If you used the bad approach // clearInterval(interval); // If you used the bad approach
}; };
} },
}; };
export default efficientPlugin; export default efficientPlugin;
``` ```
Each plugin should be in its own file and exported as the default export. The plugin should: Each plugin should be in its own file and exported as the default export. The plugin should:
1. Import necessary types and helpers 1. Import necessary types and helpers
2. Define settings if needed 2. Define settings if needed
3. Create a settings class if using settings 3. Create a settings class if using settings
@@ -308,6 +329,7 @@ Each plugin should be in its own file and exported as the default export. The pl
5. Export the plugin as default 5. Export the plugin as default
Remember to always: Remember to always:
- Use proper TypeScript types - Use proper TypeScript types
- Clean up when your plugin is disabled - Clean up when your plugin is disabled
- Handle errors gracefully - Handle errors gracefully
+1 -1
View File
@@ -7,7 +7,7 @@ export const base64Loader = {
const [filePath, query] = id.split("?"); const [filePath, query] = id.split("?");
if (query !== "base64") return null; if (query !== "base64") return null;
const data = fs.readFileSync(filePath, { encoding: 'base64' }); const data = fs.readFileSync(filePath, { encoding: "base64" });
const mimeType = mime.lookup(filePath); const mimeType = mime.lookup(filePath);
const dataURL = `data:${mimeType};base64,${data}`; const dataURL = `data:${mimeType};base64,${data}`;
+9 -9
View File
@@ -1,25 +1,25 @@
// ref: https://stackoverflow.com/a/76920975 // ref: https://stackoverflow.com/a/76920975
import type { Plugin } from 'vite'; import type { Plugin } from "vite";
export default function ClosePlugin(): Plugin { export default function ClosePlugin(): Plugin {
return { return {
name: 'ClosePlugin', // required, will show up in warnings and errors name: "ClosePlugin", // required, will show up in warnings and errors
// use this to catch errors when building // use this to catch errors when building
buildEnd(error) { buildEnd(error) {
if (error) { if (error) {
console.error('Error bundling') console.error("Error bundling");
console.error(error) console.error(error);
process.exit(1) process.exit(1);
} else { } else {
console.log('Build ended') console.log("Build ended");
} }
}, },
// use this to catch the end of a build without errors // use this to catch the end of a build without errors
closeBundle() { closeBundle() {
console.log('Bundle closed') console.log("Bundle closed");
process.exit(0) process.exit(0);
}, },
} };
} }
+4 -4
View File
@@ -1,5 +1,5 @@
import type { Browser, BuildTarget, Manifest } from './types' import type { Browser, BuildTarget, Manifest } from "./types";
import type { AnyCase } from './utils' import type { AnyCase } from "./utils";
/** /**
* *
* *
@@ -15,7 +15,7 @@ export function createManifest(
return { return {
manifest, manifest,
browser, browser,
} };
} }
/** /**
@@ -29,5 +29,5 @@ export function createManifest(
* @return {*} {@link Manifest} * @return {*} {@link Manifest}
*/ */
export function createManifestBase(manifest: Manifest): Manifest { export function createManifestBase(manifest: Manifest): Manifest {
return manifest return manifest;
} }
+18 -18
View File
@@ -1,26 +1,26 @@
// vite-plugin-inline-worker-dev.ts // vite-plugin-inline-worker-dev.ts
import { Plugin } from 'vite' import { Plugin } from "vite";
import fs from 'fs/promises' import fs from "fs/promises";
import { build, transform } from 'esbuild' import { build, transform } from "esbuild";
export default function InlineWorkerDevPlugin(): Plugin { export default function InlineWorkerDevPlugin(): Plugin {
return { return {
name: 'vite:inline-worker-dev', name: "vite:inline-worker-dev",
async load(id) { async load(id) {
if (id.includes('?inlineWorker')) { if (id.includes("?inlineWorker")) {
const [cleanPath] = id.split('?') const [cleanPath] = id.split("?");
console.log('cleanPath', cleanPath) console.log("cleanPath", cleanPath);
const code = await fs.readFile(cleanPath, 'utf-8') const code = await fs.readFile(cleanPath, "utf-8");
const result = await build({ const result = await build({
entryPoints: [cleanPath], entryPoints: [cleanPath],
bundle: true, bundle: true,
write: false, write: false,
platform: 'browser', platform: "browser",
format: 'iife', format: "iife",
target: 'esnext', target: "esnext",
}) });
const workerCode = result.outputFiles[0].text const workerCode = result.outputFiles[0].text;
const workerBlobCode = ` const workerBlobCode = `
const code = ${JSON.stringify(workerCode)}; const code = ${JSON.stringify(workerCode)};
@@ -28,10 +28,10 @@ export default function InlineWorkerDevPlugin(): Plugin {
const blob = new Blob([code], { type: 'application/javascript' }); const blob = new Blob([code], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob), { type: 'module' }); return new Worker(URL.createObjectURL(blob), { type: 'module' });
} }
` `;
return workerBlobCode return workerBlobCode;
}
return null
}
} }
return null;
},
};
} }
+65 -42
View File
@@ -1,49 +1,63 @@
const glob = require('glob'); const glob = require("glob");
const semver = require('semver'); const semver = require("semver");
const { execSync } = require('child_process'); const { execSync } = require("child_process");
const path = require('path'); const path = require("path");
function getLatestVersion(files) { function getLatestVersion(files) {
console.log('Files passed to getLatestVersion:', files); console.log("Files passed to getLatestVersion:", files);
const versions = files.map(file => { const versions = files
.map((file) => {
const match = file.match(/@([\d\.]+)-/); const match = file.match(/@([\d\.]+)-/);
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None'); console.log(
"Matching file:",
file,
"Version found:",
match ? match[1] : "None",
);
if (!match) return null; if (!match) return null;
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1) const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
const semverVersion = fullVersion.split('.').slice(0, 3).join('.'); // Trim to 3.4.5 const semverVersion = fullVersion.split(".").slice(0, 3).join("."); // Trim to 3.4.5
return { fullVersion, semverVersion }; return { fullVersion, semverVersion };
}).filter(Boolean); })
.filter(Boolean);
console.log('Extracted versions:', versions.map(v => v.semverVersion)); console.log(
"Extracted versions:",
versions.map((v) => v.semverVersion),
);
// Find latest version using the trimmed semver format // Find latest version using the trimmed semver format
const latestSemver = semver.maxSatisfying(versions.map(v => v.semverVersion), '*'); const latestSemver = semver.maxSatisfying(
console.log('Latest SemVer-compatible version:', latestSemver); versions.map((v) => v.semverVersion),
"*",
);
console.log("Latest SemVer-compatible version:", latestSemver);
// Get the full version that matches the latest SemVer version // Get the full version that matches the latest SemVer version
const latestVersion = versions.find(v => v.semverVersion === latestSemver)?.fullVersion || null; const latestVersion =
versions.find((v) => v.semverVersion === latestSemver)?.fullVersion || null;
console.log('Final selected latest version:', latestVersion); console.log("Final selected latest version:", latestVersion);
return latestVersion; return latestVersion;
} }
function getLatestFiles(browser) { function getLatestFiles(browser) {
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`; const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
console.log('Glob pattern:', pattern); console.log("Glob pattern:", pattern);
const files = glob.sync(pattern); const files = glob.sync(pattern);
console.log('Files found for browser', browser, ':', files); console.log("Files found for browser", browser, ":", files);
const latestVersion = getLatestVersion(files); const latestVersion = getLatestVersion(files);
// Find the exact file by matching the original full version // Find the exact file by matching the original full version
const latestFile = files.find(file => file.includes(`@${latestVersion}-`)); const latestFile = files.find((file) => file.includes(`@${latestVersion}-`));
console.log('Latest file for browser', browser, ':', latestFile); console.log("Latest file for browser", browser, ":", latestFile);
return latestFile; return latestFile;
} }
@@ -51,44 +65,53 @@ function zipSources() {
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`; const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
const excludePatterns = [ const excludePatterns = [
'node_modules', "node_modules",
'dist', "dist",
'.env*', ".env*",
'.git', ".git",
'.github', ".github",
'.vscode', ".vscode",
'LICENSE', "LICENSE",
'package.json' "package.json",
].map(pattern => `-x!${pattern}`).join(' '); ]
.map((pattern) => `-x!${pattern}`)
.join(" ");
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`; const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
console.log('Zipping project sources with command:', zipCommand); console.log("Zipping project sources with command:", zipCommand);
execSync(zipCommand, { stdio: 'inherit' }); execSync(zipCommand, { stdio: "inherit" });
return zipFileName; return zipFileName;
} }
function runPublishCommand(browsers) { function runPublishCommand(browsers) {
const chromeZip = browsers.includes('chrome') ? getLatestFiles('chrome') : null; const chromeZip = browsers.includes("chrome")
const firefoxZip = browsers.includes('firefox') ? getLatestFiles('firefox') : null; ? getLatestFiles("chrome")
const firefoxSourcesZip = browsers.includes('firefox') ? zipSources() : null; : null;
const firefoxZip = browsers.includes("firefox")
? getLatestFiles("firefox")
: null;
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
console.log('Chrome zip:', chromeZip); console.log("Chrome zip:", chromeZip);
console.log('Firefox zip:', firefoxZip); console.log("Firefox zip:", firefoxZip);
console.log('Firefox sources zip:', firefoxSourcesZip); console.log("Firefox sources zip:", firefoxSourcesZip);
if (browsers.length === 0) { if (browsers.length === 0) {
console.log('No browsers specified. Exiting.'); console.log("No browsers specified. Exiting.");
process.exit(0); process.exit(0);
} }
if ((browsers.includes('chrome') && !chromeZip) || (browsers.includes('firefox') && (!firefoxZip || !firefoxSourcesZip))) { if (
console.error('Could not find required zip files for specified browsers.'); (browsers.includes("chrome") && !chromeZip) ||
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
) {
console.error("Could not find required zip files for specified browsers.");
process.exit(1); process.exit(1);
} }
let command = 'publish-extension'; let command = "publish-extension";
if (chromeZip) { if (chromeZip) {
command += ` --chrome-zip ${chromeZip}`; command += ` --chrome-zip ${chromeZip}`;
} }
@@ -96,13 +119,13 @@ function runPublishCommand(browsers) {
command += ` --firefox-zip ${firefoxZip} --firefox-sources-zip ${firefoxSourcesZip}`; command += ` --firefox-zip ${firefoxZip} --firefox-sources-zip ${firefoxSourcesZip}`;
} }
console.log('Running command:', command); console.log("Running command:", command);
execSync(command, { stdio: 'inherit' }); execSync(command, { stdio: "inherit" });
} }
// Parse command-line arguments // Parse command-line arguments
const args = process.argv.slice(2); const args = process.argv.slice(2);
const browserIndex = args.indexOf('--b'); const browserIndex = args.indexOf("--b");
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : []; const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
runPublishCommand(browsers); runPublishCommand(browsers);
+8 -8
View File
@@ -1,17 +1,17 @@
import fs from 'fs'; import fs from "fs";
export default function touchGlobalCSSPlugin() { export default function touchGlobalCSSPlugin() {
return { return {
name: 'touch-global-css', name: "touch-global-css",
handleHotUpdate({ modules }) { handleHotUpdate({ modules }) {
// log all of the staticImportedUrls // log all of the staticImportedUrls
const importers = modules[0]._clientModule.importers const importers = modules[0]._clientModule.importers;
importers.forEach((importer) => { importers.forEach((importer) => {
if (importer.file.includes('.css')) { if (importer.file.includes(".css")) {
console.log("touching", importer.file) console.log("touching", importer.file);
fs.utimesSync(importer.file, new Date(), new Date()) fs.utimesSync(importer.file, new Date(), new Date());
}
})
} }
});
},
}; };
} }
+67 -67
View File
@@ -1,104 +1,104 @@
import type { ManifestV3Export } from '@crxjs/vite-plugin' import type { ManifestV3Export } from "@crxjs/vite-plugin";
import { type AnyCase, createEnum } from './utils' import { type AnyCase, createEnum } from "./utils";
export const FrameworkEnum = { export const FrameworkEnum = {
React: 'React', React: "React",
Vanilla: 'Vanilla', Vanilla: "Vanilla",
Preact: 'Preact', Preact: "Preact",
Lit: 'Lit', Lit: "Lit",
Svelte: 'Svelte', Svelte: "Svelte",
Vue: 'Vue', Vue: "Vue",
} as const } as const;
export const BrowserEnum = { export const BrowserEnum = {
Chrome: 'Chrome', Chrome: "Chrome",
Brave: 'Brave', Brave: "Brave",
Opera: 'Opera', Opera: "Opera",
Edge: 'Edge', Edge: "Edge",
Firefox: 'Firefox', Firefox: "Firefox",
Safari: 'Safari', Safari: "Safari",
} as const } as const;
const LanguageEnum = { const LanguageEnum = {
TypeScript: 'TypeScript', TypeScript: "TypeScript",
JavaScript: 'JavaScript', JavaScript: "JavaScript",
} as const } as const;
export const StyleEnum = { export const StyleEnum = {
Tailwind: 'Tailwind', Tailwind: "Tailwind",
} as const } as const;
export const PackageManagerEnum = { export const PackageManagerEnum = {
Bun: 'Bun', Bun: "Bun",
PnPm: 'PnPm', PnPm: "PnPm",
Npm: 'Npm', Npm: "Npm",
Yarn: 'Yarn', Yarn: "Yarn",
} as const } as const;
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts // see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
export type BrowserSpecificSettings = { export type BrowserSpecificSettings = {
browser_specific_settings?: { browser_specific_settings?: {
gecko?: { gecko?: {
id: string id: string;
strict_min_version?: string strict_min_version?: string;
strict_max_version?: string strict_max_version?: string;
} };
} };
} };
export type Manifest = ManifestV3Export export type Manifest = ManifestV3Export;
export type ManifestIcons = chrome.runtime.ManifestIcons export type ManifestIcons = chrome.runtime.ManifestIcons;
export type ManifestBackground = chrome.runtime.ManifestV3['background'] export type ManifestBackground = chrome.runtime.ManifestV3["background"];
export type ManifestContentScripts = export type ManifestContentScripts =
chrome.runtime.ManifestV3['content_scripts'] chrome.runtime.ManifestV3["content_scripts"];
export type ManifestWebAccessibleResources = export type ManifestWebAccessibleResources =
chrome.runtime.ManifestV3['web_accessible_resources'] chrome.runtime.ManifestV3["web_accessible_resources"];
export type ManifestCommands = chrome.runtime.ManifestV3['commands'] export type ManifestCommands = chrome.runtime.ManifestV3["commands"];
export type ManifestAction = chrome.runtime.ManifestV3['action'] export type ManifestAction = chrome.runtime.ManifestV3["action"];
export type ManifestPermissions = chrome.runtime.ManifestV3['permissions'] export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"];
export type ManifestOptionsUI = chrome.runtime.ManifestV3['options_ui'] export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"];
export type ManifestURLOverrides = export type ManifestURLOverrides =
chrome.runtime.ManifestV3['chrome_url_overrides'] chrome.runtime.ManifestV3["chrome_url_overrides"];
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T> export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>;
export type BrowserEnumType<T extends string> = { export type BrowserEnumType<T extends string> = {
[browser in BrowserName<T>]: BrowserName<T> [browser in BrowserName<T>]: BrowserName<T>;
} };
export type BuildMode = AnyCase<Browser> export type BuildMode = AnyCase<Browser>;
export type BuildTarget = { export type BuildTarget = {
manifest: Manifest manifest: Manifest;
browser: AnyCase<Browser> browser: AnyCase<Browser>;
} };
export type BuildConfig = { export type BuildConfig = {
command?: 'build' | 'serve' command?: "build" | "serve";
mode?: AnyCase<Browser> | string | undefined mode?: AnyCase<Browser> | string | undefined;
} };
export interface Repository { export interface Repository {
type: string type: string;
url?: string url?: string;
bugs?: Bugs bugs?: Bugs;
} }
export interface Bugs { export interface Bugs {
url?: string url?: string;
email?: string email?: string;
} }
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum] export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum];
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum) export const Browser: AnyCase<Browser> = createEnum(BrowserEnum);
export type PackageManager = export type PackageManager =
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum] (typeof PackageManagerEnum)[keyof typeof PackageManagerEnum];
export const PackageManager: AnyCase<PackageManager> = export const PackageManager: AnyCase<PackageManager> =
createEnum(PackageManagerEnum) createEnum(PackageManagerEnum);
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum] export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum];
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum) export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum);
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum] export type Style = (typeof StyleEnum)[keyof typeof StyleEnum];
export const Style: AnyCase<Style> = createEnum(StyleEnum) export const Style: AnyCase<Style> = createEnum(StyleEnum);
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum] export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum];
export const Language: AnyCase<Language> = createEnum(LanguageEnum) export const Language: AnyCase<Language> = createEnum(LanguageEnum);
+6 -6
View File
@@ -1,21 +1,21 @@
export type ObjectValues<T> = T[keyof T] export type ObjectValues<T> = T[keyof T];
export function createEnum<T extends Record<string, string>>(enumObj: T) { export function createEnum<T extends Record<string, string>>(enumObj: T) {
return Object.values(enumObj) as unknown as ObjectValues<T> return Object.values(enumObj) as unknown as ObjectValues<T>;
} }
export type AnyCase<T extends string> = export type AnyCase<T extends string> =
| Uppercase<T> | Uppercase<T>
| Lowercase<T> | Lowercase<T>
| Capitalize<T> | Capitalize<T>
| Uncapitalize<T> | Uncapitalize<T>;
export type AnyCaseLanguage<T extends string, K extends string> = export type AnyCaseLanguage<T extends string, K extends string> =
| Uppercase<T | K> | Uppercase<T | K>
| Lowercase<T | K> | Lowercase<T | K>
| Capitalize<T | K> | Capitalize<T | K>
| Uncapitalize<T | K> | Uncapitalize<T | K>;
export type OptionalKeys<T> = { export type OptionalKeys<T> = {
[K in keyof T as undefined extends T[K] ? K : never]: T[K] [K in keyof T as undefined extends T[K] ? K : never]: T[K];
} };
+29 -27
View File
@@ -1,55 +1,57 @@
import { import {
initializeSettingsState, initializeSettingsState,
settingsState, settingsState,
} from "@/seqta/utils/listeners/SettingsState" } from "@/seqta/utils/listeners/SettingsState";
import documentLoadCSS from "@/css/documentload.scss?inline" import documentLoadCSS from "@/css/documentload.scss?inline";
import icon48 from "@/resources/icons/icon-48.png?base64" import icon48 from "@/resources/icons/icon-48.png?base64";
import browser from "webextension-polyfill" import browser from "webextension-polyfill";
import * as plugins from "@/plugins" import * as plugins from "@/plugins";
import { main } from "@/seqta/main" import { main } from "@/seqta/main";
export let MenuOptionsOpen = false;
export let MenuOptionsOpen = false var IsSEQTAPage = false;
let hasSEQTAText = false;
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) // This check is placed outside of the document load event due to issues with EP (https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/84)
if (document.childNodes[1]) { if (document.childNodes[1]) {
hasSEQTAText = hasSEQTAText =
document.childNodes[1].textContent?.includes( document.childNodes[1].textContent?.includes(
"Copyright (c) SEQTA Software", "Copyright (c) SEQTA Software",
) ?? false ) ?? false;
init() init();
} }
async function init() { async function init() {
const hasSEQTATitle = document.title.includes("SEQTA Learn") const hasSEQTATitle = document.title.includes("SEQTA Learn");
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) { // Verify we are on a SEQTA page if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
IsSEQTAPage = true // Verify we are on a SEQTA page
console.info("[BetterSEQTA+] Verified SEQTA Page") IsSEQTAPage = true;
console.info("[BetterSEQTA+] Verified SEQTA Page");
const documentLoadStyle = document.createElement("style") const documentLoadStyle = document.createElement("style");
documentLoadStyle.textContent = documentLoadCSS documentLoadStyle.textContent = documentLoadCSS;
document.head.appendChild(documentLoadStyle) document.head.appendChild(documentLoadStyle);
const icon = document.querySelector('link[rel*="icon"]')! as HTMLLinkElement const icon = document.querySelector(
icon.href = icon48 // Change the icon 'link[rel*="icon"]',
)! as HTMLLinkElement;
icon.href = icon48; // Change the icon
try { try {
await initializeSettingsState() await initializeSettingsState();
if (typeof settingsState.onoff === "undefined") { if (typeof settingsState.onoff === "undefined") {
await browser.runtime.sendMessage({ type: "setDefaultStorage" }) await browser.runtime.sendMessage({ type: "setDefaultStorage" });
} }
await main() await main();
if (settingsState.onoff) { if (settingsState.onoff) {
// Initialize legacy plugins // Initialize legacy plugins
plugins.Monofile() plugins.Monofile();
// Initialize new plugin system // Initialize new plugin system
await plugins.initializePlugins(); await plugins.initializePlugins();
@@ -57,9 +59,9 @@ async function init() {
console.info( console.info(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
) );
} catch (error: any) { } catch (error: any) {
console.error(error) console.error(error);
} }
} }
} }
+77 -56
View File
@@ -1,63 +1,68 @@
import browser from 'webextension-polyfill' import browser from "webextension-polyfill";
import type { SettingsState } from "@/types/storage"; import type { SettingsState } from "@/types/storage";
import { fetchNews } from './background/news'; import { fetchNews } from "./background/news";
function reloadSeqtaPages() { function reloadSeqtaPages() {
const result = browser.tabs.query({}) const result = browser.tabs.query({});
function open(tabs: any) { function open(tabs: any) {
for (let tab of tabs) { for (let tab of tabs) {
if (tab.title.includes('SEQTA Learn')) { if (tab.title.includes("SEQTA Learn")) {
browser.tabs.reload(tab.id); browser.tabs.reload(tab.id);
} }
} }
} }
result.then(open, console.error) result.then(open, console.error);
} }
// @ts-ignore // @ts-ignore
browser.runtime.onMessage.addListener((request: any, _: any, sendResponse: (response?: any) => void) => { browser.runtime.onMessage.addListener(
(request: any, _: any, sendResponse: (response?: any) => void) => {
switch (request.type) { switch (request.type) {
case 'reloadTabs': case "reloadTabs":
reloadSeqtaPages(); reloadSeqtaPages();
break; break;
case 'extensionPages': case "extensionPages":
browser.tabs.query({}).then(function (tabs) { browser.tabs.query({}).then(function (tabs) {
for (let tab of tabs) { for (let tab of tabs) {
if (tab.url?.includes('chrome-extension://')) { if (tab.url?.includes("chrome-extension://")) {
browser.tabs.sendMessage(tab.id!, request); browser.tabs.sendMessage(tab.id!, request);
} }
} }
}); });
break; break;
case 'currentTab': case "currentTab":
browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) { browser.tabs
browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) { .query({ active: true, currentWindow: true })
.then(function (tabs) {
browser.tabs
.sendMessage(tabs[0].id!, request)
.then(function (response) {
sendResponse(response); sendResponse(response);
}); });
}); });
return true; return true;
case 'githubTab': case "githubTab":
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' }); browser.tabs.create({ url: "github.com/BetterSEQTA/BetterSEQTA-Plus" });
break; break;
case 'setDefaultStorage': case "setDefaultStorage":
SetStorageValue(DefaultValues); SetStorageValue(DefaultValues);
break; break;
case 'sendNews': case "sendNews":
fetchNews(request.source ?? 'australia', sendResponse); fetchNews(request.source ?? "australia", sendResponse);
return true; return true;
default: default:
console.log('Unknown request type'); console.log("Unknown request type");
} }
return false; return false;
}); },
);
const DefaultValues: SettingsState = { const DefaultValues: SettingsState = {
onoff: true, onoff: true,
@@ -86,66 +91,67 @@ const DefaultValues: SettingsState = {
}, },
menuorder: [], menuorder: [],
subjectfilters: {}, subjectfilters: {},
selectedTheme: '', selectedTheme: "",
selectedColor: 'linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)', selectedColor:
originalSelectedColor: '', "linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
originalSelectedColor: "",
DarkMode: true, DarkMode: true,
animations: true, animations: true,
assessmentsAverage: true, assessmentsAverage: true,
defaultPage: 'home', defaultPage: "home",
shortcuts: [ shortcuts: [
{ {
name: 'YouTube', name: "YouTube",
enabled: false, enabled: false,
}, },
{ {
name: 'Outlook', name: "Outlook",
enabled: true, enabled: true,
}, },
{ {
name: 'Office', name: "Office",
enabled: true, enabled: true,
}, },
{ {
name: 'Spotify', name: "Spotify",
enabled: false, enabled: false,
}, },
{ {
name: 'Google', name: "Google",
enabled: true, enabled: true,
}, },
{ {
name: 'DuckDuckGo', name: "DuckDuckGo",
enabled: false, enabled: false,
}, },
{ {
name: 'Cool Math Games', name: "Cool Math Games",
enabled: false, enabled: false,
}, },
{ {
name: 'SACE', name: "SACE",
enabled: false, enabled: false,
}, },
{ {
name: 'Google Scholar', name: "Google Scholar",
enabled: false, enabled: false,
}, },
{ {
name: 'Gmail', name: "Gmail",
enabled: false, enabled: false,
}, },
{ {
name: 'Netflix', name: "Netflix",
enabled: false, enabled: false,
}, },
{ {
name: 'Education Perfect', name: "Education Perfect",
enabled: false, enabled: false,
}, },
], ],
customshortcuts: [], customshortcuts: [],
lettergrade: false, lettergrade: false,
newsSource: 'australia', newsSource: "australia",
}; };
function SetStorageValue(object: any) { function SetStorageValue(object: any) {
@@ -158,7 +164,8 @@ function convertBksliderToSpeed(bksliderinput: number): number {
const minBase = 50; const minBase = 50;
const maxBase = 150; const maxBase = 150;
const scaledValue = 2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4; const scaledValue =
2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
const baseSpeed = 3; const baseSpeed = 3;
const speed = baseSpeed / scaledValue; const speed = baseSpeed / scaledValue;
@@ -166,50 +173,64 @@ function convertBksliderToSpeed(bksliderinput: number): number {
} }
async function migrateLegacySettings() { async function migrateLegacySettings() {
const storage = await browser.storage.local.get(null) as unknown as SettingsState; const storage = (await browser.storage.local.get(
null,
)) as unknown as SettingsState;
// Animated Background Migration // Animated Background Migration
if ('animatedbk' in storage || 'bksliderinput' in storage) { if ("animatedbk" in storage || "bksliderinput" in storage) {
const animatedSettings = { const animatedSettings = {
enabled: storage.animatedbk ?? true, enabled: storage.animatedbk ?? true,
speed: storage.bksliderinput ? convertBksliderToSpeed(parseFloat(storage.bksliderinput)) : 1 speed: storage.bksliderinput
? convertBksliderToSpeed(parseFloat(storage.bksliderinput))
: 1,
}; };
await browser.storage.local.set({ 'plugin.animated-background.settings': animatedSettings }); await browser.storage.local.set({
"plugin.animated-background.settings": animatedSettings,
});
} }
// Assessments Average Migration // Assessments Average Migration
if ('assessmentsAverage' in storage || 'lettergrade' in storage) { if ("assessmentsAverage" in storage || "lettergrade" in storage) {
const assessmentsSettings = { const assessmentsSettings = {
enabled: storage.assessmentsAverage ?? true, enabled: storage.assessmentsAverage ?? true,
lettergrade: storage.lettergrade ?? false lettergrade: storage.lettergrade ?? false,
}; };
await browser.storage.local.set({ 'plugin.assessments-average.settings': assessmentsSettings }); await browser.storage.local.set({
"plugin.assessments-average.settings": assessmentsSettings,
});
} }
if ('selectedTheme' in storage) { if ("selectedTheme" in storage) {
const themesSettings = { enabled: true }; const themesSettings = { enabled: true };
await browser.storage.local.set({ 'plugin.themes.settings': themesSettings }); await browser.storage.local.set({
"plugin.themes.settings": themesSettings,
});
} }
if (storage.notificationCollector !== false) { if (storage.notificationCollector !== false) {
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: true } }); await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: true },
});
} else { } else {
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: false } }); await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: false },
});
} }
const keysToRemove = [ const keysToRemove = [
'animatedbk', "animatedbk",
'bksliderinput', "bksliderinput",
'assessmentsAverage', "assessmentsAverage",
'lettergrade' "lettergrade",
]; ];
await browser.storage.local.remove(keysToRemove); await browser.storage.local.remove(keysToRemove);
} }
browser.runtime.onInstalled.addListener(function (event) { browser.runtime.onInstalled.addListener(function (event) {
browser.storage.local.remove(['justupdated']); browser.storage.local.remove(["justupdated"]);
browser.storage.local.remove(['data']); browser.storage.local.remove(["data"]);
if ( event.reason == 'install' || event.reason == 'update' ) { if (event.reason == "install" || event.reason == "update") {
browser.storage.local.set({ justupdated: true }); browser.storage.local.set({ justupdated: true });
migrateLegacySettings(); migrateLegacySettings();
} }
+13 -14
View File
@@ -1,11 +1,11 @@
import Parser from 'rss-parser'; import Parser from "rss-parser";
const fetchAustraliaNews = async (url: string, sendResponse: any) => { const fetchAustraliaNews = async (url: string, sendResponse: any) => {
fetch(url) fetch(url)
.then((result) => result.json()) .then((result) => result.json())
.then((response) => { .then((response) => {
if (response.code == 'rateLimited') { if (response.code == "rateLimited") {
fetchAustraliaNews(url += '%00', sendResponse); fetchAustraliaNews((url += "%00"), sendResponse);
} else { } else {
sendResponse({ news: response }); sendResponse({ news: response });
} }
@@ -31,13 +31,13 @@ const rssFeedsByCountry: Record<string, string[]> = {
"https://critica.com.pa/rss.xml", "https://critica.com.pa/rss.xml",
"https://www.panamaamerica.com.pa/rss.xml", "https://www.panamaamerica.com.pa/rss.xml",
"https://noticiassin.com/feed/", "https://noticiassin.com/feed/",
"https://elcapitalfinanciero.com/feed/" "https://elcapitalfinanciero.com/feed/",
], ],
canada: [ canada: [
"https://www.cbc.ca/cmlink/rss-topstories", "https://www.cbc.ca/cmlink/rss-topstories",
"https://calgaryherald.com/feed", "https://calgaryherald.com/feed",
"https://ottawacitizen.com/feed", "https://ottawacitizen.com/feed",
"https://www.montrealgazette.com/feed" "https://www.montrealgazette.com/feed",
], ],
singapore: [ singapore: [
"https://www.straitstimes.com/news/singapore/rss.xml", "https://www.straitstimes.com/news/singapore/rss.xml",
@@ -49,12 +49,9 @@ const rssFeedsByCountry: Record<string, string[]> = {
], ],
japan: [ japan: [
"https://www3.nhk.or.jp/nhkworld/en/news/feeds/", "https://www3.nhk.or.jp/nhkworld/en/news/feeds/",
"https://news.livedoor.com/topics/rss/int.xml" "https://news.livedoor.com/topics/rss/int.xml",
],
netherlands: [
"https://www.dutchnews.nl/feed/",
"https://www.nrc.nl/rss/"
], ],
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
}; };
export async function fetchNews(source: string, sendResponse: any) { export async function fetchNews(source: string, sendResponse: any) {
@@ -63,9 +60,9 @@ export async function fetchNews(source: string, sendResponse: any) {
const from = const from =
date.getFullYear() + date.getFullYear() +
'-' + "-" +
(date.getMonth() + 1) + (date.getMonth() + 1) +
'-' + "-" +
(date.getDate() - 5); (date.getDate() - 5);
const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`; const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`;
@@ -76,7 +73,7 @@ export async function fetchNews(source: string, sendResponse: any) {
const parser = new Parser(); const parser = new Parser();
let feeds: string[]; let feeds: string[];
console.log('fetchNews', source) console.log("fetchNews", source);
if (rssFeedsByCountry[source.toLowerCase()]) { if (rssFeedsByCountry[source.toLowerCase()]) {
// If the source is a country, fetch from predefined feeds // If the source is a country, fetch from predefined feeds
@@ -85,7 +82,9 @@ export async function fetchNews(source: string, sendResponse: any) {
// If the source is a URL, use it directly // If the source is a URL, use it directly
feeds = [source]; feeds = [source];
} else { } else {
throw new Error("Invalid source. Provide a country code or a valid RSS feed URL."); throw new Error(
"Invalid source. Provide a country code or a valid RSS feed URL.",
);
} }
const articlesPromises = feeds.map(async (feedUrl) => { const articlesPromises = feeds.map(async (feedUrl) => {
+4 -2
View File
@@ -15,7 +15,7 @@
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>. * along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
*/ */
@use 'injected/popup.scss'; @use "injected/popup.scss";
html { html {
background: #161616 !important; background: #161616 !important;
@@ -77,7 +77,9 @@ html {
transform-origin: top; transform-origin: top;
transition: transform 0.2s; transition: transform 0.2s;
} }
body:has(.outside-container:not(.hide)) #AddedSettings.tooltip:hover > .tooltiptext { body:has(.outside-container:not(.hide))
#AddedSettings.tooltip:hover
> .tooltiptext {
transform: scale(0); transform: scale(0);
} }
.assessmenttooltip svg { .assessmenttooltip svg {
+1 -1
View File
@@ -1 +1 @@
import './documentload.scss'; import "./documentload.scss";
+3 -1
View File
@@ -25,7 +25,9 @@
span, span,
body { body {
color: white !important; color: white !important;
text-shadow: 1px 1px 2px #161616, 0 0 1em #161616; text-shadow:
1px 1px 2px #161616,
0 0 1em #161616;
} }
body { body {
+23 -8
View File
@@ -1375,10 +1375,13 @@ div > ol:has(.uiFileHandlerWrapper) {
margin: 20px auto 0px; margin: 20px auto 0px;
cursor: pointer; cursor: pointer;
} }
.dark [class*="notifications__detailsBody___"] > [class*="notifications__subtitle___"] { .dark
[class*="notifications__detailsBody___"]
> [class*="notifications__subtitle___"] {
color: #c1bcbc; color: #c1bcbc;
} }
[class*="notifications__detailsBody___"] > [class*="notifications__subtitle___"] { [class*="notifications__detailsBody___"]
> [class*="notifications__subtitle___"] {
font-size: 12px; font-size: 12px;
} }
[class*="notifications__notifications___"] > button { [class*="notifications__notifications___"] > button {
@@ -1394,7 +1397,9 @@ div > ol:has(.uiFileHandlerWrapper) {
height: 25px; height: 25px;
width: 24px; width: 24px;
} }
[class*="notifications__notifications___"] > button > [class*="notifications__bubble___"] { [class*="notifications__notifications___"]
> button
> [class*="notifications__bubble___"] {
background: var(--better-alert-highlight); background: var(--better-alert-highlight);
width: 25px; width: 25px;
height: 25px; height: 25px;
@@ -1710,7 +1715,9 @@ ul {
> [class*="SelectedAssessment__meta___"] { > [class*="SelectedAssessment__meta___"] {
border-bottom: 1px solid var(--better-main); border-bottom: 1px solid var(--better-main);
} }
[class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] > li[class*="TabSet__selected___"] { [class*="TabSet__TabSet___"]
> ol[class*="TabSet__tabs___"]
> li[class*="TabSet__selected___"] {
border-bottom-color: var(--better-main); border-bottom-color: var(--better-main);
} }
[class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] { [class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] {
@@ -2181,7 +2188,9 @@ body {
border-radius: 1600px; border-radius: 1600px;
} }
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__selected___"] [class*="MessageList__MessageList___"]
> ol
> li[class*="MessageList__selected___"]
[class*="MessageList__unread___"] { [class*="MessageList__unread___"] {
box-shadow: none; box-shadow: none;
} }
@@ -2190,7 +2199,9 @@ body {
box-shadow: none; box-shadow: none;
} }
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__unread___"]::before, [class*="MessageList__MessageList___"]
> ol
> li[class*="MessageList__unread___"]::before,
[class*="MessageList__MessageList___"] > ol > li::before { [class*="MessageList__MessageList___"] > ol > li::before {
content: ""; content: "";
position: absolute; position: absolute;
@@ -2202,7 +2213,9 @@ body {
transition: width 0.1s; transition: width 0.1s;
} }
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__unread___"]::before { [class*="MessageList__MessageList___"]
> ol
> li[class*="MessageList__unread___"]::before {
width: 3px; width: 3px;
} }
.connectedNotificationsWrapper > div > button { .connectedNotificationsWrapper > div > button {
@@ -2283,7 +2296,9 @@ body {
background: var(--background-secondary); background: var(--background-secondary);
} }
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__selected___"] { [class*="MessageList__MessageList___"]
> ol
> li[class*="MessageList__selected___"] {
background: rgb(228 225 225); background: rgb(228 225 225);
color: var(--text-primary); color: var(--text-primary);
} }
+3 -1
View File
@@ -36,5 +36,7 @@
transform-origin: 70% 0; transform-origin: 70% 0;
will-change: opacity, transform; will-change: opacity, transform;
transform: translateZ(0); // promotes GPU rendering transform: translateZ(0); // promotes GPU rendering
transition: opacity 0.05s, transform 0.05s; transition:
opacity 0.05s,
transform 0.05s;
} }
+2 -2
View File
@@ -8,7 +8,6 @@ html.transparencyEffects:not(.dark) {
--background-secondary: rgba(229, 231, 235, 0.6); --background-secondary: rgba(229, 231, 235, 0.6);
} }
html.transparencyEffects { html.transparencyEffects {
/* Background Fixes */ /* Background Fixes */
[class*="notifications__item___"], [class*="notifications__item___"],
@@ -37,7 +36,8 @@ html.transparencyEffects {
[class*="LabelList__selected___"], [class*="LabelList__selected___"],
.buttonChecklist, .buttonChecklist,
.pane, .pane,
.legacy-root button, .legacy-root a, .legacy-root button,
.legacy-root a,
[class*="MessageList__MessageList___"] { [class*="MessageList__MessageList___"] {
backdrop-filter: blur(80px); backdrop-filter: blur(80px);
} }
+7 -7
View File
@@ -1,11 +1,11 @@
declare module '*.mp4'; declare module "*.mp4";
declare module '*.woff'; declare module "*.woff";
declare module '*.scss'; declare module "*.scss";
declare module '*.png'; declare module "*.png";
declare module '*.html'; declare module "*.html";
declare module '*.svelte'; declare module "*.svelte";
declare module '*?inlineWorker' { declare module "*?inlineWorker" {
const value: () => Worker; const value: () => Worker;
export default value; export default value;
} }
+42 -27
View File
@@ -1,6 +1,6 @@
import ColorPicker from "react-best-gradient-color-picker" import ColorPicker from "react-best-gradient-color-picker";
import { useEffect, useRef, useState } from "react" import { useEffect, useRef, useState } from "react";
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts";
const defaultPresets = [ 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(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)",
@@ -22,12 +22,12 @@ const defaultPresets = [
"rgba(30, 64, 175, 0.89)", "rgba(30, 64, 175, 0.89)",
"rgba(134, 25, 143, 1)", "rgba(134, 25, 143, 1)",
"rgba(14, 165, 233, 0.9)", "rgba(14, 165, 233, 0.9)",
] ];
interface PickerProps { interface PickerProps {
customOnChange?: (color: string) => void customOnChange?: (color: string) => void;
customState?: string customState?: string;
savePresets?: boolean savePresets?: boolean;
} }
export default function Picker({ export default function Picker({
@@ -35,32 +35,44 @@ export default function Picker({
customState, customState,
savePresets = true, savePresets = true,
}: PickerProps) { }: PickerProps) {
const [customThemeColor, setCustomThemeColor] = useState<string | null>() const [customThemeColor, setCustomThemeColor] = useState<string | null>();
const [presets, setPresets] = useState<string[]>() const [presets, setPresets] = useState<string[]>();
const latestValuesRef = useRef({ customThemeColor, customOnChange, savePresets, presets }); const latestValuesRef = useRef({
customThemeColor,
customOnChange,
savePresets,
presets,
});
useEffect(() => { useEffect(() => {
if (customState !== undefined && customState !== null) { if (customState !== undefined && customState !== null) {
setCustomThemeColor(customState) setCustomThemeColor(customState);
} else { } else {
setCustomThemeColor(settingsState.selectedColor ?? null) setCustomThemeColor(settingsState.selectedColor ?? null);
} }
if (presets === undefined) { if (presets === undefined) {
const savedPresets = localStorage.getItem("colorPickerPresets") const savedPresets = localStorage.getItem("colorPickerPresets");
setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets) setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets);
} }
}, []) }, []);
useEffect(() => { useEffect(() => {
latestValuesRef.current = { customThemeColor, customOnChange, savePresets, presets }; latestValuesRef.current = {
customThemeColor,
customOnChange,
savePresets,
presets,
};
}, [customThemeColor, customOnChange, savePresets, presets]); }, [customThemeColor, customOnChange, savePresets, presets]);
useEffect(() => { useEffect(() => {
return () => { return () => {
const { customThemeColor, customOnChange, savePresets, presets } = latestValuesRef.current; const { customThemeColor, customOnChange, savePresets, presets } =
if (!(customThemeColor && !customOnChange && savePresets && presets)) return; latestValuesRef.current;
if (!(customThemeColor && !customOnChange && savePresets && presets))
return;
// Only proceed if presets are different (avoid unnecessary updates) // Only proceed if presets are different (avoid unnecessary updates)
const existingIndex = presets.indexOf(customThemeColor); const existingIndex = presets.indexOf(customThemeColor);
@@ -79,15 +91,18 @@ export default function Picker({
updatedPresets = [customThemeColor, ...presets].slice(0, 18); updatedPresets = [customThemeColor, ...presets].slice(0, 18);
} }
localStorage.setItem("colorPickerPresets", JSON.stringify(updatedPresets)); localStorage.setItem(
} "colorPickerPresets",
}, []) JSON.stringify(updatedPresets),
);
};
}, []);
useEffect(() => { useEffect(() => {
if (customThemeColor && !customOnChange) { if (customThemeColor && !customOnChange) {
settingsState.selectedColor = customThemeColor settingsState.selectedColor = customThemeColor;
} }
}, [customThemeColor, customOnChange]) }, [customThemeColor, customOnChange]);
return ( return (
<ColorPicker <ColorPicker
@@ -97,12 +112,12 @@ export default function Picker({
value={customThemeColor ?? ""} value={customThemeColor ?? ""}
onChange={(color: string) => { onChange={(color: string) => {
if (customOnChange) { if (customOnChange) {
customOnChange(color) customOnChange(color);
setCustomThemeColor(color) setCustomThemeColor(color);
} else { } else {
setCustomThemeColor(color) setCustomThemeColor(color);
} }
}} }}
/> />
) );
} }
+1 -1
View File
@@ -1,4 +1,4 @@
.dark .switch[data-ison="true"], .dark .switch[data-ison="true"],
.switch[data-ison="true"] { .switch[data-ison="true"] {
background-color: #30D259; background-color: #30d259;
} }
+25 -15
View File
@@ -1,4 +1,4 @@
import { type DBSchema, type IDBPDatabase, openDB } from 'idb'; import { type DBSchema, type IDBPDatabase, openDB } from "idb";
interface BackgroundDB extends DBSchema { interface BackgroundDB extends DBSchema {
backgrounds: { backgrounds: {
@@ -16,38 +16,46 @@ let db: IDBPDatabase<BackgroundDB> | null = null;
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> { export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
if (db) return db; if (db) return db;
db = await openDB<BackgroundDB>('BackgroundDB', 1, { db = await openDB<BackgroundDB>("BackgroundDB", 1, {
upgrade(db: IDBPDatabase<BackgroundDB>) { upgrade(db: IDBPDatabase<BackgroundDB>) {
db.createObjectStore('backgrounds', { keyPath: 'id' }); db.createObjectStore("backgrounds", { keyPath: "id" });
}, },
}); });
return db; return db;
} }
export async function readAllData(): Promise<Array<{ id: string; type: string; blob: Blob }>> { export async function readAllData(): Promise<
Array<{ id: string; type: string; blob: Blob }>
> {
const db = await openDatabase(); const db = await openDatabase();
return db.getAll('backgrounds'); return db.getAll("backgrounds");
} }
export async function writeData(id: string, type: string, blob: Blob): Promise<void> { export async function writeData(
id: string,
type: string,
blob: Blob,
): Promise<void> {
const db = await openDatabase(); const db = await openDatabase();
await db.put('backgrounds', { id, type, blob }); await db.put("backgrounds", { id, type, blob });
} }
export async function deleteData(id: string): Promise<void> { export async function deleteData(id: string): Promise<void> {
const db = await openDatabase(); const db = await openDatabase();
await db.delete('backgrounds', id); await db.delete("backgrounds", id);
} }
export async function clearAllData(): Promise<void> { export async function clearAllData(): Promise<void> {
const db = await openDatabase(); const db = await openDatabase();
await db.clear('backgrounds'); await db.clear("backgrounds");
} }
export async function getDataById(id: string): Promise<{ id: string; type: string; blob: Blob } | undefined> { export async function getDataById(
id: string,
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
const db = await openDatabase(); const db = await openDatabase();
return db.get('backgrounds', id); return db.get("backgrounds", id);
} }
export function closeDatabase(): void { export function closeDatabase(): void {
@@ -59,15 +67,17 @@ export function closeDatabase(): void {
// Helper function to check if IndexedDB is supported // Helper function to check if IndexedDB is supported
export function isIndexedDBSupported(): boolean { export function isIndexedDBSupported(): boolean {
return 'indexedDB' in window; return "indexedDB" in window;
} }
// Helper function to check if there's enough storage space // Helper function to check if there's enough storage space
export async function hasEnoughStorageSpace(requiredSpace: number): Promise<boolean> { export async function hasEnoughStorageSpace(
if ('storage' in navigator && 'estimate' in navigator.storage) { requiredSpace: number,
): Promise<boolean> {
if ("storage" in navigator && "estimate" in navigator.storage) {
const { quota, usage } = await navigator.storage.estimate(); const { quota, usage } = await navigator.storage.estimate();
if (quota !== undefined && usage !== undefined) { if (quota !== undefined && usage !== undefined) {
return (quota - usage) > requiredSpace; return quota - usage > requiredSpace;
} }
} }
// If we can't determine, assume there's enough space // If we can't determine, assume there's enough space
+1 -1
View File
@@ -22,7 +22,7 @@ class BackgroundUpdates {
} }
public triggerUpdate(): void { public triggerUpdate(): void {
this.listeners.forEach(callback => callback()); this.listeners.forEach((callback) => callback());
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@ class SettingsPopup {
} }
public triggerClose(): void { public triggerClose(): void {
this.listeners.forEach(callback => callback()); this.listeners.forEach((callback) => callback());
} }
} }
+1 -1
View File
@@ -22,7 +22,7 @@ class ThemeUpdates {
} }
public triggerUpdate(): void { public triggerUpdate(): void {
this.listeners.forEach(callback => callback()); this.listeners.forEach((callback) => callback());
} }
} }
+1 -1
View File
@@ -1,4 +1,4 @@
@import './components/ColourPicker.css'; @import "./components/ColourPicker.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
+1 -1
View File
@@ -6,7 +6,7 @@
<title>BetterSEQTA+ Settings</title> <title>BetterSEQTA+ Settings</title>
</head> </head>
<body class="h-[600px]"> <body class="h-[600px]">
<div id="app" style="height: 100%;"></div> <div id="app" style="height: 100%"></div>
<script type="module" src="./index.ts"></script> <script type="module" src="./index.ts"></script>
</body> </body>
</html> </html>
+15 -15
View File
@@ -1,29 +1,29 @@
import "./index.css" import "./index.css";
import Settings from "./pages/settings.svelte" import Settings from "./pages/settings.svelte";
import IconFamily from '@/resources/fonts/IconFamily.woff' import IconFamily from "@/resources/fonts/IconFamily.woff";
import browser from "webextension-polyfill" import browser from "webextension-polyfill";
import renderSvelte from "./main" import renderSvelte from "./main";
function InjectCustomIcons() { function InjectCustomIcons() {
console.info('[BetterSEQTA+] Injecting Icons') console.info("[BetterSEQTA+] Injecting Icons");
const style = document.createElement('style') const style = document.createElement("style");
style.setAttribute('type', 'text/css') style.setAttribute("type", "text/css");
style.innerHTML = ` style.innerHTML = `
@font-face { @font-face {
font-family: 'IconFamily'; font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff'); src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
}` }`;
document.head.appendChild(style) document.head.appendChild(style);
} }
const mountPoint = document.getElementById('app') const mountPoint = document.getElementById("app");
if (!mountPoint) { if (!mountPoint) {
console.error('Mount point #app not found') console.error("Mount point #app not found");
throw new Error('Mount point #app not found') throw new Error("Mount point #app not found");
} }
InjectCustomIcons() InjectCustomIcons();
renderSvelte(Settings, mountPoint, { standalone: true }) renderSvelte(Settings, mountPoint, { standalone: true });
+1 -1
View File
@@ -1,4 +1,4 @@
import './index.css'; import "./index.css";
declare module "*.png"; declare module "*.png";
declare module "*.svg"; declare module "*.svg";
+8 -8
View File
@@ -1,6 +1,6 @@
import { mount } from "svelte" import { mount } from "svelte";
import type { SvelteComponent } from "svelte" import type { SvelteComponent } from "svelte";
import style from './index.css?inline' import style from "./index.css?inline";
export default function renderSvelte( export default function renderSvelte(
Component: SvelteComponent | any, Component: SvelteComponent | any,
@@ -13,11 +13,11 @@ export default function renderSvelte(
standalone: false, standalone: false,
...props, ...props,
}, },
}) });
const styleElement = document.createElement('style') const styleElement = document.createElement("style");
styleElement.textContent = style styleElement.textContent = style;
mountPoint.appendChild(styleElement) mountPoint.appendChild(styleElement);
return app return app;
} }
+1 -1
View File
@@ -2,6 +2,6 @@ export interface SettingsList {
title: string; title: string;
id: number; id: number;
description: string; description: string;
Component: any; /* TODO: Give this a type */ Component: any /* TODO: Give this a type */;
props?: any; props?: any;
} }
+1 -1
View File
@@ -16,7 +16,7 @@ export class Standalone {
public setStandalone(value: boolean) { public setStandalone(value: boolean) {
this._standalone = value; this._standalone = value;
this.subscribers.forEach(subscriber => subscriber(value)); this.subscribers.forEach((subscriber) => subscriber(value));
} }
public get standalone() { public get standalone() {
+31 -11
View File
@@ -1,23 +1,31 @@
import type { LoadedCustomTheme } from '@/types/CustomThemes'; import type { LoadedCustomTheme } from "@/types/CustomThemes";
export function generateImageId(): string { export function generateImageId(): string {
return Math.random().toString(36).substr(2, 9); return Math.random().toString(36).substr(2, 9);
} }
export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promise<LoadedCustomTheme> | LoadedCustomTheme { export function handleImageUpload(
event: Event,
theme: LoadedCustomTheme,
): Promise<LoadedCustomTheme> | LoadedCustomTheme {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ''; input.value = "";
if (file) { if (file) {
return new Promise((resolve) => { return new Promise((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const imageBlob = await fetch(reader.result as string).then(res => res.blob()); const imageBlob = await fetch(reader.result as string).then((res) =>
res.blob(),
);
const imageId = generateImageId(); const imageId = generateImageId();
const variableName = `custom-image-${theme.CustomImages.length}`; const variableName = `custom-image-${theme.CustomImages.length}`;
resolve({ resolve({
...theme, ...theme,
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: null }], CustomImages: [
...theme.CustomImages,
{ id: imageId, blob: imageBlob, variableName, url: null },
],
}); });
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
@@ -26,31 +34,43 @@ export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promi
return theme; return theme;
} }
export function handleRemoveImage(imageId: string, theme: LoadedCustomTheme): LoadedCustomTheme { export function handleRemoveImage(
imageId: string,
theme: LoadedCustomTheme,
): LoadedCustomTheme {
return { return {
...theme, ...theme,
CustomImages: theme.CustomImages.filter((image) => image.id !== imageId), CustomImages: theme.CustomImages.filter((image) => image.id !== imageId),
} as LoadedCustomTheme; } as LoadedCustomTheme;
} }
export function handleImageVariableChange(imageId: string, variableName: string, theme: LoadedCustomTheme): LoadedCustomTheme { export function handleImageVariableChange(
imageId: string,
variableName: string,
theme: LoadedCustomTheme,
): LoadedCustomTheme {
return { return {
...theme, ...theme,
CustomImages: theme.CustomImages.map((image) => CustomImages: theme.CustomImages.map((image) =>
image.id === imageId ? { ...image, variableName } : image image.id === imageId ? { ...image, variableName } : image,
), ),
} as LoadedCustomTheme; } as LoadedCustomTheme;
} }
export function handleCoverImageUpload(event: Event, theme: LoadedCustomTheme): Promise<LoadedCustomTheme> { export function handleCoverImageUpload(
event: Event,
theme: LoadedCustomTheme,
): Promise<LoadedCustomTheme> {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ''; input.value = "";
if (file) { if (file) {
return new Promise((resolve) => { return new Promise((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const imageBlob = await fetch(reader.result as string).then(res => res.blob()); const imageBlob = await fetch(reader.result as string).then((res) =>
res.blob(),
);
resolve({ ...theme, coverImage: imageBlob }); resolve({ ...theme, coverImage: imageBlob });
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest' import { createManifest } from "../../lib/createManifest";
import baseManifest from './manifest.json' import baseManifest from "./manifest.json";
import pkg from '../../package.json' import pkg from "../../package.json";
export const brave = createManifest({ export const brave = createManifest(
{
...baseManifest, ...baseManifest,
version: pkg.version, version: pkg.version,
description: pkg.description, description: pkg.description,
}, 'brave') },
"brave",
);
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest' import { createManifest } from "../../lib/createManifest";
import baseManifest from './manifest.json' import baseManifest from "./manifest.json";
import pkg from '../../package.json' import pkg from "../../package.json";
export const chrome = createManifest({ export const chrome = createManifest(
{
...baseManifest, ...baseManifest,
version: pkg.version, version: pkg.version,
description: pkg.description, description: pkg.description,
}, 'chrome') },
"chrome",
);
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest' import { createManifest } from "../../lib/createManifest";
import baseManifest from './manifest.json' import baseManifest from "./manifest.json";
import pkg from '../../package.json' import pkg from "../../package.json";
export const edge = createManifest({ export const edge = createManifest(
{
...baseManifest, ...baseManifest,
version: pkg.version, version: pkg.version,
description: pkg.description, description: pkg.description,
}, 'edge') },
"edge",
);
+7 -7
View File
@@ -1,6 +1,6 @@
import { createManifest } from '../../lib/createManifest' import { createManifest } from "../../lib/createManifest";
import baseManifest from './manifest.json' import baseManifest from "./manifest.json";
import pkg from '../../package.json' import pkg from "../../package.json";
const updatedFirefoxManifest = { const updatedFirefoxManifest = {
...baseManifest, ...baseManifest,
@@ -10,13 +10,13 @@ const updatedFirefoxManifest = {
scripts: [baseManifest.background.service_worker], scripts: [baseManifest.background.service_worker],
}, },
action: { action: {
"default_popup": "interface/index.html#settings", default_popup: "interface/index.html#settings",
}, },
browser_specific_settings: { browser_specific_settings: {
gecko: { gecko: {
id: pkg.author.email, id: pkg.author.email,
}, },
} },
} };
export const firefox = createManifest(updatedFirefoxManifest, 'firefox') export const firefox = createManifest(updatedFirefoxManifest, "firefox");
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest' import { createManifest } from "../../lib/createManifest";
import baseManifest from './manifest.json' import baseManifest from "./manifest.json";
import pkg from '../../package.json' import pkg from "../../package.json";
export const opera = createManifest({ export const opera = createManifest(
{
...baseManifest, ...baseManifest,
version: pkg.version, version: pkg.version,
description: pkg.description, description: pkg.description,
}, 'opera') },
"opera",
);
+7 -7
View File
@@ -1,6 +1,6 @@
import { createManifest } from '../../lib/createManifest' import { createManifest } from "../../lib/createManifest";
import baseManifest from './manifest.json' import baseManifest from "./manifest.json";
import pkg from '../../package.json' import pkg from "../../package.json";
const updatedSafariManifest = { const updatedSafariManifest = {
...baseManifest, ...baseManifest,
@@ -8,12 +8,12 @@ const updatedSafariManifest = {
description: pkg.description, description: pkg.description,
browser_specific_settings: { browser_specific_settings: {
safari: { safari: {
strict_min_version: '15.4', strict_min_version: "15.4",
strict_max_version: '*', strict_max_version: "*",
}, },
// ^^^ https://developer.apple.com/documentation/safariservices/safari_web_extensions/optimizing_your_web_extension_for_safari#3743239 // ^^^ https://developer.apple.com/documentation/safariservices/safari_web_extensions/optimizing_your_web_extension_for_safari#3743239
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings#safari_properties // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings#safari_properties
}, },
} };
export const safari = createManifest(updatedSafariManifest, 'safari') export const safari = createManifest(updatedSafariManifest, "safari");
+39 -35
View File
@@ -3,8 +3,8 @@ class ReactFiber {
this.selector = selector; this.selector = selector;
this.debug = options.debug || false; this.debug = options.debug || false;
this.nodes = [...document.querySelectorAll(selector)]; // Support multiple elements this.nodes = [...document.querySelectorAll(selector)]; // Support multiple elements
this.fibers = this.nodes.map(node => this.getFiberNode(node)); this.fibers = this.nodes.map((node) => this.getFiberNode(node));
this.components = this.fibers.map(fiber => this.getOwnerComponent(fiber)); this.components = this.fibers.map((fiber) => this.getOwnerComponent(fiber));
if (this.debug) { if (this.debug) {
console.log("Selected Nodes:", this.nodes); console.log("Selected Nodes:", this.nodes);
@@ -19,8 +19,10 @@ class ReactFiber {
getFiberNode(node) { getFiberNode(node) {
if (!node) return null; if (!node) return null;
const fiberKey = Object.getOwnPropertyNames(node).find(name => const fiberKey = Object.getOwnPropertyNames(node).find(
name.startsWith('__reactFiber') || name.startsWith('__reactInternalInstance') (name) =>
name.startsWith("__reactFiber") ||
name.startsWith("__reactInternalInstance"),
); );
return fiberKey ? node[fiberKey] : null; return fiberKey ? node[fiberKey] : null;
} }
@@ -28,7 +30,10 @@ class ReactFiber {
getOwnerComponent(fiberNode) { getOwnerComponent(fiberNode) {
let current = fiberNode; let current = fiberNode;
while (current) { while (current) {
if (current.stateNode && (current.stateNode.setState || current.stateNode.forceUpdate)) { if (
current.stateNode &&
(current.stateNode.setState || current.stateNode.forceUpdate)
) {
return current.stateNode; return current.stateNode;
} }
current = current.return; current = current.return;
@@ -42,7 +47,7 @@ class ReactFiber {
if (key === undefined) { if (key === undefined) {
return state; return state;
} else if (typeof key === 'string') { } else if (typeof key === "string") {
return state?.[key]; return state?.[key];
} else if (Array.isArray(key)) { } else if (Array.isArray(key)) {
const filteredState = {}; const filteredState = {};
@@ -57,23 +62,25 @@ class ReactFiber {
} }
setState(update) { setState(update) {
this.components.forEach(component => { this.components.forEach((component) => {
if (component?.setState) { if (component?.setState) {
if (typeof update === 'function') { if (typeof update === "function") {
// Functional update // Functional update
component.setState(prevState => { component.setState((prevState) => {
const newState = update(prevState); const newState = update(prevState);
if (this.debug) console.log("✅ Updated State (Functional):", newState); if (this.debug)
console.log("✅ Updated State (Functional):", newState);
return newState; return newState;
}); });
} else { } else {
// Object update (merge with existing state) // Object update (merge with existing state)
component.setState(prevState => { component.setState((prevState) => {
const newState = { const newState = {
...prevState, ...prevState,
...update ...update,
}; };
if (this.debug) console.log("✅ Updated State (Object Merge):", newState); if (this.debug)
console.log("✅ Updated State (Object Merge):", newState);
return newState; return newState;
}); });
} }
@@ -93,7 +100,7 @@ class ReactFiber {
} }
setProp(propName) { setProp(propName) {
this.fibers.forEach(fiber => { this.fibers.forEach((fiber) => {
if (fiber?.memoizedProps) { if (fiber?.memoizedProps) {
fiber.memoizedProps[propName] = value; fiber.memoizedProps[propName] = value;
} }
@@ -102,7 +109,7 @@ class ReactFiber {
} }
forceUpdate() { forceUpdate() {
this.components.forEach(component => { this.components.forEach((component) => {
if (component?.forceUpdate) { if (component?.forceUpdate) {
component.forceUpdate(); component.forceUpdate();
if (this.debug) console.log("🔄 Forced React Re-render"); if (this.debug) console.log("🔄 Forced React Re-render");
@@ -113,12 +120,12 @@ class ReactFiber {
} }
function makeSerializable(obj) { function makeSerializable(obj) {
if (typeof obj !== 'object' || obj === null) { if (typeof obj !== "object" || obj === null) {
return obj; return obj;
} }
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return obj.map(item => makeSerializable(item)); return obj.map((item) => makeSerializable(item));
} }
const serializableObj = {}; const serializableObj = {};
@@ -126,17 +133,17 @@ function makeSerializable(obj) {
if (Object.hasOwn(obj, key)) { if (Object.hasOwn(obj, key)) {
let value = obj[key]; let value = obj[key];
if (typeof value === 'function') { if (typeof value === "function") {
value = '[Function]'; value = "[Function]";
} else if (value instanceof HTMLElement) { } else if (value instanceof HTMLElement) {
value = { value = {
type: 'HTMLElement', type: "HTMLElement",
id: value.id, id: value.id,
tagName: value.tagName tagName: value.tagName,
}; // Replace DOM node with ID/tag info }; // Replace DOM node with ID/tag info
} else if (typeof value === 'symbol') { } else if (typeof value === "symbol") {
value = value.toString(); value = value.toString();
} else if (typeof value === 'object' && value !== null) { } else if (typeof value === "object" && value !== null) {
value = makeSerializable(value); value = makeSerializable(value);
} }
@@ -146,17 +153,11 @@ function makeSerializable(obj) {
return serializableObj; return serializableObj;
} }
window.addEventListener('message', (event) => { window.addEventListener("message", (event) => {
if (event.data.type === "reactFiberRequest") { if (event.data.type === "reactFiberRequest") {
const { const { selector, action, payload, debug, messageId } = event.data;
selector,
action,
payload,
debug,
messageId
} = event.data;
const fiberInstance = ReactFiber.find(selector, { const fiberInstance = ReactFiber.find(selector, {
debug debug,
}); });
let response; let response;
@@ -191,14 +192,17 @@ window.addEventListener('message', (event) => {
response = null; response = null;
} }
if (response !== null && typeof response === 'object') { if (response !== null && typeof response === "object") {
response = makeSerializable(response); response = makeSerializable(response);
} }
window.postMessage({ window.postMessage(
{
type: "reactFiberResponse", type: "reactFiberResponse",
response, response,
messageId, messageId,
}, "*"); },
"*",
);
} }
}); });
@@ -1,7 +1,11 @@
import { BasePlugin } from '../../core/settings'; import { BasePlugin } from "../../core/settings";
import { type Plugin } from '@/plugins/core/types'; import { type Plugin } from "@/plugins/core/types";
import { defineSettings, numberSetting, Setting } from '@/plugins/core/settingsHelpers'; import {
import styles from './styles.css?inline'; defineSettings,
numberSetting,
Setting,
} from "@/plugins/core/settingsHelpers";
import styles from "./styles.css?inline";
const settings = defineSettings({ const settings = defineSettings({
speed: numberSetting({ speed: numberSetting({
@@ -10,8 +14,8 @@ const settings = defineSettings({
description: "Controls how fast the background moves", description: "Controls how fast the background moves",
min: 0.1, min: 0.1,
max: 2, max: 2,
step: 0.05 step: 0.05,
}) }),
}); });
class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> { class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
@@ -22,10 +26,10 @@ class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
const instance = new AnimatedBackgroundPluginClass(); const instance = new AnimatedBackgroundPluginClass();
const animatedBackgroundPlugin: Plugin<typeof settings> = { const animatedBackgroundPlugin: Plugin<typeof settings> = {
id: 'animated-background', id: "animated-background",
name: 'Animated Background', name: "Animated Background",
description: 'Adds an animated background to BetterSEQTA+', description: "Adds an animated background to BetterSEQTA+",
version: '1.0.0', version: "1.0.0",
disableToggle: true, disableToggle: true,
styles: styles, styles: styles,
settings: instance.settings, settings: instance.settings,
@@ -42,12 +46,12 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
const backgrounds = [ const backgrounds = [
{ classes: ["bg"] }, { classes: ["bg"] },
{ classes: ["bg", "bg2"] }, { classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] } { classes: ["bg", "bg3"] },
]; ];
backgrounds.forEach(({ classes }) => { backgrounds.forEach(({ classes }) => {
const bk = document.createElement("div"); const bk = document.createElement("div");
classes.forEach(cls => bk.classList.add(cls)); classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu); container.insertBefore(bk, menu);
}); });
@@ -55,20 +59,23 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
updateAnimationSpeed(api.settings.speed); updateAnimationSpeed(api.settings.speed);
// Listen for speed changes // Listen for speed changes
const speedUnregister = api.settings.onChange('speed', updateAnimationSpeed); const speedUnregister = api.settings.onChange(
"speed",
updateAnimationSpeed,
);
// Return cleanup function // Return cleanup function
return () => { return () => {
speedUnregister.unregister(); speedUnregister.unregister();
// Remove background elements // Remove background elements
const backgrounds = document.getElementsByClassName('bg'); const backgrounds = document.getElementsByClassName("bg");
Array.from(backgrounds).forEach(element => element.remove()); Array.from(backgrounds).forEach((element) => element.remove());
}; };
} },
}; };
function updateAnimationSpeed(speed: number) { function updateAnimationSpeed(speed: number) {
const bgElements = document.getElementsByClassName('bg'); const bgElements = document.getElementsByClassName("bg");
Array.from(bgElements).forEach((element, index) => { Array.from(bgElements).forEach((element, index) => {
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5; const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`; (element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
@@ -13,12 +13,12 @@ export function CreateBackground() {
const backgrounds = [ const backgrounds = [
{ classes: ["bg"] }, { classes: ["bg"] },
{ classes: ["bg", "bg2"] }, { classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] } { classes: ["bg", "bg3"] },
]; ];
backgrounds.forEach(({ classes }) => { backgrounds.forEach(({ classes }) => {
const bk = document.createElement("div"); const bk = document.createElement("div");
classes.forEach(cls => bk.classList.add(cls)); classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu); container.insertBefore(bk, menu);
}); });
} }
@@ -2,5 +2,5 @@ export function RemoveBackground() {
const backgrounds = document.getElementsByClassName("bg"); const backgrounds = document.getElementsByClassName("bg");
// Convert HTMLCollection to Array and remove each element // Convert HTMLCollection to Array and remove each element
Array.from(backgrounds).forEach(element => element.remove()); Array.from(backgrounds).forEach((element) => element.remove());
} }
@@ -1,5 +1,9 @@
import { BasePlugin } from "@/plugins/core/settings"; import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, defineSettings, Setting } from "@/plugins/core/settingsHelpers"; import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
import { type Plugin } from "@/plugins/core/types"; import { type Plugin } from "@/plugins/core/types";
import stringToHTML from "@/seqta/utils/stringToHTML"; import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForElm } from "@/seqta/utils/waitForElm"; import { waitForElm } from "@/seqta/utils/waitForElm";
@@ -8,7 +12,7 @@ const settings = defineSettings({
lettergrade: booleanSetting({ lettergrade: booleanSetting({
default: false, default: false,
title: "Letter Grades", title: "Letter Grades",
description: "Display the average as a letter instead of a percentage" description: "Display the average as a letter instead of a percentage",
}), }),
}); });
@@ -34,62 +38,105 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']", "#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
true, true,
10, 10,
1000 1000,
); );
// Helper function to find actual class names by their base pattern // Helper function to find actual class names by their base pattern
const getClassByPattern = (element: Element | Document, basePattern: string): string => { const getClassByPattern = (
element: Element | Document,
basePattern: string,
): string => {
// Find all classes on the element // Find all classes on the element
const classes = Array.from(element.querySelectorAll('*')) const classes = Array.from(element.querySelectorAll("*"))
.flatMap(el => Array.from(el.classList)) .flatMap((el) => Array.from(el.classList))
.filter(className => className.startsWith(basePattern)); .filter((className) => className.startsWith(basePattern));
return classes.length ? classes[0] : ''; return classes.length ? classes[0] : "";
}; };
// Find actual class names from the DOM // Find actual class names from the DOM
const sampleAssessmentItem = document.querySelector("[class*='AssessmentItem__AssessmentItem___']"); const sampleAssessmentItem = document.querySelector(
"[class*='AssessmentItem__AssessmentItem___']",
);
if (!sampleAssessmentItem) return; if (!sampleAssessmentItem) return;
// Extract all necessary class patterns from a sample assessment item // Extract all necessary class patterns from a sample assessment item
const assessmentItemClass = Array.from(sampleAssessmentItem.classList) const assessmentItemClass =
.find(c => c.startsWith('AssessmentItem__AssessmentItem___')) || ''; Array.from(sampleAssessmentItem.classList).find((c) =>
c.startsWith("AssessmentItem__AssessmentItem___"),
) || "";
const metaContainerClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__metaContainer___'); const metaContainerClass = getClassByPattern(
const metaClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__meta___'); sampleAssessmentItem,
const simpleResultClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__simpleResult___'); "AssessmentItem__metaContainer___",
const titleClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__title___'); );
const metaClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__meta___",
);
const simpleResultClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__simpleResult___",
);
const titleClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__title___",
);
// Get Thermoscore classes // Get Thermoscore classes
const thermoscoreElement = document.querySelector("[class*='Thermoscore__Thermoscore___']"); const thermoscoreElement = document.querySelector(
"[class*='Thermoscore__Thermoscore___']",
);
if (!thermoscoreElement) return; if (!thermoscoreElement) return;
const thermoscoreClass = Array.from(thermoscoreElement.classList) const thermoscoreClass =
.find(c => c.startsWith('Thermoscore__Thermoscore___')) || ''; Array.from(thermoscoreElement.classList).find((c) =>
const fillClass = getClassByPattern(thermoscoreElement, 'Thermoscore__fill___'); c.startsWith("Thermoscore__Thermoscore___"),
const textClass = getClassByPattern(thermoscoreElement, 'Thermoscore__text___'); ) || "";
const fillClass = getClassByPattern(
thermoscoreElement,
"Thermoscore__fill___",
);
const textClass = getClassByPattern(
thermoscoreElement,
"Thermoscore__text___",
);
// Find assessment list // Find assessment list
const assessmentsList = document.querySelector("#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']"); const assessmentsList = document.querySelector(
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
);
if (!assessmentsList) return; if (!assessmentsList) return;
const gradeElements = document.querySelectorAll("[class*='Thermoscore__text___']"); const gradeElements = document.querySelectorAll(
"[class*='Thermoscore__text___']",
);
if (!gradeElements.length) return; if (!gradeElements.length) return;
// Parse and average grades // Parse and average grades
const letterToNumber: Record<string, number> = { const letterToNumber: Record<string, number> = {
"A+": 100, A: 95, "A-": 90, "A+": 100,
"B+": 85, B: 80, "B-": 75, A: 95,
"C+": 70, C: 65, "C-": 60, "A-": 90,
"D+": 55, D: 50, "D-": 45, "B+": 85,
"E+": 40, E: 35, "E-": 30, B: 80,
"B-": 75,
"C+": 70,
C: 65,
"C-": 60,
"D+": 55,
D: 50,
"D-": 45,
"E+": 40,
E: 35,
"E-": 30,
F: 0, F: 0,
}; };
function parseGrade(text: string): number { function parseGrade(text: string): number {
const str = text.trim().toUpperCase(); const str = text.trim().toUpperCase();
if (str.includes("/")) { if (str.includes("/")) {
const [raw, max] = str.split("/").map(n => parseFloat(n)); const [raw, max] = str.split("/").map((n) => parseFloat(n));
return (raw / max) * 100; return (raw / max) * 100;
} }
if (str.includes("%")) { if (str.includes("%")) {
@@ -112,16 +159,23 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
const avg = total / count; const avg = total / count;
const rounded = Math.ceil(avg / 5) * 5; const rounded = Math.ceil(avg / 5) * 5;
const numberToLetter = Object.entries(letterToNumber).reduce((acc, [k, v]) => { const numberToLetter = Object.entries(letterToNumber).reduce(
(acc, [k, v]) => {
acc[v] = k; acc[v] = k;
return acc; return acc;
}, {} as Record<number, string>); },
{} as Record<number, string>,
);
const letterAvg = numberToLetter[rounded] ?? "N/A"; const letterAvg = numberToLetter[rounded] ?? "N/A";
const display = api.settings.lettergrade ? letterAvg : `${avg.toFixed(2)}%`; const display = api.settings.lettergrade
? letterAvg
: `${avg.toFixed(2)}%`;
// Prevent duplicate // Prevent duplicate
const existing = assessmentsList.querySelector(`[class*='AssessmentItem__title___']`); const existing = assessmentsList.querySelector(
`[class*='AssessmentItem__title___']`,
);
if (existing?.textContent === "Subject Average") return; if (existing?.textContent === "Subject Average") return;
// Use the dynamic class names in the HTML template // Use the dynamic class names in the HTML template
@@ -144,7 +198,7 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild); assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
}); });
} },
}; };
export default assessmentsAveragePlugin; export default assessmentsAveragePlugin;
@@ -11,7 +11,7 @@ import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing } from "../indexing/indexer"; import { runIndexing } from "../indexing/indexer";
import { initVectorSearch } from "../search/vector/vectorSearch"; import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar"; import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from 'embeddia'; import { IndexedDbManager } from "embeddia";
const settings = defineSettings({ const settings = defineSettings({
searchHotkey: stringSetting({ searchHotkey: stringSetting({
@@ -65,11 +65,10 @@ const globalSearchPlugin: Plugin<typeof settings> = {
run: async (api) => { run: async (api) => {
const appRef = { current: null }; const appRef = { current: null };
await IndexedDbManager.create( await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
'embeddiaDB', primaryKey: "id",
'embeddiaObjectStore', autoIncrement: false,
{ primaryKey: 'id', autoIncrement: false } });
);
initVectorSearch(); initVectorSearch();
@@ -6,7 +6,7 @@ import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
export function mountSearchBar( export function mountSearchBar(
titleElement: Element, titleElement: Element,
api: any, api: any,
appRef: { current: any } appRef: { current: any },
) { ) {
if (titleElement.querySelector(".search-trigger")) { if (titleElement.querySelector(".search-trigger")) {
return; return;
@@ -15,7 +15,10 @@ async function loadProgress<T = any>(jobId: string): Promise<T | undefined> {
return rec?.progress as T | undefined; return rec?.progress as T | undefined;
} }
async function saveProgress<T = any>(jobId: string, progress: T): Promise<void> { async function saveProgress<T = any>(
jobId: string,
progress: T,
): Promise<void> {
await put(META_STORE, { jobId, progress }, `progress:${jobId}`); await put(META_STORE, { jobId, progress }, `progress:${jobId}`);
} }
/* ───────────────────────────────────────────── */ /* ───────────────────────────────────────────── */
@@ -67,7 +70,13 @@ function stopHeartbeat() {
localStorage.removeItem(LOCK_KEY); localStorage.removeItem(LOCK_KEY);
} }
function dispatchProgress(completed: number, total: number, indexing: boolean, status?: string, detail?: string) { function dispatchProgress(
completed: number,
total: number,
indexing: boolean,
status?: string,
detail?: string,
) {
const event = new CustomEvent("indexing-progress", { const event = new CustomEvent("indexing-progress", {
detail: { completed, total, indexing, status, detail }, detail: { completed, total, indexing, status, detail },
}); });
@@ -80,20 +89,28 @@ export async function loadAllStoredItems(): Promise<HydratedIndexItem[]> {
for (const jobId of jobIds) { for (const jobId of jobIds) {
try { try {
const items = await getAll(jobId) as IndexItem[]; const items = (await getAll(jobId)) as IndexItem[];
const job = jobs[jobId]; const job = jobs[jobId];
const renderComponent = renderComponentMap[job.renderComponentId]; const renderComponent = renderComponentMap[job.renderComponentId];
if (!renderComponent) { if (!renderComponent) {
console.warn(`Render component not found for job ${jobId} (ID: ${job.renderComponentId})`); console.warn(
`Render component not found for job ${jobId} (ID: ${job.renderComponentId})`,
);
} }
for (const item of items) { for (const item of items) {
// Ensure item has all required fields before pushing if (
if (item && item.id && item.text && item.category && item.actionId && job.renderComponentId) { item &&
item.id &&
item.text &&
item.category &&
item.actionId &&
job.renderComponentId
) {
all.push({ all.push({
...item, ...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found renderComponent: renderComponent || undefined,
}); });
} else { } else {
console.warn(`Skipping invalid item from job ${jobId}:`, item); console.warn(`Skipping invalid item from job ${jobId}:`, item);
@@ -103,7 +120,9 @@ export async function loadAllStoredItems(): Promise<HydratedIndexItem[]> {
console.error(`Error loading items for job ${jobId}:`, error); console.error(`Error loading items for job ${jobId}:`, error);
} }
} }
console.debug(`[Indexer] Loaded ${all.length} items from non-vector storage.`); console.debug(
`[Indexer] Loaded ${all.length} items from non-vector storage.`,
);
return all; return all;
} }
@@ -129,7 +148,12 @@ export async function runIndexing(): Promise<void> {
// --- Step 1: Run Fetching/Storing Jobs (Main Thread) --- // --- Step 1: Run Fetching/Storing Jobs (Main Thread) ---
for (const jobId of jobIds) { for (const jobId of jobIds) {
dispatchProgress(completedJobs, totalSteps, true, `Running job: ${jobs[jobId].label}`); dispatchProgress(
completedJobs,
totalSteps,
true,
`Running job: ${jobs[jobId].label}`,
);
const job = jobs[jobId]; const job = jobs[jobId];
const lastRun = await getLastRunMeta(jobId); const lastRun = await getLastRunMeta(jobId);
@@ -139,17 +163,25 @@ export async function runIndexing(): Promise<void> {
"color: gray", "color: gray",
); );
completedJobs++; completedJobs++;
dispatchProgress(completedJobs, totalSteps, true, `Skipped job: ${job.label}`); dispatchProgress(
completedJobs,
totalSteps,
true,
`Skipped job: ${job.label}`,
);
continue; continue;
} }
const getStoredItems = async (storeId?: string) => await getAll(storeId ?? jobId); const getStoredItems = async (storeId?: string) =>
await getAll(storeId ?? jobId);
const setStoredItems = async (items: IndexItem[], storeId?: string) => { const setStoredItems = async (items: IndexItem[], storeId?: string) => {
const targetStore = storeId ?? jobId; const targetStore = storeId ?? jobId;
await clear(targetStore); await clear(targetStore);
const validItems = items.filter(i => i && i.id); const validItems = items.filter((i) => i && i.id);
if (validItems.length !== items.length) { if (validItems.length !== items.length) {
console.warn(`[Indexer Job ${jobId} -> Store ${targetStore}] Filtered out ${items.length - validItems.length} invalid items before storing.`); console.warn(
`[Indexer Job ${jobId} -> Store ${targetStore}] Filtered out ${items.length - validItems.length} invalid items before storing.`,
);
} }
await Promise.all(validItems.map((i) => put(targetStore, i, i.id))); await Promise.all(validItems.map((i) => put(targetStore, i, i.id)));
}; };
@@ -158,7 +190,10 @@ export async function runIndexing(): Promise<void> {
if (item && item.id) { if (item && item.id) {
await put(targetStore, item, item.id); await put(targetStore, item, item.id);
} else { } else {
console.warn(`[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`, item); console.warn(
`[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`,
item,
);
} }
}; };
const removeItem = async (id: string, storeId?: string) => { const removeItem = async (id: string, storeId?: string) => {
@@ -193,17 +228,29 @@ export async function runIndexing(): Promise<void> {
// Hydrate items for vector processing // Hydrate items for vector processing
const renderComponent = renderComponentMap[job.renderComponentId]; const renderComponent = renderComponentMap[job.renderComponentId];
if (!renderComponent) { if (!renderComponent) {
console.warn(`Render component not found for job ${jobId} (ID: ${job.renderComponentId}) during hydration`); console.warn(
`Render component not found for job ${jobId} (ID: ${job.renderComponentId}) during hydration`,
);
} }
const hydratedItems = merged const hydratedItems = merged
.filter(item => item && item.id && item.text && item.category && item.actionId && job.renderComponentId) // Filter invalid before hydrating .filter(
(item) =>
item &&
item.id &&
item.text &&
item.category &&
item.actionId &&
job.renderComponentId,
) // Filter invalid before hydrating
.map((item) => ({ .map((item) => ({
...item, ...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found renderComponent: renderComponent || undefined, // Assign undefined if not found
})); }));
if (hydratedItems.length !== merged.length) { if (hydratedItems.length !== merged.length) {
console.warn(`[Indexer Job ${jobId}] Filtered out ${merged.length - hydratedItems.length} invalid items during hydration.`); console.warn(
`[Indexer Job ${jobId}] Filtered out ${merged.length - hydratedItems.length} invalid items during hydration.`,
);
} }
allItemsFromJobs.push(...hydratedItems); allItemsFromJobs.push(...hydratedItems);
@@ -218,7 +265,12 @@ export async function runIndexing(): Promise<void> {
} }
completedJobs++; completedJobs++;
dispatchProgress(completedJobs, totalSteps, true, `Finished job: ${job.label}`); dispatchProgress(
completedJobs,
totalSteps,
true,
`Finished job: ${job.label}`,
);
} }
// --- Step 2: Delegate Vectorization to Worker (Off Main Thread) --- // --- Step 2: Delegate Vectorization to Worker (Off Main Thread) ---
@@ -234,53 +286,112 @@ export async function runIndexing(): Promise<void> {
// Pass a progress callback to the worker manager // Pass a progress callback to the worker manager
await workerManager.processItems(allItemsFromJobs, (progress) => { await workerManager.processItems(allItemsFromJobs, (progress) => {
// Update overall progress based on worker feedback // Update overall progress based on worker feedback
let detailMessage = progress.message || ''; let detailMessage = progress.message || "";
if (progress.status === 'processing' && progress.total && progress.processed !== undefined) { if (
progress.status === "processing" &&
progress.total &&
progress.processed !== undefined
) {
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`; detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
// You could potentially update the 'completed' count more granularly here // You could potentially update the 'completed' count more granularly here
// For simplicity, we'll just update the detail message // For simplicity, we'll just update the detail message
} else if (progress.status === 'complete') { } else if (progress.status === "complete") {
detailMessage = "Vectorization complete"; detailMessage = "Vectorization complete";
// Mark the vectorization step as complete // Mark the vectorization step as complete
dispatchProgress(totalSteps, totalSteps, true, "Vectorization finished"); dispatchProgress(
} else if (progress.status === 'error') { totalSteps,
totalSteps,
true,
"Vectorization finished",
);
} else if (progress.status === "error") {
detailMessage = `Vectorization error: ${progress.message}`; detailMessage = `Vectorization error: ${progress.message}`;
dispatchProgress(completedJobs, totalSteps, true, "Vectorization failed", detailMessage); // Show error dispatchProgress(
} else if (progress.status === 'started') { completedJobs,
totalSteps,
true,
"Vectorization failed",
detailMessage,
); // Show error
} else if (progress.status === "started") {
detailMessage = `Vectorization started for ${progress.total} items`; detailMessage = `Vectorization started for ${progress.total} items`;
} else if (progress.status === 'cancelled') { } else if (progress.status === "cancelled") {
detailMessage = `Vectorization cancelled: ${progress.message}`; detailMessage = `Vectorization cancelled: ${progress.message}`;
dispatchProgress(completedJobs, totalSteps, true, "Vectorization cancelled", detailMessage); dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization cancelled",
detailMessage,
);
} }
// Update the status detail // Update the status detail
dispatchProgress(completedJobs, totalSteps, true, "Vectorization in progress", detailMessage); dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization in progress",
detailMessage,
);
// When worker signals completion of *its* task, mark the final step complete // When worker signals completion of *its* task, mark the final step complete
if (progress.status === 'complete') { if (progress.status === "complete") {
completedJobs++; // Increment completion count *after* vectorization finishes completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress(completedJobs, totalSteps, false, "Indexing finished"); // Set indexing to false dispatchProgress(
} else if (progress.status === 'error' || progress.status === 'cancelled') { completedJobs,
totalSteps,
false,
"Indexing finished",
); // Set indexing to false
} else if (
progress.status === "error" ||
progress.status === "cancelled"
) {
// Don't increment completed count on failure/cancel, just stop indexing indicator // Don't increment completed count on failure/cancel, just stop indexing indicator
dispatchProgress(completedJobs, totalSteps, false, "Indexing stopped due to error/cancel"); dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing stopped due to error/cancel",
);
} }
}); });
console.debug("%c[Indexer] Vectorization task sent to worker.", "color: green"); console.debug(
"%c[Indexer] Vectorization task sent to worker.",
"color: green",
);
// Note: runIndexing might return *before* vectorization is complete now. // Note: runIndexing might return *before* vectorization is complete now.
// The progress updates will signal the true end state. // The progress updates will signal the true end state.
} catch (error) { } catch (error) {
console.error(`%c[Indexer] ❌ Failed to send items to vector worker:`, "color: red", error); console.error(
dispatchProgress(completedJobs, totalSteps, false, "Vectorization failed", String(error)); // Stop indexing indicator `%c[Indexer] ❌ Failed to send items to vector worker:`,
"color: red",
error,
);
dispatchProgress(
completedJobs,
totalSteps,
false,
"Vectorization failed",
String(error),
); // Stop indexing indicator
} }
} else { } else {
console.debug("%c[Indexer] No items to send for vectorization.", "color: gray"); console.debug(
"%c[Indexer] No items to send for vectorization.",
"color: gray",
);
// If no vectorization needed, indexing is done here. // If no vectorization needed, indexing is done here.
completedJobs++; // Count the "skipped" vectorization step completedJobs++; // Count the "skipped" vectorization step
dispatchProgress(completedJobs, totalSteps, false, "Indexing finished (no vectorization needed)"); dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing finished (no vectorization needed)",
);
} }
// Stop heartbeat ONLY when all jobs *and* the vectorization dispatch are done. // Stop heartbeat ONLY when all jobs *and* the vectorization dispatch are done.
// The actual *completion* of vectorization is now asynchronous. // The actual *completion* of vectorization is now asynchronous.
stopHeartbeat(); stopHeartbeat();
@@ -49,21 +49,27 @@ const fetchNotifications = async () => {
const fetchAssessmentName = async ( const fetchAssessmentName = async (
assessmentId: number, assessmentId: number,
metaclassId: number, metaclassId: number,
programmeId: number programmeId: number,
): Promise<string> => { ): Promise<string> => {
const searchAssessment = (data: any): string | null => { const searchAssessment = (data: any): string | null => {
// Search syllabus // Search syllabus
for (const item of data.syllabus || []) { for (const item of data.syllabus || []) {
const found = (item.assessments || []).find((a: any) => a.id === assessmentId); const found = (item.assessments || []).find(
(a: any) => a.id === assessmentId,
);
if (found) return found.title; if (found) return found.title;
} }
// Search pending // Search pending
const foundPending = (data.pending || []).find((a: any) => a.id === assessmentId); const foundPending = (data.pending || []).find(
(a: any) => a.id === assessmentId,
);
if (foundPending) return foundPending.title; if (foundPending) return foundPending.title;
// Search tasks // Search tasks
const foundTask = (data.tasks || []).find((a: any) => a.id === assessmentId); const foundTask = (data.tasks || []).find(
(a: any) => a.id === assessmentId,
);
if (foundTask) return foundTask.title; if (foundTask) return foundTask.title;
return null; return null;
@@ -88,11 +94,17 @@ const fetchAssessmentName = async (
if (title) return title; if (title) return title;
// Try from /upcoming if not found in /past // Try from /upcoming if not found in /past
const upcomingPayload = await fetchAssessments("/seqta/student/assessment/list/upcoming"); const upcomingPayload = await fetchAssessments(
const foundUpcoming = (upcomingPayload || []).find((a: any) => a.id === assessmentId); "/seqta/student/assessment/list/upcoming",
);
const foundUpcoming = (upcomingPayload || []).find(
(a: any) => a.id === assessmentId,
);
if (foundUpcoming) return foundUpcoming.title; if (foundUpcoming) return foundUpcoming.title;
throw new Error(`Assessment with ID ${assessmentId} not found in past or upcoming.`); throw new Error(
`Assessment with ID ${assessmentId} not found in past or upcoming.`,
);
}; };
/* ------------- Job ------------- */ /* ------------- Job ------------- */
@@ -103,8 +115,9 @@ export const assessmentsJob: Job = {
frequency: { type: "expiry", afterMs: 15 * 60 * 1000 }, frequency: { type: "expiry", afterMs: 15 * 60 * 1000 },
run: async (ctx) => { run: async (ctx) => {
const progress = const progress = (await ctx.getProgress<AssessmentsProgress>()) ?? {
(await ctx.getProgress<AssessmentsProgress>()) ?? { lastTs: 0 }; lastTs: 0,
};
let notifications: Notification[]; let notifications: Notification[];
try { try {
@@ -116,8 +129,12 @@ export const assessmentsJob: Job = {
const notificationIsIndexed = async (id: string): Promise<boolean> => { const notificationIsIndexed = async (id: string): Promise<boolean> => {
const [inAssessments, inMessages] = await Promise.all([ const [inAssessments, inMessages] = await Promise.all([
ctx.getStoredItems("assessments").then((items) => items.some((i) => i.id === id)), ctx
ctx.getStoredItems("messages").then((items) => items.some((i) => i.id === id)), .getStoredItems("assessments")
.then((items) => items.some((i) => i.id === id)),
ctx
.getStoredItems("messages")
.then((items) => items.some((i) => i.id === id)),
]); ]);
return inAssessments || inMessages; return inAssessments || inMessages;
}; };
@@ -131,7 +148,11 @@ export const assessmentsJob: Job = {
if (notif.type === "coneqtassessments") { if (notif.type === "coneqtassessments") {
const a = notif.coneqtAssessments; const a = notif.coneqtAssessments;
const content = await fetchAssessmentName(a.assessmentID, a.metaclassID, a.programmeID); const content = await fetchAssessmentName(
a.assessmentID,
a.metaclassID,
a.programmeID,
);
items.push({ items.push({
id, id,
text: a.title, text: a.title,
@@ -168,7 +189,7 @@ export const assessmentsJob: Job = {
actionId: "message", actionId: "message",
renderComponentId: "message", renderComponentId: "message",
}, },
"messages" "messages",
); );
} }
} }
@@ -49,12 +49,12 @@ export const messagesJob: Job = {
run: async (ctx) => { run: async (ctx) => {
const limit = 100; const limit = 100;
const progress = const progress = (await ctx.getProgress<MessagesProgress>()) ?? {
(await ctx.getProgress<MessagesProgress>()) ?? { offset: 0, done: false }; offset: 0,
done: false,
};
const existingIds = new Set( const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
(await ctx.getStoredItems()).map((i) => i.id),
);
let consecutiveExisting = 0; let consecutiveExisting = 0;
@@ -1,11 +1,13 @@
export function htmlToPlainText(rawHtml: string): string { export function htmlToPlainText(rawHtml: string): string {
const parser = new DOMParser(); const parser = new DOMParser();
const doc = parser.parseFromString(rawHtml, 'text/html'); const doc = parser.parseFromString(rawHtml, "text/html");
const { body } = doc; const { body } = doc;
body.querySelectorAll('script,style,template,noscript,meta,link').forEach(el => el.remove()); body
.querySelectorAll("script,style,template,noscript,meta,link")
.forEach((el) => el.remove());
body.querySelectorAll('.forward').forEach(el => { body.querySelectorAll(".forward").forEach((el) => {
let n: ChildNode | null = el; let n: ChildNode | null = el;
while (n) { while (n) {
const next = n.nextSibling as ChildNode | null; const next = n.nextSibling as ChildNode | null;
@@ -14,18 +16,18 @@ export function htmlToPlainText(rawHtml: string): string {
} }
}); });
let text = body.innerText || ''; let text = body.innerText || "";
text = text text = text
.replace(/\u00A0/g, ' ') .replace(/\u00A0/g, " ")
.replace(/[ \t]{2,}/g, ' ') .replace(/[ \t]{2,}/g, " ")
.replace(/\r\n|\r/g, '\n') .replace(/\r\n|\r/g, "\n")
.replace(/\n{3,}/g, '\n\n') .replace(/\n{3,}/g, "\n\n")
.replace(/^[.\w#][^{]{0,100}\{[^}]*\}$/gm, '') .replace(/^[.\w#][^{]{0,100}\{[^}]*\}$/gm, "")
.split('\n') .split("\n")
.map(line => line.trimEnd()) .map((line) => line.trimEnd())
.filter(line => line.trim().length > 0 || line === '') .filter((line) => line.trim().length > 0 || line === "")
.join('\n') .join("\n")
.trim(); .trim();
return text; return text;
@@ -1,8 +1,4 @@
import { import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
EmbeddingIndex,
getEmbedding,
initializeModel,
} from "embeddia";
import type { HydratedIndexItem } from "../types"; import type { HydratedIndexItem } from "../types";
let vectorIndex: EmbeddingIndex | null = null; let vectorIndex: EmbeddingIndex | null = null;
@@ -1,10 +1,10 @@
import { refreshVectorCache } from '../../search/vector/vectorSearch'; import { refreshVectorCache } from "../../search/vector/vectorSearch";
import type { HydratedIndexItem } from '../types'; import type { HydratedIndexItem } from "../types";
import vectorWorker from './vectorWorker.ts?inlineWorker'; import vectorWorker from "./vectorWorker.ts?inlineWorker";
import type { SearchResult } from 'embeddia'; import type { SearchResult } from "embeddia";
export type ProgressCallback = (data: { export type ProgressCallback = (data: {
status: 'started' | 'processing' | 'complete' | 'error' | 'cancelled'; status: "started" | "processing" | "complete" | "error" | "cancelled";
total?: number; total?: number;
processed?: number; processed?: number;
message?: string; message?: string;
@@ -16,10 +16,21 @@ export class VectorWorkerManager {
private isInitialized = false; private isInitialized = false;
private readyPromise: Promise<void> | null = null; // To await initialization private readyPromise: Promise<void> | null = null; // To await initialization
private progressCallback: ProgressCallback | null = null; private progressCallback: ProgressCallback | null = null;
private searchPromises = new Map<string, { resolve: (value: SearchResult[]) => void, reject: (reason?: any) => void, timer: NodeJS.Timeout }>(); private searchPromises = new Map<
string,
{
resolve: (value: SearchResult[]) => void;
reject: (reason?: any) => void;
timer: NodeJS.Timeout;
}
>();
private debounceTimer: NodeJS.Timeout | null = null; private debounceTimer: NodeJS.Timeout | null = null;
private lastSearchParams: { query: string; topK: number; resolve: (results: SearchResult[]) => void, reject: (reason?: any) => void } | null = null; private lastSearchParams: {
query: string;
topK: number;
resolve: (results: SearchResult[]) => void;
reject: (reason?: any) => void;
} | null = null;
private constructor() { private constructor() {
// Start initialization immediately, but allow awaiting it // Start initialization immediately, but allow awaiting it
@@ -42,81 +53,90 @@ export class VectorWorkerManager {
// Create the worker // Create the worker
this.worker = vectorWorker(); this.worker = vectorWorker();
console.log('Worker initialized', this.worker); console.log("Worker initialized", this.worker);
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
console.error('Vector worker initialization timed out'); console.error("Vector worker initialization timed out");
this.worker?.terminate(); // Clean up worker if it exists this.worker?.terminate(); // Clean up worker if it exists
this.worker = null; this.worker = null;
this.isInitialized = false; // Ensure state reflects failure this.isInitialized = false; // Ensure state reflects failure
this.readyPromise = null; // Allow retrying init later this.readyPromise = null; // Allow retrying init later
reject(new Error('Worker initialization timed out')); reject(new Error("Worker initialization timed out"));
}, 10000); // Increased timeout }, 10000); // Increased timeout
// Set up message handling // Set up message handling
this.worker!.addEventListener('message', (e) => { this.worker!.addEventListener("message", (e) => {
const { type, data } = e.data; const { type, data } = e.data;
console.debug("Message from vector worker:", type, data); console.debug("Message from vector worker:", type, data);
switch (type) { switch (type) {
case 'ready': case "ready":
this.isInitialized = true; this.isInitialized = true;
clearTimeout(timeout); clearTimeout(timeout);
console.debug('Vector worker initialized and ready.'); console.debug("Vector worker initialized and ready.");
resolve(); // Resolve the init promise resolve(); // Resolve the init promise
break; break;
case 'progress': case "progress":
if (this.progressCallback) { if (this.progressCallback) {
this.progressCallback(data); this.progressCallback(data);
if (data.status === 'complete') { if (data.status === "complete") {
refreshVectorCache(); refreshVectorCache();
} }
} }
break; break;
case 'searchResults': case "searchResults":
const searchInfo = this.searchPromises.get(data.messageId); const searchInfo = this.searchPromises.get(data.messageId);
if (searchInfo) { if (searchInfo) {
clearTimeout(searchInfo.timer); // Clear timeout on success clearTimeout(searchInfo.timer); // Clear timeout on success
searchInfo.resolve(data.results); searchInfo.resolve(data.results);
this.searchPromises.delete(data.messageId); this.searchPromises.delete(data.messageId);
} else { } else {
console.warn('Received search results for unknown messageId:', data.messageId); console.warn(
"Received search results for unknown messageId:",
data.messageId,
);
} }
break; break;
case 'searchError': case "searchError":
const errorInfo = this.searchPromises.get(data.messageId); const errorInfo = this.searchPromises.get(data.messageId);
if (errorInfo) { if (errorInfo) {
clearTimeout(errorInfo.timer); // Clear timeout on error clearTimeout(errorInfo.timer); // Clear timeout on error
errorInfo.reject(new Error(data.error)); errorInfo.reject(new Error(data.error));
this.searchPromises.delete(data.messageId); this.searchPromises.delete(data.messageId);
} else { } else {
console.warn('Received search error for unknown messageId:', data.messageId); console.warn(
"Received search error for unknown messageId:",
data.messageId,
);
} }
break; break;
case 'searchCancelled': case "searchCancelled":
const cancelledInfo = this.searchPromises.get(data.messageId); const cancelledInfo = this.searchPromises.get(data.messageId);
if (cancelledInfo) { if (cancelledInfo) {
clearTimeout(cancelledInfo.timer); // Clear timeout on cancel clearTimeout(cancelledInfo.timer); // Clear timeout on cancel
// Reject with a specific cancellation error or resolve with empty? Let's reject. // Reject with a specific cancellation error or resolve with empty? Let's reject.
cancelledInfo.reject(new Error('Search cancelled by worker')); cancelledInfo.reject(new Error("Search cancelled by worker"));
this.searchPromises.delete(data.messageId); this.searchPromises.delete(data.messageId);
} else { } else {
console.debug('Received cancellation for unknown messageId:', data.messageId); console.debug(
"Received cancellation for unknown messageId:",
data.messageId,
);
} }
break; break;
default: default:
console.warn('Unknown message from worker:', type, data); console.warn("Unknown message from worker:", type, data);
} }
}); });
// Initialize the worker // Initialize the worker
this.worker!.postMessage({ type: 'init' }); this.worker!.postMessage({ type: "init" });
}); });
} }
@@ -129,11 +149,16 @@ export class VectorWorkerManager {
} }
await this.readyPromise; await this.readyPromise;
if (!this.isInitialized || !this.worker) { if (!this.isInitialized || !this.worker) {
throw new Error("Vector Worker is not available after initialization attempt."); throw new Error(
"Vector Worker is not available after initialization attempt.",
);
} }
} }
async processItems(items: HydratedIndexItem[], onProgress?: ProgressCallback) { async processItems(
items: HydratedIndexItem[],
onProgress?: ProgressCallback,
) {
await this.ensureReady(); // Wait for worker to be ready await this.ensureReady(); // Wait for worker to be ready
this.progressCallback = onProgress || null; this.progressCallback = onProgress || null;
@@ -146,13 +171,16 @@ export class VectorWorkerManager {
const serialisableItems = items.map(({ renderComponent, ...rest }) => rest); const serialisableItems = items.map(({ renderComponent, ...rest }) => rest);
this.worker!.postMessage({ this.worker!.postMessage({
type: 'process', type: "process",
data: { items: serialisableItems } data: { items: serialisableItems },
}); });
} }
// Public search method // Public search method
public async search(query: string, topK: number = 10): Promise<SearchResult[]> { public async search(
query: string,
topK: number = 10,
): Promise<SearchResult[]> {
await this.ensureReady(); await this.ensureReady();
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -169,25 +197,34 @@ export class VectorWorkerManager {
const searchTimer = setTimeout(() => { const searchTimer = setTimeout(() => {
if (this.searchPromises.has(messageId)) { if (this.searchPromises.has(messageId)) {
console.error(`Search timed out for messageId: ${messageId}`); console.error(`Search timed out for messageId: ${messageId}`);
currentParams.reject(new Error(`Search timed out after ${searchTimeout}ms`)); currentParams.reject(
new Error(`Search timed out after ${searchTimeout}ms`),
);
this.searchPromises.delete(messageId); this.searchPromises.delete(messageId);
} }
}, searchTimeout); }, searchTimeout);
this.searchPromises.set(messageId, {
resolve: currentParams.resolve,
reject: currentParams.reject,
timer: searchTimer,
});
this.searchPromises.set(messageId, { resolve: currentParams.resolve, reject: currentParams.reject, timer: searchTimer }); console.debug(
`Sending search request (ID: ${messageId}) to worker: "${currentParams.query}"`,
console.debug(`Sending search request (ID: ${messageId}) to worker: "${currentParams.query}"`); );
console.log(this.worker); console.log(this.worker);
this.worker.postMessage({ this.worker.postMessage({
type: "search", type: "search",
data: { query: currentParams.query, topK: currentParams.topK }, data: { query: currentParams.query, topK: currentParams.topK },
messageId messageId,
}); });
} else if (this.lastSearchParams) { } else if (this.lastSearchParams) {
// This case might happen if ensureReady failed but didn't throw // This case might happen if ensureReady failed but didn't throw
console.error("Worker unavailable when trying to send search request."); console.error("Worker unavailable when trying to send search request.");
this.lastSearchParams.reject(new Error("Worker unavailable for search")); this.lastSearchParams.reject(
new Error("Worker unavailable for search"),
);
this.lastSearchParams = null; this.lastSearchParams = null;
this.debounceTimer = null; this.debounceTimer = null;
} }
@@ -214,7 +251,6 @@ export class VectorWorkerManager {
} }
} }
terminate() { terminate() {
console.debug("Terminating Vector Worker Manager..."); console.debug("Terminating Vector Worker Manager...");
this.cancelAllSearches("Worker terminated"); // Cancel pending searches this.cancelAllSearches("Worker terminated"); // Cancel pending searches
@@ -1,6 +1,6 @@
import { EmbeddingIndex, getEmbedding, initializeModel } from 'embeddia'; import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { HydratedIndexItem } from '../../indexing/types'; import type { HydratedIndexItem } from "../../indexing/types";
import type { SearchResult } from 'embeddia'; import type { SearchResult } from "embeddia";
let vectorIndex: EmbeddingIndex | null = null; let vectorIndex: EmbeddingIndex | null = null;
@@ -10,7 +10,7 @@ export async function initVectorSearch() {
vectorIndex = new EmbeddingIndex([]); vectorIndex = new EmbeddingIndex([]);
vectorIndex.preloadIndexedDB(); vectorIndex.preloadIndexedDB();
} catch (e) { } catch (e) {
console.error('Error initializing vector search', e); console.error("Error initializing vector search", e);
} }
} }
@@ -18,15 +18,18 @@ export interface VectorSearchResult extends SearchResult {
object: HydratedIndexItem & { embedding: number[] }; object: HydratedIndexItem & { embedding: number[] };
} }
export async function searchVectors(query: string, topK: number = 10): Promise<VectorSearchResult[]> { export async function searchVectors(
query: string,
topK: number = 10,
): Promise<VectorSearchResult[]> {
if (!vectorIndex) await initVectorSearch(); if (!vectorIndex) await initVectorSearch();
const queryEmbedding = await getEmbedding(query.slice(0, 100)); const queryEmbedding = await getEmbedding(query.slice(0, 100));
const results = await vectorIndex!.search(queryEmbedding, { const results = await vectorIndex!.search(queryEmbedding, {
topK, topK,
useStorage: 'indexedDB', useStorage: "indexedDB",
dedupeEntries: true dedupeEntries: true,
}); });
return results as VectorSearchResult[]; return results as VectorSearchResult[];
@@ -4,4 +4,3 @@ import type { HydratedIndexItem } from "../../indexing/types";
export interface VectorSearchResult extends SearchResult { export interface VectorSearchResult extends SearchResult {
object: HydratedIndexItem & { embedding: number[] }; object: HydratedIndexItem & { embedding: number[] };
} }
@@ -1,4 +1,4 @@
import type { Plugin } from '../../core/types'; import type { Plugin } from "../../core/types";
interface NotificationCollectorStorage { interface NotificationCollectorStorage {
lastNotificationCount: number; lastNotificationCount: number;
@@ -6,10 +6,10 @@ interface NotificationCollectorStorage {
} }
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = { const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
id: 'notificationCollector', id: "notificationCollector",
name: 'Notification Collector', name: "Notification Collector",
description: 'Collects and displays SEQTA notifications', description: "Collects and displays SEQTA notifications",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
disableToggle: true, disableToggle: true,
@@ -23,22 +23,27 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
const checkNotifications = async () => { const checkNotifications = async () => {
try { try {
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement; const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (api.storage.lastNotificationCount !== 0) { if (api.storage.lastNotificationCount !== 0) {
alertDiv.textContent = api.storage.lastNotificationCount.toString(); alertDiv.textContent = api.storage.lastNotificationCount.toString();
} }
const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, { const response = await fetch(
method: 'POST', `${location.origin}/seqta/student/heartbeat?`,
{
method: "POST",
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8' "Content-Type": "application/json; charset=utf-8",
}, },
body: JSON.stringify({ body: JSON.stringify({
timestamp: "1970-01-01 00:00:00.0", timestamp: "1970-01-01 00:00:00.0",
hash: "#?page=/home", hash: "#?page=/home",
}) }),
}); },
);
const data = await response.json(); const data = await response.json();
@@ -67,7 +72,9 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
if (pollInterval) { if (pollInterval) {
window.clearInterval(pollInterval); window.clearInterval(pollInterval);
pollInterval = null; pollInterval = null;
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement; const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (alertDiv) { if (alertDiv) {
if (api.storage.lastNotificationCount > 9) { if (api.storage.lastNotificationCount > 9) {
alertDiv.textContent = "9+"; alertDiv.textContent = "9+";
@@ -85,7 +92,7 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
return () => { return () => {
stopPolling(); stopPolling();
}; };
} },
}; };
export default notificationCollectorPlugin; export default notificationCollectorPlugin;
+20 -16
View File
@@ -1,6 +1,10 @@
import type { Plugin } from '@/plugins/core/types'; import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from '@/plugins/core/settings'; import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers'; import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Step 1: Define settings with proper typing // Step 1: Define settings with proper typing
const settings = defineSettings({ const settings = defineSettings({
@@ -8,7 +12,7 @@ const settings = defineSettings({
default: true, default: true,
title: "Test Plugin", title: "Test Plugin",
description: "Some random setting", description: "Some random setting",
}) }),
}); });
// Step 2: Create the plugin class with @Setting decorators // Step 2: Create the plugin class with @Setting decorators
@@ -21,32 +25,32 @@ class TestPluginClass extends BasePlugin<typeof settings> {
const settingsInstance = new TestPluginClass(); const settingsInstance = new TestPluginClass();
const testPlugin: Plugin<typeof settings> = { const testPlugin: Plugin<typeof settings> = {
id: 'test', id: "test",
name: 'Test Plugin', name: "Test Plugin",
description: 'A test plugin for BetterSEQTA+', description: "A test plugin for BetterSEQTA+",
version: '1.0.0', version: "1.0.0",
settings: settingsInstance.settings, settings: settingsInstance.settings,
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
console.log('Test plugin running'); console.log("Test plugin running");
api.events.on('ping', (data) => { api.events.on("ping", (data) => {
console.log('Ping received! Page changed to: ', data); console.log("Ping received! Page changed to: ", data);
}); });
const { unregister } = api.seqta.onPageChange((page) => { const { unregister } = api.seqta.onPageChange((page) => {
//console.log('Page changed to', page); //console.log('Page changed to', page);
api.events.emit('ping', page); api.events.emit("ping", page);
console.log('Current setting value:', api.settings.someSetting); console.log("Current setting value:", api.settings.someSetting);
}); });
return () => { return () => {
console.log('Test plugin stopped'); console.log("Test plugin stopped");
unregister(); unregister();
} };
} },
}; };
export default testPlugin; export default testPlugin;
+65 -62
View File
@@ -1,10 +1,10 @@
import renderSvelte from "@/interface/main" import renderSvelte from "@/interface/main";
import themeCreator from "@/interface/pages/themeCreator.svelte" import themeCreator from "@/interface/pages/themeCreator.svelte";
import { unmount } from "svelte" import { unmount } from "svelte";
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager" import { ThemeManager } from "@/plugins/built-in/themes/theme-manager";
import { settingsState } from '@/seqta/utils/listeners/SettingsState' import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let themeCreatorSvelteApp: any = null let themeCreatorSvelteApp: any = null;
const themeManager = ThemeManager.getInstance(); const themeManager = ThemeManager.getInstance();
/** /**
@@ -13,76 +13,79 @@ const themeManager = ThemeManager.getInstance();
* @returns void * @returns void
*/ */
export function OpenThemeCreator(themeID: string = "") { export function OpenThemeCreator(themeID: string = "") {
CloseThemeCreator() CloseThemeCreator();
// Only store original color if we're not editing an existing theme // Only store original color if we're not editing an existing theme
localStorage.setItem('themeCreatorOpen', 'true'); localStorage.setItem("themeCreatorOpen", "true");
if (!themeID) { if (!themeID) {
localStorage.setItem('originalPreviewColor', settingsState.selectedColor); localStorage.setItem("originalPreviewColor", settingsState.selectedColor);
} }
const width = "310px" const width = "310px";
const themeCreatorDiv: HTMLDivElement = document.createElement("div") const themeCreatorDiv: HTMLDivElement = document.createElement("div");
themeCreatorDiv.id = "themeCreator" themeCreatorDiv.id = "themeCreator";
themeCreatorDiv.style.width = width themeCreatorDiv.style.width = width;
const shadow = themeCreatorDiv.attachShadow({ mode: "open" }) const shadow = themeCreatorDiv.attachShadow({ mode: "open" });
themeCreatorSvelteApp = renderSvelte(themeCreator, shadow, { themeCreatorSvelteApp = renderSvelte(themeCreator, shadow, {
themeID: themeID, themeID: themeID,
}) });
const mainContent = document.querySelector("#container") as HTMLDivElement const mainContent = document.querySelector("#container") as HTMLDivElement;
if (mainContent) mainContent.style.width = `calc(100% - ${width})` if (mainContent) mainContent.style.width = `calc(100% - ${width})`;
// close button // close button
const closeButton = document.createElement("button") const closeButton = document.createElement("button");
closeButton.classList.add("themeCloseButton") closeButton.classList.add("themeCloseButton");
closeButton.textContent = "×" closeButton.textContent = "×";
closeButton.addEventListener("click", () => { closeButton.addEventListener("click", () => {
CloseThemeCreator() CloseThemeCreator();
themeManager.clearPreview() themeManager.clearPreview();
}) });
document.body.appendChild(closeButton) document.body.appendChild(closeButton);
const resizeBar = document.createElement("div") const resizeBar = document.createElement("div");
resizeBar.classList.add("resizeBar") resizeBar.classList.add("resizeBar");
resizeBar.style.right = "307.5px" resizeBar.style.right = "307.5px";
let isDragging = false let isDragging = false;
const mouseDownHandler = (_: MouseEvent) => { const mouseDownHandler = (_: MouseEvent) => {
isDragging = true isDragging = true;
document.addEventListener("mousemove", mouseMoveHandler) document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler) document.addEventListener("mouseup", mouseUpHandler);
document.body.style.userSelect = "none" document.body.style.userSelect = "none";
themeCreatorDiv.style.pointerEvents = "none" themeCreatorDiv.style.pointerEvents = "none";
} };
const mouseMoveHandler = (e: MouseEvent) => { const mouseMoveHandler = (e: MouseEvent) => {
if (!isDragging) return if (!isDragging) return;
const windowWidth = window.innerWidth const windowWidth = window.innerWidth;
const newWidth = Math.max(310, windowWidth - e.clientX) const newWidth = Math.max(310, windowWidth - e.clientX);
themeCreatorDiv.style.width = `${newWidth}px` themeCreatorDiv.style.width = `${newWidth}px`;
mainContent.style.width = `calc(100% - ${newWidth}px)` mainContent.style.width = `calc(100% - ${newWidth}px)`;
resizeBar.style.right = `${newWidth - 2.5}px` resizeBar.style.right = `${newWidth - 2.5}px`;
} };
const mouseUpHandler = () => { const mouseUpHandler = () => {
isDragging = false isDragging = false;
document.removeEventListener("mousemove", mouseMoveHandler) document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler) document.removeEventListener("mouseup", mouseUpHandler);
document.body.style.userSelect = "" document.body.style.userSelect = "";
themeCreatorDiv.style.pointerEvents = "auto" themeCreatorDiv.style.pointerEvents = "auto";
} };
resizeBar.addEventListener("mousedown", mouseDownHandler) resizeBar.addEventListener("mousedown", mouseDownHandler);
resizeBar.addEventListener("mouseover", () => (resizeBar.style.opacity = "1")) resizeBar.addEventListener(
resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0")) "mouseover",
() => (resizeBar.style.opacity = "1"),
);
resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0"));
document.body.appendChild(themeCreatorDiv) document.body.appendChild(themeCreatorDiv);
document.body.appendChild(resizeBar) document.body.appendChild(resizeBar);
} }
/** /**
@@ -91,19 +94,19 @@ export function OpenThemeCreator(themeID: string = "") {
*/ */
export function CloseThemeCreator() { export function CloseThemeCreator() {
// Remove the stored flag // Remove the stored flag
localStorage.removeItem('themeCreatorOpen'); localStorage.removeItem("themeCreatorOpen");
const themeCreator = document.getElementById("themeCreator") const themeCreator = document.getElementById("themeCreator");
const closeButton = document.querySelector( const closeButton = document.querySelector(
".themeCloseButton", ".themeCloseButton",
) as HTMLButtonElement ) as HTMLButtonElement;
const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement;
if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp) if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp);
if (themeCreator) themeCreator.remove() if (themeCreator) themeCreator.remove();
if (closeButton) closeButton.remove() if (closeButton) closeButton.remove();
if (resizeBar) resizeBar.remove() if (resizeBar) resizeBar.remove();
const mainContent = document.querySelector("#container") as HTMLDivElement const mainContent = document.querySelector("#container") as HTMLDivElement;
if (mainContent) mainContent.style.width = "100%" if (mainContent) mainContent.style.width = "100%";
} }
+7 -7
View File
@@ -1,17 +1,17 @@
import type { Plugin } from '../../core/types'; import type { Plugin } from "../../core/types";
import { ThemeManager } from './theme-manager'; import { ThemeManager } from "./theme-manager";
const themesPlugin: Plugin = { const themesPlugin: Plugin = {
id: 'themes', id: "themes",
name: 'Themes', name: "Themes",
description: 'Adds a theme selector to the settings page', description: "Adds a theme selector to the settings page",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
run: async (_) => { run: async (_) => {
const themeManager = ThemeManager.getInstance(); const themeManager = ThemeManager.getInstance();
await themeManager.initialize(); await themeManager.initialize();
} },
}; };
export default themesPlugin; export default themesPlugin;
+220 -142
View File
@@ -1,7 +1,7 @@
import localforage from 'localforage'; import localforage from "localforage";
import type { CustomTheme, LoadedCustomTheme } from '@/types/CustomThemes'; import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes";
import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import debounce from '@/seqta/utils/debounce'; import debounce from "@/seqta/utils/debounce";
type ThemeContent = { type ThemeContent = {
id: string; id: string;
@@ -13,7 +13,7 @@ type ThemeContent = {
CustomCSS?: string; CustomCSS?: string;
hideThemeName?: boolean; hideThemeName?: boolean;
forceDark?: boolean; forceDark?: boolean;
images: { id: string, variableName: string, data: string }[]; // data: base64 images: { id: string; variableName: string; data: string }[]; // data: base64
}; };
export class ThemeManager { export class ThemeManager {
@@ -27,7 +27,7 @@ export class ThemeManager {
private imageUrlCache: Map<string, string> = new Map(); private imageUrlCache: Map<string, string> = new Map();
private constructor() { private constructor() {
console.debug('[ThemeManager] Initializing...'); console.debug("[ThemeManager] Initializing...");
} }
public static getInstance(): ThemeManager { public static getInstance(): ThemeManager {
@@ -48,12 +48,12 @@ export class ThemeManager {
* Get a theme by ID from storage * Get a theme by ID from storage
*/ */
public async getTheme(themeId: string): Promise<CustomTheme | null> { public async getTheme(themeId: string): Promise<CustomTheme | null> {
console.debug('[ThemeManager] Getting theme:', themeId); console.debug("[ThemeManager] Getting theme:", themeId);
try { try {
const theme = await localforage.getItem(themeId) as CustomTheme; const theme = (await localforage.getItem(themeId)) as CustomTheme;
return theme; return theme;
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error getting theme:', error); console.error("[ThemeManager] Error getting theme:", error);
return null; return null;
} }
} }
@@ -69,19 +69,19 @@ export class ThemeManager {
* Disable the current theme without deleting it * Disable the current theme without deleting it
*/ */
public async disableTheme(): Promise<void> { public async disableTheme(): Promise<void> {
console.debug('[ThemeManager] Disabling current theme'); console.debug("[ThemeManager] Disabling current theme");
try { try {
if (!this.currentTheme) { if (!this.currentTheme) {
console.debug('[ThemeManager] No theme to disable'); console.debug("[ThemeManager] No theme to disable");
return; return;
} }
await this.removeTheme(this.currentTheme); await this.removeTheme(this.currentTheme);
this.currentTheme = null; this.currentTheme = null;
settingsState.selectedTheme = ''; settingsState.selectedTheme = "";
console.debug('[ThemeManager] Theme disabled successfully'); console.debug("[ThemeManager] Theme disabled successfully");
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error disabling theme:', error); console.error("[ThemeManager] Error disabling theme:", error);
} }
} }
@@ -89,23 +89,28 @@ export class ThemeManager {
* Initialize the theme system and restore previous state * Initialize the theme system and restore previous state
*/ */
public async initialize(): Promise<void> { public async initialize(): Promise<void> {
console.debug('[ThemeManager] Starting initialization'); console.debug("[ThemeManager] Starting initialization");
try { try {
// Check if theme creator was open during reload // Check if theme creator was open during reload
const themeCreatorOpen = localStorage.getItem('themeCreatorOpen'); const themeCreatorOpen = localStorage.getItem("themeCreatorOpen");
if (themeCreatorOpen === 'true') { if (themeCreatorOpen === "true") {
console.debug('[ThemeManager] Theme creator was open, clearing preview state'); console.debug(
"[ThemeManager] Theme creator was open, clearing preview state",
);
this.clearPreview(); this.clearPreview();
// Clean up the flag // Clean up the flag
localStorage.removeItem('themeCreatorOpen'); localStorage.removeItem("themeCreatorOpen");
} }
if (settingsState.selectedTheme) { if (settingsState.selectedTheme) {
console.debug('[ThemeManager] Found selected theme, restoring:', settingsState.selectedTheme); console.debug(
"[ThemeManager] Found selected theme, restoring:",
settingsState.selectedTheme,
);
await this.setTheme(settingsState.selectedTheme); await this.setTheme(settingsState.selectedTheme);
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error during initialization:', error); console.error("[ThemeManager] Error during initialization:", error);
} }
} }
@@ -113,13 +118,13 @@ export class ThemeManager {
* Clean up theme system resources * Clean up theme system resources
*/ */
public async cleanup(): Promise<void> { public async cleanup(): Promise<void> {
console.debug('[ThemeManager] Cleaning up resources'); console.debug("[ThemeManager] Cleaning up resources");
try { try {
if (this.currentTheme) { if (this.currentTheme) {
await this.removeTheme(this.currentTheme, false); await this.removeTheme(this.currentTheme, false);
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error during cleanup:', error); console.error("[ThemeManager] Error during cleanup:", error);
} }
} }
@@ -127,24 +132,24 @@ export class ThemeManager {
* Set and apply a theme by ID * Set and apply a theme by ID
*/ */
public async setTheme(themeId: string): Promise<void> { public async setTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Setting theme:', themeId); console.debug("[ThemeManager] Setting theme:", themeId);
try { try {
const theme = await localforage.getItem(themeId) as CustomTheme; const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (!theme) { if (!theme) {
console.error('[ThemeManager] Theme not found:', themeId); console.error("[ThemeManager] Theme not found:", themeId);
return; return;
} }
// Store original settings before applying new theme // Store original settings before applying new theme
if (!settingsState.selectedTheme) { if (!settingsState.selectedTheme) {
console.debug('[ThemeManager] Storing original settings'); console.debug("[ThemeManager] Storing original settings");
settingsState.originalSelectedColor = settingsState.selectedColor; settingsState.originalSelectedColor = settingsState.selectedColor;
settingsState.originalDarkMode = settingsState.DarkMode; settingsState.originalDarkMode = settingsState.DarkMode;
} }
// Remove current theme if exists // Remove current theme if exists
if (this.currentTheme) { if (this.currentTheme) {
console.debug('[ThemeManager] Removing current theme'); console.debug("[ThemeManager] Removing current theme");
await this.removeTheme(this.currentTheme); await this.removeTheme(this.currentTheme);
} }
@@ -153,9 +158,8 @@ export class ThemeManager {
await this.applyTheme(theme); await this.applyTheme(theme);
this.currentTheme = theme; this.currentTheme = theme;
settingsState.selectedTheme = themeId; settingsState.selectedTheme = themeId;
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error setting theme:', error); console.error("[ThemeManager] Error setting theme:", error);
} }
} }
@@ -163,65 +167,80 @@ export class ThemeManager {
* Apply theme components (CSS, images, settings) * Apply theme components (CSS, images, settings)
*/ */
private async applyTheme(theme: CustomTheme): Promise<void> { private async applyTheme(theme: CustomTheme): Promise<void> {
console.debug('[ThemeManager] Applying theme:', theme.name); console.debug("[ThemeManager] Applying theme:", theme.name);
try { try {
// Apply custom CSS // Apply custom CSS
if (theme.CustomCSS) { if (theme.CustomCSS) {
console.debug('[ThemeManager] Applying custom CSS'); console.debug("[ThemeManager] Applying custom CSS");
this.applyCustomCSS(theme.CustomCSS); this.applyCustomCSS(theme.CustomCSS);
} }
// Apply custom images // Apply custom images
if (theme.CustomImages) { if (theme.CustomImages) {
console.debug('[ThemeManager] Applying custom images'); console.debug("[ThemeManager] Applying custom images");
theme.CustomImages.forEach((image) => { theme.CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob); const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty('--' + image.variableName, `url(${imageUrl})`); document.documentElement.style.setProperty(
"--" + image.variableName,
`url(${imageUrl})`,
);
}); });
} }
// Apply theme settings // Apply theme settings
if (theme.forceDark !== undefined) { if (theme.forceDark !== undefined) {
console.debug('[ThemeManager] Setting dark mode:', theme.forceDark); console.debug("[ThemeManager] Setting dark mode:", theme.forceDark);
settingsState.DarkMode = theme.forceDark; settingsState.DarkMode = theme.forceDark;
} }
// Use the stored selected color if available, otherwise use the default // Use the stored selected color if available, otherwise use the default
if (theme.selectedColor) { if (theme.selectedColor) {
console.debug('[ThemeManager] Restoring saved color:', theme.selectedColor); console.debug(
"[ThemeManager] Restoring saved color:",
theme.selectedColor,
);
settingsState.selectedColor = theme.selectedColor; settingsState.selectedColor = theme.selectedColor;
} else if (theme.defaultColour) { } else if (theme.defaultColour) {
console.debug('[ThemeManager] Using default color:', theme.defaultColour); console.debug(
"[ThemeManager] Using default color:",
theme.defaultColour,
);
settingsState.selectedColor = theme.defaultColour; settingsState.selectedColor = theme.defaultColour;
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error applying theme:', error); console.error("[ThemeManager] Error applying theme:", error);
} }
} }
/** /**
* Remove theme and restore original settings * Remove theme and restore original settings
*/ */
private async removeTheme(theme: CustomTheme, clearSelectedTheme: boolean = true): Promise<void> { private async removeTheme(
console.debug('[ThemeManager] Removing theme:', theme.name); theme: CustomTheme,
clearSelectedTheme: boolean = true,
): Promise<void> {
console.debug("[ThemeManager] Removing theme:", theme.name);
try { try {
// Remove custom CSS // Remove custom CSS
if (this.styleElement) { if (this.styleElement) {
console.debug('[ThemeManager] Removing custom CSS'); console.debug("[ThemeManager] Removing custom CSS");
this.styleElement.remove(); this.styleElement.remove();
this.styleElement = null; this.styleElement = null;
} }
// Remove custom images // Remove custom images
if (theme.CustomImages) { if (theme.CustomImages) {
console.debug('[ThemeManager] Removing custom images'); console.debug("[ThemeManager] Removing custom images");
theme.CustomImages.forEach((image) => { theme.CustomImages.forEach((image) => {
const value = document.documentElement.style.getPropertyValue('--' + image.variableName); const value = document.documentElement.style.getPropertyValue(
"--" + image.variableName,
);
if (value) { if (value) {
URL.revokeObjectURL(value.slice(4, -1)); // Remove url() wrapper URL.revokeObjectURL(value.slice(4, -1)); // Remove url() wrapper
} }
document.documentElement.style.removeProperty('--' + image.variableName); document.documentElement.style.removeProperty(
"--" + image.variableName,
);
}); });
} }
@@ -229,29 +248,34 @@ export class ThemeManager {
// Store the current color with the theme before removing it // Store the current color with the theme before removing it
await localforage.setItem(this.currentTheme.id, { await localforage.setItem(this.currentTheme.id, {
...this.currentTheme, ...this.currentTheme,
selectedColor: settingsState.selectedColor selectedColor: settingsState.selectedColor,
}); });
} }
// Restore original settings // Restore original settings
if (settingsState.originalSelectedColor) { if (settingsState.originalSelectedColor) {
console.debug('[ThemeManager] Restoring original color:', settingsState.originalSelectedColor); console.debug(
"[ThemeManager] Restoring original color:",
settingsState.originalSelectedColor,
);
settingsState.selectedColor = settingsState.originalSelectedColor; settingsState.selectedColor = settingsState.originalSelectedColor;
} }
if (settingsState.originalDarkMode !== undefined) { if (settingsState.originalDarkMode !== undefined) {
console.debug('[ThemeManager] Restoring original dark mode:', settingsState.originalDarkMode); console.debug(
"[ThemeManager] Restoring original dark mode:",
settingsState.originalDarkMode,
);
settingsState.DarkMode = settingsState.originalDarkMode; settingsState.DarkMode = settingsState.originalDarkMode;
settingsState.originalDarkMode = undefined; settingsState.originalDarkMode = undefined;
} }
this.currentTheme = null; this.currentTheme = null;
if (clearSelectedTheme) { if (clearSelectedTheme) {
settingsState.selectedTheme = ''; settingsState.selectedTheme = "";
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error removing theme:', error); console.error("[ThemeManager] Error removing theme:", error);
} }
} }
@@ -259,16 +283,16 @@ export class ThemeManager {
* Apply custom CSS to the document * Apply custom CSS to the document
*/ */
private applyCustomCSS(css: string): void { private applyCustomCSS(css: string): void {
console.debug('[ThemeManager] Applying custom CSS'); console.debug("[ThemeManager] Applying custom CSS");
try { try {
if (!this.styleElement) { if (!this.styleElement) {
this.styleElement = document.createElement('style'); this.styleElement = document.createElement("style");
this.styleElement.id = 'custom-theme'; this.styleElement.id = "custom-theme";
document.head.appendChild(this.styleElement); document.head.appendChild(this.styleElement);
} }
this.styleElement.textContent = css; this.styleElement.textContent = css;
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error applying custom CSS:', error); console.error("[ThemeManager] Error applying custom CSS:", error);
} }
} }
@@ -276,22 +300,24 @@ export class ThemeManager {
* Get list of available themes * Get list of available themes
*/ */
public async getAvailableThemes(): Promise<CustomTheme[]> { public async getAvailableThemes(): Promise<CustomTheme[]> {
console.debug('[ThemeManager] Getting available themes'); console.debug("[ThemeManager] Getting available themes");
try { try {
const themeIds = await localforage.getItem('customThemes') as string[] | null; const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (!themeIds) { if (!themeIds) {
return []; return [];
} }
const themes = await Promise.all( const themes = await Promise.all(
themeIds.map(async (id) => { themeIds.map(async (id) => {
return await localforage.getItem(id) as CustomTheme; return (await localforage.getItem(id)) as CustomTheme;
}) }),
); );
return themes.filter(theme => theme !== null); return themes.filter((theme) => theme !== null);
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error getting available themes:', error); console.error("[ThemeManager] Error getting available themes:", error);
return []; return [];
} }
} }
@@ -300,21 +326,23 @@ export class ThemeManager {
* Save or update a theme * Save or update a theme
*/ */
public async saveTheme(theme: LoadedCustomTheme): Promise<void> { public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug('[ThemeManager] Saving theme:', theme.name); console.debug("[ThemeManager] Saving theme:", theme.name);
try { try {
await localforage.setItem(theme.id, theme); await localforage.setItem(theme.id, theme);
const themeIds = await localforage.getItem('customThemes') as string[] | null; const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (themeIds) { if (themeIds) {
if (!themeIds.includes(theme.id)) { if (!themeIds.includes(theme.id)) {
themeIds.push(theme.id); themeIds.push(theme.id);
await localforage.setItem('customThemes', themeIds); await localforage.setItem("customThemes", themeIds);
} }
} else { } else {
await localforage.setItem('customThemes', [theme.id]); await localforage.setItem("customThemes", [theme.id]);
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error saving theme:', error); console.error("[ThemeManager] Error saving theme:", error);
} }
} }
@@ -322,40 +350,49 @@ export class ThemeManager {
* Delete a theme * Delete a theme
*/ */
public async deleteTheme(themeId: string): Promise<void> { public async deleteTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Deleting theme:', themeId); console.debug("[ThemeManager] Deleting theme:", themeId);
try { try {
const theme = await localforage.getItem(themeId) as CustomTheme; const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (theme) { if (theme) {
if (this.currentTheme?.id === themeId) { if (this.currentTheme?.id === themeId) {
await this.removeTheme(theme); await this.removeTheme(theme);
} }
await localforage.removeItem(themeId); await localforage.removeItem(themeId);
const themeIds = await localforage.getItem('customThemes') as string[] | null; const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (themeIds) { if (themeIds) {
const updatedThemeIds = themeIds.filter(id => id !== themeId); const updatedThemeIds = themeIds.filter((id) => id !== themeId);
await localforage.setItem('customThemes', updatedThemeIds); await localforage.setItem("customThemes", updatedThemeIds);
} }
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error deleting theme:', error); console.error("[ThemeManager] Error deleting theme:", error);
} }
} }
/** /**
* Download and install a theme from the store * Download and install a theme from the store
*/ */
public async downloadTheme(themeContent: { id: string; name: string; description: string; coverImage: string; }): Promise<void> { public async downloadTheme(themeContent: {
console.debug('[ThemeManager] Downloading theme:', themeContent.name); id: string;
name: string;
description: string;
coverImage: string;
}): Promise<void> {
console.debug("[ThemeManager] Downloading theme:", themeContent.name);
try { try {
if (!themeContent.id) return; if (!themeContent.id) return;
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`); const response = await fetch(
const themeData = await response.json() as ThemeContent; `https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`,
);
const themeData = (await response.json()) as ThemeContent;
await this.installTheme(themeData); await this.installTheme(themeData);
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error downloading theme:', error); console.error("[ThemeManager] Error downloading theme:", error);
} }
} }
@@ -363,62 +400,67 @@ export class ThemeManager {
* Install a theme from theme data * Install a theme from theme data
*/ */
public async installTheme(themeData: ThemeContent): Promise<void> { public async installTheme(themeData: ThemeContent): Promise<void> {
console.debug('[ThemeManager] Installing theme:', themeData.name); console.debug("[ThemeManager] Installing theme:", themeData.name);
try { try {
// Validate required fields // Validate required fields
if (!themeData.id || !themeData.name) { if (!themeData.id || !themeData.name) {
throw new Error('Theme is missing required fields (id or name)'); throw new Error("Theme is missing required fields (id or name)");
} }
// Handle cover image (optional) // Handle cover image (optional)
let coverImageBlob = null; let coverImageBlob = null;
if (themeData.coverImage) { if (themeData.coverImage) {
try { try {
const strippedCoverImage = this.stripBase64Prefix(themeData.coverImage); const strippedCoverImage = this.stripBase64Prefix(
themeData.coverImage,
);
coverImageBlob = this.base64ToBlob(strippedCoverImage); coverImageBlob = this.base64ToBlob(strippedCoverImage);
} catch (e) { } catch (e) {
console.warn('[ThemeManager] Failed to process cover image:', e); console.warn("[ThemeManager] Failed to process cover image:", e);
// Continue without cover image // Continue without cover image
} }
} }
// Handle images (optional) // Handle images (optional)
const images = themeData.images?.map((image) => { const images =
themeData.images
?.map((image) => {
try { try {
if (!image.id || !image.variableName || !image.data) { if (!image.id || !image.variableName || !image.data) {
console.warn('[ThemeManager] Skipping invalid image:', image); console.warn("[ThemeManager] Skipping invalid image:", image);
return null; return null;
} }
return { return {
...image, ...image,
blob: this.base64ToBlob(this.stripBase64Prefix(image.data)) blob: this.base64ToBlob(this.stripBase64Prefix(image.data)),
}; };
} catch (e) { } catch (e) {
console.warn('[ThemeManager] Failed to process image:', e); console.warn("[ThemeManager] Failed to process image:", e);
return null; return null;
} }
}).filter(img => img !== null) ?? []; })
.filter((img) => img !== null) ?? [];
// Create theme with defaults for optional fields // Create theme with defaults for optional fields
const theme: LoadedCustomTheme = { const theme: LoadedCustomTheme = {
id: themeData.id, id: themeData.id,
name: themeData.name, name: themeData.name,
description: themeData.description || '', description: themeData.description || "",
webURL: themeData.id, webURL: themeData.id,
coverImage: coverImageBlob, coverImage: coverImageBlob,
CustomImages: images, CustomImages: images,
CustomCSS: themeData.CustomCSS || '', CustomCSS: themeData.CustomCSS || "",
defaultColour: themeData.defaultColour || 'rgba(0, 123, 255, 1)', defaultColour: themeData.defaultColour || "rgba(0, 123, 255, 1)",
CanChangeColour: themeData.CanChangeColour ?? true, CanChangeColour: themeData.CanChangeColour ?? true,
allowBackgrounds: true, allowBackgrounds: true,
isEditable: false, isEditable: false,
hideThemeName: themeData.hideThemeName ?? false, hideThemeName: themeData.hideThemeName ?? false,
forceDark: themeData.forceDark forceDark: themeData.forceDark,
}; };
await this.saveTheme(theme); await this.saveTheme(theme);
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error installing theme:', error); console.error("[ThemeManager] Error installing theme:", error);
throw error; // Re-throw to handle in UI throw error; // Re-throw to handle in UI
} }
} }
@@ -427,11 +469,11 @@ export class ThemeManager {
* Share a theme by exporting it * Share a theme by exporting it
*/ */
public async shareTheme(themeId: string): Promise<void> { public async shareTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Sharing theme:', themeId); console.debug("[ThemeManager] Sharing theme:", themeId);
try { try {
const theme = await localforage.getItem(themeId) as LoadedCustomTheme; const theme = (await localforage.getItem(themeId)) as LoadedCustomTheme;
if (!theme) { if (!theme) {
console.error('[ThemeManager] Theme not found'); console.error("[ThemeManager] Theme not found");
return; return;
} }
@@ -447,26 +489,30 @@ export class ThemeManager {
} = theme; } = theme;
// Convert images to base64 // Convert images to base64
const finalImages = await Promise.all(CustomImages.map(async (image) => ({ const finalImages = await Promise.all(
CustomImages.map(async (image) => ({
id: image.id, id: image.id,
variableName: image.variableName, variableName: image.variableName,
data: await this.blobToBase64(image.blob) data: await this.blobToBase64(image.blob),
}))); })),
);
// Convert cover image to base64 // Convert cover image to base64
const coverImageBase64 = coverImage ? await this.blobToBase64(coverImage) : null; const coverImageBase64 = coverImage
? await this.blobToBase64(coverImage)
: null;
// Create shareable theme data with only necessary fields // Create shareable theme data with only necessary fields
const shareableTheme = { const shareableTheme = {
...themeBasics, ...themeBasics,
images: finalImages, images: finalImages,
coverImage: coverImageBase64 coverImage: coverImageBase64,
}; };
// Save theme file // Save theme file
this.saveThemeFile(shareableTheme, theme.name || 'Unnamed_Theme'); this.saveThemeFile(shareableTheme, theme.name || "Unnamed_Theme");
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error sharing theme:', error); console.error("[ThemeManager] Error sharing theme:", error);
} }
} }
@@ -474,7 +520,7 @@ export class ThemeManager {
* Preview a theme without applying it * Preview a theme without applying it
*/ */
public async previewTheme(theme: LoadedCustomTheme): Promise<void> { public async previewTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug('[ThemeManager] Previewing theme:', theme.name); console.debug("[ThemeManager] Previewing theme:", theme.name);
try { try {
const { CustomCSS, CustomImages, defaultColour, forceDark } = theme; const { CustomCSS, CustomImages, defaultColour, forceDark } = theme;
@@ -482,7 +528,10 @@ export class ThemeManager {
if (!theme.webURL) { if (!theme.webURL) {
if (this.originalPreviewColor === null) { if (this.originalPreviewColor === null) {
this.originalPreviewColor = settingsState.selectedColor; this.originalPreviewColor = settingsState.selectedColor;
localStorage.setItem('originalPreviewColor', settingsState.selectedColor); localStorage.setItem(
"originalPreviewColor",
settingsState.selectedColor,
);
} }
if (this.originalPreviewTheme === null) { if (this.originalPreviewTheme === null) {
this.originalPreviewTheme = settingsState.DarkMode; this.originalPreviewTheme = settingsState.DarkMode;
@@ -495,10 +544,12 @@ export class ThemeManager {
} }
// Apply custom images // Apply custom images
const newImageVariableNames = CustomImages.map(image => image.variableName); const newImageVariableNames = CustomImages.map(
(image) => image.variableName,
);
// Remove old preview images // Remove old preview images
this.previousImageVariableNames.forEach(variableName => { this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) { if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName); this.removeImageFromDocument(variableName);
} }
@@ -507,7 +558,10 @@ export class ThemeManager {
// Apply new images // Apply new images
CustomImages.forEach((image) => { CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob); const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${imageUrl})`); document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
}); });
// Update previousImageVariableNames // Update previousImageVariableNames
@@ -522,7 +576,7 @@ export class ThemeManager {
settingsState.selectedColor = defaultColour; settingsState.selectedColor = defaultColour;
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error previewing theme:', error); console.error("[ThemeManager] Error previewing theme:", error);
} }
} }
@@ -530,7 +584,7 @@ export class ThemeManager {
* Update the preview of a theme in real-time (for theme creator) * Update the preview of a theme in real-time (for theme creator)
*/ */
public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> { public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> {
console.debug('[ThemeManager] Updating theme preview'); console.debug("[ThemeManager] Updating theme preview");
try { try {
// Only store original settings if this is a new theme (not editing) // Only store original settings if this is a new theme (not editing)
// We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded) // We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded)
@@ -550,10 +604,12 @@ export class ThemeManager {
// Handle images if present // Handle images if present
if (theme.CustomImages) { if (theme.CustomImages) {
const newImageVariableNames = theme.CustomImages.map(image => image.variableName); const newImageVariableNames = theme.CustomImages.map(
(image) => image.variableName,
);
// Remove old preview images that are no longer present // Remove old preview images that are no longer present
this.previousImageVariableNames.forEach(variableName => { this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) { if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName); this.removeImageFromDocument(variableName);
// Clean up cached URL // Clean up cached URL
@@ -568,10 +624,16 @@ export class ThemeManager {
// Only create new URL if one doesn't exist // Only create new URL if one doesn't exist
const imageUrl = URL.createObjectURL(image.blob); const imageUrl = URL.createObjectURL(image.blob);
this.imageUrlCache.set(image.variableName, imageUrl); this.imageUrlCache.set(image.variableName, imageUrl);
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${imageUrl})`); document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
} else { } else {
// Reuse existing URL // Reuse existing URL
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${existingUrl})`); document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${existingUrl})`,
);
} }
}); });
@@ -588,7 +650,7 @@ export class ThemeManager {
settingsState.selectedColor = theme.defaultColour; settingsState.selectedColor = theme.defaultColour;
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error updating theme preview:', error); console.error("[ThemeManager] Error updating theme preview:", error);
} }
} }
@@ -596,22 +658,25 @@ export class ThemeManager {
* Update the preview of a theme (debounced) * Update the preview of a theme (debounced)
* @param theme - The theme to update the preview of * @param theme - The theme to update the preview of
*/ */
public updatePreviewDebounced = debounce((theme: Partial<LoadedCustomTheme>): void => { public updatePreviewDebounced = debounce(
(theme: Partial<LoadedCustomTheme>): void => {
this.updatePreview(theme); this.updatePreview(theme);
}, 2); },
2,
);
/** /**
* Clear theme preview * Clear theme preview
*/ */
public clearPreview(): void { public clearPreview(): void {
console.debug('[ThemeManager] Clearing theme preview'); console.debug("[ThemeManager] Clearing theme preview");
try { try {
// Remove preview images and revoke URLs // Remove preview images and revoke URLs
this.previousImageVariableNames.forEach(variableName => { this.previousImageVariableNames.forEach((variableName) => {
this.removeImageFromDocument(variableName); this.removeImageFromDocument(variableName);
}); });
// Clear all cached URLs // Clear all cached URLs
this.imageUrlCache.forEach(url => URL.revokeObjectURL(url)); this.imageUrlCache.forEach((url) => URL.revokeObjectURL(url));
this.imageUrlCache.clear(); this.imageUrlCache.clear();
this.previousImageVariableNames = []; this.previousImageVariableNames = [];
@@ -622,40 +687,51 @@ export class ThemeManager {
} }
// Restore original settings // Restore original settings
const storedColor = localStorage.getItem('originalPreviewColor'); const storedColor = localStorage.getItem("originalPreviewColor");
if (storedColor) { if (storedColor) {
settingsState.selectedColor = storedColor; settingsState.selectedColor = storedColor;
localStorage.removeItem('originalPreviewColor'); localStorage.removeItem("originalPreviewColor");
} else if (this.originalPreviewColor !== null) { } else if (this.originalPreviewColor !== null) {
console.debug('[ThemeManager] Restoring color from memory:', this.originalPreviewColor); console.debug(
"[ThemeManager] Restoring color from memory:",
this.originalPreviewColor,
);
settingsState.selectedColor = this.originalPreviewColor; settingsState.selectedColor = this.originalPreviewColor;
console.debug('[ThemeManager] Color after restore:', settingsState.selectedColor); console.debug(
"[ThemeManager] Color after restore:",
settingsState.selectedColor,
);
} else { } else {
console.debug('[ThemeManager] No color to restore found'); console.debug("[ThemeManager] No color to restore found");
} }
this.originalPreviewColor = null; this.originalPreviewColor = null;
if (this.originalPreviewTheme !== null) { if (this.originalPreviewTheme !== null) {
console.debug('[ThemeManager] Restoring dark mode:', this.originalPreviewTheme); console.debug(
"[ThemeManager] Restoring dark mode:",
this.originalPreviewTheme,
);
settingsState.DarkMode = this.originalPreviewTheme; settingsState.DarkMode = this.originalPreviewTheme;
this.originalPreviewTheme = null; this.originalPreviewTheme = null;
} }
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error clearing preview:', error); console.error("[ThemeManager] Error clearing preview:", error);
} }
} }
// Utility methods // Utility methods
private stripBase64Prefix(base64String: string): string { private stripBase64Prefix(base64String: string): string {
if (!base64String) return ''; if (!base64String) return "";
const prefixRegex = /^data:[^;]+;base64,/; const prefixRegex = /^data:[^;]+;base64,/;
try { try {
return prefixRegex.test(base64String) ? base64String.replace(prefixRegex, '') : base64String; return prefixRegex.test(base64String)
? base64String.replace(prefixRegex, "")
: base64String;
} catch (err) { } catch (err) {
console.error('[ThemeManager] Error stripping base64 prefix:', err); console.error("[ThemeManager] Error stripping base64 prefix:", err);
return ''; return "";
} }
} }
@@ -669,9 +745,9 @@ export class ThemeManager {
ia[i] = byteString.charCodeAt(i); ia[i] = byteString.charCodeAt(i);
} }
return new Blob([ab], { type: 'image/png' }); return new Blob([ab], { type: "image/png" });
} catch (err) { } catch (err) {
console.error('[ThemeManager] Error converting base64 to blob:', err); console.error("[ThemeManager] Error converting base64 to blob:", err);
return new Blob(); return new Blob();
} }
} }
@@ -681,7 +757,7 @@ export class ThemeManager {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
const base64String = reader.result as string; const base64String = reader.result as string;
const base64Data = base64String.split(',')[1]; const base64Data = base64String.split(",")[1];
resolve(base64Data); resolve(base64Data);
}; };
reader.onerror = reject; reader.onerror = reject;
@@ -692,9 +768,9 @@ export class ThemeManager {
private saveThemeFile(data: object, fileName: string): void { private saveThemeFile(data: object, fileName: string): void {
try { try {
const fileData = JSON.stringify(data, null, 2); const fileData = JSON.stringify(data, null, 2);
const blob = new Blob([fileData], { type: 'application/json' }); const blob = new Blob([fileData], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = `${fileName}.theme.json`; a.download = `${fileName}.theme.json`;
document.body.appendChild(a); document.body.appendChild(a);
@@ -702,13 +778,15 @@ export class ThemeManager {
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} catch (err) { } catch (err) {
console.error('[ThemeManager] Error saving theme file:', err); console.error("[ThemeManager] Error saving theme file:", err);
} }
} }
private removeImageFromDocument(variableName: string): void { private removeImageFromDocument(variableName: string): void {
try { try {
const value = document.documentElement.style.getPropertyValue('--' + variableName); const value = document.documentElement.style.getPropertyValue(
"--" + variableName,
);
if (value) { if (value) {
const url = this.imageUrlCache.get(variableName); const url = this.imageUrlCache.get(variableName);
if (url) { if (url) {
@@ -716,23 +794,23 @@ export class ThemeManager {
this.imageUrlCache.delete(variableName); this.imageUrlCache.delete(variableName);
} }
} }
document.documentElement.style.removeProperty('--' + variableName); document.documentElement.style.removeProperty("--" + variableName);
} catch (err) { } catch (err) {
console.error('[ThemeManager] Error removing image from document:', err); console.error("[ThemeManager] Error removing image from document:", err);
} }
} }
private applyPreviewCSS(css: string): void { private applyPreviewCSS(css: string): void {
console.debug('[ThemeManager] Applying preview CSS'); console.debug("[ThemeManager] Applying preview CSS");
try { try {
if (!this.previewStyleElement) { if (!this.previewStyleElement) {
this.previewStyleElement = document.createElement('style'); this.previewStyleElement = document.createElement("style");
this.previewStyleElement.id = 'custom-theme-preview'; this.previewStyleElement.id = "custom-theme-preview";
document.head.appendChild(this.previewStyleElement); document.head.appendChild(this.previewStyleElement);
} }
this.previewStyleElement.textContent = css; this.previewStyleElement.textContent = css;
} catch (error) { } catch (error) {
console.error('[ThemeManager] Error applying preview CSS:', error); console.error("[ThemeManager] Error applying preview CSS:", error);
} }
} }
} }
+139 -128
View File
@@ -1,268 +1,279 @@
import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { Plugin } from '../../core/types'; import type { Plugin } from "../../core/types";
import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat'; import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
import { waitForElm } from '@/seqta/utils/waitForElm'; import { waitForElm } from "@/seqta/utils/waitForElm";
const timetablePlugin: Plugin<{}, {}> = { const timetablePlugin: Plugin<{}, {}> = {
id: 'timetable', id: "timetable",
name: 'Timetable Enhancer', name: "Timetable Enhancer",
description: 'Adds extra features to the timetable view', description: "Adds extra features to the timetable view",
version: '1.0.0', version: "1.0.0",
settings: {}, settings: {},
disableToggle: true, disableToggle: true,
run: async (api) => { run: async (api) => {
const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable) const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
return () => { return () => {
// Call the unregister function to remove the mount listener // Call the unregister function to remove the mount listener
unregister(); unregister();
const timetablePage = document.querySelector('.timetablepage') const timetablePage = document.querySelector(".timetablepage");
if (timetablePage) { if (timetablePage) {
const zoomControls = document.querySelector('.timetable-zoom-controls') const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) zoomControls.remove() if (zoomControls) zoomControls.remove();
const hideControls = document.querySelector('.timetable-hide-controls') const hideControls = document.querySelector(".timetable-hide-controls");
if (hideControls) hideControls.remove() if (hideControls) hideControls.remove();
resetTimetableStyles() resetTimetableStyles();
}
}
} }
}; };
},
};
// Store event handlers globally for cleanup // Store event handlers globally for cleanup
const zoomHandlers = new WeakMap<Element, { zoomIn: () => void; zoomOut: () => void }>() const zoomHandlers = new WeakMap<
Element,
{ zoomIn: () => void; zoomOut: () => void }
>();
function resetTimetableStyles(): void { function resetTimetableStyles(): void {
const firstDayColumn = document.querySelector(".dailycal .content .days td") as HTMLElement const firstDayColumn = document.querySelector(
if (!firstDayColumn) return ".dailycal .content .days td",
) as HTMLElement;
if (!firstDayColumn) return;
const baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight const baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
const dayColumns = document.querySelectorAll(".dailycal .content .days td") const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => { dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${baseContainerHeight}px` (td as HTMLElement).style.height = `${baseContainerHeight}px`;
}) });
const timeColumn = document.querySelector(".times") const timeColumn = document.querySelector(".times");
if (timeColumn) { if (timeColumn) {
const times = timeColumn.querySelectorAll(".time") const times = timeColumn.querySelectorAll(".time");
const timeHeight = baseContainerHeight / times.length const timeHeight = baseContainerHeight / times.length;
times.forEach((time: Element) => { times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px` (time as HTMLElement).style.height = `${timeHeight}px`;
}) });
} }
const lessons = document.querySelectorAll(".dailycal .lesson") const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => { lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement const lessonEl = lesson as HTMLElement;
const originalHeight = lessonEl.getAttribute('data-original-height') const originalHeight = lessonEl.getAttribute("data-original-height");
if (originalHeight) { if (originalHeight) {
lessonEl.style.height = `${originalHeight}px` lessonEl.style.height = `${originalHeight}px`;
} }
}) });
const entries = document.querySelectorAll(".entry") const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => { entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement const entryEl = entry as HTMLElement;
entryEl.style.opacity = '1' entryEl.style.opacity = "1";
}) });
const zoomControls = document.querySelector('.timetable-zoom-controls') const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) { if (zoomControls) {
const handlers = zoomHandlers.get(zoomControls) const handlers = zoomHandlers.get(zoomControls);
if (handlers) { if (handlers) {
const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)') const zoomIn = zoomControls.querySelector(".timetable-zoom:nth-child(2)");
const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)') const zoomOut = zoomControls.querySelector(
if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn) ".timetable-zoom:nth-child(1)",
if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut) );
zoomHandlers.delete(zoomControls) if (zoomIn) zoomIn.removeEventListener("click", handlers.zoomIn);
if (zoomOut) zoomOut.removeEventListener("click", handlers.zoomOut);
zoomHandlers.delete(zoomControls);
} }
} }
} }
async function handleTimetable(): Promise<void> { async function handleTimetable(): Promise<void> {
await waitForElm(".time", true, 10) await waitForElm(".time", true, 10);
// Store original heights when timetable loads // Store original heights when timetable loads
const lessons = document.querySelectorAll(".dailycal .lesson") const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => { lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement const lessonEl = lesson as HTMLElement;
lessonEl.setAttribute( lessonEl.setAttribute(
"data-original-height", "data-original-height",
lessonEl.offsetHeight.toString(), lessonEl.offsetHeight.toString(),
) );
}) });
// Existing time format code // Existing time format code
if (settingsState.timeFormat == "12") { if (settingsState.timeFormat == "12") {
const times = document.querySelectorAll(".timetablepage .times .time") const times = document.querySelectorAll(".timetablepage .times .time");
for (const time of times) { for (const time of times) {
if (!time.textContent) continue if (!time.textContent) continue;
time.textContent = convertTo12HourFormat(time.textContent, true) time.textContent = convertTo12HourFormat(time.textContent, true);
} }
} }
handleTimetableZoom() handleTimetableZoom();
handleTimetableAssessmentHide() handleTimetableAssessmentHide();
} }
function handleTimetableZoom(): void { function handleTimetableZoom(): void {
console.log("Initializing timetable zoom controls") console.log("Initializing timetable zoom controls");
// Lazy initialize state variables only when function is first called // Lazy initialize state variables only when function is first called
let timetableZoomLevel = 1 let timetableZoomLevel = 1;
let baseContainerHeight: number | null = null let baseContainerHeight: number | null = null;
const originalEntryPositions = new Map< const originalEntryPositions = new Map<
Element, Element,
{ topRatio: number; heightRatio: number } { topRatio: number; heightRatio: number }
>() >();
// Create zoom controls // Create zoom controls
const zoomControls = document.createElement("div") const zoomControls = document.createElement("div");
zoomControls.className = "timetable-zoom-controls" zoomControls.className = "timetable-zoom-controls";
const zoomIn = document.createElement("button") const zoomIn = document.createElement("button");
zoomIn.className = "uiButton timetable-zoom iconFamily" zoomIn.className = "uiButton timetable-zoom iconFamily";
zoomIn.innerHTML = "&#xed93;" // Unicode for zoom in icon (custom iconfamily) zoomIn.innerHTML = "&#xed93;"; // Unicode for zoom in icon (custom iconfamily)
const zoomOut = document.createElement("button") const zoomOut = document.createElement("button");
zoomOut.className = "uiButton timetable-zoom iconFamily" zoomOut.className = "uiButton timetable-zoom iconFamily";
zoomOut.innerHTML = "&#xed94;" // Unicode for zoom out icon (custom iconfamily) zoomOut.innerHTML = "&#xed94;"; // Unicode for zoom out icon (custom iconfamily)
zoomControls.appendChild(zoomOut) zoomControls.appendChild(zoomOut);
zoomControls.appendChild(zoomIn) zoomControls.appendChild(zoomIn);
const toolbar = document.getElementById("toolbar") const toolbar = document.getElementById("toolbar");
toolbar?.appendChild(zoomControls) toolbar?.appendChild(zoomControls);
// Store event listener references // Store event listener references
const zoomInHandler = () => { const zoomInHandler = () => {
if (timetableZoomLevel < 2) { if (timetableZoomLevel < 2) {
timetableZoomLevel += 0.2 timetableZoomLevel += 0.2;
updateZoom() updateZoom();
}
} }
};
const zoomOutHandler = () => { const zoomOutHandler = () => {
if (timetableZoomLevel > 0.6) { if (timetableZoomLevel > 0.6) {
timetableZoomLevel -= 0.2 timetableZoomLevel -= 0.2;
updateZoom() updateZoom();
}
} }
};
zoomIn.addEventListener("click", zoomInHandler) zoomIn.addEventListener("click", zoomInHandler);
zoomOut.addEventListener("click", zoomOutHandler) zoomOut.addEventListener("click", zoomOutHandler);
// Store references for cleanup // Store references for cleanup
zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler }) zoomHandlers.set(zoomControls, {
zoomIn: zoomInHandler,
zoomOut: zoomOutHandler,
});
const initializePositions = () => { const initializePositions = () => {
// Get the base container height from the first TD // Get the base container height from the first TD
const firstDayColumn = document.querySelector( const firstDayColumn = document.querySelector(
".dailycal .content .days td", ".dailycal .content .days td",
) as HTMLElement ) as HTMLElement;
if (!firstDayColumn) return false if (!firstDayColumn) return false;
baseContainerHeight = baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
// Store original ratios // Store original ratios
const entries = document.querySelectorAll(".entriesWrapper .entry") const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => { entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement const entryEl = entry as HTMLElement;
// Calculate ratios relative to detected base height // Calculate ratios relative to detected base height
if (baseContainerHeight === null) return if (baseContainerHeight === null) return;
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight const topRatio = parseInt(entryEl.style.top) / baseContainerHeight;
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight;
originalEntryPositions.set(entry, { topRatio, heightRatio }) originalEntryPositions.set(entry, { topRatio, heightRatio });
}) });
return true return true;
} };
const updateZoom = () => { const updateZoom = () => {
// Initialize positions if not already done // Initialize positions if not already done
if (baseContainerHeight === null && !initializePositions()) { if (baseContainerHeight === null && !initializePositions()) {
console.error("Failed to initialize positions") console.error("Failed to initialize positions");
return return;
} }
console.debug(`Updating zoom level to: ${timetableZoomLevel}`) console.debug(`Updating zoom level to: ${timetableZoomLevel}`);
// Calculate new container height // Calculate new container height
if (baseContainerHeight === null) return if (baseContainerHeight === null) return;
const newContainerHeight = baseContainerHeight * timetableZoomLevel const newContainerHeight = baseContainerHeight * timetableZoomLevel;
// Update all day columns (TDs) // Update all day columns (TDs)
const dayColumns = document.querySelectorAll(".dailycal .content .days td") const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => { dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${newContainerHeight}px` (td as HTMLElement).style.height = `${newContainerHeight}px`;
}) });
// Update all entries using stored ratios // Update all entries using stored ratios
const entries = document.querySelectorAll(".entriesWrapper .entry") const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => { entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement const entryEl = entry as HTMLElement;
const originalRatios = originalEntryPositions.get(entry) const originalRatios = originalEntryPositions.get(entry);
if (originalRatios) { if (originalRatios) {
// Calculate new positions from original ratios // Calculate new positions from original ratios
const newTop = originalRatios.topRatio * newContainerHeight const newTop = originalRatios.topRatio * newContainerHeight;
const newHeight = originalRatios.heightRatio * newContainerHeight const newHeight = originalRatios.heightRatio * newContainerHeight;
// Apply new values // Apply new values
entryEl.style.top = `${Math.round(newTop)}px` entryEl.style.top = `${Math.round(newTop)}px`;
entryEl.style.height = `${Math.round(newHeight)}px` entryEl.style.height = `${Math.round(newHeight)}px`;
} }
}) });
// Update time column to match // Update time column to match
const timeColumn = document.querySelector(".times") const timeColumn = document.querySelector(".times");
if (timeColumn) { if (timeColumn) {
const times = timeColumn.querySelectorAll(".time") const times = timeColumn.querySelectorAll(".time");
const timeHeight = newContainerHeight / times.length const timeHeight = newContainerHeight / times.length;
times.forEach((time: Element) => { times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px` (time as HTMLElement).style.height = `${timeHeight}px`;
}) });
} }
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({ entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
behavior: "instant", behavior: "instant",
block: "center", block: "center",
}) });
} };
} }
function handleTimetableAssessmentHide(): void { function handleTimetableAssessmentHide(): void {
const hideControls = document.createElement("div") const hideControls = document.createElement("div");
hideControls.className = "timetable-hide-controls" hideControls.className = "timetable-hide-controls";
const hideOn = document.createElement("button") const hideOn = document.createElement("button");
hideOn.className = "uiButton timetable-hide iconFamily" hideOn.className = "uiButton timetable-hide iconFamily";
hideOn.innerHTML = "&#128065;" hideOn.innerHTML = "&#128065;";
hideControls.appendChild(hideOn) hideControls.appendChild(hideOn);
const toolbar = document.getElementById("toolbar") const toolbar = document.getElementById("toolbar");
toolbar?.appendChild(hideControls) toolbar?.appendChild(hideControls);
function hideElements(): void { function hideElements(): void {
const entries = document.querySelectorAll(".entry") const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => { entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement const entryEl = entry as HTMLElement;
if (!entryEl.classList.contains("assessment")) { if (!entryEl.classList.contains("assessment")) {
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3" entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3";
} }
}) });
} }
hideOn.addEventListener("click", hideElements) hideOn.addEventListener("click", hideElements);
} }
export default timetablePlugin; export default timetablePlugin;
+93 -46
View File
@@ -1,7 +1,16 @@
import type { EventsAPI, Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, SettingValue, StorageAPI } from './types'; import type {
import { eventManager } from '@/seqta/utils/listeners/EventManager'; EventsAPI,
import ReactFiber from '@/seqta/utils/ReactFiber'; Plugin,
import browser from 'webextension-polyfill'; PluginAPI,
PluginSettings,
SEQTAAPI,
SettingsAPI,
SettingValue,
StorageAPI,
} from "./types";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
import ReactFiber from "@/seqta/utils/ReactFiber";
import browser from "webextension-polyfill";
function createSEQTAAPI(): SEQTAAPI { function createSEQTAAPI(): SEQTAAPI {
return { return {
@@ -11,41 +20,46 @@ function createSEQTAAPI(): SEQTAAPI {
{ {
customCheck: (element) => element.matches(selector), customCheck: (element) => element.matches(selector),
}, },
callback callback,
); );
}, },
getFiber: (selector) => { getFiber: (selector) => {
return ReactFiber.find(selector); return ReactFiber.find(selector);
}, },
getCurrentPage: () => { getCurrentPage: () => {
const path = window.location.hash.split('?page=/')[1] || ''; const path = window.location.hash.split("?page=/")[1] || "";
return path.split('/')[0]; return path.split("/")[0];
}, },
onPageChange: (callback) => { onPageChange: (callback) => {
const handler = () => { const handler = () => {
const page = window.location.hash.split('?page=/')[1] || ''; const page = window.location.hash.split("?page=/")[1] || "";
callback(page.split('/')[0]); callback(page.split("/")[0]);
}; };
window.addEventListener('hashchange', handler); window.addEventListener("hashchange", handler);
// Return an unregister function // Return an unregister function
return { return {
unregister: () => { unregister: () => {
window.removeEventListener('hashchange', handler); window.removeEventListener("hashchange", handler);
} },
}; };
} },
}; };
} }
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } { function createSettingsAPI<T extends PluginSettings>(
plugin: Plugin<T>,
): SettingsAPI<T> & { loaded: Promise<void> } {
const storageKey = `plugin.${plugin.id}.settings`; const storageKey = `plugin.${plugin.id}.settings`;
const listeners = new Map<keyof T, Set<(value: any) => void>>(); const listeners = new Map<keyof T, Set<(value: any) => void>>();
// Initialize with default values // Initialize with default values
const settingsWithMeta: any = { const settingsWithMeta: any = {
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => { onChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => {
if (!listeners.has(key)) { if (!listeners.has(key)) {
listeners.set(key, new Set()); listeners.set(key, new Set());
} }
@@ -53,13 +67,16 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return { return {
unregister: () => { unregister: () => {
listeners.get(key)!.delete(callback); listeners.get(key)!.delete(callback);
} },
}; };
}, },
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => { offChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => {
listeners.get(key)?.delete(callback); listeners.get(key)?.delete(callback);
}, },
loaded: Promise.resolve() // will be replaced below loaded: Promise.resolve(), // will be replaced below
}; };
// Fill with defaults first // Fill with defaults first
@@ -71,33 +88,45 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
const loaded = (async () => { const loaded = (async () => {
try { try {
const stored = await browser.storage.local.get(storageKey); const stored = await browser.storage.local.get(storageKey);
const storedSettings = stored[storageKey] as Partial<Record<keyof T, any>>; const storedSettings = stored[storageKey] as Partial<
Record<keyof T, any>
>;
if (storedSettings) { if (storedSettings) {
for (const key in storedSettings) { for (const key in storedSettings) {
if (key in settingsWithMeta) { if (key in settingsWithMeta) {
settingsWithMeta[key] = storedSettings[key]; settingsWithMeta[key] = storedSettings[key];
listeners.get(key as keyof T)?.forEach(cb => cb(storedSettings[key])); listeners
.get(key as keyof T)
?.forEach((cb) => cb(storedSettings[key]));
} }
} }
} }
} catch (error) { } catch (error) {
console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error); console.error(
`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`,
error,
);
} }
})(); })();
settingsWithMeta.loaded = loaded; settingsWithMeta.loaded = loaded;
// Listen for storage changes and update settingsWithMeta // Listen for storage changes and update settingsWithMeta
const handleStorageChange = (changes: { [key: string]: browser.Storage.StorageChange }, area: string) => { const handleStorageChange = (
if (area !== 'local' || !(storageKey in changes)) return; changes: { [key: string]: browser.Storage.StorageChange },
area: string,
) => {
if (area !== "local" || !(storageKey in changes)) return;
const newValue = changes[storageKey].newValue as Partial<Record<keyof T, any>> | undefined; const newValue = changes[storageKey].newValue as
| Partial<Record<keyof T, any>>
| undefined;
if (!newValue) return; if (!newValue) return;
for (const key in newValue) { for (const key in newValue) {
const typedKey = key as keyof T; const typedKey = key as keyof T;
settingsWithMeta[typedKey] = newValue[typedKey]; settingsWithMeta[typedKey] = newValue[typedKey];
listeners.get(typedKey)?.forEach(cb => cb(newValue[typedKey])); listeners.get(typedKey)?.forEach((cb) => cb(newValue[typedKey]));
} }
}; };
@@ -108,7 +137,8 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return target[prop]; return target[prop];
}, },
set(target, prop, value) { set(target, prop, value) {
if (['onChange', 'offChange', 'loaded'].includes(prop as string)) return false; if (["onChange", "offChange", "loaded"].includes(prop as string))
return false;
target[prop] = value; target[prop] = value;
@@ -120,19 +150,23 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
browser.storage.local.set({ [storageKey]: dataToStore }); browser.storage.local.set({ [storageKey]: dataToStore });
listeners.get(prop as keyof T)?.forEach(cb => cb(value)); listeners.get(prop as keyof T)?.forEach((cb) => cb(value));
return true; return true;
} },
}) as SettingsAPI<T> & { loaded: Promise<void> }; }) as SettingsAPI<T> & { loaded: Promise<void> };
return proxy; return proxy;
} }
function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in keyof T]: T[K] } { function createStorageAPI<T = any>(
pluginId: string,
): StorageAPI<T> & { [K in keyof T]: T[K] } {
const prefix = `plugin.${pluginId}.storage.`; const prefix = `plugin.${pluginId}.storage.`;
const cache: Record<string, any> = {}; const cache: Record<string, any> = {};
const listeners = new Map<string, Set<(value: any) => void>>(); const listeners = new Map<string, Set<(value: any) => void>>();
const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>(); const storageListeners = new Set<
(changes: { [key: string]: any }, area: string) => void
>();
// Load all existing storage values for this plugin // Load all existing storage values for this plugin
const loadStoragePromise = (async () => { const loadStoragePromise = (async () => {
@@ -147,20 +181,28 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
} }
}); });
} catch (error) { } catch (error) {
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error); console.error(
`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`,
error,
);
} }
})(); })();
// Listen for storage changes // Listen for storage changes
const handleStorageChange = (changes: { [key: string]: any }, area: string) => { const handleStorageChange = (
if (area === 'local') { changes: { [key: string]: any },
area: string,
) => {
if (area === "local") {
Object.entries(changes).forEach(([key, change]) => { Object.entries(changes).forEach(([key, change]) => {
if (key.startsWith(prefix)) { if (key.startsWith(prefix)) {
const shortKey = key.slice(prefix.length); const shortKey = key.slice(prefix.length);
cache[shortKey] = change.newValue; cache[shortKey] = change.newValue;
// Notify listeners // Notify listeners
listeners.get(shortKey)?.forEach(callback => callback(change.newValue)); listeners
.get(shortKey)
?.forEach((callback) => callback(change.newValue));
} }
}); });
} }
@@ -171,7 +213,7 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
// Create the proxy for direct property access // Create the proxy for direct property access
return new Proxy(cache, { return new Proxy(cache, {
get(target, prop: string) { get(target, prop: string) {
if (prop === 'onChange') { if (prop === "onChange") {
return (key: keyof T, callback: (value: T[keyof T]) => void) => { return (key: keyof T, callback: (value: T[keyof T]) => void) => {
if (!listeners.has(key as string)) { if (!listeners.has(key as string)) {
listeners.set(key as string, new Set()); listeners.set(key as string, new Set());
@@ -180,16 +222,16 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
return { return {
unregister: () => { unregister: () => {
listeners.get(key as string)?.delete(callback); listeners.get(key as string)?.delete(callback);
} },
}; };
}; };
} }
if (prop === 'offChange') { if (prop === "offChange") {
return (key: keyof T, callback: (value: T[keyof T]) => void) => { return (key: keyof T, callback: (value: T[keyof T]) => void) => {
listeners.get(key as string)?.delete(callback); listeners.get(key as string)?.delete(callback);
}; };
} }
if (prop === 'loaded') { if (prop === "loaded") {
return loadStoragePromise; return loadStoragePromise;
} }
@@ -197,7 +239,7 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
return target[prop]; return target[prop];
}, },
set(target, prop: string, value: any) { set(target, prop: string, value: any) {
if (['onChange', 'offChange', 'loaded'].includes(prop)) { if (["onChange", "offChange", "loaded"].includes(prop)) {
return false; return false;
} }
@@ -206,16 +248,19 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
browser.storage.local.set({ [prefix + prop]: value }); browser.storage.local.set({ [prefix + prop]: value });
// Notify listeners // Notify listeners
listeners.get(prop)?.forEach(callback => callback(value)); listeners.get(prop)?.forEach((callback) => callback(value));
return true; return true;
} },
}) as StorageAPI<T> & { [K in keyof T]: T[K] }; }) as StorageAPI<T> & { [K in keyof T]: T[K] };
} }
function createEventsAPI(pluginId: string): EventsAPI { function createEventsAPI(pluginId: string): EventsAPI {
const prefix = `plugin.${pluginId}.`; const prefix = `plugin.${pluginId}.`;
const eventListeners = new Map<string, Set<{ callback: (...args: any[]) => void, listener: EventListener }>>(); const eventListeners = new Map<
string,
Set<{ callback: (...args: any[]) => void; listener: EventListener }>
>();
return { return {
on: (event, callback) => { on: (event, callback) => {
@@ -235,20 +280,22 @@ function createEventsAPI(pluginId: string): EventsAPI {
unregister: () => { unregister: () => {
document.removeEventListener(fullEventName, listener); document.removeEventListener(fullEventName, listener);
eventListeners.get(event)?.delete({ callback, listener }); eventListeners.get(event)?.delete({ callback, listener });
} },
}; };
}, },
emit: (event, ...args) => { emit: (event, ...args) => {
document.dispatchEvent( document.dispatchEvent(
new CustomEvent(prefix + event, { new CustomEvent(prefix + event, {
detail: args.length > 0 ? args : null detail: args.length > 0 ? args : null,
}) }),
); );
}, },
}; };
} }
export function createPluginAPI<T extends PluginSettings, S = any>(plugin: Plugin<T, S>): PluginAPI<T, S> { export function createPluginAPI<T extends PluginSettings, S = any>(
plugin: Plugin<T, S>,
): PluginAPI<T, S> {
return { return {
seqta: createSEQTAAPI(), seqta: createSEQTAAPI(),
settings: createSettingsAPI(plugin), settings: createSettingsAPI(plugin),
+78 -44
View File
@@ -1,6 +1,13 @@
import type { BooleanSetting, NumberSetting, Plugin, PluginSettings, SelectSetting, StringSetting } from './types'; import type {
import { createPluginAPI } from './createAPI'; BooleanSetting,
import browser from 'webextension-polyfill'; NumberSetting,
Plugin,
PluginSettings,
SelectSetting,
StringSetting,
} from "./types";
import { createPluginAPI } from "./createAPI";
import browser from "webextension-polyfill";
interface PluginSettingsStorage { interface PluginSettingsStorage {
enabled?: boolean; enabled?: boolean;
@@ -49,7 +56,7 @@ export class PluginManager {
private async processBackloggedEvents(pluginId: string) { private async processBackloggedEvents(pluginId: string) {
for (const [key, argsList] of this.eventBacklog.entries()) { for (const [key, argsList] of this.eventBacklog.entries()) {
const [eventPluginId, event] = key.split(':'); const [eventPluginId, event] = key.split(":");
if (eventPluginId === pluginId) { if (eventPluginId === pluginId) {
for (const args of argsList) { for (const args of argsList) {
this.dispatchPluginEvent(pluginId, event, args); this.dispatchPluginEvent(pluginId, event, args);
@@ -59,7 +66,9 @@ export class PluginManager {
} }
} }
public registerPlugin<T extends PluginSettings, S>(plugin: Plugin<T, S>): void { public registerPlugin<T extends PluginSettings, S>(
plugin: Plugin<T, S>,
): void {
if (this.plugins.has(plugin.id)) { if (this.plugins.has(plugin.id)) {
throw new Error(`Plugin with id "${plugin.id}" is already registered`); throw new Error(`Plugin with id "${plugin.id}" is already registered`);
} }
@@ -82,31 +91,35 @@ export class PluginManager {
// Check if plugin is enabled before starting // Check if plugin is enabled before starting
if (plugin.disableToggle) { if (plugin.disableToggle) {
const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`); const settings = await browser.storage.local.get(
const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined; `plugin.${pluginId}.settings`,
const enabled = pluginSettings?.enabled ?? plugin.defaultEnabled ?? true; );
const pluginSettings = settings[`plugin.${pluginId}.settings`] as
| PluginSettingsStorage
| undefined;
const enabled =
pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
if (!enabled) { if (!enabled) {
console.info(`Plugin "${pluginId}" is disabled, skipping initialization`); console.info(
`Plugin "${pluginId}" is disabled, skipping initialization`,
);
return; return;
} }
} }
// Inject plugin styles if provided // Inject plugin styles if provided
if (plugin.styles) { if (plugin.styles) {
const styleElement = document.createElement('style'); const styleElement = document.createElement("style");
styleElement.textContent = plugin.styles; styleElement.textContent = plugin.styles;
document.head.appendChild(styleElement); document.head.appendChild(styleElement);
this.styleElements.set(pluginId, styleElement); this.styleElements.set(pluginId, styleElement);
} }
// Wait for both settings and storage to be loaded before starting the plugin // Wait for both settings and storage to be loaded before starting the plugin
await Promise.all([ await Promise.all([(api.settings as any).loaded, api.storage.loaded]);
(api.settings as any).loaded,
api.storage.loaded
]);
const result = await plugin.run(api); const result = await plugin.run(api);
if (typeof result === 'function') { if (typeof result === "function") {
this.cleanupFunctions.set(plugin.id, result); this.cleanupFunctions.set(plugin.id, result);
} }
this.runningPlugins.set(pluginId, true); this.runningPlugins.set(pluginId, true);
@@ -115,17 +128,20 @@ export class PluginManager {
// Process any backlogged events // Process any backlogged events
await this.processBackloggedEvents(pluginId); await this.processBackloggedEvents(pluginId);
} catch (error) { } catch (error) {
console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error); console.error(
`[BetterSEQTA+] Failed to start plugin ${pluginId}:`,
error,
);
throw error; throw error;
} }
} }
public async startAllPlugins(): Promise<void> { public async startAllPlugins(): Promise<void> {
const startPromises = Array.from(this.plugins.keys()).map(id => const startPromises = Array.from(this.plugins.keys()).map((id) =>
this.startPlugin(id).catch(error => { this.startPlugin(id).catch((error) => {
console.error(`Failed to start plugin "${id}":`, error); console.error(`Failed to start plugin "${id}":`, error);
return Promise.reject(error); return Promise.reject(error);
}) }),
); );
await Promise.allSettled(startPromises); await Promise.allSettled(startPromises);
@@ -146,11 +162,11 @@ export class PluginManager {
} }
this.runningPlugins.set(pluginId, false); this.runningPlugins.set(pluginId, false);
console.info(`Plugin "${pluginId}" stopped`); console.info(`Plugin "${pluginId}" stopped`);
this.emit('plugin.stopped', pluginId); this.emit("plugin.stopped", pluginId);
} }
public stopAllPlugins(): void { public stopAllPlugins(): void {
Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id)); Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
} }
public getPlugin(pluginId: string): Plugin | undefined { public getPlugin(pluginId: string): Plugin | undefined {
@@ -166,40 +182,49 @@ export class PluginManager {
name: string; name: string;
description: string; description: string;
settings: { settings: {
[key: string]: (Omit<BooleanSetting, 'type'> & { type: 'boolean', id: string }) | [key: string]:
(Omit<StringSetting, 'type'> & { type: 'string', id: string }) | | (Omit<BooleanSetting, "type"> & { type: "boolean"; id: string })
(Omit<NumberSetting, 'type'> & { type: 'number', id: string }) | | (Omit<StringSetting, "type"> & { type: "string"; id: string })
(Omit<SelectSetting<string>, 'type'> & { type: 'select', id: string, options: Array<{ value: string, label: string }> }); | (Omit<NumberSetting, "type"> & { type: "number"; id: string })
} | (Omit<SelectSetting<string>, "type"> & {
type: "select";
id: string;
options: Array<{ value: string; label: string }>;
});
};
}> { }> {
return Array.from(this.plugins.entries()).map(([id, plugin]) => { return Array.from(this.plugins.entries()).map(([id, plugin]) => {
const settingsEntries = Object.entries(plugin.settings).map(([key, setting]) => { const settingsEntries = Object.entries(plugin.settings).map(
([key, setting]) => {
const settingObj = setting as any; const settingObj = setting as any;
// Create a copy of the setting object without any functions // Create a copy of the setting object without any functions
const result: any = Object.fromEntries( const result: any = Object.fromEntries(
Object.entries(settingObj) Object.entries(settingObj).filter(
.filter(([_, value]) => typeof value !== 'function') ([_, value]) => typeof value !== "function",
),
); );
// Ensure required properties are present // Ensure required properties are present
result.id = key; result.id = key;
result.title = result.title || key; result.title = result.title || key;
result.description = result.description || ''; result.description = result.description || "";
result.defaultEnabled = plugin.defaultEnabled ?? true; result.defaultEnabled = plugin.defaultEnabled ?? true;
return [key, result]; return [key, result];
}); },
);
if (plugin.disableToggle) { if (plugin.disableToggle) {
settingsEntries.push([ settingsEntries.push([
'enabled', { "enabled",
id: 'enabled', {
id: "enabled",
title: plugin.name, title: plugin.name,
description: plugin.description, description: plugin.description,
type: 'boolean', type: "boolean",
default: plugin.defaultEnabled ?? true default: plugin.defaultEnabled ?? true,
} },
]) ]);
} }
return { return {
pluginId: id, pluginId: id,
@@ -218,7 +243,7 @@ export class PluginManager {
private emit(event: string, ...args: any[]): void { private emit(event: string, ...args: any[]): void {
const listeners = this.listeners.get(event); const listeners = this.listeners.get(event);
if (listeners) { if (listeners) {
listeners.forEach(listener => listener(...args)); listeners.forEach((listener) => listener(...args));
} }
} }
@@ -237,7 +262,10 @@ export class PluginManager {
} }
// Add handler for plugin enable/disable state changes // Add handler for plugin enable/disable state changes
private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise<void> { private async handlePluginStateChange(
pluginId: string,
enabled: boolean,
): Promise<void> {
if (enabled) { if (enabled) {
await this.startPlugin(pluginId); await this.startPlugin(pluginId);
} else { } else {
@@ -247,8 +275,9 @@ export class PluginManager {
// Add listener for plugin settings changes // Add listener for plugin settings changes
private setupPluginStateListener(): void { private setupPluginStateListener(): void {
browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => { browser.storage.onChanged.addListener(
if (area !== 'local') return; (changes: { [key: string]: StorageChange }, area: string) => {
if (area !== "local") return;
for (const [key, change] of Object.entries(changes)) { for (const [key, change] of Object.entries(changes)) {
const match = key.match(/^plugin\.(.+)\.settings$/); const match = key.match(/^plugin\.(.+)\.settings$/);
@@ -258,13 +287,18 @@ export class PluginManager {
const plugin = this.plugins.get(pluginId); const plugin = this.plugins.get(pluginId);
if (!plugin?.disableToggle) continue; if (!plugin?.disableToggle) continue;
const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true; const enabled =
const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? plugin.defaultEnabled ?? true; (change.newValue as PluginSettingsStorage)?.enabled ?? true;
const wasEnabled =
(change.oldValue as PluginSettingsStorage)?.enabled ??
plugin.defaultEnabled ??
true;
if (enabled !== wasEnabled) { if (enabled !== wasEnabled) {
this.handlePluginStateChange(pluginId, enabled); this.handlePluginStateChange(pluginId, enabled);
} }
} }
}); },
);
} }
} }
+5 -5
View File
@@ -1,14 +1,14 @@
import type { PluginSettings } from './types'; import type { PluginSettings } from "./types";
export function Setting(settingDef: any): PropertyDecorator { export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => { return (target, propertyKey) => {
const proto = target.constructor.prototype; const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) { if (!proto.hasOwnProperty("settings")) {
Object.defineProperty(proto, 'settings', { Object.defineProperty(proto, "settings", {
value: {}, value: {},
writable: true, writable: true,
configurable: true, configurable: true,
enumerable: true enumerable: true,
}); });
} }
@@ -27,7 +27,7 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
// Copy settings from the prototype to the instance // Copy settings from the prototype to the instance
// This ensures that each instance has its own settings object // This ensures that each instance has its own settings object
// IMPORTANT: Ensure the prototype actually HAS settings before copying // IMPORTANT: Ensure the prototype actually HAS settings before copying
if (this.constructor.prototype.hasOwnProperty('settings')) { if (this.constructor.prototype.hasOwnProperty("settings")) {
// Deep clone might be safer if settings objects become complex, // Deep clone might be safer if settings objects become complex,
// but a shallow clone is usually fine for this structure. // but a shallow clone is usually fine for this structure.
this.settings = { ...this.constructor.prototype.settings } as T; this.settings = { ...this.constructor.prototype.settings } as T;
+29 -17
View File
@@ -1,30 +1,43 @@
import type { BooleanSetting, NumberSetting, SelectSetting, StringSetting } from './types'; import type {
BooleanSetting,
NumberSetting,
SelectSetting,
StringSetting,
} from "./types";
export function numberSetting(options: Omit<NumberSetting, 'type'>): NumberSetting { export function numberSetting(
options: Omit<NumberSetting, "type">,
): NumberSetting {
return { return {
type: 'number', type: "number",
...options ...options,
}; };
} }
export function booleanSetting(options: Omit<BooleanSetting, 'type'>): BooleanSetting { export function booleanSetting(
options: Omit<BooleanSetting, "type">,
): BooleanSetting {
return { return {
type: 'boolean', type: "boolean",
...options ...options,
}; };
} }
export function stringSetting(options: Omit<StringSetting, 'type'>): StringSetting { export function stringSetting(
options: Omit<StringSetting, "type">,
): StringSetting {
return { return {
type: 'string', type: "string",
...options ...options,
}; };
} }
export function selectSetting<T extends string>(options: Omit<SelectSetting<T>, 'type'>): SelectSetting<T> { export function selectSetting<T extends string>(
options: Omit<SelectSetting<T>, "type">,
): SelectSetting<T> {
return { return {
type: 'select', type: "select",
...options ...options,
}; };
} }
@@ -35,16 +48,15 @@ export function defineSettings<T extends Record<string, any>>(settings: T): T {
export function Setting(settingDef: any): PropertyDecorator { export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => { return (target, propertyKey) => {
const proto = target.constructor.prototype; const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) { if (!proto.hasOwnProperty("settings")) {
Object.defineProperty(proto, 'settings', { Object.defineProperty(proto, "settings", {
value: {}, value: {},
writable: true, writable: true,
configurable: true, configurable: true,
enumerable: true enumerable: true,
}); });
} }
proto.settings[propertyKey] = settingDef; proto.settings[propertyKey] = settingDef;
}; };
} }
+48 -21
View File
@@ -1,14 +1,14 @@
import ReactFiber from '@/seqta/utils/ReactFiber'; import ReactFiber from "@/seqta/utils/ReactFiber";
export interface BooleanSetting { export interface BooleanSetting {
type: 'boolean'; type: "boolean";
default: boolean; default: boolean;
title: string; title: string;
description?: string; description?: string;
} }
export interface StringSetting { export interface StringSetting {
type: 'string'; type: "string";
default: string; default: string;
title: string; title: string;
description?: string; description?: string;
@@ -17,7 +17,7 @@ export interface StringSetting {
} }
export interface NumberSetting { export interface NumberSetting {
type: 'number'; type: "number";
default: number; default: number;
title: string; title: string;
description?: string; description?: string;
@@ -27,46 +27,68 @@ export interface NumberSetting {
} }
export interface SelectSetting<T extends string> { export interface SelectSetting<T extends string> {
type: 'select'; type: "select";
options: readonly T[]; options: readonly T[];
default: T; default: T;
title: string; title: string;
description?: string; description?: string;
} }
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>; export type PluginSetting =
| BooleanSetting
| StringSetting
| NumberSetting
| SelectSetting<string>;
export type PluginSettings = { export type PluginSettings = {
[key: string]: PluginSetting; [key: string]: PluginSetting;
} };
// Helper type to extract the actual value type from a setting // Helper type to extract the actual value type from a setting
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean : export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
T extends StringSetting ? string : ? boolean
T extends NumberSetting ? number : : T extends StringSetting
T extends SelectSetting<infer O> ? O : ? string
never; : T extends NumberSetting
? number
: T extends SelectSetting<infer O>
? O
: never;
export type SettingsAPI<T extends PluginSettings> = { export type SettingsAPI<T extends PluginSettings> = {
[K in keyof T]: SettingValue<T[K]>; [K in keyof T]: SettingValue<T[K]>;
} & { } & {
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => { unregister: () => void }; onChange: <K extends keyof T>(
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void; key: K,
callback: (value: SettingValue<T[K]>) => void,
) => { unregister: () => void };
offChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => void;
loaded: Promise<void>; loaded: Promise<void>;
} };
export interface SEQTAAPI { export interface SEQTAAPI {
onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void }; onMount: (
selector: string,
callback: (element: Element) => void,
) => { unregister: () => void };
getFiber: (selector: string) => ReactFiber; getFiber: (selector: string) => ReactFiber;
getCurrentPage: () => string; getCurrentPage: () => string;
onPageChange: (callback: (page: string) => void) => { unregister: () => void }; onPageChange: (callback: (page: string) => void) => {
unregister: () => void;
};
} }
export interface StorageAPI<T = any> { export interface StorageAPI<T = any> {
/** /**
* Register a callback to be called when a storage value changes * Register a callback to be called when a storage value changes
*/ */
onChange: <K extends keyof T>(key: K, callback: (value: T[K]) => void) => { unregister: () => void }; onChange: <K extends keyof T>(
key: K,
callback: (value: T[K]) => void,
) => { unregister: () => void };
/** /**
* Promise that resolves when storage values are loaded * Promise that resolves when storage values are loaded
@@ -76,10 +98,13 @@ export interface StorageAPI<T = any> {
export type TypedStorageAPI<T> = StorageAPI<T> & { export type TypedStorageAPI<T> = StorageAPI<T> & {
[K in keyof T]: T[K]; [K in keyof T]: T[K];
} };
export interface EventsAPI { export interface EventsAPI {
on: (event: string, callback: (...args: any[]) => void) => { unregister: () => void }; on: (
event: string,
callback: (...args: any[]) => void,
) => { unregister: () => void };
emit: (event: string, ...args: any[]) => void; emit: (event: string, ...args: any[]) => void;
} }
@@ -99,5 +124,7 @@ export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
styles?: string; // Optional CSS styles for the plugin styles?: string; // Optional CSS styles for the plugin
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>; run: (
api: PluginAPI<T, S>,
) => void | Promise<void> | (() => void) | Promise<() => void>;
} }
+8 -8
View File
@@ -1,12 +1,12 @@
import { PluginManager } from './core/manager'; import { PluginManager } from "./core/manager";
// plugins // plugins
import timetablePlugin from './built-in/timetable'; import timetablePlugin from "./built-in/timetable";
import notificationCollectorPlugin from './built-in/notificationCollector'; import notificationCollectorPlugin from "./built-in/notificationCollector";
import themesPlugin from './built-in/themes'; import themesPlugin from "./built-in/themes";
import animatedBackgroundPlugin from './built-in/animatedBackground'; import animatedBackgroundPlugin from "./built-in/animatedBackground";
import assessmentsAveragePlugin from './built-in/assessmentsAverage'; import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
import globalSearchPlugin from './built-in/globalSearch/src/core'; import globalSearchPlugin from "./built-in/globalSearch/src/core";
//import testPlugin from './built-in/test'; //import testPlugin from './built-in/test';
// Initialize plugin manager // Initialize plugin manager
@@ -21,7 +21,7 @@ pluginManager.registerPlugin(timetablePlugin);
pluginManager.registerPlugin(globalSearchPlugin); pluginManager.registerPlugin(globalSearchPlugin);
//pluginManager.registerPlugin(testPlugin); //pluginManager.registerPlugin(testPlugin);
export { init as Monofile } from './monofile'; export { init as Monofile } from "./monofile";
export async function initializePlugins(): Promise<void> { export async function initializePlugins(): Promise<void> {
await pluginManager.startAllPlugins(); await pluginManager.startAllPlugins();
+242 -239
View File
@@ -1,135 +1,134 @@
// Third-party libraries // Third-party libraries
import browser from "webextension-polyfill" import browser from "webextension-polyfill";
import { animate, stagger } from "motion" import { animate, stagger } from "motion";
// Internal utilities and functions // Internal utilities and functions
import { ChangeMenuItemPositions, MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions"
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"
import { waitForElm } from "@/seqta/utils/waitForElm"
import { delay } from "@/seqta/utils/delay"
import stringToHTML from "@/seqta/utils/stringToHTML"
import { MessageHandler } from "@/seqta/utils/listeners/MessageListener"
import { import {
settingsState, ChangeMenuItemPositions,
} from "@/seqta/utils/listeners/SettingsState" MenuOptionsOpen,
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges" } from "@/seqta/utils/Openers/OpenMenuOptions";
import { eventManager } from "@/seqta/utils/listeners/EventManager" import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { delay } from "@/seqta/utils/delay";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { MessageHandler } from "@/seqta/utils/listeners/MessageListener";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
// UI and theme management // UI and theme management
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners" import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners";
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements" import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements";
import { updateAllColors } from "@/seqta/ui/colors/Manager" import { updateAllColors } from "@/seqta/ui/colors/Manager";
import loading from "@/seqta/ui/Loading" import loading from "@/seqta/ui/Loading";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage" import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage" import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew" import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
// JSON content // JSON content
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json" import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
// Icons and fonts // Icons and fonts
import IconFamily from "@/resources/fonts/IconFamily.woff" import IconFamily from "@/resources/fonts/IconFamily.woff";
// Stylesheets // Stylesheets
import iframeCSS from "@/css/iframe.scss?raw" import iframeCSS from "@/css/iframe.scss?raw";
function SetDisplayNone(ElementName: string) { function SetDisplayNone(ElementName: string) {
return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}` return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}`;
} }
async function HideMenuItems(): Promise<void> { async function HideMenuItems(): Promise<void> {
try { try {
let stylesheetInnerText: string = "" let stylesheetInnerText: string = "";
for (const [menuItem, { toggle }] of Object.entries( for (const [menuItem, { toggle }] of Object.entries(
settingsState.menuitems, settingsState.menuitems,
)) { )) {
if (!toggle) { if (!toggle) {
stylesheetInnerText += SetDisplayNone(menuItem) stylesheetInnerText += SetDisplayNone(menuItem);
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`) console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
} }
} }
const menuItemStyle: HTMLStyleElement = document.createElement("style") const menuItemStyle: HTMLStyleElement = document.createElement("style");
menuItemStyle.innerText = stylesheetInnerText menuItemStyle.innerText = stylesheetInnerText;
document.head.appendChild(menuItemStyle) document.head.appendChild(menuItemStyle);
} catch (error) { } catch (error) {
console.error("[BetterSEQTA+] An error occurred:", error) console.error("[BetterSEQTA+] An error occurred:", error);
} }
} }
export function hideSideBar() { export function hideSideBar() {
const sidebar = document.getElementById("menu") // The sidebar element to be closed const sidebar = document.getElementById("menu"); // The sidebar element to be closed
const main = document.getElementById("main") // The main content element that must be resized to fill the page const main = document.getElementById("main"); // The main content element that must be resized to fill the page
const currentMenuWidth = window.getComputedStyle(sidebar!).width // Get the styles of the different elements const currentMenuWidth = window.getComputedStyle(sidebar!).width; // Get the styles of the different elements
const currentContentPosition = window.getComputedStyle(main!).position const currentContentPosition = window.getComputedStyle(main!).position;
if (currentMenuWidth != "0") { if (currentMenuWidth != "0") {
// Actually modify it to collapse the sidebar // Actually modify it to collapse the sidebar
sidebar!.style.width = "0" sidebar!.style.width = "0";
} else { } else {
sidebar!.style.width = "100%" sidebar!.style.width = "100%";
} }
if (currentContentPosition != "relative") { if (currentContentPosition != "relative") {
main!.style.position = "relative" main!.style.position = "relative";
} else { } else {
main!.style.position = "absolute" main!.style.position = "absolute";
} }
} }
export async function finishLoad() { export async function finishLoad() {
try { try {
document.querySelector(".legacy-root")?.classList.remove("hidden") document.querySelector(".legacy-root")?.classList.remove("hidden");
const loadingbk = document.getElementById("loading") const loadingbk = document.getElementById("loading");
loadingbk?.classList.add("closeLoading") loadingbk?.classList.add("closeLoading");
await delay(501) await delay(501);
loadingbk?.remove() loadingbk?.remove();
} catch (err) { } catch (err) {
console.error("Error during loading cleanup:", err) console.error("Error during loading cleanup:", err);
} }
if (settingsState.justupdated && !document.getElementById("whatsnewbk")) { if (settingsState.justupdated && !document.getElementById("whatsnewbk")) {
OpenWhatsNewPopup() OpenWhatsNewPopup();
} }
} }
export function GetCSSElement(file: string) { export function GetCSSElement(file: string) {
const cssFile = browser.runtime.getURL(file) const cssFile = browser.runtime.getURL(file);
const fileref = document.createElement("link") const fileref = document.createElement("link");
fileref.setAttribute("rel", "stylesheet") fileref.setAttribute("rel", "stylesheet");
fileref.setAttribute("type", "text/css") fileref.setAttribute("type", "text/css");
fileref.setAttribute("href", cssFile) fileref.setAttribute("href", cssFile);
return fileref return fileref;
} }
function removeThemeTagsFromNotices() { function removeThemeTagsFromNotices() {
// Grabs an array of the notice iFrames // Grabs an array of the notice iFrames
const userHTMLArray = document.getElementsByClassName("userHTML") const userHTMLArray = document.getElementsByClassName("userHTML");
// Iterates through the array, applying the iFrame css // Iterates through the array, applying the iFrame css
for (const item of userHTMLArray) { for (const item of userHTMLArray) {
// Grabs the HTML of the body tag // Grabs the HTML of the body tag
const item1 = item as HTMLIFrameElement const item1 = item as HTMLIFrameElement;
const body = item1.contentWindow!.document.querySelectorAll("body")[0] const body = item1.contentWindow!.document.querySelectorAll("body")[0];
if (body) { if (body) {
// Replaces the theme tag with nothing // Replaces the theme tag with nothing
const bodyText = body.innerHTML const bodyText = body.innerHTML;
body.innerHTML = bodyText body.innerHTML = bodyText
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/ +/, " ") .replace(/ +/, " ");
} }
} }
} }
async function updateIframesWithDarkMode(): Promise<void> { async function updateIframesWithDarkMode(): Promise<void> {
const cssLink = document.createElement("style") const cssLink = document.createElement("style");
cssLink.classList.add("iframecss") cssLink.classList.add("iframecss");
const cssContent = document.createTextNode(iframeCSS) const cssContent = document.createTextNode(iframeCSS);
cssLink.appendChild(cssContent) cssLink.appendChild(cssContent);
eventManager.register( eventManager.register(
"iframeAdded", "iframeAdded",
@@ -139,63 +138,63 @@ async function updateIframesWithDarkMode(): Promise<void> {
!element.classList.contains("iframecss"), !element.classList.contains("iframecss"),
}, },
(element) => { (element) => {
const iframe = element as HTMLIFrameElement const iframe = element as HTMLIFrameElement;
try { try {
applyDarkModeToIframe(iframe, cssLink) applyDarkModeToIframe(iframe, cssLink);
if (element.classList.contains("cke_wysiwyg_frame")) { if (element.classList.contains("cke_wysiwyg_frame")) {
(async () => { (async () => {
await delay(100) await delay(100);
iframe.contentDocument?.body.setAttribute("spellcheck", "true") iframe.contentDocument?.body.setAttribute("spellcheck", "true");
})() })();
} }
} catch (error) { } catch (error) {
console.error("Error applying dark mode:", error) console.error("Error applying dark mode:", error);
} }
}, },
) );
} }
function applyDarkModeToIframe( function applyDarkModeToIframe(
iframe: HTMLIFrameElement, iframe: HTMLIFrameElement,
cssLink: HTMLStyleElement, cssLink: HTMLStyleElement,
): void { ): void {
const iframeDocument = iframe.contentDocument const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return if (!iframeDocument) return;
iframe.onload = () => { iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink) applyDarkModeToIframe(iframe, cssLink);
} };
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
iframeDocument.documentElement.classList.add("dark") iframeDocument.documentElement.classList.add("dark");
} }
const head = iframeDocument.head const head = iframeDocument.head;
if (head && !head.innerHTML.includes("iframecss")) { if (head && !head.innerHTML.includes("iframecss")) {
head.innerHTML += cssLink.outerHTML head.innerHTML += cssLink.outerHTML;
} }
} }
function SortMessagePageItems(messagesParentElement: any) { function SortMessagePageItems(messagesParentElement: any) {
try { try {
let filterbutton = document.createElement("div") let filterbutton = document.createElement("div");
filterbutton.classList.add("messages-filterbutton") filterbutton.classList.add("messages-filterbutton");
filterbutton.innerText = "Filter" filterbutton.innerText = "Filter";
let header = document.querySelector( let header = document.querySelector(
"[class*='MessageList__MessageList___']", "[class*='MessageList__MessageList___']",
) as HTMLElement ) as HTMLElement;
header.append(filterbutton) header.append(filterbutton);
messagesParentElement messagesParentElement;
} catch (error) { } catch (error) {
console.error("Error sorting message page items:", error) console.error("Error sorting message page items:", error);
} }
} }
async function LoadPageElements(): Promise<void> { async function LoadPageElements(): Promise<void> {
await AddBetterSEQTAElements() await AddBetterSEQTAElements();
const sublink: string | undefined = window.location.href.split("/")[4] const sublink: string | undefined = window.location.href.split("/")[4];
eventManager.register( eventManager.register(
"messagesAdded", "messagesAdded",
@@ -204,7 +203,7 @@ async function LoadPageElements(): Promise<void> {
className: "messages", className: "messages",
}, },
handleMessages, handleMessages,
) );
eventManager.register( eventManager.register(
"noticesAdded", "noticesAdded",
@@ -213,7 +212,7 @@ async function LoadPageElements(): Promise<void> {
className: "notices", className: "notices",
}, },
CheckNoticeTextColour, CheckNoticeTextColour,
) );
eventManager.register( eventManager.register(
"dashboardAdded", "dashboardAdded",
@@ -222,7 +221,7 @@ async function LoadPageElements(): Promise<void> {
className: "dashboard", className: "dashboard",
}, },
handleDashboard, handleDashboard,
) );
eventManager.register( eventManager.register(
"documentsAdded", "documentsAdded",
@@ -231,7 +230,7 @@ async function LoadPageElements(): Promise<void> {
className: "documents", className: "documents",
}, },
handleDocuments, handleDocuments,
) );
eventManager.register( eventManager.register(
"reportsAdded", "reportsAdded",
@@ -240,7 +239,7 @@ async function LoadPageElements(): Promise<void> {
className: "reports", className: "reports",
}, },
handleReports, handleReports,
) );
/* eventManager.register( /* eventManager.register(
"timetableAdded", "timetableAdded",
@@ -258,21 +257,21 @@ async function LoadPageElements(): Promise<void> {
className: "notice", className: "notice",
}, },
handleNotices, handleNotices,
) );
RegisterClickListeners() RegisterClickListeners();
await handleSublink(sublink) await handleSublink(sublink);
} }
async function handleNotices(node: Element): Promise<void> { async function handleNotices(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return if (!settingsState.animations) return;
node.style.opacity = "0" node.style.opacity = "0";
// get index of node in relation to parent // get index of node in relation to parent
const index = Array.from(node.parentElement!.children).indexOf(node) const index = Array.from(node.parentElement!.children).indexOf(node);
animate( animate(
node, node,
@@ -283,71 +282,73 @@ async function handleNotices(node: Element): Promise<void> {
stiffness: 250, stiffness: 250,
damping: 20, damping: 20,
}, },
) );
} }
async function handleSublink(sublink: string | undefined): Promise<void> { async function handleSublink(sublink: string | undefined): Promise<void> {
switch (sublink) { switch (sublink) {
case "news": case "news":
await handleNewsPage() await handleNewsPage();
break break;
case undefined: case undefined:
window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`) window.location.replace(
if (settingsState.defaultPage === "home") loadHomePage() `${location.origin}/#?page=/${settingsState.defaultPage}`,
);
if (settingsState.defaultPage === "home") loadHomePage();
if (settingsState.defaultPage === "documents") if (settingsState.defaultPage === "documents")
handleDocuments(document.querySelector(".documents")!) handleDocuments(document.querySelector(".documents")!);
if (settingsState.defaultPage === "reports") if (settingsState.defaultPage === "reports")
handleReports(document.querySelector(".reports")!) handleReports(document.querySelector(".reports")!);
if (settingsState.defaultPage === "messages") if (settingsState.defaultPage === "messages")
handleMessages(document.querySelector(".messages")!) handleMessages(document.querySelector(".messages")!);
finishLoad() finishLoad();
break break;
case "home": case "home":
window.location.replace(`${location.origin}/#?page=/home`) window.location.replace(`${location.origin}/#?page=/home`);
console.info("[BetterSEQTA+] Started Init") console.info("[BetterSEQTA+] Started Init");
if (settingsState.onoff) loadHomePage() if (settingsState.onoff) loadHomePage();
finishLoad() finishLoad();
break break;
default: default:
await handleDefault() await handleDefault();
break break;
} }
} }
async function handleNewsPage(): Promise<void> { async function handleNewsPage(): Promise<void> {
console.info("[BetterSEQTA+] Started Init") console.info("[BetterSEQTA+] Started Init");
if (settingsState.onoff) { if (settingsState.onoff) {
SendNewsPage() SendNewsPage();
finishLoad() finishLoad();
} }
} }
async function handleDefault(): Promise<void> { async function handleDefault(): Promise<void> {
finishLoad() finishLoad();
} }
async function handleMessages(node: Element): Promise<void> { async function handleMessages(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return if (!(node instanceof HTMLElement)) return;
const element = document.getElementById("title")!.firstChild as HTMLElement const element = document.getElementById("title")!.firstChild as HTMLElement;
element.innerText = "Direct Messages" element.innerText = "Direct Messages";
document.title = "Direct Messages ― SEQTA Learn" document.title = "Direct Messages ― SEQTA Learn";
SortMessagePageItems(node) SortMessagePageItems(node);
if (!settingsState.animations) return if (!settingsState.animations) return;
// Hides messages on page load // Hides messages on page load
const style = document.createElement("style") const style = document.createElement("style");
style.classList.add("messageHider") style.classList.add("messageHider");
style.innerHTML = "[data-message]{opacity: 0 !important;}" style.innerHTML = "[data-message]{opacity: 0 !important;}";
document.head.append(style) document.head.append(style);
await waitForElm("[data-message]", true, 10) await waitForElm("[data-message]", true, 10);
const messages = Array.from( const messages = Array.from(
document.querySelectorAll("[data-message]"), document.querySelectorAll("[data-message]"),
).slice(0, 35) ).slice(0, 35);
animate( animate(
messages, messages,
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -356,21 +357,21 @@ async function handleMessages(node: Element): Promise<void> {
duration: 0.5, duration: 0.5,
ease: [0.22, 0.03, 0.26, 1], ease: [0.22, 0.03, 0.26, 1],
}, },
) );
document.head.querySelector("style.messageHider")?.remove() document.head.querySelector("style.messageHider")?.remove();
} }
async function handleDashboard(node: Element): Promise<void> { async function handleDashboard(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return if (!settingsState.animations) return;
const style = document.createElement("style") const style = document.createElement("style");
style.classList.add("dashboardHider") style.classList.add("dashboardHider");
style.innerHTML = ".dashboard{opacity: 0 !important;}" style.innerHTML = ".dashboard{opacity: 0 !important;}";
document.head.append(style) document.head.append(style);
await waitForElm(".dashlet", true, 10) await waitForElm(".dashlet", true, 10);
animate( animate(
".dashboard > *", ".dashboard > *",
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -379,16 +380,16 @@ async function handleDashboard(node: Element): Promise<void> {
duration: 0.5, duration: 0.5,
ease: [0.22, 0.03, 0.26, 1], ease: [0.22, 0.03, 0.26, 1],
}, },
) );
document.head.querySelector("style.dashboardHider")?.remove() document.head.querySelector("style.dashboardHider")?.remove();
} }
async function handleDocuments(node: Element): Promise<void> { async function handleDocuments(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return if (!settingsState.animations) return;
await waitForElm(".document", true, 10) await waitForElm(".document", true, 10);
animate( animate(
".documents tbody tr.document", ".documents tbody tr.document",
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -397,14 +398,14 @@ async function handleDocuments(node: Element): Promise<void> {
duration: 0.5, duration: 0.5,
ease: [0.22, 0.03, 0.26, 1], ease: [0.22, 0.03, 0.26, 1],
}, },
) );
} }
async function handleReports(node: Element): Promise<void> { async function handleReports(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return if (!settingsState.animations) return;
await waitForElm(".report", true, 10) await waitForElm(".report", true, 10);
animate( animate(
".reports .item", ".reports .item",
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
@@ -413,7 +414,7 @@ async function handleReports(node: Element): Promise<void> {
duration: 0.5, duration: 0.5,
ease: [0.22, 0.03, 0.26, 1], ease: [0.22, 0.03, 0.26, 1],
}, },
) );
} }
function CheckNoticeTextColour(notice: any) { function CheckNoticeTextColour(notice: any) {
@@ -425,60 +426,60 @@ function CheckNoticeTextColour(notice: any) {
parentElement: notice, parentElement: notice,
}, },
(node) => { (node) => {
var hex = (node as HTMLElement).style.cssText.split(" ")[1] var hex = (node as HTMLElement).style.cssText.split(" ")[1];
if (hex) { if (hex) {
const hex1 = hex.slice(0, -1) const hex1 = hex.slice(0, -1);
var threshold = GetThresholdOfColor(hex1) var threshold = GetThresholdOfColor(hex1);
if (settingsState.DarkMode && threshold < 100) { if (settingsState.DarkMode && threshold < 100) {
(node as HTMLElement).style.cssText = "--color: undefined;" (node as HTMLElement).style.cssText = "--color: undefined;";
} }
} }
}, },
) );
} }
export function tryLoad() { export function tryLoad() {
waitForElm(".login").then(() => { waitForElm(".login").then(() => {
finishLoad() finishLoad();
}) });
waitForElm(".day-container").then(() => { waitForElm(".day-container").then(() => {
finishLoad() finishLoad();
}) });
waitForElm("[data-key=welcome]").then((elm: any) => { waitForElm("[data-key=welcome]").then((elm: any) => {
elm.classList.remove("active") elm.classList.remove("active");
}) });
waitForElm(".code", true, 50).then((elm: any) => { waitForElm(".code", true, 50).then((elm: any) => {
if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements() if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements();
}) });
updateIframesWithDarkMode() updateIframesWithDarkMode();
// Waits for page to call on load, run scripts // Waits for page to call on load, run scripts
document.addEventListener( document.addEventListener(
"load", "load",
function () { function () {
removeThemeTagsFromNotices() removeThemeTagsFromNotices();
}, },
true, true,
) );
} }
function ReplaceMenuSVG(element: HTMLElement, svg: string) { function ReplaceMenuSVG(element: HTMLElement, svg: string) {
let item = element.firstChild as HTMLElement let item = element.firstChild as HTMLElement;
item!.firstChild!.remove() item!.firstChild!.remove();
item.innerHTML = `<span>${item.innerHTML}</span>` item.innerHTML = `<span>${item.innerHTML}</span>`;
let newsvg = stringToHTML(svg).firstChild let newsvg = stringToHTML(svg).firstChild;
item.insertBefore(newsvg as Node, item.firstChild) item.insertBefore(newsvg as Node, item.firstChild);
} }
const processedSymbol = Symbol('processed') const processedSymbol = Symbol("processed");
export async function ObserveMenuItemPosition() { export async function ObserveMenuItemPosition() {
await waitForElm("#menu > ul > li") await waitForElm("#menu > ul > li");
eventManager.register( eventManager.register(
"menuList", "menuList",
@@ -486,71 +487,73 @@ export async function ObserveMenuItemPosition() {
parentElement: document.querySelector("#menu")!.firstChild as Element, parentElement: document.querySelector("#menu")!.firstChild as Element,
}, },
(element: Element) => { (element: Element) => {
const node = element as HTMLElement const node = element as HTMLElement;
// Only process top-level menu items and skip everything else // Only process top-level menu items and skip everything else
if (!node.classList.contains('item') || if (
node.nodeName !== 'LI' || !node.classList.contains("item") ||
node.parentElement?.parentElement?.id !== 'menu') { node.nodeName !== "LI" ||
return node.parentElement?.parentElement?.id !== "menu"
) {
return;
} }
// Early exit if already processed // Early exit if already processed
if ((element as any)[processedSymbol]) { if ((element as any)[processedSymbol]) {
return return;
} }
if (!node?.dataset?.checked && !MenuOptionsOpen) { if (!node?.dataset?.checked && !MenuOptionsOpen) {
const key = const key =
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey] MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
if (key) { if (key) {
ReplaceMenuSVG( ReplaceMenuSVG(
node, node,
MenuitemSVGKey[node.dataset.key as keyof typeof MenuitemSVGKey], MenuitemSVGKey[node.dataset.key as keyof typeof MenuitemSVGKey],
) );
} else if (node?.firstChild?.nodeName === "LABEL") { } else if (node?.firstChild?.nodeName === "LABEL") {
const label = node.firstChild as HTMLElement const label = node.firstChild as HTMLElement;
let textNode = label.lastChild as HTMLElement let textNode = label.lastChild as HTMLElement;
if ( if (
textNode.nodeType === 3 && textNode.nodeType === 3 &&
textNode.parentNode && textNode.parentNode &&
textNode.parentNode.nodeName !== "SPAN" textNode.parentNode.nodeName !== "SPAN"
) { ) {
const span = document.createElement("span") const span = document.createElement("span");
span.textContent = textNode.nodeValue span.textContent = textNode.nodeValue;
label.replaceChild(span, textNode) label.replaceChild(span, textNode);
} }
} }
ChangeMenuItemPositions(settingsState.menuorder); ChangeMenuItemPositions(settingsState.menuorder);
(element as any)[processedSymbol] = true (element as any)[processedSymbol] = true;
} }
}, },
) );
} }
export function showConflictPopup() { export function showConflictPopup() {
if (document.getElementById("conflict-popup")) return if (document.getElementById("conflict-popup")) return;
document.body.classList.remove("hidden") document.body.classList.remove("hidden");
const background = document.createElement("div") const background = document.createElement("div");
background.id = "conflict-popup" background.id = "conflict-popup";
background.classList.add("whatsnewBackground") background.classList.add("whatsnewBackground");
background.style.zIndex = "10000000" background.style.zIndex = "10000000";
const container = document.createElement("div") const container = document.createElement("div");
container.classList.add("whatsnewContainer") container.classList.add("whatsnewContainer");
container.style.height = "auto" container.style.height = "auto";
const headerHTML = /* html */ ` const headerHTML = /* html */ `
<div class="whatsnewHeader"> <div class="whatsnewHeader">
<h1>Extension Conflict Detected</h1> <h1>Extension Conflict Detected</h1>
<p>Legacy BetterSEQTA Installed</p> <p>Legacy BetterSEQTA Installed</p>
</div> </div>
` `;
const header = stringToHTML(headerHTML).firstChild const header = stringToHTML(headerHTML).firstChild;
const textHTML = /* html */ ` const textHTML = /* html */ `
<div class="whatsnewTextContainer" style="overflow-y: auto; font-size: 1.3rem;"> <div class="whatsnewTextContainer" style="overflow-y: auto; font-size: 1.3rem;">
@@ -562,91 +565,91 @@ export function showConflictPopup() {
Please remove the older BetterSEQTA extension to ensure that BetterSEQTA+ works correctly. Please remove the older BetterSEQTA extension to ensure that BetterSEQTA+ works correctly.
</p> </p>
</div> </div>
` `;
const text = stringToHTML(textHTML).firstChild const text = stringToHTML(textHTML).firstChild;
const exitButton = document.createElement("div") const exitButton = document.createElement("div");
exitButton.id = "whatsnewclosebutton" exitButton.id = "whatsnewclosebutton";
if (header) container.append(header) if (header) container.append(header);
if (text) container.append(text) if (text) container.append(text);
container.append(exitButton) container.append(exitButton);
background.append(container) background.append(container);
document.getElementById("container")?.append(background) document.getElementById("container")?.append(background);
if (settingsState.animations) { if (settingsState.animations) {
animate([background as HTMLElement], { opacity: [0, 1] }) animate([background as HTMLElement], { opacity: [0, 1] });
} }
background.addEventListener("click", (event) => { background.addEventListener("click", (event) => {
if (event.target === background) { if (event.target === background) {
background.remove() background.remove();
} }
}) });
exitButton.addEventListener("click", () => { exitButton.addEventListener("click", () => {
background.remove() background.remove();
}) });
} }
export function init() { export function init() {
const handleDisabled = () => { const handleDisabled = () => {
waitForElm(".code", true, 50).then(AppendElementsToDisabledPage) waitForElm(".code", true, 50).then(AppendElementsToDisabledPage);
} };
if (settingsState.onoff) { if (settingsState.onoff) {
console.info("[BetterSEQTA+] Enabled") console.info("[BetterSEQTA+] Enabled");
if (settingsState.DarkMode) document.documentElement.classList.add("dark"); if (settingsState.DarkMode) document.documentElement.classList.add("dark");
document.querySelector(".legacy-root")?.classList.add("hidden") document.querySelector(".legacy-root")?.classList.add("hidden");
ObserveMenuItemPosition(); ObserveMenuItemPosition();
new StorageChangeHandler() new StorageChangeHandler();
new MessageHandler() new MessageHandler();
updateAllColors() updateAllColors();
loading() loading();
InjectCustomIcons() InjectCustomIcons();
HideMenuItems() HideMenuItems();
tryLoad() tryLoad();
setTimeout(() => { setTimeout(() => {
const legacyElement = document.querySelector( const legacyElement = document.querySelector(
".outside-container .bottom-container", ".outside-container .bottom-container",
) );
if (legacyElement) { if (legacyElement) {
console.log("Legacy extension detected") console.log("Legacy extension detected");
showConflictPopup() showConflictPopup();
} }
}, 1000) }, 1000);
} else { } else {
handleDisabled() handleDisabled();
window.addEventListener("load", handleDisabled) window.addEventListener("load", handleDisabled);
} }
} }
function InjectCustomIcons() { function InjectCustomIcons() {
console.info("[BetterSEQTA+] Injecting Icons") console.info("[BetterSEQTA+] Injecting Icons");
const style = document.createElement("style") const style = document.createElement("style");
style.setAttribute("type", "text/css") style.setAttribute("type", "text/css");
style.innerHTML = ` style.innerHTML = `
@font-face { @font-face {
font-family: 'IconFamily'; font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff'); src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
}` }`;
document.head.appendChild(style) document.head.appendChild(style);
} }
export function AppendElementsToDisabledPage() { export function AppendElementsToDisabledPage() {
console.info("[BetterSEQTA+] Appending elements to disabled page") console.info("[BetterSEQTA+] Appending elements to disabled page");
AddBetterSEQTAElements() AddBetterSEQTAElements();
let settingsStyle = document.createElement("style") let settingsStyle = document.createElement("style");
settingsStyle.innerHTML = /* css */ ` settingsStyle.innerHTML = /* css */ `
.addedButton { .addedButton {
position: absolute !important; position: absolute !important;
@@ -671,6 +674,6 @@ export function AppendElementsToDisabledPage() {
box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6); box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6);
transform-origin: 70% 0; transform-origin: 70% 0;
} }
` `;
document.head.append(settingsStyle) document.head.append(settingsStyle);
} }
+2 -3
View File
@@ -2,6 +2,5 @@ module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
} },
} };
+3 -2
View File
@@ -1,6 +1,7 @@
@font-face { @font-face {
font-family: 'IconFamily'; font-family: "IconFamily";
src: local('IconFamily') local('Icon Family') url('/fonts/IconFamily.woff') format('woff'); src: local("IconFamily") local("Icon Family") url("/fonts/IconFamily.woff")
format("woff");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
File diff suppressed because it is too large Load Diff
+26 -18
View File
@@ -12,22 +12,25 @@ h1 {
font-weight: normal; font-weight: normal;
} }
small { small {
font-size: .66666667em; font-size: 0.66666667em;
} }
a { a {
color: #e74c3c; color: #e74c3c;
text-decoration: none; text-decoration: none;
} }
a:hover, a:focus { a:hover,
a:focus {
box-shadow: 0 1px #e74c3c; box-shadow: 0 1px #e74c3c;
} }
.bshadow0, input { .bshadow0,
input {
box-shadow: inset 0 -2px #e7e7e7; box-shadow: inset 0 -2px #e7e7e7;
} }
input:hover { input:hover {
box-shadow: inset 0 -2px #ccc; box-shadow: inset 0 -2px #ccc;
} }
input, fieldset { input,
fieldset {
font-family: sans-serif; font-family: sans-serif;
font-size: 1em; font-size: 1em;
margin: 0; margin: 0;
@@ -38,7 +41,7 @@ input {
color: inherit; color: inherit;
line-height: 1.5; line-height: 1.5;
height: 1.5em; height: 1.5em;
padding: .25em 0; padding: 0.25em 0;
} }
input:focus { input:focus {
outline: none; outline: none;
@@ -77,19 +80,22 @@ p {
margin-bottom: 1em; margin-bottom: 1em;
} }
.mvm { .mvm {
margin-top: .75em; margin-top: 0.75em;
margin-bottom: .75em; margin-bottom: 0.75em;
} }
.mtn { .mtn {
margin-top: 0; margin-top: 0;
} }
.mtl, .mal { .mtl,
.mal {
margin-top: 1.5em; margin-top: 1.5em;
} }
.mbl, .mal { .mbl,
.mal {
margin-bottom: 1.5em; margin-bottom: 1.5em;
} }
.mal, .mhl { .mal,
.mhl {
margin-left: 1.5em; margin-left: 1.5em;
margin-right: 1.5em; margin-right: 1.5em;
} }
@@ -98,16 +104,18 @@ p {
margin-right: 1em; margin-right: 1em;
} }
.mls { .mls {
margin-left: .25em; margin-left: 0.25em;
} }
.ptl { .ptl {
padding-top: 1.5em; padding-top: 1.5em;
} }
.pbs, .pvs { .pbs,
padding-bottom: .25em; .pvs {
padding-bottom: 0.25em;
} }
.pvs, .pts { .pvs,
padding-top: .25em; .pts {
padding-top: 0.25em;
} }
.unit { .unit {
float: left; float: left;
@@ -121,7 +129,8 @@ p {
.size1of1 { .size1of1 {
width: 100%; width: 100%;
} }
.clearfix:before, .clearfix:after { .clearfix:before,
.clearfix:after {
content: " "; content: " ";
display: table; display: table;
} }
@@ -134,7 +143,7 @@ p {
.textbox0 { .textbox0 {
width: 3em; width: 3em;
background: #f1f1f1; background: #f1f1f1;
padding: .25em .5em; padding: 0.25em 0.5em;
line-height: 1.5; line-height: 1.5;
height: 1.5em; height: 1.5em;
} }
@@ -149,4 +158,3 @@ p {
.fs1 { .fs1 {
font-size: 16px; font-size: 16px;
} }
+14 -12
View File
@@ -1,19 +1,21 @@
if (!('boxShadow' in document.body.style)) { if (!("boxShadow" in document.body.style)) {
document.body.setAttribute('class', 'noBoxShadow'); document.body.setAttribute("class", "noBoxShadow");
} }
document.body.addEventListener("click", function (e) { document.body.addEventListener("click", function (e) {
var target = e.target; var target = e.target;
if (target.tagName === "INPUT" && if (
target.getAttribute('class').indexOf('liga') === -1) { target.tagName === "INPUT" &&
target.getAttribute("class").indexOf("liga") === -1
) {
target.select(); target.select();
} }
}); });
(function () { (function () {
var fontSize = document.getElementById('fontSize'), var fontSize = document.getElementById("fontSize"),
testDrive = document.getElementById('testDrive'), testDrive = document.getElementById("testDrive"),
testText = document.getElementById('testText'); testText = document.getElementById("testText");
function updateTest() { function updateTest() {
testDrive.innerHTML = testText.value || String.fromCharCode(160); testDrive.innerHTML = testText.value || String.fromCharCode(160);
if (window.icomoonLiga) { if (window.icomoonLiga) {
@@ -21,10 +23,10 @@ document.body.addEventListener("click", function(e) {
} }
} }
function updateSize() { function updateSize() {
testDrive.style.fontSize = fontSize.value + 'px'; testDrive.style.fontSize = fontSize.value + "px";
} }
fontSize.addEventListener('change', updateSize, false); fontSize.addEventListener("change", updateSize, false);
testText.addEventListener('input', updateTest, false); testText.addEventListener("input", updateTest, false);
testText.addEventListener('change', updateTest, false); testText.addEventListener("change", updateTest, false);
updateSize(); updateSize();
}()); })();
+10 -8
View File
@@ -1,21 +1,23 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: "icomoon";
src: url('fonts/icomoon.eot?biv4go'); src: url("fonts/icomoon.eot?biv4go");
src: url('fonts/icomoon.eot?biv4go#iefix') format('embedded-opentype'), src:
url('IconFamily.woff') format('woff'), url("fonts/icomoon.eot?biv4go#iefix") format("embedded-opentype"),
url('fonts/icomoon.svg?biv4go#icomoon') format('svg'); url("IconFamily.woff") format("woff"),
url("fonts/icomoon.svg?biv4go#icomoon") format("svg");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
} }
input.unitRight { input.unitRight {
font-family: 'icomoon'; font-family: "icomoon";
} }
[class^="icon-"], [class*=" icon-"] { [class^="icon-"],
[class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important; font-family: "icomoon" !important;
speak: never; speak: never;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
+16 -18
View File
@@ -1,42 +1,40 @@
// Third-party libraries // Third-party libraries
import browser from "webextension-polyfill" import browser from "webextension-polyfill";
// Internal utilities and functions // Internal utilities and functions
import { import { settingsState } from "@/seqta/utils/listeners/SettingsState";
settingsState,
} from "@/seqta/utils/listeners/SettingsState"
// UI and theme management // UI and theme management
import pageState from "@/pageState.js?url" import pageState from "@/pageState.js?url";
// Stylesheets // Stylesheets
import injectedCSS from "@/css/injected.scss?inline" import injectedCSS from "@/css/injected.scss?inline";
export async function main() { export async function main() {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
if (settingsState.onoff) { if (settingsState.onoff) {
injectPageState() injectPageState();
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs // TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
if (import.meta.env.MODE === "development") { if (import.meta.env.MODE === "development") {
import("../css/injected.scss") import("../css/injected.scss");
} else { } else {
const injectedStyle = document.createElement("style") const injectedStyle = document.createElement("style");
injectedStyle.textContent = injectedCSS injectedStyle.textContent = injectedCSS;
document.head.appendChild(injectedStyle) document.head.appendChild(injectedStyle);
} }
} }
resolve(true) resolve(true);
} catch (error: any) { } catch (error: any) {
console.error(error) console.error(error);
reject(error) reject(error);
} }
}) });
} }
function injectPageState() { function injectPageState() {
const mainScript = document.createElement("script") const mainScript = document.createElement("script");
mainScript.src = browser.runtime.getURL(pageState) mainScript.src = browser.runtime.getURL(pageState);
document.head.appendChild(mainScript) document.head.appendChild(mainScript);
} }
+79 -61
View File
@@ -3,7 +3,6 @@ import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage"; import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton"; import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"; import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { appendBackgroundToUI } from "./ImageBackgrounds"; import { appendBackgroundToUI } from "./ImageBackgrounds";
import stringToHTML from "@/seqta/utils/stringToHTML"; import stringToHTML from "@/seqta/utils/stringToHTML";
@@ -18,12 +17,12 @@ async function getUserInfo() {
try { try {
const response = await fetch(`${location.origin}/seqta/student/login`, { const response = await fetch(`${location.origin}/seqta/student/login`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', "Content-Type": "application/json; charset=utf-8",
}, },
body: JSON.stringify({ body: JSON.stringify({
mode: 'normal', mode: "normal",
query: null, query: null,
redirect_url: location.origin, redirect_url: location.origin,
}), }),
@@ -33,7 +32,7 @@ async function getUserInfo() {
cachedUserInfo = responseData.payload; cachedUserInfo = responseData.payload;
return cachedUserInfo; return cachedUserInfo;
} catch (error) { } catch (error) {
console.error('Error fetching user info:', error); console.error("Error fetching user info:", error);
throw error; throw error;
} }
} }
@@ -41,11 +40,11 @@ async function getUserInfo() {
export async function AddBetterSEQTAElements() { export async function AddBetterSEQTAElements() {
if (settingsState.onoff) { if (settingsState.onoff) {
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add("dark");
} }
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
const menu = document.getElementById('menu')!; const menu = document.getElementById("menu")!;
const menuList = menu.firstChild as HTMLElement; const menuList = menu.firstChild as HTMLElement;
createHomeButton(fragment, menuList); createHomeButton(fragment, menuList);
@@ -57,10 +56,10 @@ export async function AddBetterSEQTAElements() {
await Promise.all([ await Promise.all([
appendBackgroundToUI(), appendBackgroundToUI(),
handleUserInfo(), handleUserInfo(),
handleStudentData() handleStudentData(),
]); ]);
} catch (error) { } catch (error) {
console.error('Error initializing UI elements:', error); console.error("Error initializing UI elements:", error);
} }
setupEventListeners(); setupEventListeners();
@@ -74,12 +73,14 @@ export async function AddBetterSEQTAElements() {
} }
function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) { function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) {
const container = document.getElementById('content')!; const container = document.getElementById("content")!;
const div = document.createElement('div'); const div = document.createElement("div");
div.classList.add('titlebar'); div.classList.add("titlebar");
container.append(div); container.append(div);
const NewButton = stringToHTML('<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>'); const NewButton = stringToHTML(
'<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>',
);
if (NewButton.firstChild) { if (NewButton.firstChild) {
fragment.appendChild(NewButton.firstChild); fragment.appendChild(NewButton.firstChild);
} }
@@ -90,7 +91,7 @@ async function handleUserInfo() {
const info = await getUserInfo(); const info = await getUserInfo();
updateUserInfo(info); updateUserInfo(info);
} catch (error) { } catch (error) {
console.error('Error fetching and processing student data:', error); console.error("Error fetching and processing student data:", error);
} }
} }
@@ -112,7 +113,7 @@ function updateUserInfo(info: {
userDesc: string | null; userDesc: string | null;
userName: string | null; userName: string | null;
}) { }) {
const titlebar = document.getElementsByClassName('titlebar')[0]; const titlebar = document.getElementsByClassName("titlebar")[0];
const userInfo = stringToHTML(/* html */ ` const userInfo = stringToHTML(/* html */ `
<div class="userInfosvgdiv tooltip"> <div class="userInfosvgdiv tooltip">
@@ -135,26 +136,29 @@ function updateUserInfo(info: {
`).firstChild; `).firstChild;
titlebar.append(userinfo!); titlebar.append(userinfo!);
var logoutbutton = document.getElementsByClassName('logout')[0]; var logoutbutton = document.getElementsByClassName("logout")[0];
var userInfosvgdiv = document.getElementById('logouttooltip')!; var userInfosvgdiv = document.getElementById("logouttooltip")!;
userInfosvgdiv.appendChild(logoutbutton); userInfosvgdiv.appendChild(logoutbutton);
} }
async function handleStudentData() { async function handleStudentData() {
try { try {
const response = await fetch(`${location.origin}/seqta/student/load/message/people`, { const response = await fetch(
method: 'POST', `${location.origin}/seqta/student/load/message/people`,
{
method: "POST",
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', "Content-Type": "application/json; charset=utf-8",
}, },
body: JSON.stringify({ mode: 'student' }), body: JSON.stringify({ mode: "student" }),
}); },
);
const responseData = await response.json(); const responseData = await response.json();
let students = responseData.payload; let students = responseData.payload;
await updateStudentInfo(students); await updateStudentInfo(students);
} catch (error) { } catch (error) {
console.error('Error fetching and processing student data:', error); console.error("Error fetching and processing student data:", error);
} }
} }
@@ -162,12 +166,12 @@ async function updateStudentInfo(students: any) {
const info = await getUserInfo(); const info = await getUserInfo();
var index = students.findIndex(function (person: any) { var index = students.findIndex(function (person: any) {
return ( return (
person.firstname == info.userDesc.split(' ')[0] && person.firstname == info.userDesc.split(" ")[0] &&
person.surname == info.userDesc.split(' ')[1] person.surname == info.userDesc.split(" ")[1]
); );
}); });
let houseelement1 = document.getElementsByClassName('userInfohouse')[0]; let houseelement1 = document.getElementsByClassName("userInfohouse")[0];
const houseelement = houseelement1 as HTMLElement; const houseelement = houseelement1 as HTMLElement;
if (students[index]?.house) { if (students[index]?.house) {
@@ -175,7 +179,8 @@ async function updateStudentInfo(students: any) {
houseelement.style.background = students[index].house_colour; houseelement.style.background = students[index].house_colour;
try { try {
let colorresult = GetThresholdOfColor(students[index]?.house_colour); let colorresult = GetThresholdOfColor(students[index]?.house_colour);
houseelement.style.color = colorresult && colorresult > 300 ? 'black' : 'white'; houseelement.style.color =
colorresult && colorresult > 300 ? "black" : "white";
houseelement.innerText = students[index].year + students[index].house; houseelement.innerText = students[index].year + students[index].house;
} catch (error) { } catch (error) {
houseelement.innerText = students[index].house; houseelement.innerText = students[index].house;
@@ -185,46 +190,55 @@ async function updateStudentInfo(students: any) {
try { try {
houseelement.innerText = students[index].year; houseelement.innerText = students[index].year;
} catch (err) { } catch (err) {
houseelement.innerText = 'N/A'; houseelement.innerText = "N/A";
} }
} }
} }
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) { function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
const NewsButtonStr = '<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>'; const NewsButtonStr =
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>';
const NewsButton = stringToHTML(NewsButtonStr); const NewsButton = stringToHTML(NewsButtonStr);
if (NewsButton.firstChild) { if (NewsButton.firstChild) {
fragment.appendChild(NewsButton.firstChild); fragment.appendChild(NewsButton.firstChild);
} }
let iconCover = document.createElement('div'); let iconCover = document.createElement("div");
iconCover.classList.add('icon-cover'); iconCover.classList.add("icon-cover");
iconCover.id = 'icon-cover'; iconCover.id = "icon-cover";
menu.appendChild(iconCover); menu.appendChild(iconCover);
} }
function setupEventListeners() { function setupEventListeners() {
const menuCover = document.querySelector('#icon-cover'); const menuCover = document.querySelector("#icon-cover");
const homebutton = document.getElementById('homebutton'); const homebutton = document.getElementById("homebutton");
const newsbutton = document.getElementById('newsbutton'); const newsbutton = document.getElementById("newsbutton");
homebutton?.addEventListener('click', function() { homebutton?.addEventListener("click", function () {
if (!homebutton.classList.contains('draggable') && !homebutton.classList.contains('active')) { if (
!homebutton.classList.contains("draggable") &&
!homebutton.classList.contains("active")
) {
loadHomePage(); loadHomePage();
} }
}); });
newsbutton?.addEventListener('click', function() { newsbutton?.addEventListener("click", function () {
if (!newsbutton.classList.contains('draggable') && !newsbutton.classList.contains('active')) { if (
!newsbutton.classList.contains("draggable") &&
!newsbutton.classList.contains("active")
) {
SendNewsPage(); SendNewsPage();
} }
}); });
menuCover?.addEventListener('click', function() { menuCover?.addEventListener("click", function () {
location.href = '../#?page=/home'; location.href = "../#?page=/home";
loadHomePage(); loadHomePage();
(document.getElementById('menu')!.firstChild! as HTMLElement).classList.remove('noscroll'); (
document.getElementById("menu")!.firstChild! as HTMLElement
).classList.remove("noscroll");
}); });
} }
@@ -234,26 +248,26 @@ async function createSettingsButton() {
<svg width="24" height="24" viewBox="0 0 24 24"> <svg width="24" height="24" viewBox="0 0 24 24">
<g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g> <g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g>
</svg> </svg>
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ''} ${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
</button> </button>
`); `);
let ContentDiv = document.getElementById('content'); let ContentDiv = document.getElementById("content");
ContentDiv!.append(SettingsButton.firstChild!); ContentDiv!.append(SettingsButton.firstChild!);
} }
function GetLightDarkModeString() { function GetLightDarkModeString() {
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
return 'Switch to light theme' return "Switch to light theme";
} else { } else {
return 'Switch to dark theme' return "Switch to dark theme";
} }
} }
async function addDarkLightToggle() { async function addDarkLightToggle() {
const tooltipString = GetLightDarkModeString(); const tooltipString = GetLightDarkModeString();
const svgContent = settingsState.DarkMode ? const svgContent = settingsState.DarkMode
/* html */`<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>` : ? /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`
/* html */`<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`; : /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
const LightDarkModeButton = stringToHTML(/* html */ ` const LightDarkModeButton = stringToHTML(/* html */ `
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton"> <button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
@@ -262,22 +276,27 @@ async function addDarkLightToggle() {
</button> </button>
`); `);
let ContentDiv = document.getElementById('content'); let ContentDiv = document.getElementById("content");
ContentDiv!.append(LightDarkModeButton.firstChild!); ContentDiv!.append(LightDarkModeButton.firstChild!);
updateAllColors(); updateAllColors();
document.getElementById('LightDarkModeButton')!.addEventListener('click', async () => { document
const darklightText = document.getElementById('darklighttooliptext'); .getElementById("LightDarkModeButton")!
.addEventListener("click", async () => {
const darklightText = document.getElementById("darklighttooliptext");
if (settingsState.originalDarkMode != undefined && settingsState.selectedTheme) { if (
darklightText!.innerText = 'Locked by current theme'; settingsState.originalDarkMode != undefined &&
settingsState.selectedTheme
) {
darklightText!.innerText = "Locked by current theme";
await delay(1000) await delay(1000);
darklightText!.innerText = GetLightDarkModeString(); darklightText!.innerText = GetLightDarkModeString();
return return;
} }
settingsState.DarkMode = !settingsState.DarkMode; settingsState.DarkMode = !settingsState.DarkMode;
@@ -289,15 +308,14 @@ async function addDarkLightToggle() {
} }
function customizeMenuToggle() { function customizeMenuToggle() {
const menuToggle = document.getElementById('menuToggle'); const menuToggle = document.getElementById("menuToggle");
if (menuToggle) { if (menuToggle) {
menuToggle.innerHTML = ''; menuToggle.innerHTML = "";
} }
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const line = document.createElement('div'); const line = document.createElement("div");
line.className = 'hamburger-line'; line.className = "hamburger-line";
menuToggle!.appendChild(line); menuToggle!.appendChild(line);
} }
} }
+28 -24
View File
@@ -1,15 +1,18 @@
import { getDataById, isIndexedDBSupported } from '@/interface/hooks/BackgroundDataLoader'; import {
getDataById,
isIndexedDBSupported,
} from "@/interface/hooks/BackgroundDataLoader";
export async function appendBackgroundToUI() { export async function appendBackgroundToUI() {
const parent = document.getElementById('container'); const parent = document.getElementById("container");
if (!parent) return; if (!parent) return;
const backgroundContainer = document.createElement('div'); const backgroundContainer = document.createElement("div");
backgroundContainer.classList.add('imageBackground'); backgroundContainer.classList.add("imageBackground");
backgroundContainer.setAttribute('excludeDarkCheck', 'true'); backgroundContainer.setAttribute("excludeDarkCheck", "true");
const mediaContainer = document.createElement('div'); const mediaContainer = document.createElement("div");
mediaContainer.id = 'media-container'; mediaContainer.id = "media-container";
backgroundContainer.appendChild(mediaContainer); backgroundContainer.appendChild(mediaContainer);
parent.appendChild(backgroundContainer); parent.appendChild(backgroundContainer);
@@ -24,9 +27,9 @@ export async function loadBackground() {
} }
try { try {
const selectedBackgroundId = localStorage.getItem('selectedBackground'); const selectedBackgroundId = localStorage.getItem("selectedBackground");
if (!selectedBackgroundId) { if (!selectedBackgroundId) {
const backgroundContainer = document.querySelector('.imageBackground'); const backgroundContainer = document.querySelector(".imageBackground");
if (backgroundContainer) { if (backgroundContainer) {
backgroundContainer.remove(); backgroundContainer.remove();
} }
@@ -36,35 +39,36 @@ export async function loadBackground() {
const background = await getDataById(selectedBackgroundId); const background = await getDataById(selectedBackgroundId);
if (!background) return; if (!background) return;
let backgroundContainer = document.querySelector('.imageBackground'); let backgroundContainer = document.querySelector(".imageBackground");
if (!backgroundContainer) { if (!backgroundContainer) {
backgroundContainer = document.createElement('div'); backgroundContainer = document.createElement("div");
backgroundContainer.classList.add('imageBackground'); backgroundContainer.classList.add("imageBackground");
backgroundContainer.setAttribute('excludeDarkCheck', 'true'); backgroundContainer.setAttribute("excludeDarkCheck", "true");
const parent = document.getElementById('container'); const parent = document.getElementById("container");
if (parent) { if (parent) {
parent.appendChild(backgroundContainer); parent.appendChild(backgroundContainer);
} }
} }
let mediaContainer = document.getElementById('media-container'); let mediaContainer = document.getElementById("media-container");
if (!mediaContainer) { if (!mediaContainer) {
mediaContainer = document.createElement('div'); mediaContainer = document.createElement("div");
mediaContainer.id = 'media-container'; mediaContainer.id = "media-container";
backgroundContainer.appendChild(mediaContainer); backgroundContainer.appendChild(mediaContainer);
} }
mediaContainer = document.getElementById('media-container'); mediaContainer = document.getElementById("media-container");
if (!mediaContainer) return; if (!mediaContainer) return;
mediaContainer.innerHTML = ''; mediaContainer.innerHTML = "";
const mediaElement = background.type === 'video' const mediaElement =
? document.createElement('video') background.type === "video"
: document.createElement('img'); ? document.createElement("video")
: document.createElement("img");
mediaElement.src = URL.createObjectURL(background.blob); mediaElement.src = URL.createObjectURL(background.blob);
mediaElement.classList.add('background'); mediaElement.classList.add("background");
if (mediaElement instanceof HTMLVideoElement) { if (mediaElement instanceof HTMLVideoElement) {
mediaElement.loop = true; mediaElement.loop = true;
@@ -74,6 +78,6 @@ export async function loadBackground() {
mediaContainer.appendChild(mediaElement); mediaContainer.appendChild(mediaElement);
} catch (error) { } catch (error) {
console.error('Error loading background:', error); console.error("Error loading background:", error);
} }
} }
File diff suppressed because one or more lines are too long
+7 -4
View File
@@ -6,19 +6,22 @@ import { debounce } from "lodash";
export class SettingsResizer { export class SettingsResizer {
constructor() { constructor() {
this.adjustPopupHeight(); this.adjustPopupHeight();
window.addEventListener('resize', debounce(this.adjustPopupHeight, 250) as EventListener); window.addEventListener(
document.addEventListener('DOMContentLoaded', this.adjustPopupHeight); "resize",
debounce(this.adjustPopupHeight, 250) as EventListener,
);
document.addEventListener("DOMContentLoaded", this.adjustPopupHeight);
} }
private adjustPopupHeight() { private adjustPopupHeight() {
const iframePopup = document.getElementById('ExtensionPopup'); const iframePopup = document.getElementById("ExtensionPopup");
if (!iframePopup) return; if (!iframePopup) return;
const viewportHeight = window.innerHeight; const viewportHeight = window.innerHeight;
const idealHeight = viewportHeight - 80 - 15; // -80px for the top of the popup const idealHeight = viewportHeight - 80 - 15; // -80px for the top of the popup
if (idealHeight > 600) { if (idealHeight > 600) {
iframePopup.style.height = '600px'; iframePopup.style.height = "600px";
} else { } else {
iframePopup.style.height = `${idealHeight}px`; iframePopup.style.height = `${idealHeight}px`;
} }
+9 -6
View File
@@ -1,4 +1,4 @@
import Color from 'color'; import Color from "color";
function adjustLuminance(color: any, lum: any) { function adjustLuminance(color: any, lum: any) {
let adjustedColor = Color(color.toLowerCase()); let adjustedColor = Color(color.toLowerCase());
@@ -8,20 +8,20 @@ function adjustLuminance(color: any, lum: any) {
adjustedColor = Color.rgb( adjustedColor = Color.rgb(
Math.min(Math.max(0, rgbObj.r + rgbObj.r * lum), 255), Math.min(Math.max(0, rgbObj.r + rgbObj.r * lum), 255),
Math.min(Math.max(0, rgbObj.g + rgbObj.g * lum), 255), Math.min(Math.max(0, rgbObj.g + rgbObj.g * lum), 255),
Math.min(Math.max(0, rgbObj.b + rgbObj.b * lum), 255) Math.min(Math.max(0, rgbObj.b + rgbObj.b * lum), 255),
); );
return adjustedColor.string(); return adjustedColor.string();
} }
export default function ColorLuminance(color: any, lum = 0) { export default function ColorLuminance(color: any, lum = 0) {
if (color == '' || color == null) { if (color == "" || color == null) {
// light cyan blue // light cyan blue
return '#00bfff'; return "#00bfff";
} }
const colorRegex = /rgba?\(([^)]+)\)/gi; // Case-insensitive match for rgb() or rgba() const colorRegex = /rgba?\(([^)]+)\)/gi; // Case-insensitive match for rgb() or rgba()
if (color.toLowerCase().includes('gradient')) { if (color.toLowerCase().includes("gradient")) {
let gradient = color; let gradient = color;
let uniqueColorSet = new Set(); let uniqueColorSet = new Set();
@@ -35,7 +35,10 @@ export default function ColorLuminance(color: any, lum = 0) {
// Adjust luminance for each unique color stop // Adjust luminance for each unique color stop
for (let colorStop of uniqueColorSet) { for (let colorStop of uniqueColorSet) {
const adjustedColor = adjustLuminance(colorStop, lum); const adjustedColor = adjustLuminance(colorStop, lum);
gradient = gradient.replace(new RegExp(colorStop as string, 'gi'), adjustedColor); gradient = gradient.replace(
new RegExp(colorStop as string, "gi"),
adjustedColor,
);
} }
return gradient; return gradient;
+38 -29
View File
@@ -1,72 +1,81 @@
import browser from 'webextension-polyfill' import browser from "webextension-polyfill";
import { GetThresholdOfColor } from '@/seqta/ui/colors/getThresholdColour'; import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { lightenAndPaleColor } from './lightenAndPaleColor'; import { lightenAndPaleColor } from "./lightenAndPaleColor";
import ColorLuminance from './ColorLuminance'; import ColorLuminance from "./ColorLuminance";
import { settingsState } from '@/seqta/utils/listeners/SettingsState'; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import darkLogo from '@/resources/icons/betterseqta-light-full.png'; import darkLogo from "@/resources/icons/betterseqta-light-full.png";
import lightLogo from '@/resources/icons/betterseqta-dark-full.png'; import lightLogo from "@/resources/icons/betterseqta-dark-full.png";
// Helper functions // Helper functions
const setCSSVar = (varName: any, value: any) => document.documentElement.style.setProperty(varName, value); const setCSSVar = (varName: any, value: any) =>
const applyProperties = (props: any) => Object.entries(props).forEach(([key, value]) => setCSSVar(key, value)); document.documentElement.style.setProperty(varName, value);
const applyProperties = (props: any) =>
Object.entries(props).forEach(([key, value]) => setCSSVar(key, value));
export function updateAllColors() { export function updateAllColors() {
// Determine the color to use // Determine the color to use
const selectedColor = settingsState.selectedColor !== '' ? settingsState.selectedColor : '#007bff'; const selectedColor =
settingsState.selectedColor !== ""
? settingsState.selectedColor
: "#007bff";
if (settingsState.transparencyEffects) { if (settingsState.transparencyEffects) {
document.documentElement.classList.add('transparencyEffects'); document.documentElement.classList.add("transparencyEffects");
} }
// Common properties, always applied // Common properties, always applied
const commonProps = { const commonProps = {
'--better-sub': '#161616', "--better-sub": "#161616",
'--better-alert-highlight': '#c61851', "--better-alert-highlight": "#c61851",
'--better-main': settingsState.selectedColor "--better-main": settingsState.selectedColor,
}; };
// Mode-based properties, applied if storedSetting is provided // Mode-based properties, applied if storedSetting is provided
let modeProps = {}; let modeProps = {};
modeProps = settingsState.DarkMode ? { modeProps = settingsState.DarkMode
'--betterseqta-logo': `url(${browser.runtime.getURL(darkLogo)})` ? {
} : { "--betterseqta-logo": `url(${browser.runtime.getURL(darkLogo)})`,
'--better-pale': lightenAndPaleColor(selectedColor), }
'--betterseqta-logo': `url(${browser.runtime.getURL(lightLogo)})` : {
"--better-pale": lightenAndPaleColor(selectedColor),
"--betterseqta-logo": `url(${browser.runtime.getURL(lightLogo)})`,
}; };
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
document.documentElement.style.removeProperty('--better-pale'); document.documentElement.style.removeProperty("--better-pale");
document.documentElement.classList.add('dark'); document.documentElement.classList.add("dark");
} else { } else {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove("dark");
} }
// Dynamic properties, always applied // Dynamic properties, always applied
const rgbThreshold = GetThresholdOfColor(selectedColor); const rgbThreshold = GetThresholdOfColor(selectedColor);
const isBright = rgbThreshold > 210; const isBright = rgbThreshold > 210;
const dynamicProps = { const dynamicProps = {
'--text-color': isBright ? 'black' : 'white', "--text-color": isBright ? "black" : "white",
'--better-light': selectedColor === '#ffffff' ? '#b7b7b7' : ColorLuminance(selectedColor, 0.95) "--better-light":
selectedColor === "#ffffff"
? "#b7b7b7"
: ColorLuminance(selectedColor, 0.95),
}; };
// Apply all the properties // Apply all the properties
applyProperties({ ...commonProps, ...modeProps, ...dynamicProps }); applyProperties({ ...commonProps, ...modeProps, ...dynamicProps });
let alliframes = document.getElementsByTagName('iframe'); let alliframes = document.getElementsByTagName("iframe");
for (let i = 0; i < alliframes.length; i++) { for (let i = 0; i < alliframes.length; i++) {
const element = alliframes[i]; const element = alliframes[i];
if (element.getAttribute('excludeDarkCheck') == 'true') { if (element.getAttribute("excludeDarkCheck") == "true") {
continue; continue;
} }
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
element.contentDocument?.documentElement.classList.add('dark'); element.contentDocument?.documentElement.classList.add("dark");
} else { } else {
element.contentDocument?.documentElement.classList.remove('dark'); element.contentDocument?.documentElement.classList.remove("dark");
} }
} }
} }
+13 -13
View File
@@ -1,38 +1,38 @@
import Color from "color" import Color from "color";
export function GetThresholdOfColor(color: any) { export function GetThresholdOfColor(color: any) {
if (!color) return 0 if (!color) return 0;
// Case-insensitive regular expression for matching RGBA colors // Case-insensitive regular expression for matching RGBA colors
const rgbaRegex = /rgba?\(([^)]+)\)/gi const rgbaRegex = /rgba?\(([^)]+)\)/gi;
// Check if the color string is a gradient (linear or radial) // Check if the color string is a gradient (linear or radial)
if (color.includes("gradient")) { if (color.includes("gradient")) {
let gradientThresholds = [] let gradientThresholds = [];
// Find and replace all instances of RGBA in the gradient // Find and replace all instances of RGBA in the gradient
let match let match;
while ((match = rgbaRegex.exec(color)) !== null) { while ((match = rgbaRegex.exec(color)) !== null) {
// Extract the individual components (r, g, b, a) // Extract the individual components (r, g, b, a)
const rgbaString = match[1] const rgbaString = match[1];
const [r, g, b] = rgbaString.split(",").map((str) => str.trim()) const [r, g, b] = rgbaString.split(",").map((str) => str.trim());
// Compute the threshold using your existing algorithm // Compute the threshold using your existing algorithm
const threshold = Math.sqrt( const threshold = Math.sqrt(
parseInt(r) ** 2 + parseInt(g) ** 2 + parseInt(b) ** 2, parseInt(r) ** 2 + parseInt(g) ** 2 + parseInt(b) ** 2,
) );
// Store the computed threshold // Store the computed threshold
gradientThresholds.push(threshold) gradientThresholds.push(threshold);
} }
// Calculate the average threshold // Calculate the average threshold
const averageThreshold = const averageThreshold =
gradientThresholds.reduce((acc, val) => acc + val, 0) / gradientThresholds.reduce((acc, val) => acc + val, 0) /
gradientThresholds.length gradientThresholds.length;
return averageThreshold return averageThreshold;
} else { } else {
// Handle the color as a simple RGBA (or hex, or whatever the Color library supports) // Handle the color as a simple RGBA (or hex, or whatever the Color library supports)
const rgb = Color.rgb(color).object() const rgb = Color.rgb(color).object();
return Math.sqrt(rgb.r ** 2 + rgb.g ** 2 + rgb.b ** 2) return Math.sqrt(rgb.r ** 2 + rgb.g ** 2 + rgb.b ** 2);
} }
} }
+23 -9
View File
@@ -1,9 +1,13 @@
import Color from 'color'; import Color from "color";
export function lightenAndPaleColor(inputColor: any, lightenFactor = 0.75, paleFactor = 0.55) { export function lightenAndPaleColor(
inputColor: any,
lightenFactor = 0.75,
paleFactor = 0.55,
) {
if (!inputColor) return; if (!inputColor) return;
if (inputColor.includes('gradient')) { if (inputColor.includes("gradient")) {
const baseColor = findMatchingColor(inputColor); const baseColor = findMatchingColor(inputColor);
return lightenAndPaleColor(baseColor, lightenFactor, paleFactor); return lightenAndPaleColor(baseColor, lightenFactor, paleFactor);
@@ -29,27 +33,38 @@ export function lightenAndPaleColor(inputColor: any, lightenFactor = 0.75, paleF
} }
// Utility function to average an array of Color objects // Utility function to average an array of Color objects
function averageColors(colors: any) { function averageColors(colors: any) {
let avgR = 0, avgG = 0, avgB = 0; let avgR = 0,
avgG = 0,
avgB = 0;
colors.forEach((color: any) => { colors.forEach((color: any) => {
avgR += color.red(); avgR += color.red();
avgG += color.green(); avgG += color.green();
avgB += color.blue(); avgB += color.blue();
}); });
return Color.rgb(avgR / colors.length, avgG / colors.length, avgB / colors.length); return Color.rgb(
avgR / colors.length,
avgG / colors.length,
avgB / colors.length,
);
} }
// Main function to find a matching color for a CSS gradient // Main function to find a matching color for a CSS gradient
function findMatchingColor(cssGradient: any) { function findMatchingColor(cssGradient: any) {
try { try {
// Step 1: Parse the gradient to extract color stops (case-insensitive) // Step 1: Parse the gradient to extract color stops (case-insensitive)
const regex = /#[0-9a-fA-F]{6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)/gi; const regex =
/#[0-9a-fA-F]{6}|rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)|rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)/gi;
const colorStops = cssGradient.match(regex); const colorStops = cssGradient.match(regex);
if (!colorStops) { if (!colorStops) {
throw new Error('No valid color stops found in the provided CSS gradient.'); throw new Error(
"No valid color stops found in the provided CSS gradient.",
);
} }
// Normalize and trim the color stops // Normalize and trim the color stops
const normalizedColorStops = colorStops.map((color: any) => color.toLowerCase().replace(/\s+/g, '')); const normalizedColorStops = colorStops.map((color: any) =>
color.toLowerCase().replace(/\s+/g, ""),
);
// Convert the color stops to Color objects // Convert the color stops to Color objects
const colorObjects = normalizedColorStops.map((color: any) => Color(color)); const colorObjects = normalizedColorStops.map((color: any) => Color(color));
@@ -57,7 +72,6 @@ function findMatchingColor(cssGradient: any) {
// Step 2: Average the color stops // Step 2: Average the color stops
const baseColor = averageColors(colorObjects); const baseColor = averageColors(colorObjects);
// Step 4: Return the matching color in HEX format // Step 4: Return the matching color in HEX format
return baseColor.hex(); return baseColor.hex();
} catch (err: any) { } catch (err: any) {
+261 -96
View File
@@ -20,191 +20,356 @@ function generateMockUserCode(): string {
function getRandomDate(): Date { function getRandomDate(): Date {
const start = new Date(); const start = new Date();
const end = new Date(start.getTime() + 60 * 24 * 60 * 60 * 1000); // 60 days from now const end = new Date(start.getTime() + 60 * 24 * 60 * 60 * 1000); // 60 days from now
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); return new Date(
start.getTime() + Math.random() * (end.getTime() - start.getTime()),
);
} }
const contentConfig: ContentConfig = { const contentConfig: ContentConfig = {
lessonTitle: { lessonTitle: {
selector: '.day h2', selector: ".day h2",
action: (element) => { element.textContent = getRandomElement(mockData.subjects); } action: (element) => {
element.textContent = getRandomElement(mockData.subjects);
},
}, },
teacher: { teacher: {
selector: '.day h3:first-of-type', selector: ".day h3:first-of-type",
action: (element) => { element.textContent = getRandomElement(mockData.teachers); } action: (element) => {
element.textContent = getRandomElement(mockData.teachers);
},
}, },
classroom: { classroom: {
selector: '.day h3:last-of-type', selector: ".day h3:last-of-type",
action: (element) => { element.textContent = getRandomElement(mockData.classrooms); } action: (element) => {
element.textContent = getRandomElement(mockData.classrooms);
},
}, },
userName: { userName: {
selector: '.userInfoName, .name', selector: ".userInfoName, .name",
action: (element) => { element.textContent = getRandomElement(mockData.names); } action: (element) => {
element.textContent = getRandomElement(mockData.names);
},
}, },
userCode: { userCode: {
selector: '.userInfoText > .userInfoCode', selector: ".userInfoText > .userInfoCode",
action: (element) => { element.textContent = generateMockUserCode(); } action: (element) => {
element.textContent = generateMockUserCode();
},
}, },
assessmentTitle: { assessmentTitle: {
selector: '.upcoming-assessment .upcoming-assessment-title', selector: ".upcoming-assessment .upcoming-assessment-title",
action: (element) => { element.textContent = getRandomElement(mockData.assessmentTitles); } action: (element) => {
element.textContent = getRandomElement(mockData.assessmentTitles);
},
}, },
assessmentSubject: { assessmentSubject: {
selector: '.upcoming-assessment .upcoming-details h5', selector: ".upcoming-assessment .upcoming-details h5",
action: (element) => { element.textContent = getRandomElement(mockData.subjects); } action: (element) => {
element.textContent = getRandomElement(mockData.subjects);
},
}, },
noticeTitle: { noticeTitle: {
selector: '.notice h3', selector: ".notice h3",
action: (element) => { element.textContent = getRandomElement(mockData.notices); } action: (element) => {
element.textContent = getRandomElement(mockData.notices);
},
}, },
noticeContent: { noticeContent: {
selector: '.notice .contents', selector: ".notice .contents",
action: (element) => { element.textContent = 'Content has been redacted for privacy.'; } action: (element) => {
element.textContent = "Content has been redacted for privacy.";
},
}, },
upcomingCheckboxes: { upcomingCheckboxes: {
selector: '.upcoming-checkbox-container', selector: ".upcoming-checkbox-container",
action: (element) => { element.firstChild!.textContent = 'SUBJ'; } action: (element) => {
element.firstChild!.textContent = "SUBJ";
},
}, },
dates: { dates: {
selector: '.upcoming-date-title h5, input[type="date"]', selector: '.upcoming-date-title h5, input[type="date"]',
action: (element) => { action: (element) => {
const randomDate = getRandomDate(); const randomDate = getRandomDate();
if (element instanceof HTMLInputElement) { if (element instanceof HTMLInputElement) {
element.value = randomDate.toISOString().split('T')[0]; element.value = randomDate.toISOString().split("T")[0];
} else { } else {
element.textContent = randomDate.toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'long' }); element.textContent = randomDate.toLocaleDateString("en-US", {
} weekday: "long",
day: "numeric",
month: "long",
});
} }
}, },
},
messageSubject: { messageSubject: {
selector: '[class*="MessageList__subject___"]', selector: '[class*="MessageList__subject___"]',
action: (element) => { element.textContent = getRandomElement(mockData.messages.subjects); } action: (element) => {
element.textContent = getRandomElement(mockData.messages.subjects);
},
}, },
messageSender: { messageSender: {
selector: '[class*="MessageList__value___"]', selector: '[class*="MessageList__value___"]',
action: (element) => { element.textContent = getRandomElement(mockData.messages.sender); } action: (element) => {
element.textContent = getRandomElement(mockData.messages.sender);
},
}, },
messageRecipients: { messageRecipients: {
selector: '[class*="MessageList__recipients___"] [class*="MessageList__value___"]', selector:
action: (element) => { element.textContent = 'Recipient(s) Redacted'; } '[class*="MessageList__recipients___"] [class*="MessageList__value___"]',
action: (element) => {
element.textContent = "Recipient(s) Redacted";
},
}, },
messageDate: { messageDate: {
selector: '[class*="MessageList__date___"]', selector: '[class*="MessageList__date___"]',
action: (element) => { element.textContent = getRandomDate().toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'long' }); } action: (element) => {
element.textContent = getRandomDate().toLocaleDateString("en-US", {
weekday: "long",
day: "numeric",
month: "long",
});
},
}, },
avatarImage: { avatarImage: {
selector: '[class*="Avatar__Avatar___"]', selector: '[class*="Avatar__Avatar___"]',
action: (element) => { action: (element) => {
if (element instanceof HTMLElement) { if (element instanceof HTMLElement) {
element.style.removeProperty('background-image'); element.style.removeProperty("background-image");
element.firstChild!.firstChild!.textContent = getRandomElement(mockData.names)[0]; element.firstChild!.firstChild!.textContent = getRandomElement(
} mockData.names,
)[0];
} }
}, },
},
notificationCount: { notificationCount: {
selector: '[class*="notifications__bubble___"]', selector: '[class*="notifications__bubble___"]',
action: (element) => { element.textContent = Math.floor(Math.random() * 100).toString(); } action: (element) => {
element.textContent = Math.floor(Math.random() * 100).toString();
},
}, },
schoolName: { schoolName: {
selector: 'title', selector: "title",
action: (element) => { element.textContent = 'School Portal'; } action: (element) => {
element.textContent = "School Portal";
},
}, },
documentNames: { documentNames: {
selector: '.document td.title', selector: ".document td.title",
action: (element) => { element.textContent = 'Document Name Redacted'; } action: (element) => {
element.textContent = "Document Name Redacted";
},
}, },
forumTopics: { forumTopics: {
selector: '#menu .sub ul li label', selector: "#menu .sub ul li label",
action: (element) => { element.textContent = 'Forum Topic Redacted'; } action: (element) => {
element.textContent = "Forum Topic Redacted";
},
}, },
courseNames: { courseNames: {
selector: '#menu .sub ul li[data-colour] label', selector: "#menu .sub ul li[data-colour] label",
action: (element) => { element.textContent = 'Course Name Redacted'; } action: (element) => {
element.textContent = "Course Name Redacted";
},
}, },
yearGroups: { yearGroups: {
selector: '#menu .sub > ul > li > label', selector: "#menu .sub > ul > li > label",
action: (element) => { element.textContent = 'Year Group Redacted'; } action: (element) => {
element.textContent = "Year Group Redacted";
},
}, },
newsArticleTitle: { newsArticleTitle: {
selector: '.ArticleText a', selector: ".ArticleText a",
action: (element) => { element.textContent = 'News Article Title Redacted'; } action: (element) => {
element.textContent = "News Article Title Redacted";
},
}, },
newsArticleContent: { newsArticleContent: {
selector: '.ArticleText p', selector: ".ArticleText p",
action: (element) => { element.textContent = 'News Article Content Redacted'; } action: (element) => {
element.textContent = "News Article Content Redacted";
},
}, },
userHouse: { userHouse: {
selector: '.userInfohouse', selector: ".userInfohouse",
action: (element) => { element.textContent = 'House'; } action: (element) => {
} element.textContent = "House";
},
},
}; };
const mockData = { const mockData = {
subjects: [ subjects: [
"Mathematics", "English", "Science", "History", "Geography", "Mathematics",
"Art", "Music", "Physical Education", "Chemistry", "Physics", "English",
"Biology", "Economics", "Business Studies", "French", "Spanish", "Science",
"Computer Science", "Literature", "Environmental Studies", "History",
"Political Science", "Sociology" "Geography",
"Art",
"Music",
"Physical Education",
"Chemistry",
"Physics",
"Biology",
"Economics",
"Business Studies",
"French",
"Spanish",
"Computer Science",
"Literature",
"Environmental Studies",
"Political Science",
"Sociology",
], ],
teachers: [ teachers: [
"Mr. Smith", "Mrs. Johnson", "Ms. Williams", "Dr. Brown", "Mr. Smith",
"Mr. Davis", "Mrs. Miller", "Mr. Wilson", "Ms. Moore", "Mrs. Johnson",
"Dr. Taylor", "Mrs. Anderson", "Mr. Garcia", "Mrs. Martinez", "Ms. Williams",
"Ms. Thompson", "Dr. Lee", "Mr. Robinson", "Mrs. Hall", "Dr. Brown",
"Ms. White", "Dr. Clark", "Mr. Lewis", "Mrs. King" "Mr. Davis",
"Mrs. Miller",
"Mr. Wilson",
"Ms. Moore",
"Dr. Taylor",
"Mrs. Anderson",
"Mr. Garcia",
"Mrs. Martinez",
"Ms. Thompson",
"Dr. Lee",
"Mr. Robinson",
"Mrs. Hall",
"Ms. White",
"Dr. Clark",
"Mr. Lewis",
"Mrs. King",
], ],
classrooms: [ classrooms: [
"A101", "B205", "C304", "D102", "E201", "A101",
"F103", "G204", "H301", "I202", "J105", "B205",
"K107", "L206", "M303", "N104", "O209" "C304",
"D102",
"E201",
"F103",
"G204",
"H301",
"I202",
"J105",
"K107",
"L206",
"M303",
"N104",
"O209",
], ],
names: [ names: [
"John Doe", "Jane Smith", "Michael Johnson", "Emily Brown", "John Doe",
"David Lee", "Sarah Davis", "Robert Wilson", "Lisa Taylor", "Jane Smith",
"William Moore", "Jennifer Anderson", "Thomas Garcia", "Michael Johnson",
"Olivia Martinez", "Daniel Thompson", "Sophia Lee", "Emily Brown",
"Matthew Robinson", "Ava Hall", "Jacob White", "David Lee",
"Mia Clark", "James Lewis", "Lily King" "Sarah Davis",
"Robert Wilson",
"Lisa Taylor",
"William Moore",
"Jennifer Anderson",
"Thomas Garcia",
"Olivia Martinez",
"Daniel Thompson",
"Sophia Lee",
"Matthew Robinson",
"Ava Hall",
"Jacob White",
"Mia Clark",
"James Lewis",
"Lily King",
], ],
assessmentTitles: [ assessmentTitles: [
"Mid-term Exam", "Final Project", "Research Paper", "Mid-term Exam",
"Oral Presentation", "Lab Report", "Essay", "Final Project",
"Group Assignment", "Portfolio Review", "Quiz", "Research Paper",
"Practical Test", "Class Presentation", "Oral Presentation",
"Online Assessment", "Case Study", "Field Report", "Lab Report",
"Peer Review", "Coding Challenge", "Math Test", "Essay",
"Literary Analysis", "Debate", "Design Project" "Group Assignment",
"Portfolio Review",
"Quiz",
"Practical Test",
"Class Presentation",
"Online Assessment",
"Case Study",
"Field Report",
"Peer Review",
"Coding Challenge",
"Math Test",
"Literary Analysis",
"Debate",
"Design Project",
], ],
notices: [ notices: [
"School Assembly", "Excursion Reminder", "Fundraising Event", "School Assembly",
"Parent-Teacher Meetings", "Sports Day", "Book Fair", "Excursion Reminder",
"Career Day", "Music Concert", "Art Exhibition", "Fundraising Event",
"Science Fair", "Holiday Celebration", "Community Service Day", "Parent-Teacher Meetings",
"Graduation Ceremony", "Award Ceremony", "Workshop", "Sports Day",
"Open House", "Seminar", "Club Meeting", "Book Fair",
"Field Trip", "Cultural Festival" "Career Day",
"Music Concert",
"Art Exhibition",
"Science Fair",
"Holiday Celebration",
"Community Service Day",
"Graduation Ceremony",
"Award Ceremony",
"Workshop",
"Open House",
"Seminar",
"Club Meeting",
"Field Trip",
"Cultural Festival",
], ],
messages: { messages: {
subjects: [ subjects: [
"Mid-year Exams", "Science project due soon", "Mufti Day coming up!", "Mid-year Exams",
"School Assembly", "Excursion Reminder", "Fundraising Event", "Science project due soon",
"Parent-Teacher Meetings", "Sports Day", "Book Fair", "Mufti Day coming up!",
"Career Day", "Music Concert", "Art Exhibition", "School Assembly",
"Science Fair", "Holiday Celebration", "Community Service Day", "Excursion Reminder",
"Graduation Ceremony", "Award Ceremony", "Workshop", "Fundraising Event",
"Open House", "Seminar", "Club Meeting", "Parent-Teacher Meetings",
"Field Trip", "Cultural Festival" "Sports Day",
"Book Fair",
"Career Day",
"Music Concert",
"Art Exhibition",
"Science Fair",
"Holiday Celebration",
"Community Service Day",
"Graduation Ceremony",
"Award Ceremony",
"Workshop",
"Open House",
"Seminar",
"Club Meeting",
"Field Trip",
"Cultural Festival",
], ],
sender: [ sender: [
"Mr. Smith", "Mrs. Johnson", "Ms. Williams", "Dr. Brown", "Mr. Smith",
"Mr. Davis", "Mrs. Miller", "Mr. Wilson", "Ms. Moore", "Mrs. Johnson",
"Dr. Taylor", "Mrs. Anderson", "Mr. Garcia", "Mrs. Martinez", "Ms. Williams",
] "Dr. Brown",
} "Mr. Davis",
"Mrs. Miller",
"Mr. Wilson",
"Ms. Moore",
"Dr. Taylor",
"Mrs. Anderson",
"Mr. Garcia",
"Mrs. Martinez",
],
},
}; };
export default function hideSensitiveContent() { export default function hideSensitiveContent() {
+15 -15
View File
@@ -1,35 +1,35 @@
import renderSvelte from '@/interface/main'; import renderSvelte from "@/interface/main";
import Store from '@/interface/pages/store.svelte' import Store from "@/interface/pages/store.svelte";
import { unmount } from 'svelte' import { unmount } from "svelte";
let remove: () => void let remove: () => void;
export function OpenStorePage() { export function OpenStorePage() {
remove = renderStore() remove = renderStore();
} }
export function renderStore() { export function renderStore() {
const container = document.querySelector('#container'); const container = document.querySelector("#container");
if (!container) { if (!container) {
throw new Error('Container not found'); throw new Error("Container not found");
} }
const child = document.createElement('div'); const child = document.createElement("div");
child.id = 'store'; child.id = "store";
container!.appendChild(child); container!.appendChild(child);
const shadow = child.attachShadow({ mode: 'open' }); const shadow = child.attachShadow({ mode: "open" });
const app = renderSvelte(Store, shadow); const app = renderSvelte(Store, shadow);
return () => unmount(app) return () => unmount(app);
} }
export function closeStore() { export function closeStore() {
document.getElementById('store')!.classList.add('hide') document.getElementById("store")!.classList.add("hide");
setTimeout(() => { setTimeout(() => {
remove() remove();
document.getElementById('store')!.remove() document.getElementById("store")!.remove();
}, 500) }, 500);
} }
+22 -18
View File
@@ -1,36 +1,40 @@
import { changeSettingsClicked, closeExtensionPopup, SettingsClicked } from "../Closers/closeExtensionPopup" import {
import renderSvelte from "@/interface/main" changeSettingsClicked,
import { SettingsResizer } from "@/seqta/ui/SettingsResizer" closeExtensionPopup,
import Settings from "@/interface/pages/settings.svelte" SettingsClicked,
} from "../Closers/closeExtensionPopup";
import renderSvelte from "@/interface/main";
import { SettingsResizer } from "@/seqta/ui/SettingsResizer";
import Settings from "@/interface/pages/settings.svelte";
export function addExtensionSettings() { export function addExtensionSettings() {
const extensionPopup = document.createElement("div") const extensionPopup = document.createElement("div");
extensionPopup.classList.add("outside-container", "hide") extensionPopup.classList.add("outside-container", "hide");
extensionPopup.id = "ExtensionPopup" extensionPopup.id = "ExtensionPopup";
const extensionContainer = document.querySelector( const extensionContainer = document.querySelector(
"#container", "#container",
) as HTMLDivElement ) as HTMLDivElement;
if (extensionContainer) extensionContainer.appendChild(extensionPopup) if (extensionContainer) extensionContainer.appendChild(extensionPopup);
// create shadow dom and render svelte app // create shadow dom and render svelte app
try { try {
const shadow = extensionPopup.attachShadow({ mode: "open" }) const shadow = extensionPopup.attachShadow({ mode: "open" });
requestIdleCallback(() => renderSvelte(Settings, shadow)) requestIdleCallback(() => renderSvelte(Settings, shadow));
} catch (err) { } catch (err) {
console.error(err) console.error(err);
} }
const container = document.getElementById("container") const container = document.getElementById("container");
new SettingsResizer() new SettingsResizer();
container!.onclick = (event) => { container!.onclick = (event) => {
if (!SettingsClicked) return if (!SettingsClicked) return;
if (!(event.target as HTMLElement).closest("#AddedSettings")) { if (!(event.target as HTMLElement).closest("#AddedSettings")) {
if (event.target == extensionPopup) return if (event.target == extensionPopup) return;
changeSettingsClicked(closeExtensionPopup()) changeSettingsClicked(closeExtensionPopup());
}
} }
};
} }
+20 -20
View File
@@ -1,24 +1,24 @@
import ShortcutLinks from "@/seqta/content/links.json" import ShortcutLinks from "@/seqta/content/links.json";
import stringToHTML from "../stringToHTML" import stringToHTML from "../stringToHTML";
export function addShortcuts(shortcuts: any) { export function addShortcuts(shortcuts: any) {
for (let i = 0; i < shortcuts.length; i++) { for (let i = 0; i < shortcuts.length; i++) {
const currentShortcut = shortcuts[i] const currentShortcut = shortcuts[i];
if (currentShortcut?.enabled) { if (currentShortcut?.enabled) {
const Itemname = (currentShortcut?.name ?? "").replace(/\s/g, "") const Itemname = (currentShortcut?.name ?? "").replace(/\s/g, "");
const linkDetails = const linkDetails =
ShortcutLinks?.[Itemname as keyof typeof ShortcutLinks] ShortcutLinks?.[Itemname as keyof typeof ShortcutLinks];
if (linkDetails) { if (linkDetails) {
createNewShortcut( createNewShortcut(
linkDetails.link, linkDetails.link,
linkDetails.icon, linkDetails.icon,
linkDetails.viewBox, linkDetails.viewBox,
currentShortcut?.name, currentShortcut?.name,
) );
} else { } else {
console.warn(`No link details found for '${Itemname}'`) console.warn(`No link details found for '${Itemname}'`);
} }
} }
} }
@@ -26,21 +26,21 @@ export function addShortcuts(shortcuts: any) {
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) { function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
// Creates the stucture and element information for each seperate shortcut // Creates the stucture and element information for each seperate shortcut
let shortcut = document.createElement("a") let shortcut = document.createElement("a");
shortcut.setAttribute("href", link) shortcut.setAttribute("href", link);
shortcut.setAttribute("target", "_blank") shortcut.setAttribute("target", "_blank");
let shortcutdiv = document.createElement("div") let shortcutdiv = document.createElement("div");
shortcutdiv.classList.add("shortcut") shortcutdiv.classList.add("shortcut");
let image = stringToHTML( let image = stringToHTML(
`<svg style="width:39px;height:39px" viewBox="${viewBox}"><path fill="currentColor" d="${icon}" /></svg>`, `<svg style="width:39px;height:39px" viewBox="${viewBox}"><path fill="currentColor" d="${icon}" /></svg>`,
).firstChild ).firstChild;
;(image! as HTMLElement).classList.add("shortcuticondiv") (image! as HTMLElement).classList.add("shortcuticondiv");
let text = document.createElement("p") let text = document.createElement("p");
text.textContent = title text.textContent = title;
shortcutdiv.append(image as HTMLElement) shortcutdiv.append(image as HTMLElement);
shortcutdiv.append(text) shortcutdiv.append(text);
shortcut.append(shortcutdiv) shortcut.append(shortcutdiv);
document.getElementById("shortcuts")!.appendChild(shortcut) document.getElementById("shortcuts")!.appendChild(shortcut);
} }
+14 -14
View File
@@ -1,34 +1,34 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState"; import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { animate } from "motion" import { animate } from "motion";
import { settingsPopup } from "@/interface/hooks/SettingsPopup" import { settingsPopup } from "@/interface/hooks/SettingsPopup";
export let SettingsClicked = false export let SettingsClicked = false;
export const closeExtensionPopup = (extensionPopup?: HTMLElement) => { export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
if (!extensionPopup) if (!extensionPopup)
extensionPopup = document.getElementById("ExtensionPopup")! extensionPopup = document.getElementById("ExtensionPopup")!;
extensionPopup.classList.add("hide") extensionPopup.classList.add("hide");
if (settingsState.animations) { if (settingsState.animations) {
animate(1, 0, { animate(1, 0, {
onUpdate: (progress) => { onUpdate: (progress) => {
extensionPopup.style.opacity = Math.max(0, progress).toString() extensionPopup.style.opacity = Math.max(0, progress).toString();
extensionPopup.style.transform = `scale(${Math.max(0, progress)})` extensionPopup.style.transform = `scale(${Math.max(0, progress)})`;
}, },
type: "spring", type: "spring",
stiffness: 520, stiffness: 520,
damping: 20, damping: 20,
}) });
} else { } else {
extensionPopup.style.opacity = "0" extensionPopup.style.opacity = "0";
extensionPopup.style.transform = "scale(0)" extensionPopup.style.transform = "scale(0)";
} }
settingsPopup.triggerClose() settingsPopup.triggerClose();
return SettingsClicked = false return (SettingsClicked = false);
} };
export function changeSettingsClicked(newVal: boolean) { export function changeSettingsClicked(newVal: boolean) {
SettingsClicked = newVal SettingsClicked = newVal;
} }
@@ -1,13 +1,13 @@
import stringToHTML from "../stringToHTML" import stringToHTML from "../stringToHTML";
export function CreateCustomShortcutDiv(element: any) { export function CreateCustomShortcutDiv(element: any) {
// Creates the stucture and element information for each seperate shortcut // Creates the stucture and element information for each seperate shortcut
var shortcut = document.createElement("a") var shortcut = document.createElement("a");
shortcut.setAttribute("href", element.url) shortcut.setAttribute("href", element.url);
shortcut.setAttribute("target", "_blank") shortcut.setAttribute("target", "_blank");
var shortcutdiv = document.createElement("div") var shortcutdiv = document.createElement("div");
shortcutdiv.classList.add("shortcut") shortcutdiv.classList.add("shortcut");
shortcutdiv.classList.add("customshortcut") shortcutdiv.classList.add("customshortcut");
let image = stringToHTML( let image = stringToHTML(
` `
@@ -25,13 +25,13 @@ export function CreateCustomShortcutDiv(element: any) {
</text> </text>
</svg> </svg>
`, `,
).firstChild ).firstChild;
;(image as HTMLElement).classList.add("shortcuticondiv") (image as HTMLElement).classList.add("shortcuticondiv");
var text = document.createElement("p") var text = document.createElement("p");
text.textContent = element.name text.textContent = element.name;
shortcutdiv.append(image!) shortcutdiv.append(image!);
shortcutdiv.append(text) shortcutdiv.append(text);
shortcut.append(shortcutdiv) shortcut.append(shortcutdiv);
document.getElementById("shortcuts")!.append(shortcut) document.getElementById("shortcuts")!.append(shortcut);
} }
@@ -6,21 +6,21 @@ export function CreateElement(
innerHTML?: string, innerHTML?: string,
style?: string, style?: string,
) { ) {
let element = document.createElement(type) let element = document.createElement(type);
if (class_ !== undefined) { if (class_ !== undefined) {
element.classList.add(class_) element.classList.add(class_);
} }
if (id !== undefined) { if (id !== undefined) {
element.id = id element.id = id;
} }
if (innerText !== undefined) { if (innerText !== undefined) {
element.innerText = innerText element.innerText = innerText;
} }
if (innerHTML !== undefined) { if (innerHTML !== undefined) {
element.innerHTML = innerHTML element.innerHTML = innerHTML;
} }
if (style !== undefined) { if (style !== undefined) {
element.style.cssText = style element.style.cssText = style;
} }
return element return element;
} }
@@ -1,24 +1,24 @@
export function RemoveShortcutDiv(elements: any) { export function RemoveShortcutDiv(elements: any) {
if (elements.length === 0) return if (elements.length === 0) return;
elements.forEach((element: any) => { elements.forEach((element: any) => {
const shortcuts = document.querySelectorAll(".shortcut") const shortcuts = document.querySelectorAll(".shortcut");
shortcuts.forEach((shortcut) => { shortcuts.forEach((shortcut) => {
const anchorElement = shortcut.parentElement // the <a> element is the parent const anchorElement = shortcut.parentElement; // the <a> element is the parent
const textElement = shortcut.querySelector("p") // <p> is a direct child of .shortcut const textElement = shortcut.querySelector("p"); // <p> is a direct child of .shortcut
const title = textElement ? textElement.textContent : "" const title = textElement ? textElement.textContent : "";
let shouldRemove = title === element.name let shouldRemove = title === element.name;
// Check href only if element.url exists // Check href only if element.url exists
if (element.url) { if (element.url) {
shouldRemove = shouldRemove =
shouldRemove && anchorElement!.getAttribute("href") === element.url shouldRemove && anchorElement!.getAttribute("href") === element.url;
} }
if (shouldRemove) { if (shouldRemove) {
anchorElement!.remove() anchorElement!.remove();
} }
}) });
}) });
} }

Some files were not shown because too many files have changed in this diff Show More