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
+102 -119
View File
@@ -2,87 +2,83 @@
module.exports = {
forbidden: [
{
name: 'no-circular',
severity: 'warn',
name: "no-circular",
severity: "warn",
comment:
'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) ',
"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) ",
from: {},
to: {
circular: true
}
circular: true,
},
},
{
name: 'no-orphans',
name: "no-orphans",
comment:
"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), " +
"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 " +
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
severity: 'warn',
severity: "warn",
from: {
orphan: true,
pathNot: [
'(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$', // dot files
'[.]d[.]ts$', // TypeScript declaration files
'(^|/)tsconfig[.]json$', // TypeScript config
'(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$' // other configs
]
"(^|/)[.][^/]+[.](?:js|cjs|mjs|ts|cts|mts|json)$", // dot files
"[.]d[.]ts$", // TypeScript declaration files
"(^|/)tsconfig[.]json$", // TypeScript config
"(^|/)(?:babel|webpack)[.]config[.](?:js|cjs|mjs|ts|cts|mts|json)$", // other configs
],
},
to: {},
},
{
name: 'no-deprecated-core',
name: "no-deprecated-core",
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.",
severity: 'warn',
severity: "warn",
from: {},
to: {
dependencyTypes: [
'core'
],
dependencyTypes: ["core"],
path: [
'^v8/tools/codemap$',
'^v8/tools/consarray$',
'^v8/tools/csvparser$',
'^v8/tools/logreader$',
'^v8/tools/profile_view$',
'^v8/tools/profile$',
'^v8/tools/SourceMap$',
'^v8/tools/splaytree$',
'^v8/tools/tickprocessor-driver$',
'^v8/tools/tickprocessor$',
'^node-inspect/lib/_inspect$',
'^node-inspect/lib/internal/inspect_client$',
'^node-inspect/lib/internal/inspect_repl$',
'^async_hooks$',
'^punycode$',
'^domain$',
'^constants$',
'^sys$',
'^_linklist$',
'^_stream_wrap$'
"^v8/tools/codemap$",
"^v8/tools/consarray$",
"^v8/tools/csvparser$",
"^v8/tools/logreader$",
"^v8/tools/profile_view$",
"^v8/tools/profile$",
"^v8/tools/SourceMap$",
"^v8/tools/splaytree$",
"^v8/tools/tickprocessor-driver$",
"^v8/tools/tickprocessor$",
"^node-inspect/lib/_inspect$",
"^node-inspect/lib/internal/inspect_client$",
"^node-inspect/lib/internal/inspect_repl$",
"^async_hooks$",
"^punycode$",
"^domain$",
"^constants$",
"^sys$",
"^_linklist$",
"^_stream_wrap$",
],
}
},
},
{
name: 'not-to-deprecated',
name: "not-to-deprecated",
comment:
'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.',
severity: 'warn',
"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.",
severity: "warn",
from: {},
to: {
dependencyTypes: [
'deprecated'
]
}
dependencyTypes: ["deprecated"],
},
},
{
name: 'no-non-package-json',
severity: 'error',
name: "no-non-package-json",
severity: "error",
comment:
"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 " +
@@ -90,84 +86,75 @@ module.exports = {
"in your package.json.",
from: {},
to: {
dependencyTypes: [
'npm-no-pkg',
'npm-unknown'
]
}
dependencyTypes: ["npm-no-pkg", "npm-unknown"],
},
},
{
name: 'not-to-unresolvable',
name: "not-to-unresolvable",
comment:
"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.',
severity: 'error',
"module: add it to your package.json. In all other cases you likely already know what to do.",
severity: "error",
from: {},
to: {
couldNotResolve: true
}
couldNotResolve: true,
},
},
{
name: 'no-duplicate-dep-types',
name: "no-duplicate-dep-types",
comment:
"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 " +
"maintenance problems later on.",
severity: 'warn',
severity: "warn",
from: {},
to: {
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
// types for this rule
dependencyTypesNot: ["type-only"]
}
dependencyTypesNot: ["type-only"],
},
},
/* rules you might want to tweak for your specific situation: */
{
name: 'not-to-spec',
name: "not-to-spec",
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 " +
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
severity: 'error',
"responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.",
severity: "error",
from: {},
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',
severity: 'error',
name: "not-to-dev-dep",
severity: "error",
comment:
"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'" +
'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',
"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: {
path: '^(src)',
pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
path: "^(src)",
pathNot: "[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$",
},
to: {
dependencyTypes: [
'npm-dev',
],
dependencyTypes: ["npm-dev"],
// type only dependencies are not a problem as they don't end up in the
// production code or are ignored by the runtime.
dependencyTypesNot: [
'type-only'
],
pathNot: [
'node_modules/@types/'
]
}
dependencyTypesNot: ["type-only"],
pathNot: ["node_modules/@types/"],
},
},
{
name: 'optional-deps-used',
severity: 'info',
name: "optional-deps-used",
severity: "info",
comment:
"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. " +
@@ -175,33 +162,28 @@ module.exports = {
"dependency-cruiser configuration.",
from: {},
to: {
dependencyTypes: [
'npm-optional'
]
}
dependencyTypes: ["npm-optional"],
},
},
{
name: 'peer-deps-used',
name: "peer-deps-used",
comment:
"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 " +
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
"add an exception to your dependency-cruiser configuration.",
severity: 'warn',
severity: "warn",
from: {},
to: {
dependencyTypes: [
'npm-peer'
]
}
}
dependencyTypes: ["npm-peer"],
},
},
],
options: {
/* Which modules not to follow further when encountered */
doNotFollow: {
/* path: an array of regular expressions in strings to match against */
path: ['node_modules']
path: ["node_modules"],
},
/* Which modules to exclude */
@@ -224,7 +206,7 @@ module.exports = {
As in practice only commonjs ('cjs') and ecmascript modules ('es6')
are widely used, you can limit the moduleSystems to those.
*/
// moduleSystems: ['cjs', 'es6'],
/*
@@ -249,7 +231,7 @@ module.exports = {
"specify": for each dependency identify whether it only exists before compilation or also after
*/
tsPreCompilationDeps: true,
/* list of extensions to scan that aren't javascript or compile-to-javascript.
Empty by default. Only put extensions in here that you want to take into
account that are _not_ parsable.
@@ -274,7 +256,7 @@ module.exports = {
defaults to './tsconfig.json'.
*/
tsConfig: {
fileName: 'tsconfig.json'
fileName: "tsconfig.json",
},
/* Webpack configuration to use to get resolve options from.
@@ -306,7 +288,7 @@ module.exports = {
a hack.
*/
// exoticRequireStrings: [],
/* options to pass on to enhanced-resolve, the package dependency-cruiser
uses to resolve module references to disk. The values below should be
suitable for most situations
@@ -315,7 +297,7 @@ module.exports = {
there will override the ones specified here.
*/
enhancedResolveOptions: {
/* What to consider as an 'exports' field in package.jsons */
/* What to consider as an 'exports' field in package.jsons */
exportsFields: ["exports"],
/* List of conditions to check for in the exports field.
Only works when the 'exportsFields' array is non-empty.
@@ -348,13 +330,13 @@ module.exports = {
for details
*/
skipAnalysisNotInRules: true,
/* List of built-in modules to use on top of the ones node declares.
See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#builtinmodules-influencing-what-to-consider-built-in--core-modules
for details
*/
builtInModules: {
builtInModules: {
add: [
"bun",
"bun:ffi",
@@ -364,8 +346,8 @@ module.exports = {
"bun:wrap",
"detect-libc",
"undici",
"ws"
]
"ws",
],
},
reporterOptions: {
@@ -375,7 +357,7 @@ module.exports = {
collapses everything in node_modules to one folder deep so you see
the external modules, but their innards.
*/
collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)',
collapsePattern: "node_modules/(?:@[^/]+/[^/]+|[^/]+)",
/* Options to tweak the appearance of your graph.See
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
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
theme for 'archi' dependency-cruiser will use the one specified in the
@@ -405,10 +388,10 @@ module.exports = {
*/
// theme: { },
},
"text": {
"highlightFocused": true
text: {
highlightFocused: true,
},
}
}
},
},
};
// generated: dependency-cruiser@16.10.0 on 2025-02-16T22:32:01.621Z
+10 -7
View File
@@ -12,12 +12,15 @@
},
"rules": {
// allow importing ts extensions
"sort-imports": ["error", {
"ignoreCase": true,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
}],
"sort-imports": [
"error",
{
"ignoreCase": true,
"ignoreDeclarationSort": true,
"ignoreMemberSort": false,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
}
],
"import/extensions": [
"error",
"ignorePackages",
@@ -29,4 +32,4 @@
]
},
"plugins": ["import"]
}
}
+47 -47
View File
@@ -3,54 +3,54 @@ description: Report an issue with the modpack in its unmodified state. For other
labels: bug
title: "[BUG]"
body:
- type: markdown
attributes:
value: |
Before reporting an issue, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) to make sure it has not already been reported (make sure to search closed issues as well!).
- type: markdown
attributes:
value: |
Before reporting an issue, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) to make sure it has not already been reported (make sure to search closed issues as well!).
- type: textarea
attributes:
label: Describe the bug
description: Describe your issue. For general issues and questions you'll get a faster answer [from our Discord.](https://discord.gg/YzmbnCDkat)
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: Describe your issue. For general issues and questions you'll get a faster answer [from our Discord.](https://discord.gg/YzmbnCDkat)
validations:
required: true
- type: input
attributes:
label: Extension version
description: What version of the extension are you using?
placeholder: Find it by opening the config menu and clicking the about icon in the top right.
validations:
required: true
- type: input
attributes:
label: Extension version
description: What version of the extension are you using?
placeholder: Find it by opening the config menu and clicking the about icon in the top right.
validations:
required: true
- type: dropdown
attributes:
label: Browser
description: Which Browser are you using?
options:
- Chrome
- Firefox
- Brave
- Safari
- DuckDuckGO
- Microsoft Edge
- Other Chromium-Based Browser
- Other Non-Chromium-Based Browser
validations:
required: true
- type: dropdown
attributes:
label: Browser
description: Which Browser are you using?
options:
- Chrome
- Firefox
- Brave
- Safari
- DuckDuckGO
- Microsoft Edge
- Other Chromium-Based Browser
- Other Non-Chromium-Based Browser
validations:
required: true
- type: checkboxes
attributes:
label: Confirm
options:
- label: This bug report is about an issue with the extension itself. I have not modified the extension nor added any unsupported plugins. If this is not the case, I know that I should post the issue to the extension's Discord support channel instead.
required: true
- type: textarea
attributes:
label: Additional context
description: Screenshots, video or any other information. Include photos of the console if possible
placeholder: |
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
- type: checkboxes
attributes:
label: Confirm
options:
- label: This bug report is about an issue with the extension itself. I have not modified the extension nor added any unsupported plugins. If this is not the case, I know that I should post the issue to the extension's Discord support channel instead.
required: true
- type: textarea
attributes:
label: Additional context
description: Screenshots, video or any other information. Include photos of the console if possible
placeholder: |
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
+37 -40
View File
@@ -3,52 +3,49 @@ description: Suggest a new Feature to be added or replaced in BetterSeqtaPLUS
labels: enhancement
title: "[FR]"
body:
- type: checkboxes
attributes:
label: Confirm
options:
- label: "Is this feature request related to a Bug report?"
required: false
- type: checkboxes
attributes:
label: Confirm
options:
- label: "Is this feature request related to a Bug report?"
required: false
- type: input
attributes:
- type: input
attributes:
label: Bug report link
description: "If this feature request is related to a bug report, please insert the link to the bug report here"
placeholder: "https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/..."
validations:
validations:
required: false
- type: markdown
attributes:
value: |
## 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!)
- type: markdown
attributes:
value: |
## 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!)
- type: dropdown
attributes:
label: Feature type
multiple: false
options:
- Graphical
- Functional
- Not Sure
validations:
required: true
- type: dropdown
attributes:
label: Feature type
multiple: false
options:
- Graphical
- Functional
- Not Sure
validations:
required: true
- type: input
attributes:
label: Feature Details
description: Please write, with as much detail as possible, what you would like to see from this feature.
placeholder: it would be cool if
validations:
required: false
- type: input
attributes:
label: Feature Details
description: Please write, with as much detail as possible, what you would like to see from this feature.
placeholder: it would be cool if
validations:
required: false
- type: textarea
attributes:
label: Additional details
description: Anything else that would help describe your vision (reference images, descriptions, etc)
validations:
required: false
- type: textarea
attributes:
label: Additional details
description: Anything else that would help describe your vision (reference images, descriptions, etc)
validations:
required: false
+19 -19
View File
@@ -2,9 +2,9 @@ name: NodeJS Build
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
jobs:
build:
@@ -15,24 +15,24 @@ jobs:
node-version: [20.x]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build
run: |
npm install --legacy-peer-deps
npm run build
- name: Build
run: |
npm install --legacy-peer-deps
npm run build
- name: Zip dist folder
run: |
zip -r dist.zip dist
- name: Zip dist folder
run: |
zip -r dist.zip dist
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: dist-zip
path: dist.zip
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: dist-zip
path: dist.zip
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
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
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
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
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
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+8 -9
View File
@@ -1,5 +1,4 @@
#
#
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
@@ -53,7 +52,7 @@
## Creating Custom Themes
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/BetterSEQTA/BetterSEQTA-Theme-Generator).
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/BetterSEQTA/BetterSEQTA-Theme-Generator).
Don't worry- if you get stuck feel free to ask around in the [discord](https://discord.gg/YzmbnCDkat). We're open and happy to help out! Happy creating :)
@@ -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
```
1. Install dependencies
You may install the dependencies like below:
@@ -75,20 +72,20 @@ You may install the dependencies like below:
npm install # or your preferred package manager like pnpm or yarn
```
But it is recommended to do it like this:
But it is recommended to do it like this:
```
npm install --legacy-peer-deps # Only NPM supported
```
### Running Development
2. Run the dev script (it updates as you save files)
```
npm run dev # or use your perferred package manager
```
### Building for production
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
```
3. Load the extension into chrome
- Go to `chrome://extensions`
@@ -116,7 +114,7 @@ Just remember, in order to update changes to the extension if you are running in
The folder structure is as follows:
- The `src` folder contains source files that are compiled to the build directory.
-
-
- The `src/plugins` folder contains vital loaders required for BetterSEQTA+ functionality.
- The `src/interface` folder contains source React & Svelte files that are required for the Settings page.
@@ -130,6 +128,7 @@ The folder structure is as follows:
</a>
Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/CONTRIBUTING.md)
## 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)
+5 -4
View File
@@ -4,12 +4,13 @@
Below here is the supported versions of BetterSEQTA+. Anything older than this is not supported and contains bugs.
| Version | Supported |
| ------- | ------------------ |
| 3.4.3 | ✅ |
| < 3.4.3 | :x: |
| Version | Supported |
| ------- | --------- |
| 3.4.3 | ✅ |
| < 3.4.3 | :x: |
`*` May not work on other devices.
## 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
+3 -1
View File
@@ -7,11 +7,13 @@ Welcome to the BetterSEQTA+ documentation! This documentation will help you unde
## Table of Contents
### Getting Started
- [Project Overview](./README.md) - This file
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
### Plugin System
- [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
@@ -47,4 +49,4 @@ To contribute to the documentation:
## License
BetterSEQTA+ is licensed under the [MIT License](../LICENSE).
BetterSEQTA+ is licensed under the [MIT License](../LICENSE).
+7 -1
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.
Key points:
- Be respectful and inclusive
- Focus on what is best for the community
- Show empathy towards other community members
@@ -105,6 +106,7 @@ git checkout -b feature/my-new-feature
2. **Write Clear Commit Messages**
Follow the conventional commits format:
```
feat: add new feature
fix: resolve bug with timetable
@@ -118,6 +120,7 @@ git checkout -b feature/my-new-feature
4. **Run Tests**
Make sure all tests pass before submitting your PR:
```bash
npm test
```
@@ -157,6 +160,7 @@ We follow TypeScript best practices and have a consistent code style:
5. **Use Linters**
We use ESLint and Prettier. Run them before submitting your PR:
```bash
npm run lint
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**
Fill in all sections of the bug report template:
- Description
- Steps to reproduce
- Expected behavior
@@ -195,6 +200,7 @@ We welcome feature suggestions! To suggest a new feature:
2. **Use the Feature Request Template**
Fill in all sections of the feature request template:
- Description
- Use case
- Potential implementation
@@ -259,4 +265,4 @@ If you have any questions about contributing, please:
2. Ask in the Discord server
3. Open a GitHub Discussion
Thank you for contributing to BetterSEQTA+! Your efforts help make SEQTA better for students and teachers everywhere.
Thank you for contributing to BetterSEQTA+! Your efforts help make SEQTA better for students and teachers everywhere.
+3 -1
View File
@@ -132,6 +132,7 @@ bun install
#### Extension not appearing in SEQTA
Make sure:
- You're visiting a SEQTA Learn page
- The extension is enabled
- You've refreshed the page after installing the extension
@@ -139,6 +140,7 @@ Make sure:
#### Development build not updating
Try:
1. Stopping the development server
2. Clearing your browser cache
3. Removing the extension from your browser
@@ -177,4 +179,4 @@ bun run dev
Now that you have BetterSEQTA+ installed, you can:
- [Getting Started with Plugins](./plugins/getting-started.md)
- [Contribute to the project](../CONTRIBUTING.md)
- [Contribute to the project](../CONTRIBUTING.md)
+52 -42
View File
@@ -5,6 +5,7 @@ Hey there! 👋 So you want to create a plugin for BetterSEQTA+? That's awesome!
## 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:
- Changes how SEQTA looks
- Adds new buttons or features
- Shows extra information on your timetable
@@ -16,40 +17,40 @@ 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:
```typescript
import type { Plugin } from '@/plugins/core/types';
import type { Plugin } from "@/plugins/core/types";
const myFirstPlugin: Plugin = {
// Every plugin needs these basic details
id: 'my-first-plugin',
name: 'My First Plugin',
description: 'Adds a friendly message to SEQTA',
version: '1.0.0',
id: "my-first-plugin",
name: "My First Plugin",
description: "Adds a friendly message to SEQTA",
version: "1.0.0",
// This tells BetterSEQTA+ that users can turn our plugin on/off
disableToggle: true,
// This is where the magic happens!
// This is where the magic happens!
run: async (api) => {
// Wait for the homepage to load
api.seqta.onMount('.home-page', (homePage) => {
api.seqta.onMount(".home-page", (homePage) => {
// Create our message
const message = document.createElement('div');
message.textContent = 'Hello from my first plugin! 🎉';
message.style.padding = '20px';
message.style.backgroundColor = '#e9f5ff';
message.style.borderRadius = '8px';
message.style.margin = '20px';
const message = document.createElement("div");
message.textContent = "Hello from my first plugin! 🎉";
message.style.padding = "20px";
message.style.backgroundColor = "#e9f5ff";
message.style.borderRadius = "8px";
message.style.margin = "20px";
// Add it to the page
homePage.prepend(message);
});
// Return a cleanup function that removes our message when the plugin is disabled
return () => {
const message = document.querySelector('.home-page > div');
const message = document.querySelector(".home-page > div");
message?.remove();
};
}
},
};
export default myFirstPlugin;
@@ -79,13 +80,13 @@ This helps you interact with SEQTA's pages:
```typescript
// 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
});
// Know when the user changes pages
api.seqta.onPageChange((page) => {
console.log('User went to:', page);
console.log("User went to:", page);
});
// Get the current page
@@ -97,8 +98,12 @@ const currentPage = api.seqta.getCurrentPage();
Want to let users customize your plugin? Use settings!
```typescript
import { BasePlugin } from '@/plugins/core/settings';
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Define your settings
const settings = defineSettings({
@@ -106,7 +111,7 @@ const settings = defineSettings({
default: true,
title: "Show Welcome Message",
description: "Show a friendly message on the homepage",
})
}),
});
// Create a class for your plugin
@@ -121,22 +126,22 @@ const settingsInstance = new MyPluginClass();
const myPlugin: Plugin<typeof settings> = {
// ... other plugin details ...
settings: settingsInstance.settings,
run: async (api) => {
// Use the setting
if (api.settings.showMessage) {
// Show the message
}
// Listen for setting changes
api.settings.onChange('showMessage', (newValue) => {
api.settings.onChange("showMessage", (newValue) => {
if (newValue) {
// Show the message
} else {
// Hide the message
}
});
}
},
};
```
@@ -146,14 +151,14 @@ Need to save some data? The storage API has got you covered:
```typescript
// Save some data
await api.storage.set('lastVisit', new Date().toISOString());
await api.storage.set("lastVisit", new Date().toISOString());
// Get it back later
const lastVisit = await api.storage.get('lastVisit');
const lastVisit = await api.storage.get("lastVisit");
// Listen for changes
api.storage.onChange('lastVisit', (newValue) => {
console.log('Last visit updated:', newValue);
api.storage.onChange("lastVisit", (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
// Listen for an event
api.events.on('myCustomEvent', (data) => {
console.log('Got event:', data);
api.events.on("myCustomEvent", (data) => {
console.log("Got event:", data);
});
// Send an event
api.events.emit('myCustomEvent', { some: 'data' });
api.events.emit("myCustomEvent", { some: "data" });
```
## Adding Styles
@@ -178,7 +183,7 @@ Want to make your plugin look pretty? You can add CSS styles:
```typescript
const myPlugin: Plugin = {
// ... other plugin details ...
// Add your CSS here
styles: `
.my-plugin-message {
@@ -196,10 +201,10 @@ const myPlugin: Plugin = {
to { transform: translateY(0); opacity: 1; }
}
`,
run: async (api) => {
// Your plugin code here
}
},
};
```
@@ -208,28 +213,31 @@ const myPlugin: Plugin = {
Here are some tips to make your plugin awesome:
1. **Always Clean Up**: When your plugin is disabled, clean up any changes you made:
```typescript
run: async (api) => {
// Add stuff to the page
const element = document.createElement('div');
const element = document.createElement("div");
document.body.appendChild(element);
// Return a cleanup function
return () => {
element.remove();
};
}
};
```
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:
- When SEQTA is loading
- When the user switches pages
- When the plugin is enabled/disabled
- When settings are changed
4. **Keep It Fast**: Don't slow down SEQTA:
- Use `onMount` instead of intervals or timeouts
- Clean up event listeners when they're not needed
- Don't do heavy calculations on the main thread
@@ -242,6 +250,7 @@ Here are some tips to make your plugin awesome:
## Examples
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
- [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
@@ -250,8 +259,9 @@ Want to see more examples? Check out our built-in plugins:
## Need Help?
Got stuck? No worries! Here's where you can get help:
- Join our [Discord server](https://discord.gg/YzmbnCDkat)
- 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)
Happy coding and feel free to checkout the api reference [here](./api-reference.md)
Happy coding and feel free to checkout the api reference [here](./api-reference.md)
+100 -78
View File
@@ -7,9 +7,13 @@ This document provides detailed technical information about BetterSEQTA+'s plugi
Here's how a plugin is structured:
```typescript
import type { Plugin } from '@/plugins/core/types';
import { BasePlugin } from '@/plugins/core/settings';
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// First, define your settings
const settings = defineSettings({
@@ -17,7 +21,7 @@ const settings = defineSettings({
default: true,
title: "Enable Feature",
description: "Turn this feature on or off",
})
}),
});
// Create a class to handle your settings
@@ -31,28 +35,28 @@ const settingsInstance = new MyPluginClass();
// Create your plugin
const myPlugin: Plugin<typeof settings> = {
id: 'my-plugin',
name: 'My Plugin',
description: 'A cool plugin that does things',
version: '1.0.0',
id: "my-plugin",
name: "My Plugin",
description: "A cool plugin that does things",
version: "1.0.0",
settings: settingsInstance.settings,
disableToggle: true,
run: async (api) => {
console.log('Plugin is running!');
console.log("Plugin is running!");
// Do stuff when settings change
api.settings.onChange('enabled', (enabled) => {
api.settings.onChange("enabled", (enabled) => {
if (enabled) {
console.log('Feature enabled!');
console.log("Feature enabled!");
}
});
// Return a cleanup function
return () => {
console.log('Plugin cleanup');
console.log("Plugin cleanup");
};
}
},
};
export default myPlugin;
@@ -63,27 +67,30 @@ export default myPlugin;
The SEQTA API helps you interact with SEQTA's pages:
```typescript
import type { Plugin } from '@/plugins/core/types';
import type { Plugin } from "@/plugins/core/types";
const seqtaPlugin: Plugin<typeof settings> = {
id: 'seqta-example',
name: 'SEQTA Example',
description: 'Shows how to use the SEQTA API',
version: '1.0.0',
id: "seqta-example",
name: "SEQTA Example",
description: "Shows how to use the SEQTA API",
version: "1.0.0",
settings: {},
disableToggle: true,
run: async (api) => {
// Wait for elements to appear
const { unregister: timetableUnregister } = api.seqta.onMount('.timetable', (timetable) => {
const button = document.createElement('button');
button.textContent = 'Export';
timetable.appendChild(button);
});
const { unregister: timetableUnregister } = api.seqta.onMount(
".timetable",
(timetable) => {
const button = document.createElement("button");
button.textContent = "Export";
timetable.appendChild(button);
},
);
// Track page changes
const { unregister: pageUnregister } = api.seqta.onPageChange((page) => {
console.log('User went to:', page);
console.log("User went to:", page);
});
// Clean up when disabled
@@ -91,7 +98,7 @@ const seqtaPlugin: Plugin<typeof settings> = {
timetableUnregister();
pageUnregister();
};
}
},
};
export default seqtaPlugin;
@@ -102,22 +109,29 @@ export default seqtaPlugin;
Here's how to add settings to your plugin:
```typescript
import type { Plugin } from '@/plugins/core/types';
import { BasePlugin } from '@/plugins/core/settings';
import { booleanSetting, stringSetting, numberSetting, selectSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
stringSetting,
numberSetting,
selectSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Define your settings
const settings = defineSettings({
darkMode: booleanSetting({
default: false,
title: "Dark Mode",
description: "Enable dark mode"
description: "Enable dark mode",
}),
userName: stringSetting({
default: "",
title: "User Name",
description: "Your display name",
placeholder: "Enter your name..."
placeholder: "Enter your name...",
}),
theme: selectSetting({
default: "light",
@@ -125,9 +139,9 @@ const settings = defineSettings({
description: "Choose your theme",
options: [
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" }
]
})
{ value: "dark", label: "Dark" },
],
}),
});
// Create your settings class
@@ -144,29 +158,29 @@ class ThemePluginClass extends BasePlugin<typeof settings> {
// Create the plugin
const themePlugin: Plugin<typeof settings> = {
id: 'theme-example',
name: 'Theme Example',
description: 'Shows how to use settings',
version: '1.0.0',
id: "theme-example",
name: "Theme Example",
description: "Shows how to use settings",
version: "1.0.0",
settings: new ThemePluginClass().settings,
disableToggle: true,
run: async (api) => {
// Apply initial settings
if (api.settings.darkMode) {
document.body.classList.add('dark');
document.body.classList.add("dark");
}
// Listen for changes
const { unregister } = api.settings.onChange('darkMode', (enabled) => {
document.body.classList.toggle('dark', enabled);
const { unregister } = api.settings.onChange("darkMode", (enabled) => {
document.body.classList.toggle("dark", enabled);
});
return () => {
unregister();
document.body.classList.remove('dark');
document.body.classList.remove("dark");
};
}
},
};
export default themePlugin;
@@ -177,13 +191,13 @@ export default themePlugin;
Here's how to use storage in your plugin:
```typescript
import type { Plugin } from '@/plugins/core/types';
import type { Plugin } from "@/plugins/core/types";
const storagePlugin: Plugin<typeof settings> = {
id: 'storage-example',
name: 'Storage Example',
description: 'Shows how to use storage',
version: '1.0.0',
id: "storage-example",
name: "Storage Example",
description: "Shows how to use storage",
version: "1.0.0",
settings: {},
disableToggle: true,
@@ -192,21 +206,21 @@ const storagePlugin: Plugin<typeof settings> = {
await api.storage.loaded;
// Save some data
await api.storage.set('lastVisit', new Date().toISOString());
await api.storage.set("lastVisit", new Date().toISOString());
// Get saved data
const lastVisit = await api.storage.get('lastVisit');
console.log('Last visit:', lastVisit);
const lastVisit = await api.storage.get("lastVisit");
console.log("Last visit:", lastVisit);
// Listen for changes
const { unregister } = api.storage.onChange('lastVisit', (newValue) => {
console.log('Last visit updated:', newValue);
const { unregister } = api.storage.onChange("lastVisit", (newValue) => {
console.log("Last visit updated:", newValue);
});
return () => {
unregister();
};
}
},
};
export default storagePlugin;
@@ -217,33 +231,39 @@ export default storagePlugin;
Here's how to use events in your plugin:
```typescript
import type { Plugin } from '@/plugins/core/types';
import type { Plugin } from "@/plugins/core/types";
const eventsPlugin: Plugin<typeof settings> = {
id: 'events-example',
name: 'Events Example',
description: 'Shows how to use events',
version: '1.0.0',
id: "events-example",
name: "Events Example",
description: "Shows how to use events",
version: "1.0.0",
settings: {},
disableToggle: true,
run: async (api) => {
// Listen for theme changes
const { unregister: themeListener } = api.events.on('theme.changed', (theme) => {
console.log('Theme changed to:', theme);
});
const { unregister: themeListener } = api.events.on(
"theme.changed",
(theme) => {
console.log("Theme changed to:", theme);
},
);
// Listen for notifications
const { unregister: notifyListener } = api.events.on('notification.new', (notification) => {
console.log('New notification:', notification);
});
const { unregister: notifyListener } = api.events.on(
"notification.new",
(notification) => {
console.log("New notification:", notification);
},
);
// Clean up listeners
return () => {
themeListener();
notifyListener();
};
}
},
};
export default eventsPlugin;
@@ -254,20 +274,20 @@ export default eventsPlugin;
Here's how to write efficient plugins:
```typescript
import type { Plugin } from '@/plugins/core/types';
import type { Plugin } from "@/plugins/core/types";
const efficientPlugin: Plugin<typeof settings> = {
id: 'efficient-example',
name: 'Efficient Example',
description: 'Shows performance best practices',
version: '1.0.0',
id: "efficient-example",
name: "Efficient Example",
description: "Shows performance best practices",
version: "1.0.0",
settings: {},
disableToggle: true,
run: async (api) => {
// ✅ Good: Use onMount
const { unregister } = api.seqta.onMount('.timetable', (el) => {
el.classList.add('enhanced');
const { unregister } = api.seqta.onMount(".timetable", (el) => {
el.classList.add("enhanced");
});
// ❌ Bad: Don't use intervals
@@ -277,7 +297,7 @@ const efficientPlugin: Plugin<typeof settings> = {
// }, 100);
// ✅ Good: Cache DOM elements
const header = document.querySelector('.header');
const header = document.querySelector(".header");
if (header) {
// Reuse header instead of querying again
}
@@ -285,7 +305,7 @@ const efficientPlugin: Plugin<typeof settings> = {
// ✅ Good: Batch DOM updates
const fragment = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
const div = document.createElement('div');
const div = document.createElement("div");
fragment.appendChild(div);
}
document.body.appendChild(fragment);
@@ -294,13 +314,14 @@ const efficientPlugin: Plugin<typeof settings> = {
unregister();
// clearInterval(interval); // If you used the bad approach
};
}
},
};
export default efficientPlugin;
```
Each plugin should be in its own file and exported as the default export. The plugin should:
1. Import necessary types and helpers
2. Define settings if needed
3. Create a settings class if using settings
@@ -308,7 +329,8 @@ Each plugin should be in its own file and exported as the default export. The pl
5. Export the plugin as default
Remember to always:
- Use proper TypeScript types
- Clean up when your plugin is disabled
- Handle errors gracefully
- Follow the plugin structure shown above
- Follow the plugin structure shown above
+2 -2
View File
@@ -7,10 +7,10 @@ export const base64Loader = {
const [filePath, query] = id.split("?");
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 dataURL = `data:${mimeType};base64,${data}`;
return `export default '${dataURL}';`;
},
};
};
+10 -10
View File
@@ -1,25 +1,25 @@
// ref: https://stackoverflow.com/a/76920975
import type { Plugin } from 'vite';
import type { Plugin } from "vite";
export default function ClosePlugin(): Plugin {
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
buildEnd(error) {
if(error) {
console.error('Error bundling')
console.error(error)
process.exit(1)
if (error) {
console.error("Error bundling");
console.error(error);
process.exit(1);
} else {
console.log('Build ended')
console.log("Build ended");
}
},
// use this to catch the end of a build without errors
closeBundle() {
console.log('Bundle closed')
process.exit(0)
console.log("Bundle closed");
process.exit(0);
},
}
};
}
+5 -5
View File
@@ -1,5 +1,5 @@
import type { Browser, BuildTarget, Manifest } from './types'
import type { AnyCase } from './utils'
import type { Browser, BuildTarget, Manifest } from "./types";
import type { AnyCase } from "./utils";
/**
*
*
@@ -15,7 +15,7 @@ export function createManifest(
return {
manifest,
browser,
}
};
}
/**
@@ -29,5 +29,5 @@ export function createManifest(
* @return {*} {@link Manifest}
*/
export function createManifestBase(manifest: Manifest): Manifest {
return manifest
}
return manifest;
}
+19 -19
View File
@@ -1,26 +1,26 @@
// vite-plugin-inline-worker-dev.ts
import { Plugin } from 'vite'
import fs from 'fs/promises'
import { build, transform } from 'esbuild'
import { Plugin } from "vite";
import fs from "fs/promises";
import { build, transform } from "esbuild";
export default function InlineWorkerDevPlugin(): Plugin {
return {
name: 'vite:inline-worker-dev',
name: "vite:inline-worker-dev",
async load(id) {
if (id.includes('?inlineWorker')) {
const [cleanPath] = id.split('?')
console.log('cleanPath', cleanPath)
const code = await fs.readFile(cleanPath, 'utf-8')
if (id.includes("?inlineWorker")) {
const [cleanPath] = id.split("?");
console.log("cleanPath", cleanPath);
const code = await fs.readFile(cleanPath, "utf-8");
const result = await build({
entryPoints: [cleanPath],
bundle: true,
write: false,
platform: 'browser',
format: 'iife',
target: 'esnext',
})
const workerCode = result.outputFiles[0].text
platform: "browser",
format: "iife",
target: "esnext",
});
const workerCode = result.outputFiles[0].text;
const workerBlobCode = `
const code = ${JSON.stringify(workerCode)};
@@ -28,10 +28,10 @@ export default function InlineWorkerDevPlugin(): Plugin {
const blob = new Blob([code], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob), { type: 'module' });
}
`
return workerBlobCode
`;
return workerBlobCode;
}
return null
}
}
return null;
},
};
}
+72 -49
View File
@@ -1,49 +1,63 @@
const glob = require('glob');
const semver = require('semver');
const { execSync } = require('child_process');
const path = require('path');
const glob = require("glob");
const semver = require("semver");
const { execSync } = require("child_process");
const path = require("path");
function getLatestVersion(files) {
console.log('Files passed to getLatestVersion:', files);
console.log("Files passed to getLatestVersion:", files);
const versions = files.map(file => {
const match = file.match(/@([\d\.]+)-/);
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
const versions = files
.map((file) => {
const match = file.match(/@([\d\.]+)-/);
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 semverVersion = fullVersion.split('.').slice(0, 3).join('.'); // Trim to 3.4.5
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
return { fullVersion, semverVersion };
}).filter(Boolean);
return { fullVersion, semverVersion };
})
.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
const latestSemver = semver.maxSatisfying(versions.map(v => v.semverVersion), '*');
console.log('Latest SemVer-compatible version:', latestSemver);
const latestSemver = semver.maxSatisfying(
versions.map((v) => v.semverVersion),
"*",
);
console.log("Latest SemVer-compatible version:", latestSemver);
// 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;
}
function getLatestFiles(browser) {
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
console.log('Glob pattern:', pattern);
console.log("Glob pattern:", 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);
// 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;
}
@@ -51,44 +65,53 @@ function zipSources() {
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
const excludePatterns = [
'node_modules',
'dist',
'.env*',
'.git',
'.github',
'.vscode',
'LICENSE',
'package.json'
].map(pattern => `-x!${pattern}`).join(' ');
"node_modules",
"dist",
".env*",
".git",
".github",
".vscode",
"LICENSE",
"package.json",
]
.map((pattern) => `-x!${pattern}`)
.join(" ");
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
console.log('Zipping project sources with command:', zipCommand);
execSync(zipCommand, { stdio: 'inherit' });
console.log("Zipping project sources with command:", zipCommand);
execSync(zipCommand, { stdio: "inherit" });
return zipFileName;
}
function runPublishCommand(browsers) {
const chromeZip = browsers.includes('chrome') ? getLatestFiles('chrome') : null;
const firefoxZip = browsers.includes('firefox') ? getLatestFiles('firefox') : null;
const firefoxSourcesZip = browsers.includes('firefox') ? zipSources() : null;
const chromeZip = browsers.includes("chrome")
? getLatestFiles("chrome")
: null;
const firefoxZip = browsers.includes("firefox")
? getLatestFiles("firefox")
: null;
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
console.log('Chrome zip:', chromeZip);
console.log('Firefox zip:', firefoxZip);
console.log('Firefox sources zip:', firefoxSourcesZip);
console.log("Chrome zip:", chromeZip);
console.log("Firefox zip:", firefoxZip);
console.log("Firefox sources zip:", firefoxSourcesZip);
if (browsers.length === 0) {
console.log('No browsers specified. Exiting.');
console.log("No browsers specified. Exiting.");
process.exit(0);
}
if ((browsers.includes('chrome') && !chromeZip) || (browsers.includes('firefox') && (!firefoxZip || !firefoxSourcesZip))) {
console.error('Could not find required zip files for specified browsers.');
if (
(browsers.includes("chrome") && !chromeZip) ||
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
) {
console.error("Could not find required zip files for specified browsers.");
process.exit(1);
}
let command = 'publish-extension';
let command = "publish-extension";
if (chromeZip) {
command += ` --chrome-zip ${chromeZip}`;
}
@@ -96,13 +119,13 @@ function runPublishCommand(browsers) {
command += ` --firefox-zip ${firefoxZip} --firefox-sources-zip ${firefoxSourcesZip}`;
}
console.log('Running command:', command);
execSync(command, { stdio: 'inherit' });
console.log("Running command:", command);
execSync(command, { stdio: "inherit" });
}
// Parse command-line arguments
const args = process.argv.slice(2);
const browserIndex = args.indexOf('--b');
const browserIndex = args.indexOf("--b");
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() {
return {
name: 'touch-global-css',
name: "touch-global-css",
handleHotUpdate({ modules }) {
// log all of the staticImportedUrls
const importers = modules[0]._clientModule.importers
const importers = modules[0]._clientModule.importers;
importers.forEach((importer) => {
if (importer.file.includes('.css')) {
console.log("touching", importer.file)
fs.utimesSync(importer.file, new Date(), new Date())
if (importer.file.includes(".css")) {
console.log("touching", importer.file);
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 AnyCase, createEnum } from './utils'
import type { ManifestV3Export } from "@crxjs/vite-plugin";
import { type AnyCase, createEnum } from "./utils";
export const FrameworkEnum = {
React: 'React',
Vanilla: 'Vanilla',
Preact: 'Preact',
Lit: 'Lit',
Svelte: 'Svelte',
Vue: 'Vue',
} as const
React: "React",
Vanilla: "Vanilla",
Preact: "Preact",
Lit: "Lit",
Svelte: "Svelte",
Vue: "Vue",
} as const;
export const BrowserEnum = {
Chrome: 'Chrome',
Brave: 'Brave',
Opera: 'Opera',
Edge: 'Edge',
Firefox: 'Firefox',
Safari: 'Safari',
} as const
Chrome: "Chrome",
Brave: "Brave",
Opera: "Opera",
Edge: "Edge",
Firefox: "Firefox",
Safari: "Safari",
} as const;
const LanguageEnum = {
TypeScript: 'TypeScript',
JavaScript: 'JavaScript',
} as const
TypeScript: "TypeScript",
JavaScript: "JavaScript",
} as const;
export const StyleEnum = {
Tailwind: 'Tailwind',
} as const
Tailwind: "Tailwind",
} as const;
export const PackageManagerEnum = {
Bun: 'Bun',
PnPm: 'PnPm',
Npm: 'Npm',
Yarn: 'Yarn',
} as const
Bun: "Bun",
PnPm: "PnPm",
Npm: "Npm",
Yarn: "Yarn",
} as const;
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
export type BrowserSpecificSettings = {
browser_specific_settings?: {
gecko?: {
id: string
strict_min_version?: string
strict_max_version?: string
}
}
}
id: string;
strict_min_version?: string;
strict_max_version?: string;
};
};
};
export type Manifest = ManifestV3Export
export type ManifestIcons = chrome.runtime.ManifestIcons
export type ManifestBackground = chrome.runtime.ManifestV3['background']
export type Manifest = ManifestV3Export;
export type ManifestIcons = chrome.runtime.ManifestIcons;
export type ManifestBackground = chrome.runtime.ManifestV3["background"];
export type ManifestContentScripts =
chrome.runtime.ManifestV3['content_scripts']
chrome.runtime.ManifestV3["content_scripts"];
export type ManifestWebAccessibleResources =
chrome.runtime.ManifestV3['web_accessible_resources']
export type ManifestCommands = chrome.runtime.ManifestV3['commands']
export type ManifestAction = chrome.runtime.ManifestV3['action']
export type ManifestPermissions = chrome.runtime.ManifestV3['permissions']
export type ManifestOptionsUI = chrome.runtime.ManifestV3['options_ui']
chrome.runtime.ManifestV3["web_accessible_resources"];
export type ManifestCommands = chrome.runtime.ManifestV3["commands"];
export type ManifestAction = chrome.runtime.ManifestV3["action"];
export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"];
export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"];
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> = {
[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 = {
manifest: Manifest
browser: AnyCase<Browser>
}
manifest: Manifest;
browser: AnyCase<Browser>;
};
export type BuildConfig = {
command?: 'build' | 'serve'
mode?: AnyCase<Browser> | string | undefined
}
command?: "build" | "serve";
mode?: AnyCase<Browser> | string | undefined;
};
export interface Repository {
type: string
url?: string
bugs?: Bugs
type: string;
url?: string;
bugs?: Bugs;
}
export interface Bugs {
url?: string
email?: string
url?: string;
email?: string;
}
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum]
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum)
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum];
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum);
export type PackageManager =
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum]
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum];
export const PackageManager: AnyCase<PackageManager> =
createEnum(PackageManagerEnum)
createEnum(PackageManagerEnum);
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum]
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum)
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum];
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum);
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum]
export const Style: AnyCase<Style> = createEnum(StyleEnum)
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum];
export const Style: AnyCase<Style> = createEnum(StyleEnum);
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum]
export const Language: AnyCase<Language> = createEnum(LanguageEnum)
export type Language = (typeof LanguageEnum)[keyof typeof 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) {
return Object.values(enumObj) as unknown as ObjectValues<T>
return Object.values(enumObj) as unknown as ObjectValues<T>;
}
export type AnyCase<T extends string> =
| Uppercase<T>
| Lowercase<T>
| Capitalize<T>
| Uncapitalize<T>
| Uncapitalize<T>;
export type AnyCaseLanguage<T extends string, K extends string> =
| Uppercase<T | K>
| Lowercase<T | K>
| Capitalize<T | K>
| Uncapitalize<T | K>
| Uncapitalize<T | K>;
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];
};
+31 -29
View File
@@ -1,65 +1,67 @@
import {
initializeSettingsState,
settingsState,
} from "@/seqta/utils/listeners/SettingsState"
import documentLoadCSS from "@/css/documentload.scss?inline"
import icon48 from "@/resources/icons/icon-48.png?base64"
import browser from "webextension-polyfill"
} from "@/seqta/utils/listeners/SettingsState";
import documentLoadCSS from "@/css/documentload.scss?inline";
import icon48 from "@/resources/icons/icon-48.png?base64";
import browser from "webextension-polyfill";
import * as plugins from "@/plugins"
import { main } from "@/seqta/main"
import * as plugins from "@/plugins";
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)
if (document.childNodes[1]) {
hasSEQTAText =
document.childNodes[1].textContent?.includes(
"Copyright (c) SEQTA Software",
) ?? false
init()
) ?? false;
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
IsSEQTAPage = true
console.info("[BetterSEQTA+] Verified SEQTA Page")
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
// Verify we are on a SEQTA page
IsSEQTAPage = true;
console.info("[BetterSEQTA+] Verified SEQTA Page");
const documentLoadStyle = document.createElement("style")
documentLoadStyle.textContent = documentLoadCSS
document.head.appendChild(documentLoadStyle)
const documentLoadStyle = document.createElement("style");
documentLoadStyle.textContent = documentLoadCSS;
document.head.appendChild(documentLoadStyle);
const icon = document.querySelector('link[rel*="icon"]')! as HTMLLinkElement
icon.href = icon48 // Change the icon
const icon = document.querySelector(
'link[rel*="icon"]',
)! as HTMLLinkElement;
icon.href = icon48; // Change the icon
try {
await initializeSettingsState()
await initializeSettingsState();
if (typeof settingsState.onoff === "undefined") {
await browser.runtime.sendMessage({ type: "setDefaultStorage" })
await browser.runtime.sendMessage({ type: "setDefaultStorage" });
}
await main()
await main();
if (settingsState.onoff) {
// Initialize legacy plugins
plugins.Monofile()
plugins.Monofile();
// Initialize new plugin system
await plugins.initializePlugins();
}
console.info(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
)
);
} catch (error: any) {
console.error(error)
console.error(error);
}
}
}
}
+101 -80
View File
@@ -1,63 +1,68 @@
import browser from 'webextension-polyfill'
import browser from "webextension-polyfill";
import type { SettingsState } from "@/types/storage";
import { fetchNews } from './background/news';
import { fetchNews } from "./background/news";
function reloadSeqtaPages() {
const result = browser.tabs.query({})
function open (tabs: any) {
const result = browser.tabs.query({});
function open(tabs: any) {
for (let tab of tabs) {
if (tab.title.includes('SEQTA Learn')) {
if (tab.title.includes("SEQTA Learn")) {
browser.tabs.reload(tab.id);
}
}
}
result.then(open, console.error)
result.then(open, console.error);
}
// @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) {
case "reloadTabs":
reloadSeqtaPages();
break;
switch (request.type) {
case 'reloadTabs':
reloadSeqtaPages();
break;
case 'extensionPages':
browser.tabs.query({}).then(function (tabs) {
for (let tab of tabs) {
if (tab.url?.includes('chrome-extension://')) {
browser.tabs.sendMessage(tab.id!, request);
case "extensionPages":
browser.tabs.query({}).then(function (tabs) {
for (let tab of tabs) {
if (tab.url?.includes("chrome-extension://")) {
browser.tabs.sendMessage(tab.id!, request);
}
}
}
});
break;
case 'currentTab':
browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) {
sendResponse(response);
});
});
return true;
break;
case 'githubTab':
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' });
break;
case 'setDefaultStorage':
SetStorageValue(DefaultValues);
break;
case "currentTab":
browser.tabs
.query({ active: true, currentWindow: true })
.then(function (tabs) {
browser.tabs
.sendMessage(tabs[0].id!, request)
.then(function (response) {
sendResponse(response);
});
});
return true;
case 'sendNews':
fetchNews(request.source ?? 'australia', sendResponse);
return true;
default:
console.log('Unknown request type');
}
return false;
});
case "githubTab":
browser.tabs.create({ url: "github.com/BetterSEQTA/BetterSEQTA-Plus" });
break;
case "setDefaultStorage":
SetStorageValue(DefaultValues);
break;
case "sendNews":
fetchNews(request.source ?? "australia", sendResponse);
return true;
default:
console.log("Unknown request type");
}
return false;
},
);
const DefaultValues: SettingsState = {
onoff: true,
@@ -86,66 +91,67 @@ const DefaultValues: SettingsState = {
},
menuorder: [],
subjectfilters: {},
selectedTheme: '',
selectedColor: 'linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)',
originalSelectedColor: '',
selectedTheme: "",
selectedColor:
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
originalSelectedColor: "",
DarkMode: true,
animations: true,
assessmentsAverage: true,
defaultPage: 'home',
defaultPage: "home",
shortcuts: [
{
name: 'YouTube',
name: "YouTube",
enabled: false,
},
{
name: 'Outlook',
name: "Outlook",
enabled: true,
},
{
name: 'Office',
name: "Office",
enabled: true,
},
{
name: 'Spotify',
name: "Spotify",
enabled: false,
},
{
name: 'Google',
name: "Google",
enabled: true,
},
{
name: 'DuckDuckGo',
name: "DuckDuckGo",
enabled: false,
},
{
name: 'Cool Math Games',
name: "Cool Math Games",
enabled: false,
},
{
name: 'SACE',
name: "SACE",
enabled: false,
},
{
name: 'Google Scholar',
name: "Google Scholar",
enabled: false,
},
{
name: 'Gmail',
name: "Gmail",
enabled: false,
},
{
name: 'Netflix',
name: "Netflix",
enabled: false,
},
{
name: 'Education Perfect',
name: "Education Perfect",
enabled: false,
},
],
customshortcuts: [],
lettergrade: false,
newsSource: 'australia',
newsSource: "australia",
};
function SetStorageValue(object: any) {
@@ -158,7 +164,8 @@ function convertBksliderToSpeed(bksliderinput: number): number {
const minBase = 50;
const maxBase = 150;
const scaledValue = 2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
const scaledValue =
2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
const baseSpeed = 3;
const speed = baseSpeed / scaledValue;
@@ -166,50 +173,64 @@ function convertBksliderToSpeed(bksliderinput: number): number {
}
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
if ('animatedbk' in storage || 'bksliderinput' in storage) {
if ("animatedbk" in storage || "bksliderinput" in storage) {
const animatedSettings = {
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
if ('assessmentsAverage' in storage || 'lettergrade' in storage) {
if ("assessmentsAverage" in storage || "lettergrade" in storage) {
const assessmentsSettings = {
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 };
await browser.storage.local.set({ 'plugin.themes.settings': themesSettings });
await browser.storage.local.set({
"plugin.themes.settings": themesSettings,
});
}
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 {
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: false } });
await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: false },
});
}
const keysToRemove = [
'animatedbk',
'bksliderinput',
'assessmentsAverage',
'lettergrade'
"animatedbk",
"bksliderinput",
"assessmentsAverage",
"lettergrade",
];
await browser.storage.local.remove(keysToRemove);
}
browser.runtime.onInstalled.addListener(function (event) {
browser.storage.local.remove(['justupdated']);
browser.storage.local.remove(['data']);
browser.storage.local.remove(["justupdated"]);
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 });
migrateLegacySettings();
}
+14 -15
View File
@@ -1,11 +1,11 @@
import Parser from 'rss-parser';
import Parser from "rss-parser";
const fetchAustraliaNews = async (url: string, sendResponse: any) => {
fetch(url)
.then((result) => result.json())
.then((response) => {
if (response.code == 'rateLimited') {
fetchAustraliaNews(url += '%00', sendResponse);
if (response.code == "rateLimited") {
fetchAustraliaNews((url += "%00"), sendResponse);
} else {
sendResponse({ news: response });
}
@@ -31,13 +31,13 @@ const rssFeedsByCountry: Record<string, string[]> = {
"https://critica.com.pa/rss.xml",
"https://www.panamaamerica.com.pa/rss.xml",
"https://noticiassin.com/feed/",
"https://elcapitalfinanciero.com/feed/"
"https://elcapitalfinanciero.com/feed/",
],
canada: [
"https://www.cbc.ca/cmlink/rss-topstories",
"https://calgaryherald.com/feed",
"https://ottawacitizen.com/feed",
"https://www.montrealgazette.com/feed"
"https://www.montrealgazette.com/feed",
],
singapore: [
"https://www.straitstimes.com/news/singapore/rss.xml",
@@ -49,12 +49,9 @@ const rssFeedsByCountry: Record<string, string[]> = {
],
japan: [
"https://www3.nhk.or.jp/nhkworld/en/news/feeds/",
"https://news.livedoor.com/topics/rss/int.xml"
],
netherlands: [
"https://www.dutchnews.nl/feed/",
"https://www.nrc.nl/rss/"
"https://news.livedoor.com/topics/rss/int.xml",
],
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
};
export async function fetchNews(source: string, sendResponse: any) {
@@ -63,11 +60,11 @@ export async function fetchNews(source: string, sendResponse: any) {
const from =
date.getFullYear() +
'-' +
"-" +
(date.getMonth() + 1) +
'-' +
"-" +
(date.getDate() - 5);
const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`;
fetchAustraliaNews(url, sendResponse);
@@ -76,7 +73,7 @@ export async function fetchNews(source: string, sendResponse: any) {
const parser = new Parser();
let feeds: string[];
console.log('fetchNews', source)
console.log("fetchNews", source);
if (rssFeedsByCountry[source.toLowerCase()]) {
// 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
feeds = [source];
} 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) => {
+5 -3
View File
@@ -15,7 +15,7 @@
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
*/
@use 'injected/popup.scss';
@use "injected/popup.scss";
html {
background: #161616 !important;
@@ -77,7 +77,9 @@ html {
transform-origin: top;
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);
}
.assessmenttooltip svg {
@@ -91,4 +93,4 @@ body:has(.outside-container:not(.hide)) #AddedSettings.tooltip:hover > .tooltipt
top: 115% !important;
background: var(--text-primary) !important;
color: var(--theme-primary) !important;
}
}
+1 -1
View File
@@ -1 +1 @@
import './documentload.scss';
import "./documentload.scss";
+5 -3
View File
@@ -15,7 +15,7 @@
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
*/
body {
body {
background: transparent;
}
@@ -25,7 +25,9 @@
span,
body {
color: white !important;
text-shadow: 1px 1px 2px #161616, 0 0 1em #161616;
text-shadow:
1px 1px 2px #161616,
0 0 1em #161616;
}
body {
@@ -111,4 +113,4 @@
text-decoration: underline;
transition: text-shadow 0.5s;
}
}
}
+24 -9
View File
@@ -758,7 +758,7 @@ ol > [data-label] {
margin-left: 4px;
margin-bottom: 4px;
}
[class*="Message__Message___"] > .uiFrameWrapper .iframeWrapper {
[class*="Message__Message___"] > .uiFrameWrapper .iframeWrapper {
background: transparent;
}
[class*="Viewer__newMessage___"] {
@@ -1375,10 +1375,13 @@ div > ol:has(.uiFileHandlerWrapper) {
margin: 20px auto 0px;
cursor: pointer;
}
.dark [class*="notifications__detailsBody___"] > [class*="notifications__subtitle___"] {
.dark
[class*="notifications__detailsBody___"]
> [class*="notifications__subtitle___"] {
color: #c1bcbc;
}
[class*="notifications__detailsBody___"] > [class*="notifications__subtitle___"] {
[class*="notifications__detailsBody___"]
> [class*="notifications__subtitle___"] {
font-size: 12px;
}
[class*="notifications__notifications___"] > button {
@@ -1394,7 +1397,9 @@ div > ol:has(.uiFileHandlerWrapper) {
height: 25px;
width: 24px;
}
[class*="notifications__notifications___"] > button > [class*="notifications__bubble___"] {
[class*="notifications__notifications___"]
> button
> [class*="notifications__bubble___"] {
background: var(--better-alert-highlight);
width: 25px;
height: 25px;
@@ -1710,7 +1715,9 @@ ul {
> [class*="SelectedAssessment__meta___"] {
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);
}
[class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] {
@@ -2181,7 +2188,9 @@ body {
border-radius: 1600px;
}
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__selected___"]
[class*="MessageList__MessageList___"]
> ol
> li[class*="MessageList__selected___"]
[class*="MessageList__unread___"] {
box-shadow: none;
}
@@ -2190,7 +2199,9 @@ body {
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 {
content: "";
position: absolute;
@@ -2202,7 +2213,9 @@ body {
transition: width 0.1s;
}
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__unread___"]::before {
[class*="MessageList__MessageList___"]
> ol
> li[class*="MessageList__unread___"]::before {
width: 3px;
}
.connectedNotificationsWrapper > div > button {
@@ -2283,7 +2296,9 @@ body {
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);
color: var(--text-primary);
}
+4 -2
View File
@@ -36,5 +36,7 @@
transform-origin: 70% 0;
will-change: opacity, transform;
transform: translateZ(0); // promotes GPU rendering
transition: opacity 0.05s, transform 0.05s;
}
transition:
opacity 0.05s,
transform 0.05s;
}
+2 -2
View File
@@ -25,7 +25,7 @@
padding-top: 2px;
}
.sub:has(ul>li.hasChildren.active) > .nav > .back {
.sub:has(ul > li.hasChildren.active) > .nav > .back {
display: none !important;
}
@@ -40,4 +40,4 @@
#menu > ul:has(li.hasChildren.active) > li.active {
background: transparent !important;
}
}
+1 -1
View File
@@ -12,4 +12,4 @@ html:not(.dark) {
--theme-primary: #ffffff;
--theme-secondary: #e5e7eb;
--text-primary: black;
}
}
+4 -4
View File
@@ -8,7 +8,6 @@ html.transparencyEffects:not(.dark) {
--background-secondary: rgba(229, 231, 235, 0.6);
}
html.transparencyEffects {
/* Background Fixes */
[class*="notifications__item___"],
@@ -37,11 +36,12 @@ html.transparencyEffects {
[class*="LabelList__selected___"],
.buttonChecklist,
.pane,
.legacy-root button, .legacy-root a,
.legacy-root button,
.legacy-root a,
[class*="MessageList__MessageList___"] {
backdrop-filter: blur(80px);
}
.report {
backdrop-filter: blur(10px) !important;
}
@@ -71,4 +71,4 @@ html.transparencyEffects {
backdrop-filter: blur(8px);
}
}
}
}
+8 -8
View File
@@ -1,11 +1,11 @@
declare module '*.mp4';
declare module '*.woff';
declare module '*.scss';
declare module '*.png';
declare module '*.html';
declare module '*.svelte';
declare module "*.mp4";
declare module "*.woff";
declare module "*.scss";
declare module "*.png";
declare module "*.html";
declare module "*.svelte";
declare module '*?inlineWorker' {
declare module "*?inlineWorker" {
const value: () => Worker;
export default value;
}
@@ -33,4 +33,4 @@ declare module "*.gif?base64" {
declare module "*.svg?base64" {
const value: string;
export default value;
}
}
+45 -30
View File
@@ -1,6 +1,6 @@
import ColorPicker from "react-best-gradient-color-picker"
import { useEffect, useRef, useState } from "react"
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
import ColorPicker from "react-best-gradient-color-picker";
import { useEffect, useRef, useState } from "react";
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts";
const defaultPresets = [
"linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)",
@@ -22,12 +22,12 @@ const defaultPresets = [
"rgba(30, 64, 175, 0.89)",
"rgba(134, 25, 143, 1)",
"rgba(14, 165, 233, 0.9)",
]
];
interface PickerProps {
customOnChange?: (color: string) => void
customState?: string
savePresets?: boolean
customOnChange?: (color: string) => void;
customState?: string;
savePresets?: boolean;
}
export default function Picker({
@@ -35,37 +35,49 @@ export default function Picker({
customState,
savePresets = true,
}: PickerProps) {
const [customThemeColor, setCustomThemeColor] = useState<string | null>()
const [presets, setPresets] = useState<string[]>()
const [customThemeColor, setCustomThemeColor] = useState<string | null>();
const [presets, setPresets] = useState<string[]>();
const latestValuesRef = useRef({ customThemeColor, customOnChange, savePresets, presets });
const latestValuesRef = useRef({
customThemeColor,
customOnChange,
savePresets,
presets,
});
useEffect(() => {
if (customState !== undefined && customState !== null) {
setCustomThemeColor(customState)
setCustomThemeColor(customState);
} else {
setCustomThemeColor(settingsState.selectedColor ?? null)
setCustomThemeColor(settingsState.selectedColor ?? null);
}
if (presets === undefined) {
const savedPresets = localStorage.getItem("colorPickerPresets")
setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets)
const savedPresets = localStorage.getItem("colorPickerPresets");
setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets);
}
}, [])
}, []);
useEffect(() => {
latestValuesRef.current = { customThemeColor, customOnChange, savePresets, presets };
latestValuesRef.current = {
customThemeColor,
customOnChange,
savePresets,
presets,
};
}, [customThemeColor, customOnChange, savePresets, presets]);
useEffect(() => {
return () => {
const { customThemeColor, customOnChange, savePresets, presets } = latestValuesRef.current;
if (!(customThemeColor && !customOnChange && savePresets && presets)) return;
const { customThemeColor, customOnChange, savePresets, presets } =
latestValuesRef.current;
if (!(customThemeColor && !customOnChange && savePresets && presets))
return;
// Only proceed if presets are different (avoid unnecessary updates)
const existingIndex = presets.indexOf(customThemeColor);
let updatedPresets;
if (existingIndex === 0) {
// No need to update if the selected color is already the first element
return;
@@ -78,16 +90,19 @@ export default function Picker({
} else {
updatedPresets = [customThemeColor, ...presets].slice(0, 18);
}
localStorage.setItem("colorPickerPresets", JSON.stringify(updatedPresets));
}
}, [])
localStorage.setItem(
"colorPickerPresets",
JSON.stringify(updatedPresets),
);
};
}, []);
useEffect(() => {
if (customThemeColor && !customOnChange) {
settingsState.selectedColor = customThemeColor
settingsState.selectedColor = customThemeColor;
}
}, [customThemeColor, customOnChange])
}, [customThemeColor, customOnChange]);
return (
<ColorPicker
@@ -97,12 +112,12 @@ export default function Picker({
value={customThemeColor ?? ""}
onChange={(color: string) => {
if (customOnChange) {
customOnChange(color)
setCustomThemeColor(color)
customOnChange(color);
setCustomThemeColor(color);
} else {
setCustomThemeColor(color)
setCustomThemeColor(color);
}
}}
/>
)
);
}
+2 -2
View File
@@ -1,4 +1,4 @@
.dark .switch[data-ison="true"],
.switch[data-ison="true"] {
background-color: #30D259;
}
background-color: #30d259;
}
+1 -1
View File
@@ -1,3 +1,3 @@
.tab-width {
width: var(--tab-width);
}
}
+26 -16
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 {
backgrounds: {
@@ -16,38 +16,46 @@ let db: IDBPDatabase<BackgroundDB> | null = null;
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
if (db) return db;
db = await openDB<BackgroundDB>('BackgroundDB', 1, {
db = await openDB<BackgroundDB>("BackgroundDB", 1, {
upgrade(db: IDBPDatabase<BackgroundDB>) {
db.createObjectStore('backgrounds', { keyPath: 'id' });
db.createObjectStore("backgrounds", { keyPath: "id" });
},
});
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();
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();
await db.put('backgrounds', { id, type, blob });
await db.put("backgrounds", { id, type, blob });
}
export async function deleteData(id: string): Promise<void> {
const db = await openDatabase();
await db.delete('backgrounds', id);
await db.delete("backgrounds", id);
}
export async function clearAllData(): Promise<void> {
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();
return db.get('backgrounds', id);
return db.get("backgrounds", id);
}
export function closeDatabase(): void {
@@ -59,17 +67,19 @@ export function closeDatabase(): void {
// Helper function to check if IndexedDB is supported
export function isIndexedDBSupported(): boolean {
return 'indexedDB' in window;
return "indexedDB" in window;
}
// Helper function to check if there's enough storage space
export async function hasEnoughStorageSpace(requiredSpace: number): Promise<boolean> {
if ('storage' in navigator && 'estimate' in navigator.storage) {
export async function hasEnoughStorageSpace(
requiredSpace: number,
): Promise<boolean> {
if ("storage" in navigator && "estimate" in navigator.storage) {
const { quota, usage } = await navigator.storage.estimate();
if (quota !== undefined && usage !== undefined) {
return (quota - usage) > requiredSpace;
return quota - usage > requiredSpace;
}
}
// If we can't determine, assume there's enough space
return true;
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ class BackgroundUpdates {
}
public triggerUpdate(): void {
this.listeners.forEach(callback => callback());
this.listeners.forEach((callback) => callback());
}
}
+3 -3
View File
@@ -1,13 +1,13 @@
type SettingsPopupCallback = () => void;
/**
/**
* This is a singleton that triggers an update when the settings popup is closed.
* This is used to close the colour picker.
* Usage:
* settingsPopup.addListener(() => {
* console.log('Settings popup closed');
* });
*/
*/
class SettingsPopup {
private static instance: SettingsPopup;
private listeners: Set<SettingsPopupCallback> = new Set();
@@ -30,7 +30,7 @@ class SettingsPopup {
}
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 {
this.listeners.forEach(callback => callback());
this.listeners.forEach((callback) => callback());
}
}
@@ -1 +1 @@
export let selectedBackground = $state<string | null>(null);
export let selectedBackground = $state<string | null>(null);
+2 -2
View File
@@ -1,4 +1,4 @@
@import './components/ColourPicker.css';
@import "./components/ColourPicker.css";
@tailwind base;
@tailwind components;
@@ -47,4 +47,4 @@ input {
.editorHeight {
height: calc(100vh - 58px);
}
}
+2 -2
View File
@@ -6,7 +6,7 @@
<title>BetterSEQTA+ Settings</title>
</head>
<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>
</body>
</html>
</html>
+15 -15
View File
@@ -1,29 +1,29 @@
import "./index.css"
import Settings from "./pages/settings.svelte"
import IconFamily from '@/resources/fonts/IconFamily.woff'
import browser from "webextension-polyfill"
import renderSvelte from "./main"
import "./index.css";
import Settings from "./pages/settings.svelte";
import IconFamily from "@/resources/fonts/IconFamily.woff";
import browser from "webextension-polyfill";
import renderSvelte from "./main";
function InjectCustomIcons() {
console.info('[BetterSEQTA+] Injecting Icons')
console.info("[BetterSEQTA+] Injecting Icons");
const style = document.createElement('style')
style.setAttribute('type', 'text/css')
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.innerHTML = `
@font-face {
font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal;
font-style: normal;
}`
document.head.appendChild(style)
}`;
document.head.appendChild(style);
}
const mountPoint = document.getElementById('app')
const mountPoint = document.getElementById("app");
if (!mountPoint) {
console.error('Mount point #app not found')
throw new Error('Mount point #app not found')
console.error("Mount point #app not found");
throw new Error("Mount point #app not found");
}
InjectCustomIcons()
renderSvelte(Settings, mountPoint, { standalone: true })
InjectCustomIcons();
renderSvelte(Settings, mountPoint, { standalone: true });
+2 -2
View File
@@ -1,6 +1,6 @@
import './index.css';
import "./index.css";
declare module "*.png";
declare module "*.svg";
declare module "*.jpeg";
declare module "*.jpg";
declare module "*.jpg";
+8 -8
View File
@@ -1,6 +1,6 @@
import { mount } from "svelte"
import type { SvelteComponent } from "svelte"
import style from './index.css?inline'
import { mount } from "svelte";
import type { SvelteComponent } from "svelte";
import style from "./index.css?inline";
export default function renderSvelte(
Component: SvelteComponent | any,
@@ -13,11 +13,11 @@ export default function renderSvelte(
standalone: false,
...props,
},
})
});
const styleElement = document.createElement('style')
styleElement.textContent = style
mountPoint.appendChild(styleElement)
const styleElement = document.createElement("style");
styleElement.textContent = style;
mountPoint.appendChild(styleElement);
return app
return app;
}
+2 -2
View File
@@ -2,6 +2,6 @@ export interface SettingsList {
title: string;
id: number;
description: string;
Component: any; /* TODO: Give this a type */
Component: any /* TODO: Give this a type */;
props?: any;
}
}
+1 -1
View File
@@ -4,4 +4,4 @@ export type Theme = {
coverImage: string;
marqueeImage: string;
id: string;
};
};
+25 -25
View File
@@ -1,36 +1,36 @@
import type { Subscriber, Unsubscriber } from "svelte/store";
export class Standalone {
private static instance: Standalone;
private _standalone = $state(false);
private subscribers = new Set<Subscriber<boolean>>();
private static instance: Standalone;
private _standalone = $state(false);
private subscribers = new Set<Subscriber<boolean>>();
private constructor() {}
private constructor() {}
public static getInstance(): Standalone {
if (!Standalone.instance) {
Standalone.instance = new Standalone();
}
return Standalone.instance;
}
public static getInstance(): Standalone {
if (!Standalone.instance) {
Standalone.instance = new Standalone();
}
return Standalone.instance;
}
public setStandalone(value: boolean) {
this._standalone = value;
this.subscribers.forEach(subscriber => subscriber(value));
}
public setStandalone(value: boolean) {
this._standalone = value;
this.subscribers.forEach((subscriber) => subscriber(value));
}
public get standalone() {
return this._standalone;
}
public get standalone() {
return this._standalone;
}
public subscribe(run: Subscriber<boolean>): Unsubscriber {
this.subscribers.add(run);
run(this._standalone);
public subscribe(run: Subscriber<boolean>): Unsubscriber {
this.subscribers.add(run);
run(this._standalone);
return () => {
this.subscribers.delete(run);
};
}
return () => {
this.subscribers.delete(run);
};
}
}
export const standalone = Standalone.getInstance();
export const standalone = Standalone.getInstance();
+32 -12
View File
@@ -1,23 +1,31 @@
import type { LoadedCustomTheme } from '@/types/CustomThemes';
import type { LoadedCustomTheme } from "@/types/CustomThemes";
export function generateImageId(): string {
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 file = input.files?.[0];
input.value = '';
input.value = "";
if (file) {
return new Promise((resolve) => {
const reader = new FileReader();
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 variableName = `custom-image-${theme.CustomImages.length}`;
resolve({
...theme,
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: null }],
CustomImages: [
...theme.CustomImages,
{ id: imageId, blob: imageBlob, variableName, url: null },
],
});
};
reader.readAsDataURL(file);
@@ -26,35 +34,47 @@ export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promi
return theme;
}
export function handleRemoveImage(imageId: string, theme: LoadedCustomTheme): LoadedCustomTheme {
export function handleRemoveImage(
imageId: string,
theme: LoadedCustomTheme,
): LoadedCustomTheme {
return {
...theme,
CustomImages: theme.CustomImages.filter((image) => image.id !== imageId),
} as LoadedCustomTheme;
}
export function handleImageVariableChange(imageId: string, variableName: string, theme: LoadedCustomTheme): LoadedCustomTheme {
export function handleImageVariableChange(
imageId: string,
variableName: string,
theme: LoadedCustomTheme,
): LoadedCustomTheme {
return {
...theme,
CustomImages: theme.CustomImages.map((image) =>
image.id === imageId ? { ...image, variableName } : image
image.id === imageId ? { ...image, variableName } : image,
),
} 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 file = input.files?.[0];
input.value = '';
input.value = "";
if (file) {
return new Promise((resolve) => {
const reader = new FileReader();
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 });
};
reader.readAsDataURL(file);
});
}
return Promise.resolve(theme);
}
}
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest'
import baseManifest from './manifest.json'
import pkg from '../../package.json'
import { createManifest } from "../../lib/createManifest";
import baseManifest from "./manifest.json";
import pkg from "../../package.json";
export const brave = createManifest({
export const brave = createManifest(
{
...baseManifest,
version: pkg.version,
description: pkg.description,
}, 'brave')
},
"brave",
);
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest'
import baseManifest from './manifest.json'
import pkg from '../../package.json'
import { createManifest } from "../../lib/createManifest";
import baseManifest from "./manifest.json";
import pkg from "../../package.json";
export const chrome = createManifest({
export const chrome = createManifest(
{
...baseManifest,
version: pkg.version,
description: pkg.description,
}, 'chrome')
},
"chrome",
);
+8 -5
View File
@@ -1,9 +1,12 @@
import { createManifest } from '../../lib/createManifest'
import baseManifest from './manifest.json'
import pkg from '../../package.json'
import { createManifest } from "../../lib/createManifest";
import baseManifest from "./manifest.json";
import pkg from "../../package.json";
export const edge = createManifest({
export const edge = createManifest(
{
...baseManifest,
version: pkg.version,
description: pkg.description,
}, 'edge')
},
"edge",
);
+7 -7
View File
@@ -1,6 +1,6 @@
import { createManifest } from '../../lib/createManifest'
import baseManifest from './manifest.json'
import pkg from '../../package.json'
import { createManifest } from "../../lib/createManifest";
import baseManifest from "./manifest.json";
import pkg from "../../package.json";
const updatedFirefoxManifest = {
...baseManifest,
@@ -10,13 +10,13 @@ const updatedFirefoxManifest = {
scripts: [baseManifest.background.service_worker],
},
action: {
"default_popup": "interface/index.html#settings",
default_popup: "interface/index.html#settings",
},
browser_specific_settings: {
gecko: {
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 baseManifest from './manifest.json'
import pkg from '../../package.json'
import { createManifest } from "../../lib/createManifest";
import baseManifest from "./manifest.json";
import pkg from "../../package.json";
export const opera = createManifest({
export const opera = createManifest(
{
...baseManifest,
version: pkg.version,
description: pkg.description,
}, 'opera')
},
"opera",
);
+7 -7
View File
@@ -1,6 +1,6 @@
import { createManifest } from '../../lib/createManifest'
import baseManifest from './manifest.json'
import pkg from '../../package.json'
import { createManifest } from "../../lib/createManifest";
import baseManifest from "./manifest.json";
import pkg from "../../package.json";
const updatedSafariManifest = {
...baseManifest,
@@ -8,12 +8,12 @@ const updatedSafariManifest = {
description: pkg.description,
browser_specific_settings: {
safari: {
strict_min_version: '15.4',
strict_max_version: '*',
strict_min_version: "15.4",
strict_max_version: "*",
},
// ^^^ 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
},
}
};
export const safari = createManifest(updatedSafariManifest, 'safari')
export const safari = createManifest(updatedSafariManifest, "safari");
+43 -39
View File
@@ -3,8 +3,8 @@ class ReactFiber {
this.selector = selector;
this.debug = options.debug || false;
this.nodes = [...document.querySelectorAll(selector)]; // Support multiple elements
this.fibers = this.nodes.map(node => this.getFiberNode(node));
this.components = this.fibers.map(fiber => this.getOwnerComponent(fiber));
this.fibers = this.nodes.map((node) => this.getFiberNode(node));
this.components = this.fibers.map((fiber) => this.getOwnerComponent(fiber));
if (this.debug) {
console.log("Selected Nodes:", this.nodes);
@@ -19,8 +19,10 @@ class ReactFiber {
getFiberNode(node) {
if (!node) return null;
const fiberKey = Object.getOwnPropertyNames(node).find(name =>
name.startsWith('__reactFiber') || name.startsWith('__reactInternalInstance')
const fiberKey = Object.getOwnPropertyNames(node).find(
(name) =>
name.startsWith("__reactFiber") ||
name.startsWith("__reactInternalInstance"),
);
return fiberKey ? node[fiberKey] : null;
}
@@ -28,7 +30,10 @@ class ReactFiber {
getOwnerComponent(fiberNode) {
let current = fiberNode;
while (current) {
if (current.stateNode && (current.stateNode.setState || current.stateNode.forceUpdate)) {
if (
current.stateNode &&
(current.stateNode.setState || current.stateNode.forceUpdate)
) {
return current.stateNode;
}
current = current.return;
@@ -42,7 +47,7 @@ class ReactFiber {
if (key === undefined) {
return state;
} else if (typeof key === 'string') {
} else if (typeof key === "string") {
return state?.[key];
} else if (Array.isArray(key)) {
const filteredState = {};
@@ -57,23 +62,25 @@ class ReactFiber {
}
setState(update) {
this.components.forEach(component => {
this.components.forEach((component) => {
if (component?.setState) {
if (typeof update === 'function') {
if (typeof update === "function") {
// Functional update
component.setState(prevState => {
component.setState((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;
});
} else {
// Object update (merge with existing state)
component.setState(prevState => {
component.setState((prevState) => {
const newState = {
...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;
});
}
@@ -93,7 +100,7 @@ class ReactFiber {
}
setProp(propName) {
this.fibers.forEach(fiber => {
this.fibers.forEach((fiber) => {
if (fiber?.memoizedProps) {
fiber.memoizedProps[propName] = value;
}
@@ -102,7 +109,7 @@ class ReactFiber {
}
forceUpdate() {
this.components.forEach(component => {
this.components.forEach((component) => {
if (component?.forceUpdate) {
component.forceUpdate();
if (this.debug) console.log("🔄 Forced React Re-render");
@@ -113,12 +120,12 @@ class ReactFiber {
}
function makeSerializable(obj) {
if (typeof obj !== 'object' || obj === null) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => makeSerializable(item));
return obj.map((item) => makeSerializable(item));
}
const serializableObj = {};
@@ -126,17 +133,17 @@ function makeSerializable(obj) {
if (Object.hasOwn(obj, key)) {
let value = obj[key];
if (typeof value === 'function') {
value = '[Function]';
if (typeof value === "function") {
value = "[Function]";
} else if (value instanceof HTMLElement) {
value = {
type: 'HTMLElement',
type: "HTMLElement",
id: value.id,
tagName: value.tagName
tagName: value.tagName,
}; // Replace DOM node with ID/tag info
} else if (typeof value === 'symbol') {
} else if (typeof value === "symbol") {
value = value.toString();
} else if (typeof value === 'object' && value !== null) {
} else if (typeof value === "object" && value !== null) {
value = makeSerializable(value);
}
@@ -146,17 +153,11 @@ function makeSerializable(obj) {
return serializableObj;
}
window.addEventListener('message', (event) => {
window.addEventListener("message", (event) => {
if (event.data.type === "reactFiberRequest") {
const {
selector,
action,
payload,
debug,
messageId
} = event.data;
const { selector, action, payload, debug, messageId } = event.data;
const fiberInstance = ReactFiber.find(selector, {
debug
debug,
});
let response;
@@ -191,14 +192,17 @@ window.addEventListener('message', (event) => {
response = null;
}
if (response !== null && typeof response === 'object') {
if (response !== null && typeof response === "object") {
response = makeSerializable(response);
}
window.postMessage({
type: "reactFiberResponse",
response,
messageId,
}, "*");
window.postMessage(
{
type: "reactFiberResponse",
response,
messageId,
},
"*",
);
}
});
});
@@ -1,7 +1,11 @@
import { BasePlugin } from '../../core/settings';
import { type Plugin } from '@/plugins/core/types';
import { defineSettings, numberSetting, Setting } from '@/plugins/core/settingsHelpers';
import styles from './styles.css?inline';
import { BasePlugin } from "../../core/settings";
import { type Plugin } from "@/plugins/core/types";
import {
defineSettings,
numberSetting,
Setting,
} from "@/plugins/core/settingsHelpers";
import styles from "./styles.css?inline";
const settings = defineSettings({
speed: numberSetting({
@@ -10,8 +14,8 @@ const settings = defineSettings({
description: "Controls how fast the background moves",
min: 0.1,
max: 2,
step: 0.05
})
step: 0.05,
}),
});
class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
@@ -22,10 +26,10 @@ class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
const instance = new AnimatedBackgroundPluginClass();
const animatedBackgroundPlugin: Plugin<typeof settings> = {
id: 'animated-background',
name: 'Animated Background',
description: 'Adds an animated background to BetterSEQTA+',
version: '1.0.0',
id: "animated-background",
name: "Animated Background",
description: "Adds an animated background to BetterSEQTA+",
version: "1.0.0",
disableToggle: true,
styles: styles,
settings: instance.settings,
@@ -34,7 +38,7 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
// Create the background elements
const container = document.getElementById("container");
const menu = document.getElementById("menu");
if (!container || !menu) {
return () => {};
}
@@ -42,12 +46,12 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
const backgrounds = [
{ classes: ["bg"] },
{ classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] }
{ classes: ["bg", "bg3"] },
];
backgrounds.forEach(({ classes }) => {
const bk = document.createElement("div");
classes.forEach(cls => bk.classList.add(cls));
classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu);
});
@@ -55,24 +59,27 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
updateAnimationSpeed(api.settings.speed);
// Listen for speed changes
const speedUnregister = api.settings.onChange('speed', updateAnimationSpeed);
const speedUnregister = api.settings.onChange(
"speed",
updateAnimationSpeed,
);
// Return cleanup function
return () => {
speedUnregister.unregister();
// Remove background elements
const backgrounds = document.getElementsByClassName('bg');
Array.from(backgrounds).forEach(element => element.remove());
const backgrounds = document.getElementsByClassName("bg");
Array.from(backgrounds).forEach((element) => element.remove());
};
}
},
};
function updateAnimationSpeed(speed: number) {
const bgElements = document.getElementsByClassName('bg');
const bgElements = document.getElementsByClassName("bg");
Array.from(bgElements).forEach((element, index) => {
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
});
}
export default animatedBackgroundPlugin;
export default animatedBackgroundPlugin;
@@ -28,4 +28,4 @@
100% {
transform: translateX(5%) rotate(-60deg);
}
}
}
@@ -7,18 +7,18 @@ export function CreateBackground() {
// Creating and inserting 3 divs containing the background applied to the pages
const container = document.getElementById("container");
const menu = document.getElementById("menu");
if (!container || !menu) return;
const backgrounds = [
{ classes: ["bg"] },
{ classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] }
{ classes: ["bg", "bg3"] },
];
backgrounds.forEach(({ classes }) => {
const bk = document.createElement("div");
classes.forEach(cls => bk.classList.add(cls));
classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu);
});
}
}
@@ -1,6 +1,6 @@
export function RemoveBackground() {
const backgrounds = document.getElementsByClassName("bg");
// Convert HTMLCollection to Array and remove each element
Array.from(backgrounds).forEach(element => element.remove());
}
Array.from(backgrounds).forEach((element) => element.remove());
}
@@ -1,5 +1,9 @@
import { BasePlugin } from "@/plugins/core/settings";
import { booleanSetting, defineSettings, Setting } from "@/plugins/core/settingsHelpers";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
import { type Plugin } from "@/plugins/core/types";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { waitForElm } from "@/seqta/utils/waitForElm";
@@ -8,7 +12,7 @@ const settings = defineSettings({
lettergrade: booleanSetting({
default: false,
title: "Letter Grades",
description: "Display the average as a letter instead of a percentage"
description: "Display the average as a letter instead of a percentage",
}),
});
@@ -34,62 +38,105 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
true,
10,
1000
1000,
);
// Helper function to find actual class names by their base pattern
const getClassByPattern = (element: Element | Document, basePattern: string): string => {
const getClassByPattern = (
element: Element | Document,
basePattern: string,
): string => {
// Find all classes on the element
const classes = Array.from(element.querySelectorAll('*'))
.flatMap(el => Array.from(el.classList))
.filter(className => className.startsWith(basePattern));
return classes.length ? classes[0] : '';
const classes = Array.from(element.querySelectorAll("*"))
.flatMap((el) => Array.from(el.classList))
.filter((className) => className.startsWith(basePattern));
return classes.length ? classes[0] : "";
};
// Find actual class names from the DOM
const sampleAssessmentItem = document.querySelector("[class*='AssessmentItem__AssessmentItem___']");
const sampleAssessmentItem = document.querySelector(
"[class*='AssessmentItem__AssessmentItem___']",
);
if (!sampleAssessmentItem) return;
// Extract all necessary class patterns from a sample assessment item
const assessmentItemClass = Array.from(sampleAssessmentItem.classList)
.find(c => c.startsWith('AssessmentItem__AssessmentItem___')) || '';
const metaContainerClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__metaContainer___');
const metaClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__meta___');
const simpleResultClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__simpleResult___');
const titleClass = getClassByPattern(sampleAssessmentItem, 'AssessmentItem__title___');
const assessmentItemClass =
Array.from(sampleAssessmentItem.classList).find((c) =>
c.startsWith("AssessmentItem__AssessmentItem___"),
) || "";
const metaContainerClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__metaContainer___",
);
const metaClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__meta___",
);
const simpleResultClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__simpleResult___",
);
const titleClass = getClassByPattern(
sampleAssessmentItem,
"AssessmentItem__title___",
);
// Get Thermoscore classes
const thermoscoreElement = document.querySelector("[class*='Thermoscore__Thermoscore___']");
const thermoscoreElement = document.querySelector(
"[class*='Thermoscore__Thermoscore___']",
);
if (!thermoscoreElement) return;
const thermoscoreClass = Array.from(thermoscoreElement.classList)
.find(c => c.startsWith('Thermoscore__Thermoscore___')) || '';
const fillClass = getClassByPattern(thermoscoreElement, 'Thermoscore__fill___');
const textClass = getClassByPattern(thermoscoreElement, 'Thermoscore__text___');
const thermoscoreClass =
Array.from(thermoscoreElement.classList).find((c) =>
c.startsWith("Thermoscore__Thermoscore___"),
) || "";
const fillClass = getClassByPattern(
thermoscoreElement,
"Thermoscore__fill___",
);
const textClass = getClassByPattern(
thermoscoreElement,
"Thermoscore__text___",
);
// Find assessment list
const assessmentsList = document.querySelector("#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']");
const assessmentsList = document.querySelector(
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
);
if (!assessmentsList) return;
const gradeElements = document.querySelectorAll("[class*='Thermoscore__text___']");
const gradeElements = document.querySelectorAll(
"[class*='Thermoscore__text___']",
);
if (!gradeElements.length) return;
// Parse and average grades
const letterToNumber: Record<string, number> = {
"A+": 100, A: 95, "A-": 90,
"B+": 85, B: 80, "B-": 75,
"C+": 70, C: 65, "C-": 60,
"D+": 55, D: 50, "D-": 45,
"E+": 40, E: 35, "E-": 30,
"A+": 100,
A: 95,
"A-": 90,
"B+": 85,
B: 80,
"B-": 75,
"C+": 70,
C: 65,
"C-": 60,
"D+": 55,
D: 50,
"D-": 45,
"E+": 40,
E: 35,
"E-": 30,
F: 0,
};
function parseGrade(text: string): number {
const str = text.trim().toUpperCase();
if (str.includes("/")) {
const [raw, max] = str.split("/").map(n => parseFloat(n));
const [raw, max] = str.split("/").map((n) => parseFloat(n));
return (raw / max) * 100;
}
if (str.includes("%")) {
@@ -112,16 +159,23 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
const avg = total / count;
const rounded = Math.ceil(avg / 5) * 5;
const numberToLetter = Object.entries(letterToNumber).reduce((acc, [k, v]) => {
acc[v] = k;
return acc;
}, {} as Record<number, string>);
const numberToLetter = Object.entries(letterToNumber).reduce(
(acc, [k, v]) => {
acc[v] = k;
return acc;
},
{} as Record<number, string>,
);
const letterAvg = numberToLetter[rounded] ?? "N/A";
const display = api.settings.lettergrade ? letterAvg : `${avg.toFixed(2)}%`;
const display = api.settings.lettergrade
? letterAvg
: `${avg.toFixed(2)}%`;
// Prevent duplicate
const existing = assessmentsList.querySelector(`[class*='AssessmentItem__title___']`);
const existing = assessmentsList.querySelector(
`[class*='AssessmentItem__title___']`,
);
if (existing?.textContent === "Subject Average") return;
// Use the dynamic class names in the HTML template
@@ -144,7 +198,7 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
});
}
},
};
export default assessmentsAveragePlugin;
export default assessmentsAveragePlugin;
@@ -11,7 +11,7 @@ import { waitForElm } from "@/seqta/utils/waitForElm";
import { runIndexing } from "../indexing/indexer";
import { initVectorSearch } from "../search/vector/vectorSearch";
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
import { IndexedDbManager } from 'embeddia';
import { IndexedDbManager } from "embeddia";
const settings = defineSettings({
searchHotkey: stringSetting({
@@ -65,12 +65,11 @@ const globalSearchPlugin: Plugin<typeof settings> = {
run: async (api) => {
const appRef = { current: null };
await IndexedDbManager.create(
'embeddiaDB',
'embeddiaObjectStore',
{ primaryKey: 'id', autoIncrement: false }
);
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
primaryKey: "id",
autoIncrement: false,
});
initVectorSearch();
if (api.settings.runIndexingOnLoad) {
@@ -6,7 +6,7 @@ import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
export function mountSearchBar(
titleElement: Element,
api: any,
appRef: { current: any }
appRef: { current: any },
) {
if (titleElement.querySelector(".search-trigger")) {
return;
@@ -15,7 +15,10 @@ async function loadProgress<T = any>(jobId: string): Promise<T | undefined> {
return rec?.progress as T | undefined;
}
async function saveProgress<T = any>(jobId: string, progress: T): Promise<void> {
async function saveProgress<T = any>(
jobId: string,
progress: T,
): Promise<void> {
await put(META_STORE, { jobId, progress }, `progress:${jobId}`);
}
/* ───────────────────────────────────────────── */
@@ -67,7 +70,13 @@ function stopHeartbeat() {
localStorage.removeItem(LOCK_KEY);
}
function dispatchProgress(completed: number, total: number, indexing: boolean, status?: string, detail?: string) {
function dispatchProgress(
completed: number,
total: number,
indexing: boolean,
status?: string,
detail?: string,
) {
const event = new CustomEvent("indexing-progress", {
detail: { completed, total, indexing, status, detail },
});
@@ -79,31 +88,41 @@ export async function loadAllStoredItems(): Promise<HydratedIndexItem[]> {
const jobIds = Object.keys(jobs);
for (const jobId of jobIds) {
try {
const items = await getAll(jobId) as IndexItem[];
const job = jobs[jobId];
const renderComponent = renderComponentMap[job.renderComponentId];
try {
const items = (await getAll(jobId)) as IndexItem[];
const job = jobs[jobId];
const renderComponent = renderComponentMap[job.renderComponentId];
if (!renderComponent) {
console.warn(`Render component not found for job ${jobId} (ID: ${job.renderComponentId})`);
}
for (const item of items) {
// Ensure item has all required fields before pushing
if (item && item.id && item.text && item.category && item.actionId && job.renderComponentId) {
all.push({
...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found
});
} else {
console.warn(`Skipping invalid item from job ${jobId}:`, item);
}
}
} catch (error) {
console.error(`Error loading items for job ${jobId}:`, error);
if (!renderComponent) {
console.warn(
`Render component not found for job ${jobId} (ID: ${job.renderComponentId})`,
);
}
for (const item of items) {
if (
item &&
item.id &&
item.text &&
item.category &&
item.actionId &&
job.renderComponentId
) {
all.push({
...item,
renderComponent: renderComponent || undefined,
});
} else {
console.warn(`Skipping invalid item from job ${jobId}:`, item);
}
}
} catch (error) {
console.error(`Error loading items for job ${jobId}:`, error);
}
}
console.debug(`[Indexer] Loaded ${all.length} items from non-vector storage.`);
console.debug(
`[Indexer] Loaded ${all.length} items from non-vector storage.`,
);
return all;
}
@@ -129,7 +148,12 @@ export async function runIndexing(): Promise<void> {
// --- Step 1: Run Fetching/Storing Jobs (Main Thread) ---
for (const jobId of jobIds) {
dispatchProgress(completedJobs, totalSteps, true, `Running job: ${jobs[jobId].label}`);
dispatchProgress(
completedJobs,
totalSteps,
true,
`Running job: ${jobs[jobId].label}`,
);
const job = jobs[jobId];
const lastRun = await getLastRunMeta(jobId);
@@ -139,26 +163,37 @@ export async function runIndexing(): Promise<void> {
"color: gray",
);
completedJobs++;
dispatchProgress(completedJobs, totalSteps, true, `Skipped job: ${job.label}`);
dispatchProgress(
completedJobs,
totalSteps,
true,
`Skipped job: ${job.label}`,
);
continue;
}
const getStoredItems = async (storeId?: string) => await getAll(storeId ?? jobId);
const getStoredItems = async (storeId?: string) =>
await getAll(storeId ?? jobId);
const setStoredItems = async (items: IndexItem[], storeId?: string) => {
const targetStore = storeId ?? jobId;
await clear(targetStore);
const validItems = items.filter(i => i && i.id);
const validItems = items.filter((i) => i && i.id);
if (validItems.length !== items.length) {
console.warn(`[Indexer Job ${jobId} -> Store ${targetStore}] Filtered out ${items.length - validItems.length} invalid items before storing.`);
console.warn(
`[Indexer Job ${jobId} -> Store ${targetStore}] Filtered out ${items.length - validItems.length} invalid items before storing.`,
);
}
await Promise.all(validItems.map((i) => put(targetStore, i, i.id)));
};
const addItem = async (item: IndexItem, storeId?: string) => {
const targetStore = storeId ?? jobId;
if (item && item.id) {
await put(targetStore, item, item.id);
await put(targetStore, item, item.id);
} else {
console.warn(`[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`, item);
console.warn(
`[Indexer Job ${jobId} -> Store ${targetStore}] Attempted to add invalid item:`,
item,
);
}
};
const removeItem = async (id: string, storeId?: string) => {
@@ -193,18 +228,30 @@ export async function runIndexing(): Promise<void> {
// Hydrate items for vector processing
const renderComponent = renderComponentMap[job.renderComponentId];
if (!renderComponent) {
console.warn(`Render component not found for job ${jobId} (ID: ${job.renderComponentId}) during hydration`);
console.warn(
`Render component not found for job ${jobId} (ID: ${job.renderComponentId}) during hydration`,
);
}
const hydratedItems = merged
.filter(item => item && item.id && item.text && item.category && item.actionId && job.renderComponentId) // Filter invalid before hydrating
.map((item) => ({
...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found
}));
.filter(
(item) =>
item &&
item.id &&
item.text &&
item.category &&
item.actionId &&
job.renderComponentId,
) // Filter invalid before hydrating
.map((item) => ({
...item,
renderComponent: renderComponent || undefined, // Assign undefined if not found
}));
if (hydratedItems.length !== merged.length) {
console.warn(`[Indexer Job ${jobId}] Filtered out ${merged.length - hydratedItems.length} invalid items during hydration.`);
}
if (hydratedItems.length !== merged.length) {
console.warn(
`[Indexer Job ${jobId}] Filtered out ${merged.length - hydratedItems.length} invalid items during hydration.`,
);
}
allItemsFromJobs.push(...hydratedItems);
@@ -218,7 +265,12 @@ export async function runIndexing(): Promise<void> {
}
completedJobs++;
dispatchProgress(completedJobs, totalSteps, true, `Finished job: ${job.label}`);
dispatchProgress(
completedJobs,
totalSteps,
true,
`Finished job: ${job.label}`,
);
}
// --- Step 2: Delegate Vectorization to Worker (Off Main Thread) ---
@@ -233,54 +285,113 @@ export async function runIndexing(): Promise<void> {
const workerManager = VectorWorkerManager.getInstance();
// Pass a progress callback to the worker manager
await workerManager.processItems(allItemsFromJobs, (progress) => {
// Update overall progress based on worker feedback
let detailMessage = progress.message || '';
if (progress.status === 'processing' && progress.total && progress.processed !== undefined) {
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
// You could potentially update the 'completed' count more granularly here
// For simplicity, we'll just update the detail message
} else if (progress.status === 'complete') {
detailMessage = "Vectorization complete";
// Mark the vectorization step as complete
dispatchProgress(totalSteps, totalSteps, true, "Vectorization finished");
} else if (progress.status === 'error') {
detailMessage = `Vectorization error: ${progress.message}`;
dispatchProgress(completedJobs, totalSteps, true, "Vectorization failed", detailMessage); // Show error
} else if (progress.status === 'started') {
detailMessage = `Vectorization started for ${progress.total} items`;
} else if (progress.status === 'cancelled') {
detailMessage = `Vectorization cancelled: ${progress.message}`;
dispatchProgress(completedJobs, totalSteps, true, "Vectorization cancelled", detailMessage);
}
// Update overall progress based on worker feedback
let detailMessage = progress.message || "";
if (
progress.status === "processing" &&
progress.total &&
progress.processed !== undefined
) {
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
// You could potentially update the 'completed' count more granularly here
// For simplicity, we'll just update the detail message
} else if (progress.status === "complete") {
detailMessage = "Vectorization complete";
// Mark the vectorization step as complete
dispatchProgress(
totalSteps,
totalSteps,
true,
"Vectorization finished",
);
} else if (progress.status === "error") {
detailMessage = `Vectorization error: ${progress.message}`;
dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization failed",
detailMessage,
); // Show error
} else if (progress.status === "started") {
detailMessage = `Vectorization started for ${progress.total} items`;
} else if (progress.status === "cancelled") {
detailMessage = `Vectorization cancelled: ${progress.message}`;
dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization cancelled",
detailMessage,
);
}
// Update the status detail
dispatchProgress(completedJobs, totalSteps, true, "Vectorization in progress", detailMessage);
// Update the status detail
dispatchProgress(
completedJobs,
totalSteps,
true,
"Vectorization in progress",
detailMessage,
);
// When worker signals completion of *its* task, mark the final step complete
if (progress.status === 'complete') {
completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress(completedJobs, totalSteps, false, "Indexing finished"); // Set indexing to false
} else if (progress.status === 'error' || progress.status === 'cancelled') {
// Don't increment completed count on failure/cancel, just stop indexing indicator
dispatchProgress(completedJobs, totalSteps, false, "Indexing stopped due to error/cancel");
}
// When worker signals completion of *its* task, mark the final step complete
if (progress.status === "complete") {
completedJobs++; // Increment completion count *after* vectorization finishes
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing finished",
); // Set indexing to false
} else if (
progress.status === "error" ||
progress.status === "cancelled"
) {
// Don't increment completed count on failure/cancel, just stop indexing indicator
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing stopped due to error/cancel",
);
}
});
console.debug("%c[Indexer] Vectorization task sent to worker.", "color: green");
console.debug(
"%c[Indexer] Vectorization task sent to worker.",
"color: green",
);
// Note: runIndexing might return *before* vectorization is complete now.
// The progress updates will signal the true end state.
} catch (error) {
console.error(`%c[Indexer] ❌ Failed to send items to vector worker:`, "color: red", error);
dispatchProgress(completedJobs, totalSteps, false, "Vectorization failed", String(error)); // Stop indexing indicator
console.error(
`%c[Indexer] ❌ Failed to send items to vector worker:`,
"color: red",
error,
);
dispatchProgress(
completedJobs,
totalSteps,
false,
"Vectorization failed",
String(error),
); // Stop indexing indicator
}
} else {
console.debug("%c[Indexer] No items to send for vectorization.", "color: gray");
console.debug(
"%c[Indexer] No items to send for vectorization.",
"color: gray",
);
// If no vectorization needed, indexing is done here.
completedJobs++; // Count the "skipped" vectorization step
dispatchProgress(completedJobs, totalSteps, false, "Indexing finished (no vectorization needed)");
dispatchProgress(
completedJobs,
totalSteps,
false,
"Indexing finished (no vectorization needed)",
);
}
// Stop heartbeat ONLY when all jobs *and* the vectorization dispatch are done.
// The actual *completion* of vectorization is now asynchronous.
stopHeartbeat();
@@ -292,10 +403,10 @@ function mergeItems(existing: IndexItem[], incoming: IndexItem[]): IndexItem[] {
const map = new Map<string, IndexItem>();
// Prioritize incoming items if IDs clash
for (const item of existing) {
if (item && item.id) map.set(item.id, item);
if (item && item.id) map.set(item.id, item);
}
for (const item of incoming) {
if (item && item.id) map.set(item.id, item);
if (item && item.id) map.set(item.id, item);
}
return Array.from(map.values());
}
}
@@ -5,4 +5,4 @@ import { assessmentsJob } from "./jobs/assessments";
export const jobs: Record<string, Job> = {
messages: messagesJob,
assessments: assessmentsJob,
};
};
@@ -49,21 +49,27 @@ const fetchNotifications = async () => {
const fetchAssessmentName = async (
assessmentId: number,
metaclassId: number,
programmeId: number
programmeId: number,
): Promise<string> => {
const searchAssessment = (data: any): string | null => {
// Search syllabus
for (const item of data.syllabus || []) {
const found = (item.assessments || []).find((a: any) => a.id === assessmentId);
const found = (item.assessments || []).find(
(a: any) => a.id === assessmentId,
);
if (found) return found.title;
}
// Search pending
const foundPending = (data.pending || []).find((a: any) => a.id === assessmentId);
const foundPending = (data.pending || []).find(
(a: any) => a.id === assessmentId,
);
if (foundPending) return foundPending.title;
// Search tasks
const foundTask = (data.tasks || []).find((a: any) => a.id === assessmentId);
const foundTask = (data.tasks || []).find(
(a: any) => a.id === assessmentId,
);
if (foundTask) return foundTask.title;
return null;
@@ -88,11 +94,17 @@ const fetchAssessmentName = async (
if (title) return title;
// Try from /upcoming if not found in /past
const upcomingPayload = await fetchAssessments("/seqta/student/assessment/list/upcoming");
const foundUpcoming = (upcomingPayload || []).find((a: any) => a.id === assessmentId);
const upcomingPayload = await fetchAssessments(
"/seqta/student/assessment/list/upcoming",
);
const foundUpcoming = (upcomingPayload || []).find(
(a: any) => a.id === assessmentId,
);
if (foundUpcoming) return foundUpcoming.title;
throw new Error(`Assessment with ID ${assessmentId} not found in past or upcoming.`);
throw new Error(
`Assessment with ID ${assessmentId} not found in past or upcoming.`,
);
};
/* ------------- Job ------------- */
@@ -103,9 +115,10 @@ export const assessmentsJob: Job = {
frequency: { type: "expiry", afterMs: 15 * 60 * 1000 },
run: async (ctx) => {
const progress =
(await ctx.getProgress<AssessmentsProgress>()) ?? { lastTs: 0 };
const progress = (await ctx.getProgress<AssessmentsProgress>()) ?? {
lastTs: 0,
};
let notifications: Notification[];
try {
notifications = await fetchNotifications();
@@ -113,25 +126,33 @@ export const assessmentsJob: Job = {
console.error("[Assessments job] fetch failed:", e);
return [];
}
const notificationIsIndexed = async (id: string): Promise<boolean> => {
const [inAssessments, inMessages] = await Promise.all([
ctx.getStoredItems("assessments").then((items) => items.some((i) => i.id === id)),
ctx.getStoredItems("messages").then((items) => items.some((i) => i.id === id)),
ctx
.getStoredItems("assessments")
.then((items) => items.some((i) => i.id === id)),
ctx
.getStoredItems("messages")
.then((items) => items.some((i) => i.id === id)),
]);
return inAssessments || inMessages;
};
const items: IndexItem[] = [];
for (const notif of notifications) {
const id = notif.notificationID.toString();
if (await notificationIsIndexed(id)) continue;
if (notif.type === "coneqtassessments") {
const a = notif.coneqtAssessments;
const content = await fetchAssessmentName(a.assessmentID, a.metaclassID, a.programmeID);
const content = await fetchAssessmentName(
a.assessmentID,
a.metaclassID,
a.programmeID,
);
items.push({
id,
text: a.title,
@@ -168,11 +189,11 @@ export const assessmentsJob: Job = {
actionId: "message",
renderComponentId: "message",
},
"messages"
"messages",
);
}
}
if (items.length) {
const latest = Math.max(
...items.map((i) => i.dateAdded),
@@ -190,4 +211,4 @@ export const assessmentsJob: Job = {
date.setHours(0, 0, 0, 0);
return items.filter((i) => i.dateAdded >= date.getTime());
},
};
};
@@ -49,12 +49,12 @@ export const messagesJob: Job = {
run: async (ctx) => {
const limit = 100;
const progress =
(await ctx.getProgress<MessagesProgress>()) ?? { offset: 0, done: false };
const progress = (await ctx.getProgress<MessagesProgress>()) ?? {
offset: 0,
done: false,
};
const existingIds = new Set(
(await ctx.getStoredItems()).map((i) => i.id),
);
const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
let consecutiveExisting = 0;
@@ -129,4 +129,4 @@ export const messagesJob: Job = {
const fourYears = Date.now() - 4 * 365 * 24 * 60 * 60 * 1000;
return items.filter((i) => i.dateAdded >= fourYears);
},
};
};
@@ -36,4 +36,4 @@ export interface Job {
renderComponentId: string;
run: (ctx: JobContext) => Promise<IndexItem[]>;
purge?: (items: IndexItem[]) => IndexItem[];
}
}
@@ -1,11 +1,13 @@
export function htmlToPlainText(rawHtml: string): string {
const parser = new DOMParser();
const doc = parser.parseFromString(rawHtml, 'text/html');
const doc = parser.parseFromString(rawHtml, "text/html");
const { body } = doc;
body.querySelectorAll('script,style,template,noscript,meta,link').forEach(el => el.remove());
body
.querySelectorAll("script,style,template,noscript,meta,link")
.forEach((el) => el.remove());
body.querySelectorAll('.forward').forEach(el => {
body.querySelectorAll(".forward").forEach((el) => {
let n: ChildNode | null = el;
while (n) {
const next = n.nextSibling as ChildNode | null;
@@ -14,19 +16,19 @@ export function htmlToPlainText(rawHtml: string): string {
}
});
let text = body.innerText || '';
let text = body.innerText || "";
text = text
.replace(/\u00A0/g, ' ')
.replace(/[ \t]{2,}/g, ' ')
.replace(/\r\n|\r/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.replace(/^[.\w#][^{]{0,100}\{[^}]*\}$/gm, '')
.split('\n')
.map(line => line.trimEnd())
.filter(line => line.trim().length > 0 || line === '')
.join('\n')
.replace(/\u00A0/g, " ")
.replace(/[ \t]{2,}/g, " ")
.replace(/\r\n|\r/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/^[.\w#][^{]{0,100}\{[^}]*\}$/gm, "")
.split("\n")
.map((line) => line.trimEnd())
.filter((line) => line.trim().length > 0 || line === "")
.join("\n")
.trim();
return text;
}
}
@@ -1,8 +1,4 @@
import {
EmbeddingIndex,
getEmbedding,
initializeModel,
} from "embeddia";
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { HydratedIndexItem } from "../types";
let vectorIndex: EmbeddingIndex | null = null;
@@ -1,10 +1,10 @@
import { refreshVectorCache } from '../../search/vector/vectorSearch';
import type { HydratedIndexItem } from '../types';
import vectorWorker from './vectorWorker.ts?inlineWorker';
import type { SearchResult } from 'embeddia';
import { refreshVectorCache } from "../../search/vector/vectorSearch";
import type { HydratedIndexItem } from "../types";
import vectorWorker from "./vectorWorker.ts?inlineWorker";
import type { SearchResult } from "embeddia";
export type ProgressCallback = (data: {
status: 'started' | 'processing' | 'complete' | 'error' | 'cancelled';
status: "started" | "processing" | "complete" | "error" | "cancelled";
total?: number;
processed?: number;
message?: string;
@@ -16,10 +16,21 @@ export class VectorWorkerManager {
private isInitialized = false;
private readyPromise: Promise<void> | null = null; // To await initialization
private progressCallback: ProgressCallback | null = null;
private searchPromises = new Map<string, { resolve: (value: SearchResult[]) => void, reject: (reason?: any) => void, timer: NodeJS.Timeout }>();
private searchPromises = new Map<
string,
{
resolve: (value: SearchResult[]) => void;
reject: (reason?: any) => void;
timer: NodeJS.Timeout;
}
>();
private debounceTimer: NodeJS.Timeout | null = null;
private lastSearchParams: { query: string; topK: number; resolve: (results: SearchResult[]) => void, reject: (reason?: any) => void } | null = null;
private lastSearchParams: {
query: string;
topK: number;
resolve: (results: SearchResult[]) => void;
reject: (reason?: any) => void;
} | null = null;
private constructor() {
// Start initialization immediately, but allow awaiting it
@@ -39,101 +50,115 @@ export class VectorWorkerManager {
if (this.readyPromise) return this.readyPromise;
return new Promise<void>((resolve, reject) => {
// Create the worker
this.worker = vectorWorker();
// Create the worker
this.worker = vectorWorker();
console.log('Worker initialized', this.worker);
console.log("Worker initialized", this.worker);
const timeout = setTimeout(() => {
console.error('Vector worker initialization timed out');
this.worker?.terminate(); // Clean up worker if it exists
this.worker = null;
this.isInitialized = false; // Ensure state reflects failure
this.readyPromise = null; // Allow retrying init later
reject(new Error('Worker initialization timed out'));
}, 10000); // Increased timeout
const timeout = setTimeout(() => {
console.error("Vector worker initialization timed out");
this.worker?.terminate(); // Clean up worker if it exists
this.worker = null;
this.isInitialized = false; // Ensure state reflects failure
this.readyPromise = null; // Allow retrying init later
reject(new Error("Worker initialization timed out"));
}, 10000); // Increased timeout
// Set up message handling
this.worker!.addEventListener('message', (e) => {
const { type, data } = e.data;
console.debug("Message from vector worker:", type, data);
// Set up message handling
this.worker!.addEventListener("message", (e) => {
const { type, data } = e.data;
console.debug("Message from vector worker:", type, data);
switch (type) {
case 'ready':
this.isInitialized = true;
clearTimeout(timeout);
console.debug('Vector worker initialized and ready.');
resolve(); // Resolve the init promise
break;
switch (type) {
case "ready":
this.isInitialized = true;
clearTimeout(timeout);
console.debug("Vector worker initialized and ready.");
resolve(); // Resolve the init promise
break;
case 'progress':
if (this.progressCallback) {
this.progressCallback(data);
case "progress":
if (this.progressCallback) {
this.progressCallback(data);
if (data.status === 'complete') {
refreshVectorCache();
}
if (data.status === "complete") {
refreshVectorCache();
}
break;
}
break;
case 'searchResults':
const searchInfo = this.searchPromises.get(data.messageId);
if (searchInfo) {
clearTimeout(searchInfo.timer); // Clear timeout on success
searchInfo.resolve(data.results);
this.searchPromises.delete(data.messageId);
} else {
console.warn('Received search results for unknown messageId:', data.messageId);
}
break;
case "searchResults":
const searchInfo = this.searchPromises.get(data.messageId);
if (searchInfo) {
clearTimeout(searchInfo.timer); // Clear timeout on success
searchInfo.resolve(data.results);
this.searchPromises.delete(data.messageId);
} else {
console.warn(
"Received search results for unknown messageId:",
data.messageId,
);
}
break;
case 'searchError':
const errorInfo = this.searchPromises.get(data.messageId);
if (errorInfo) {
clearTimeout(errorInfo.timer); // Clear timeout on error
errorInfo.reject(new Error(data.error));
this.searchPromises.delete(data.messageId);
} else {
console.warn('Received search error for unknown messageId:', data.messageId);
}
break;
case "searchError":
const errorInfo = this.searchPromises.get(data.messageId);
if (errorInfo) {
clearTimeout(errorInfo.timer); // Clear timeout on error
errorInfo.reject(new Error(data.error));
this.searchPromises.delete(data.messageId);
} else {
console.warn(
"Received search error for unknown messageId:",
data.messageId,
);
}
break;
case 'searchCancelled':
const cancelledInfo = this.searchPromises.get(data.messageId);
if (cancelledInfo) {
clearTimeout(cancelledInfo.timer); // Clear timeout on cancel
// Reject with a specific cancellation error or resolve with empty? Let's reject.
cancelledInfo.reject(new Error('Search cancelled by worker'));
this.searchPromises.delete(data.messageId);
} else {
console.debug('Received cancellation for unknown messageId:', data.messageId);
}
break;
case "searchCancelled":
const cancelledInfo = this.searchPromises.get(data.messageId);
if (cancelledInfo) {
clearTimeout(cancelledInfo.timer); // Clear timeout on cancel
// Reject with a specific cancellation error or resolve with empty? Let's reject.
cancelledInfo.reject(new Error("Search cancelled by worker"));
this.searchPromises.delete(data.messageId);
} else {
console.debug(
"Received cancellation for unknown messageId:",
data.messageId,
);
}
break;
default:
console.warn('Unknown message from worker:', type, data);
}
});
default:
console.warn("Unknown message from worker:", type, data);
}
});
// Initialize the worker
this.worker!.postMessage({ type: 'init' });
// Initialize the worker
this.worker!.postMessage({ type: "init" });
});
}
// Ensures worker is ready before proceeding
private async ensureReady() {
if (!this.readyPromise) {
// If init wasn't called or failed, try again
console.warn("Worker not initialized, attempting init...");
this.readyPromise = this.initWorker();
// If init wasn't called or failed, try again
console.warn("Worker not initialized, attempting init...");
this.readyPromise = this.initWorker();
}
await this.readyPromise;
if (!this.isInitialized || !this.worker) {
throw new Error("Vector Worker is not available after initialization attempt.");
throw new Error(
"Vector Worker is not available after initialization attempt.",
);
}
}
async processItems(items: HydratedIndexItem[], onProgress?: ProgressCallback) {
async processItems(
items: HydratedIndexItem[],
onProgress?: ProgressCallback,
) {
await this.ensureReady(); // Wait for worker to be ready
this.progressCallback = onProgress || null;
@@ -146,13 +171,16 @@ export class VectorWorkerManager {
const serialisableItems = items.map(({ renderComponent, ...rest }) => rest);
this.worker!.postMessage({
type: 'process',
data: { items: serialisableItems }
type: "process",
data: { items: serialisableItems },
});
}
// Public search method
public async search(query: string, topK: number = 10): Promise<SearchResult[]> {
public async search(
query: string,
topK: number = 10,
): Promise<SearchResult[]> {
await this.ensureReady();
return new Promise((resolve, reject) => {
@@ -167,54 +195,62 @@ export class VectorWorkerManager {
// Set a timeout for the search operation itself
const searchTimeout = 10000; // e.g., 10 seconds
const searchTimer = setTimeout(() => {
if (this.searchPromises.has(messageId)) {
console.error(`Search timed out for messageId: ${messageId}`);
currentParams.reject(new Error(`Search timed out after ${searchTimeout}ms`));
this.searchPromises.delete(messageId);
}
if (this.searchPromises.has(messageId)) {
console.error(`Search timed out for messageId: ${messageId}`);
currentParams.reject(
new Error(`Search timed out after ${searchTimeout}ms`),
);
this.searchPromises.delete(messageId);
}
}, searchTimeout);
this.searchPromises.set(messageId, {
resolve: currentParams.resolve,
reject: currentParams.reject,
timer: searchTimer,
});
this.searchPromises.set(messageId, { resolve: currentParams.resolve, reject: currentParams.reject, timer: searchTimer });
console.debug(`Sending search request (ID: ${messageId}) to worker: "${currentParams.query}"`);
console.debug(
`Sending search request (ID: ${messageId}) to worker: "${currentParams.query}"`,
);
console.log(this.worker);
this.worker.postMessage({
type: "search",
data: { query: currentParams.query, topK: currentParams.topK },
messageId
messageId,
});
} else if (this.lastSearchParams) {
// This case might happen if ensureReady failed but didn't throw
console.error("Worker unavailable when trying to send search request.");
this.lastSearchParams.reject(new Error("Worker unavailable for search"));
this.lastSearchParams = null;
this.debounceTimer = null;
// This case might happen if ensureReady failed but didn't throw
console.error("Worker unavailable when trying to send search request.");
this.lastSearchParams.reject(
new Error("Worker unavailable for search"),
);
this.lastSearchParams = null;
this.debounceTimer = null;
}
});
}
// Method to cancel all pending/debounced searches
private cancelAllSearches(reason: string = "Cancelled") {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
if (this.lastSearchParams) {
this.lastSearchParams.reject(new Error(`Search cancelled: ${reason}`));
this.lastSearchParams = null;
}
}
// We might also want to tell the worker to cancel its *current* search
// if it supports it, but this requires worker modification.
// For now, just reject pending promises in the manager.
for (const [messageId, promiseInfo] of this.searchPromises.entries()) {
clearTimeout(promiseInfo.timer);
promiseInfo.reject(new Error(`Search cancelled: ${reason}`));
this.searchPromises.delete(messageId);
}
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
if (this.lastSearchParams) {
this.lastSearchParams.reject(new Error(`Search cancelled: ${reason}`));
this.lastSearchParams = null;
}
}
// We might also want to tell the worker to cancel its *current* search
// if it supports it, but this requires worker modification.
// For now, just reject pending promises in the manager.
for (const [messageId, promiseInfo] of this.searchPromises.entries()) {
clearTimeout(promiseInfo.timer);
promiseInfo.reject(new Error(`Search cancelled: ${reason}`));
this.searchPromises.delete(messageId);
}
}
terminate() {
console.debug("Terminating Vector Worker Manager...");
this.cancelAllSearches("Worker terminated"); // Cancel pending searches
@@ -229,4 +265,4 @@ export class VectorWorkerManager {
// Clear the static instance? Or assume app lifecycle handles this?
// VectorWorkerManager.instance = null; // Uncomment if needed
}
}
}
@@ -1,6 +1,6 @@
import { EmbeddingIndex, getEmbedding, initializeModel } from 'embeddia';
import type { HydratedIndexItem } from '../../indexing/types';
import type { SearchResult } from 'embeddia';
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
import type { HydratedIndexItem } from "../../indexing/types";
import type { SearchResult } from "embeddia";
let vectorIndex: EmbeddingIndex | null = null;
@@ -10,7 +10,7 @@ export async function initVectorSearch() {
vectorIndex = new EmbeddingIndex([]);
vectorIndex.preloadIndexedDB();
} catch (e) {
console.error('Error initializing vector search', e);
console.error("Error initializing vector search", e);
}
}
@@ -18,17 +18,20 @@ export interface VectorSearchResult extends SearchResult {
object: HydratedIndexItem & { embedding: number[] };
}
export async function searchVectors(query: string, topK: number = 10): Promise<VectorSearchResult[]> {
export async function searchVectors(
query: string,
topK: number = 10,
): Promise<VectorSearchResult[]> {
if (!vectorIndex) await initVectorSearch();
const queryEmbedding = await getEmbedding(query.slice(0, 100));
const results = await vectorIndex!.search(queryEmbedding, {
const results = await vectorIndex!.search(queryEmbedding, {
topK,
useStorage: 'indexedDB',
dedupeEntries: true
useStorage: "indexedDB",
dedupeEntries: true,
});
return results as VectorSearchResult[];
}
@@ -36,4 +39,4 @@ export async function refreshVectorCache() {
if (!vectorIndex) await initVectorSearch();
vectorIndex!.clearIndexedDBCache();
vectorIndex!.preloadIndexedDB();
}
}
@@ -4,4 +4,3 @@ import type { HydratedIndexItem } from "../../indexing/types";
export interface VectorSearchResult extends SearchResult {
object: HydratedIndexItem & { embedding: number[] };
}
@@ -1,4 +1,4 @@
import type { Plugin } from '../../core/types';
import type { Plugin } from "../../core/types";
interface NotificationCollectorStorage {
lastNotificationCount: number;
@@ -6,10 +6,10 @@ interface NotificationCollectorStorage {
}
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
id: 'notificationCollector',
name: 'Notification Collector',
description: 'Collects and displays SEQTA notifications',
version: '1.0.0',
id: "notificationCollector",
name: "Notification Collector",
description: "Collects and displays SEQTA notifications",
version: "1.0.0",
settings: {},
disableToggle: true,
@@ -23,30 +23,35 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
const checkNotifications = async () => {
try {
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement;
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (api.storage.lastNotificationCount !== 0) {
alertDiv.textContent = api.storage.lastNotificationCount.toString();
}
const response = await fetch(`${location.origin}/seqta/student/heartbeat?`, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
const response = await fetch(
`${location.origin}/seqta/student/heartbeat?`,
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
timestamp: "1970-01-01 00:00:00.0",
hash: "#?page=/home",
}),
},
body: JSON.stringify({
timestamp: "1970-01-01 00:00:00.0",
hash: "#?page=/home",
})
});
);
const data = await response.json();
// Store notification count for history
const notificationCount = data.payload.notifications.length;
api.storage.lastNotificationCount = notificationCount;
api.storage.lastCheckedTime = new Date().toISOString();
if (alertDiv) {
alertDiv.textContent = notificationCount.toString();
} else {
@@ -67,7 +72,9 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
if (pollInterval) {
window.clearInterval(pollInterval);
pollInterval = null;
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement;
const alertDiv = document.querySelector(
"[class*='notifications__bubble___']",
) as HTMLElement;
if (alertDiv) {
if (api.storage.lastNotificationCount > 9) {
alertDiv.textContent = "9+";
@@ -85,7 +92,7 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
return () => {
stopPolling();
};
}
},
};
export default notificationCollectorPlugin;
export default notificationCollectorPlugin;
+20 -16
View File
@@ -1,6 +1,10 @@
import type { Plugin } from '@/plugins/core/types';
import { BasePlugin } from '@/plugins/core/settings';
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from "@/plugins/core/settings";
import {
booleanSetting,
defineSettings,
Setting,
} from "@/plugins/core/settingsHelpers";
// Step 1: Define settings with proper typing
const settings = defineSettings({
@@ -8,7 +12,7 @@ const settings = defineSettings({
default: true,
title: "Test Plugin",
description: "Some random setting",
})
}),
});
// Step 2: Create the plugin class with @Setting decorators
@@ -21,32 +25,32 @@ class TestPluginClass extends BasePlugin<typeof settings> {
const settingsInstance = new TestPluginClass();
const testPlugin: Plugin<typeof settings> = {
id: 'test',
name: 'Test Plugin',
description: 'A test plugin for BetterSEQTA+',
version: '1.0.0',
id: "test",
name: "Test Plugin",
description: "A test plugin for BetterSEQTA+",
version: "1.0.0",
settings: settingsInstance.settings,
disableToggle: true,
run: async (api) => {
console.log('Test plugin running');
console.log("Test plugin running");
api.events.on('ping', (data) => {
console.log('Ping received! Page changed to: ', data);
api.events.on("ping", (data) => {
console.log("Ping received! Page changed to: ", data);
});
const { unregister } = api.seqta.onPageChange((page) => {
//console.log('Page changed to', page);
api.events.emit('ping', page);
api.events.emit("ping", page);
console.log('Current setting value:', api.settings.someSetting);
console.log("Current setting value:", api.settings.someSetting);
});
return () => {
console.log('Test plugin stopped');
console.log("Test plugin stopped");
unregister();
}
}
};
},
};
export default testPlugin;
+65 -62
View File
@@ -1,10 +1,10 @@
import renderSvelte from "@/interface/main"
import themeCreator from "@/interface/pages/themeCreator.svelte"
import { unmount } from "svelte"
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager"
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import renderSvelte from "@/interface/main";
import themeCreator from "@/interface/pages/themeCreator.svelte";
import { unmount } from "svelte";
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
let themeCreatorSvelteApp: any = null
let themeCreatorSvelteApp: any = null;
const themeManager = ThemeManager.getInstance();
/**
@@ -13,76 +13,79 @@ const themeManager = ThemeManager.getInstance();
* @returns void
*/
export function OpenThemeCreator(themeID: string = "") {
CloseThemeCreator()
CloseThemeCreator();
// Only store original color if we're not editing an existing theme
localStorage.setItem('themeCreatorOpen', 'true');
localStorage.setItem("themeCreatorOpen", "true");
if (!themeID) {
localStorage.setItem('originalPreviewColor', settingsState.selectedColor);
localStorage.setItem("originalPreviewColor", settingsState.selectedColor);
}
const width = "310px"
const width = "310px";
const themeCreatorDiv: HTMLDivElement = document.createElement("div")
themeCreatorDiv.id = "themeCreator"
themeCreatorDiv.style.width = width
const themeCreatorDiv: HTMLDivElement = document.createElement("div");
themeCreatorDiv.id = "themeCreator";
themeCreatorDiv.style.width = width;
const shadow = themeCreatorDiv.attachShadow({ mode: "open" })
const shadow = themeCreatorDiv.attachShadow({ mode: "open" });
themeCreatorSvelteApp = renderSvelte(themeCreator, shadow, {
themeID: themeID,
})
});
const mainContent = document.querySelector("#container") as HTMLDivElement
if (mainContent) mainContent.style.width = `calc(100% - ${width})`
const mainContent = document.querySelector("#container") as HTMLDivElement;
if (mainContent) mainContent.style.width = `calc(100% - ${width})`;
// close button
const closeButton = document.createElement("button")
closeButton.classList.add("themeCloseButton")
closeButton.textContent = "×"
const closeButton = document.createElement("button");
closeButton.classList.add("themeCloseButton");
closeButton.textContent = "×";
closeButton.addEventListener("click", () => {
CloseThemeCreator()
themeManager.clearPreview()
})
CloseThemeCreator();
themeManager.clearPreview();
});
document.body.appendChild(closeButton)
document.body.appendChild(closeButton);
const resizeBar = document.createElement("div")
resizeBar.classList.add("resizeBar")
resizeBar.style.right = "307.5px"
const resizeBar = document.createElement("div");
resizeBar.classList.add("resizeBar");
resizeBar.style.right = "307.5px";
let isDragging = false
let isDragging = false;
const mouseDownHandler = (_: MouseEvent) => {
isDragging = true
document.addEventListener("mousemove", mouseMoveHandler)
document.addEventListener("mouseup", mouseUpHandler)
document.body.style.userSelect = "none"
themeCreatorDiv.style.pointerEvents = "none"
}
isDragging = true;
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler);
document.body.style.userSelect = "none";
themeCreatorDiv.style.pointerEvents = "none";
};
const mouseMoveHandler = (e: MouseEvent) => {
if (!isDragging) return
const windowWidth = window.innerWidth
const newWidth = Math.max(310, windowWidth - e.clientX)
themeCreatorDiv.style.width = `${newWidth}px`
mainContent.style.width = `calc(100% - ${newWidth}px)`
resizeBar.style.right = `${newWidth - 2.5}px`
}
if (!isDragging) return;
const windowWidth = window.innerWidth;
const newWidth = Math.max(310, windowWidth - e.clientX);
themeCreatorDiv.style.width = `${newWidth}px`;
mainContent.style.width = `calc(100% - ${newWidth}px)`;
resizeBar.style.right = `${newWidth - 2.5}px`;
};
const mouseUpHandler = () => {
isDragging = false
document.removeEventListener("mousemove", mouseMoveHandler)
document.removeEventListener("mouseup", mouseUpHandler)
document.body.style.userSelect = ""
themeCreatorDiv.style.pointerEvents = "auto"
}
isDragging = false;
document.removeEventListener("mousemove", mouseMoveHandler);
document.removeEventListener("mouseup", mouseUpHandler);
document.body.style.userSelect = "";
themeCreatorDiv.style.pointerEvents = "auto";
};
resizeBar.addEventListener("mousedown", mouseDownHandler)
resizeBar.addEventListener("mouseover", () => (resizeBar.style.opacity = "1"))
resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0"))
resizeBar.addEventListener("mousedown", mouseDownHandler);
resizeBar.addEventListener(
"mouseover",
() => (resizeBar.style.opacity = "1"),
);
resizeBar.addEventListener("mouseout", () => (resizeBar.style.opacity = "0"));
document.body.appendChild(themeCreatorDiv)
document.body.appendChild(resizeBar)
document.body.appendChild(themeCreatorDiv);
document.body.appendChild(resizeBar);
}
/**
@@ -91,19 +94,19 @@ export function OpenThemeCreator(themeID: string = "") {
*/
export function CloseThemeCreator() {
// Remove the stored flag
localStorage.removeItem('themeCreatorOpen');
localStorage.removeItem("themeCreatorOpen");
const themeCreator = document.getElementById("themeCreator")
const themeCreator = document.getElementById("themeCreator");
const closeButton = document.querySelector(
".themeCloseButton",
) as HTMLButtonElement
const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement
) as HTMLButtonElement;
const resizeBar = document.querySelector(".resizeBar") as HTMLDivElement;
if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp)
if (themeCreator) themeCreator.remove()
if (closeButton) closeButton.remove()
if (resizeBar) resizeBar.remove()
if (themeCreatorSvelteApp) unmount(themeCreatorSvelteApp);
if (themeCreator) themeCreator.remove();
if (closeButton) closeButton.remove();
if (resizeBar) resizeBar.remove();
const mainContent = document.querySelector("#container") as HTMLDivElement
if (mainContent) mainContent.style.width = "100%"
const mainContent = document.querySelector("#container") as HTMLDivElement;
if (mainContent) mainContent.style.width = "100%";
}
+8 -8
View File
@@ -1,17 +1,17 @@
import type { Plugin } from '../../core/types';
import { ThemeManager } from './theme-manager';
import type { Plugin } from "../../core/types";
import { ThemeManager } from "./theme-manager";
const themesPlugin: Plugin = {
id: 'themes',
name: 'Themes',
description: 'Adds a theme selector to the settings page',
version: '1.0.0',
id: "themes",
name: "Themes",
description: "Adds a theme selector to the settings page",
version: "1.0.0",
settings: {},
run: async (_) => {
const themeManager = ThemeManager.getInstance();
await themeManager.initialize();
}
},
};
export default themesPlugin;
export default themesPlugin;
+247 -169
View File
@@ -1,7 +1,7 @@
import localforage from 'localforage';
import type { CustomTheme, LoadedCustomTheme } from '@/types/CustomThemes';
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
import debounce from '@/seqta/utils/debounce';
import localforage from "localforage";
import type { CustomTheme, LoadedCustomTheme } from "@/types/CustomThemes";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import debounce from "@/seqta/utils/debounce";
type ThemeContent = {
id: string;
@@ -13,7 +13,7 @@ type ThemeContent = {
CustomCSS?: string;
hideThemeName?: boolean;
forceDark?: boolean;
images: { id: string, variableName: string, data: string }[]; // data: base64
images: { id: string; variableName: string; data: string }[]; // data: base64
};
export class ThemeManager {
@@ -27,7 +27,7 @@ export class ThemeManager {
private imageUrlCache: Map<string, string> = new Map();
private constructor() {
console.debug('[ThemeManager] Initializing...');
console.debug("[ThemeManager] Initializing...");
}
public static getInstance(): ThemeManager {
@@ -48,12 +48,12 @@ export class ThemeManager {
* Get a theme by ID from storage
*/
public async getTheme(themeId: string): Promise<CustomTheme | null> {
console.debug('[ThemeManager] Getting theme:', themeId);
console.debug("[ThemeManager] Getting theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as CustomTheme;
const theme = (await localforage.getItem(themeId)) as CustomTheme;
return theme;
} catch (error) {
console.error('[ThemeManager] Error getting theme:', error);
console.error("[ThemeManager] Error getting theme:", error);
return null;
}
}
@@ -69,19 +69,19 @@ export class ThemeManager {
* Disable the current theme without deleting it
*/
public async disableTheme(): Promise<void> {
console.debug('[ThemeManager] Disabling current theme');
console.debug("[ThemeManager] Disabling current theme");
try {
if (!this.currentTheme) {
console.debug('[ThemeManager] No theme to disable');
console.debug("[ThemeManager] No theme to disable");
return;
}
await this.removeTheme(this.currentTheme);
this.currentTheme = null;
settingsState.selectedTheme = '';
console.debug('[ThemeManager] Theme disabled successfully');
settingsState.selectedTheme = "";
console.debug("[ThemeManager] Theme disabled successfully");
} catch (error) {
console.error('[ThemeManager] Error disabling theme:', error);
console.error("[ThemeManager] Error disabling theme:", error);
}
}
@@ -89,23 +89,28 @@ export class ThemeManager {
* Initialize the theme system and restore previous state
*/
public async initialize(): Promise<void> {
console.debug('[ThemeManager] Starting initialization');
console.debug("[ThemeManager] Starting initialization");
try {
// Check if theme creator was open during reload
const themeCreatorOpen = localStorage.getItem('themeCreatorOpen');
if (themeCreatorOpen === 'true') {
console.debug('[ThemeManager] Theme creator was open, clearing preview state');
const themeCreatorOpen = localStorage.getItem("themeCreatorOpen");
if (themeCreatorOpen === "true") {
console.debug(
"[ThemeManager] Theme creator was open, clearing preview state",
);
this.clearPreview();
// Clean up the flag
localStorage.removeItem('themeCreatorOpen');
localStorage.removeItem("themeCreatorOpen");
}
if (settingsState.selectedTheme) {
console.debug('[ThemeManager] Found selected theme, restoring:', settingsState.selectedTheme);
console.debug(
"[ThemeManager] Found selected theme, restoring:",
settingsState.selectedTheme,
);
await this.setTheme(settingsState.selectedTheme);
}
} catch (error) {
console.error('[ThemeManager] Error during initialization:', error);
console.error("[ThemeManager] Error during initialization:", error);
}
}
@@ -113,13 +118,13 @@ export class ThemeManager {
* Clean up theme system resources
*/
public async cleanup(): Promise<void> {
console.debug('[ThemeManager] Cleaning up resources');
console.debug("[ThemeManager] Cleaning up resources");
try {
if (this.currentTheme) {
await this.removeTheme(this.currentTheme, false);
}
} catch (error) {
console.error('[ThemeManager] Error during cleanup:', error);
console.error("[ThemeManager] Error during cleanup:", error);
}
}
@@ -127,24 +132,24 @@ export class ThemeManager {
* Set and apply a theme by ID
*/
public async setTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Setting theme:', themeId);
console.debug("[ThemeManager] Setting theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as CustomTheme;
const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (!theme) {
console.error('[ThemeManager] Theme not found:', themeId);
console.error("[ThemeManager] Theme not found:", themeId);
return;
}
// Store original settings before applying new theme
if (!settingsState.selectedTheme) {
console.debug('[ThemeManager] Storing original settings');
console.debug("[ThemeManager] Storing original settings");
settingsState.originalSelectedColor = settingsState.selectedColor;
settingsState.originalDarkMode = settingsState.DarkMode;
}
// Remove current theme if exists
if (this.currentTheme) {
console.debug('[ThemeManager] Removing current theme');
console.debug("[ThemeManager] Removing current theme");
await this.removeTheme(this.currentTheme);
}
@@ -153,9 +158,8 @@ export class ThemeManager {
await this.applyTheme(theme);
this.currentTheme = theme;
settingsState.selectedTheme = themeId;
} catch (error) {
console.error('[ThemeManager] Error setting theme:', error);
console.error("[ThemeManager] Error setting theme:", error);
}
}
@@ -163,65 +167,80 @@ export class ThemeManager {
* Apply theme components (CSS, images, settings)
*/
private async applyTheme(theme: CustomTheme): Promise<void> {
console.debug('[ThemeManager] Applying theme:', theme.name);
console.debug("[ThemeManager] Applying theme:", theme.name);
try {
// Apply custom CSS
if (theme.CustomCSS) {
console.debug('[ThemeManager] Applying custom CSS');
console.debug("[ThemeManager] Applying custom CSS");
this.applyCustomCSS(theme.CustomCSS);
}
// Apply custom images
if (theme.CustomImages) {
console.debug('[ThemeManager] Applying custom images');
console.debug("[ThemeManager] Applying custom images");
theme.CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty('--' + image.variableName, `url(${imageUrl})`);
document.documentElement.style.setProperty(
"--" + image.variableName,
`url(${imageUrl})`,
);
});
}
// Apply theme settings
if (theme.forceDark !== undefined) {
console.debug('[ThemeManager] Setting dark mode:', theme.forceDark);
console.debug("[ThemeManager] Setting dark mode:", theme.forceDark);
settingsState.DarkMode = theme.forceDark;
}
// Use the stored selected color if available, otherwise use the default
if (theme.selectedColor) {
console.debug('[ThemeManager] Restoring saved color:', theme.selectedColor);
console.debug(
"[ThemeManager] Restoring saved color:",
theme.selectedColor,
);
settingsState.selectedColor = theme.selectedColor;
} else if (theme.defaultColour) {
console.debug('[ThemeManager] Using default color:', theme.defaultColour);
console.debug(
"[ThemeManager] Using default color:",
theme.defaultColour,
);
settingsState.selectedColor = theme.defaultColour;
}
} catch (error) {
console.error('[ThemeManager] Error applying theme:', error);
console.error("[ThemeManager] Error applying theme:", error);
}
}
/**
* Remove theme and restore original settings
*/
private async removeTheme(theme: CustomTheme, clearSelectedTheme: boolean = true): Promise<void> {
console.debug('[ThemeManager] Removing theme:', theme.name);
private async removeTheme(
theme: CustomTheme,
clearSelectedTheme: boolean = true,
): Promise<void> {
console.debug("[ThemeManager] Removing theme:", theme.name);
try {
// Remove custom CSS
if (this.styleElement) {
console.debug('[ThemeManager] Removing custom CSS');
console.debug("[ThemeManager] Removing custom CSS");
this.styleElement.remove();
this.styleElement = null;
}
// Remove custom images
if (theme.CustomImages) {
console.debug('[ThemeManager] Removing custom images');
console.debug("[ThemeManager] Removing custom images");
theme.CustomImages.forEach((image) => {
const value = document.documentElement.style.getPropertyValue('--' + image.variableName);
const value = document.documentElement.style.getPropertyValue(
"--" + image.variableName,
);
if (value) {
URL.revokeObjectURL(value.slice(4, -1)); // Remove url() wrapper
}
document.documentElement.style.removeProperty('--' + image.variableName);
document.documentElement.style.removeProperty(
"--" + image.variableName,
);
});
}
@@ -229,29 +248,34 @@ export class ThemeManager {
// Store the current color with the theme before removing it
await localforage.setItem(this.currentTheme.id, {
...this.currentTheme,
selectedColor: settingsState.selectedColor
selectedColor: settingsState.selectedColor,
});
}
// Restore original settings
if (settingsState.originalSelectedColor) {
console.debug('[ThemeManager] Restoring original color:', settingsState.originalSelectedColor);
console.debug(
"[ThemeManager] Restoring original color:",
settingsState.originalSelectedColor,
);
settingsState.selectedColor = settingsState.originalSelectedColor;
}
if (settingsState.originalDarkMode !== undefined) {
console.debug('[ThemeManager] Restoring original dark mode:', settingsState.originalDarkMode);
console.debug(
"[ThemeManager] Restoring original dark mode:",
settingsState.originalDarkMode,
);
settingsState.DarkMode = settingsState.originalDarkMode;
settingsState.originalDarkMode = undefined;
}
this.currentTheme = null;
if (clearSelectedTheme) {
settingsState.selectedTheme = '';
settingsState.selectedTheme = "";
}
} catch (error) {
console.error('[ThemeManager] Error removing theme:', error);
console.error("[ThemeManager] Error removing theme:", error);
}
}
@@ -259,16 +283,16 @@ export class ThemeManager {
* Apply custom CSS to the document
*/
private applyCustomCSS(css: string): void {
console.debug('[ThemeManager] Applying custom CSS');
console.debug("[ThemeManager] Applying custom CSS");
try {
if (!this.styleElement) {
this.styleElement = document.createElement('style');
this.styleElement.id = 'custom-theme';
this.styleElement = document.createElement("style");
this.styleElement.id = "custom-theme";
document.head.appendChild(this.styleElement);
}
this.styleElement.textContent = css;
} catch (error) {
console.error('[ThemeManager] Error applying custom CSS:', error);
console.error("[ThemeManager] Error applying custom CSS:", error);
}
}
@@ -276,22 +300,24 @@ export class ThemeManager {
* Get list of available themes
*/
public async getAvailableThemes(): Promise<CustomTheme[]> {
console.debug('[ThemeManager] Getting available themes');
console.debug("[ThemeManager] Getting available themes");
try {
const themeIds = await localforage.getItem('customThemes') as string[] | null;
const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (!themeIds) {
return [];
}
const themes = await Promise.all(
themeIds.map(async (id) => {
return await localforage.getItem(id) as CustomTheme;
})
return (await localforage.getItem(id)) as CustomTheme;
}),
);
return themes.filter(theme => theme !== null);
return themes.filter((theme) => theme !== null);
} catch (error) {
console.error('[ThemeManager] Error getting available themes:', error);
console.error("[ThemeManager] Error getting available themes:", error);
return [];
}
}
@@ -300,21 +326,23 @@ export class ThemeManager {
* Save or update a theme
*/
public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug('[ThemeManager] Saving theme:', theme.name);
console.debug("[ThemeManager] Saving theme:", theme.name);
try {
await localforage.setItem(theme.id, theme);
const themeIds = await localforage.getItem('customThemes') as string[] | null;
const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (themeIds) {
if (!themeIds.includes(theme.id)) {
themeIds.push(theme.id);
await localforage.setItem('customThemes', themeIds);
await localforage.setItem("customThemes", themeIds);
}
} else {
await localforage.setItem('customThemes', [theme.id]);
await localforage.setItem("customThemes", [theme.id]);
}
} catch (error) {
console.error('[ThemeManager] Error saving theme:', error);
console.error("[ThemeManager] Error saving theme:", error);
}
}
@@ -322,40 +350,49 @@ export class ThemeManager {
* Delete a theme
*/
public async deleteTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Deleting theme:', themeId);
console.debug("[ThemeManager] Deleting theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as CustomTheme;
const theme = (await localforage.getItem(themeId)) as CustomTheme;
if (theme) {
if (this.currentTheme?.id === themeId) {
await this.removeTheme(theme);
}
await localforage.removeItem(themeId);
const themeIds = await localforage.getItem('customThemes') as string[] | null;
const themeIds = (await localforage.getItem("customThemes")) as
| string[]
| null;
if (themeIds) {
const updatedThemeIds = themeIds.filter(id => id !== themeId);
await localforage.setItem('customThemes', updatedThemeIds);
const updatedThemeIds = themeIds.filter((id) => id !== themeId);
await localforage.setItem("customThemes", updatedThemeIds);
}
}
} catch (error) {
console.error('[ThemeManager] Error deleting theme:', error);
console.error("[ThemeManager] Error deleting theme:", error);
}
}
/**
* Download and install a theme from the store
*/
public async downloadTheme(themeContent: { id: string; name: string; description: string; coverImage: string; }): Promise<void> {
console.debug('[ThemeManager] Downloading theme:', themeContent.name);
public async downloadTheme(themeContent: {
id: string;
name: string;
description: string;
coverImage: string;
}): Promise<void> {
console.debug("[ThemeManager] Downloading theme:", themeContent.name);
try {
if (!themeContent.id) return;
const response = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`);
const themeData = await response.json() as ThemeContent;
const response = await fetch(
`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${themeContent.id}/theme.json`,
);
const themeData = (await response.json()) as ThemeContent;
await this.installTheme(themeData);
} catch (error) {
console.error('[ThemeManager] Error downloading theme:', error);
console.error("[ThemeManager] Error downloading theme:", error);
}
}
@@ -363,62 +400,67 @@ export class ThemeManager {
* Install a theme from theme data
*/
public async installTheme(themeData: ThemeContent): Promise<void> {
console.debug('[ThemeManager] Installing theme:', themeData.name);
console.debug("[ThemeManager] Installing theme:", themeData.name);
try {
// Validate required fields
if (!themeData.id || !themeData.name) {
throw new Error('Theme is missing required fields (id or name)');
throw new Error("Theme is missing required fields (id or name)");
}
// Handle cover image (optional)
let coverImageBlob = null;
if (themeData.coverImage) {
try {
const strippedCoverImage = this.stripBase64Prefix(themeData.coverImage);
const strippedCoverImage = this.stripBase64Prefix(
themeData.coverImage,
);
coverImageBlob = this.base64ToBlob(strippedCoverImage);
} catch (e) {
console.warn('[ThemeManager] Failed to process cover image:', e);
console.warn("[ThemeManager] Failed to process cover image:", e);
// Continue without cover image
}
}
// Handle images (optional)
const images = themeData.images?.map((image) => {
try {
if (!image.id || !image.variableName || !image.data) {
console.warn('[ThemeManager] Skipping invalid image:', image);
return null;
}
return {
...image,
blob: this.base64ToBlob(this.stripBase64Prefix(image.data))
};
} catch (e) {
console.warn('[ThemeManager] Failed to process image:', e);
return null;
}
}).filter(img => img !== null) ?? [];
const images =
themeData.images
?.map((image) => {
try {
if (!image.id || !image.variableName || !image.data) {
console.warn("[ThemeManager] Skipping invalid image:", image);
return null;
}
return {
...image,
blob: this.base64ToBlob(this.stripBase64Prefix(image.data)),
};
} catch (e) {
console.warn("[ThemeManager] Failed to process image:", e);
return null;
}
})
.filter((img) => img !== null) ?? [];
// Create theme with defaults for optional fields
const theme: LoadedCustomTheme = {
id: themeData.id,
name: themeData.name,
description: themeData.description || '',
description: themeData.description || "",
webURL: themeData.id,
coverImage: coverImageBlob,
CustomImages: images,
CustomCSS: themeData.CustomCSS || '',
defaultColour: themeData.defaultColour || 'rgba(0, 123, 255, 1)',
CustomCSS: themeData.CustomCSS || "",
defaultColour: themeData.defaultColour || "rgba(0, 123, 255, 1)",
CanChangeColour: themeData.CanChangeColour ?? true,
allowBackgrounds: true,
isEditable: false,
hideThemeName: themeData.hideThemeName ?? false,
forceDark: themeData.forceDark
forceDark: themeData.forceDark,
};
await this.saveTheme(theme);
} catch (error) {
console.error('[ThemeManager] Error installing theme:', error);
console.error("[ThemeManager] Error installing theme:", error);
throw error; // Re-throw to handle in UI
}
}
@@ -427,11 +469,11 @@ export class ThemeManager {
* Share a theme by exporting it
*/
public async shareTheme(themeId: string): Promise<void> {
console.debug('[ThemeManager] Sharing theme:', themeId);
console.debug("[ThemeManager] Sharing theme:", themeId);
try {
const theme = await localforage.getItem(themeId) as LoadedCustomTheme;
const theme = (await localforage.getItem(themeId)) as LoadedCustomTheme;
if (!theme) {
console.error('[ThemeManager] Theme not found');
console.error("[ThemeManager] Theme not found");
return;
}
@@ -447,26 +489,30 @@ export class ThemeManager {
} = theme;
// Convert images to base64
const finalImages = await Promise.all(CustomImages.map(async (image) => ({
id: image.id,
variableName: image.variableName,
data: await this.blobToBase64(image.blob)
})));
const finalImages = await Promise.all(
CustomImages.map(async (image) => ({
id: image.id,
variableName: image.variableName,
data: await this.blobToBase64(image.blob),
})),
);
// Convert cover image to base64
const coverImageBase64 = coverImage ? await this.blobToBase64(coverImage) : null;
const coverImageBase64 = coverImage
? await this.blobToBase64(coverImage)
: null;
// Create shareable theme data with only necessary fields
const shareableTheme = {
...themeBasics,
images: finalImages,
coverImage: coverImageBase64
coverImage: coverImageBase64,
};
// Save theme file
this.saveThemeFile(shareableTheme, theme.name || 'Unnamed_Theme');
this.saveThemeFile(shareableTheme, theme.name || "Unnamed_Theme");
} catch (error) {
console.error('[ThemeManager] Error sharing theme:', error);
console.error("[ThemeManager] Error sharing theme:", error);
}
}
@@ -474,7 +520,7 @@ export class ThemeManager {
* Preview a theme without applying it
*/
public async previewTheme(theme: LoadedCustomTheme): Promise<void> {
console.debug('[ThemeManager] Previewing theme:', theme.name);
console.debug("[ThemeManager] Previewing theme:", theme.name);
try {
const { CustomCSS, CustomImages, defaultColour, forceDark } = theme;
@@ -482,7 +528,10 @@ export class ThemeManager {
if (!theme.webURL) {
if (this.originalPreviewColor === null) {
this.originalPreviewColor = settingsState.selectedColor;
localStorage.setItem('originalPreviewColor', settingsState.selectedColor);
localStorage.setItem(
"originalPreviewColor",
settingsState.selectedColor,
);
}
if (this.originalPreviewTheme === null) {
this.originalPreviewTheme = settingsState.DarkMode;
@@ -495,10 +544,12 @@ export class ThemeManager {
}
// Apply custom images
const newImageVariableNames = CustomImages.map(image => image.variableName);
const newImageVariableNames = CustomImages.map(
(image) => image.variableName,
);
// Remove old preview images
this.previousImageVariableNames.forEach(variableName => {
this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName);
}
@@ -507,7 +558,10 @@ export class ThemeManager {
// Apply new images
CustomImages.forEach((image) => {
const imageUrl = URL.createObjectURL(image.blob);
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${imageUrl})`);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
});
// Update previousImageVariableNames
@@ -517,12 +571,12 @@ export class ThemeManager {
if (forceDark !== undefined) {
settingsState.DarkMode = forceDark;
}
if (defaultColour) {
settingsState.selectedColor = defaultColour;
}
} catch (error) {
console.error('[ThemeManager] Error previewing theme:', error);
console.error("[ThemeManager] Error previewing theme:", error);
}
}
@@ -530,7 +584,7 @@ export class ThemeManager {
* Update the preview of a theme in real-time (for theme creator)
*/
public async updatePreview(theme: Partial<LoadedCustomTheme>): Promise<void> {
console.debug('[ThemeManager] Updating theme preview');
console.debug("[ThemeManager] Updating theme preview");
try {
// Only store original settings if this is a new theme (not editing)
// We can tell it's a new theme if it has no webURL (which is set when a theme is saved/loaded)
@@ -550,10 +604,12 @@ export class ThemeManager {
// Handle images if present
if (theme.CustomImages) {
const newImageVariableNames = theme.CustomImages.map(image => image.variableName);
const newImageVariableNames = theme.CustomImages.map(
(image) => image.variableName,
);
// Remove old preview images that are no longer present
this.previousImageVariableNames.forEach(variableName => {
this.previousImageVariableNames.forEach((variableName) => {
if (!newImageVariableNames.includes(variableName)) {
this.removeImageFromDocument(variableName);
// Clean up cached URL
@@ -568,10 +624,16 @@ export class ThemeManager {
// Only create new URL if one doesn't exist
const imageUrl = URL.createObjectURL(image.blob);
this.imageUrlCache.set(image.variableName, imageUrl);
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${imageUrl})`);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${imageUrl})`,
);
} else {
// Reuse existing URL
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${existingUrl})`);
document.documentElement.style.setProperty(
`--${image.variableName}`,
`url(${existingUrl})`,
);
}
});
@@ -588,7 +650,7 @@ export class ThemeManager {
settingsState.selectedColor = theme.defaultColour;
}
} catch (error) {
console.error('[ThemeManager] Error updating theme preview:', error);
console.error("[ThemeManager] Error updating theme preview:", error);
}
}
@@ -596,22 +658,25 @@ export class ThemeManager {
* Update the preview of a theme (debounced)
* @param theme - The theme to update the preview of
*/
public updatePreviewDebounced = debounce((theme: Partial<LoadedCustomTheme>): void => {
this.updatePreview(theme);
}, 2);
public updatePreviewDebounced = debounce(
(theme: Partial<LoadedCustomTheme>): void => {
this.updatePreview(theme);
},
2,
);
/**
* Clear theme preview
*/
public clearPreview(): void {
console.debug('[ThemeManager] Clearing theme preview');
console.debug("[ThemeManager] Clearing theme preview");
try {
// Remove preview images and revoke URLs
this.previousImageVariableNames.forEach(variableName => {
this.previousImageVariableNames.forEach((variableName) => {
this.removeImageFromDocument(variableName);
});
// Clear all cached URLs
this.imageUrlCache.forEach(url => URL.revokeObjectURL(url));
this.imageUrlCache.forEach((url) => URL.revokeObjectURL(url));
this.imageUrlCache.clear();
this.previousImageVariableNames = [];
@@ -622,40 +687,51 @@ export class ThemeManager {
}
// Restore original settings
const storedColor = localStorage.getItem('originalPreviewColor');
const storedColor = localStorage.getItem("originalPreviewColor");
if (storedColor) {
settingsState.selectedColor = storedColor;
localStorage.removeItem('originalPreviewColor');
localStorage.removeItem("originalPreviewColor");
} else if (this.originalPreviewColor !== null) {
console.debug('[ThemeManager] Restoring color from memory:', this.originalPreviewColor);
console.debug(
"[ThemeManager] Restoring color from memory:",
this.originalPreviewColor,
);
settingsState.selectedColor = this.originalPreviewColor;
console.debug('[ThemeManager] Color after restore:', settingsState.selectedColor);
console.debug(
"[ThemeManager] Color after restore:",
settingsState.selectedColor,
);
} else {
console.debug('[ThemeManager] No color to restore found');
console.debug("[ThemeManager] No color to restore found");
}
this.originalPreviewColor = null;
if (this.originalPreviewTheme !== null) {
console.debug('[ThemeManager] Restoring dark mode:', this.originalPreviewTheme);
console.debug(
"[ThemeManager] Restoring dark mode:",
this.originalPreviewTheme,
);
settingsState.DarkMode = this.originalPreviewTheme;
this.originalPreviewTheme = null;
}
} catch (error) {
console.error('[ThemeManager] Error clearing preview:', error);
console.error("[ThemeManager] Error clearing preview:", error);
}
}
// Utility methods
private stripBase64Prefix(base64String: string): string {
if (!base64String) return '';
if (!base64String) return "";
const prefixRegex = /^data:[^;]+;base64,/;
try {
return prefixRegex.test(base64String) ? base64String.replace(prefixRegex, '') : base64String;
} catch(err) {
console.error('[ThemeManager] Error stripping base64 prefix:', err);
return '';
return prefixRegex.test(base64String)
? base64String.replace(prefixRegex, "")
: base64String;
} catch (err) {
console.error("[ThemeManager] Error stripping base64 prefix:", err);
return "";
}
}
@@ -664,14 +740,14 @@ export class ThemeManager {
const byteString = atob(base64);
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: 'image/png' });
} catch(err) {
console.error('[ThemeManager] Error converting base64 to blob:', err);
return new Blob([ab], { type: "image/png" });
} catch (err) {
console.error("[ThemeManager] Error converting base64 to blob:", err);
return new Blob();
}
}
@@ -681,7 +757,7 @@ export class ThemeManager {
const reader = new FileReader();
reader.onloadend = () => {
const base64String = reader.result as string;
const base64Data = base64String.split(',')[1];
const base64Data = base64String.split(",")[1];
resolve(base64Data);
};
reader.onerror = reject;
@@ -692,23 +768,25 @@ export class ThemeManager {
private saveThemeFile(data: object, fileName: string): void {
try {
const fileData = JSON.stringify(data, null, 2);
const blob = new Blob([fileData], { type: 'application/json' });
const blob = new Blob([fileData], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const a = document.createElement("a");
a.href = url;
a.download = `${fileName}.theme.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch(err) {
console.error('[ThemeManager] Error saving theme file:', err);
} catch (err) {
console.error("[ThemeManager] Error saving theme file:", err);
}
}
private removeImageFromDocument(variableName: string): void {
try {
const value = document.documentElement.style.getPropertyValue('--' + variableName);
const value = document.documentElement.style.getPropertyValue(
"--" + variableName,
);
if (value) {
const url = this.imageUrlCache.get(variableName);
if (url) {
@@ -716,23 +794,23 @@ export class ThemeManager {
this.imageUrlCache.delete(variableName);
}
}
document.documentElement.style.removeProperty('--' + variableName);
} catch(err) {
console.error('[ThemeManager] Error removing image from document:', err);
document.documentElement.style.removeProperty("--" + variableName);
} catch (err) {
console.error("[ThemeManager] Error removing image from document:", err);
}
}
private applyPreviewCSS(css: string): void {
console.debug('[ThemeManager] Applying preview CSS');
console.debug("[ThemeManager] Applying preview CSS");
try {
if (!this.previewStyleElement) {
this.previewStyleElement = document.createElement('style');
this.previewStyleElement.id = 'custom-theme-preview';
this.previewStyleElement = document.createElement("style");
this.previewStyleElement.id = "custom-theme-preview";
document.head.appendChild(this.previewStyleElement);
}
this.previewStyleElement.textContent = css;
} catch (error) {
console.error('[ThemeManager] Error applying preview CSS:', error);
console.error("[ThemeManager] Error applying preview CSS:", error);
}
}
}
}
+149 -138
View File
@@ -1,268 +1,279 @@
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
import type { Plugin } from '../../core/types';
import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat';
import { waitForElm } from '@/seqta/utils/waitForElm';
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import type { Plugin } from "../../core/types";
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
import { waitForElm } from "@/seqta/utils/waitForElm";
const timetablePlugin: Plugin<{}, {}> = {
id: 'timetable',
name: 'Timetable Enhancer',
description: 'Adds extra features to the timetable view',
version: '1.0.0',
id: "timetable",
name: "Timetable Enhancer",
description: "Adds extra features to the timetable view",
version: "1.0.0",
settings: {},
disableToggle: true,
run: async (api) => {
const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable)
const { unregister } = api.seqta.onMount(".timetablepage", handleTimetable);
return () => {
// Call the unregister function to remove the mount listener
unregister();
const timetablePage = document.querySelector('.timetablepage')
const timetablePage = document.querySelector(".timetablepage");
if (timetablePage) {
const zoomControls = document.querySelector('.timetable-zoom-controls')
if (zoomControls) zoomControls.remove()
const hideControls = document.querySelector('.timetable-hide-controls')
if (hideControls) hideControls.remove()
resetTimetableStyles()
const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) zoomControls.remove();
const hideControls = document.querySelector(".timetable-hide-controls");
if (hideControls) hideControls.remove();
resetTimetableStyles();
}
}
}
};
},
};
// Store event handlers globally for cleanup
const zoomHandlers = new WeakMap<Element, { zoomIn: () => void; zoomOut: () => void }>()
const zoomHandlers = new WeakMap<
Element,
{ zoomIn: () => void; zoomOut: () => void }
>();
function resetTimetableStyles(): void {
const firstDayColumn = document.querySelector(".dailycal .content .days td") as HTMLElement
if (!firstDayColumn) return
const baseContainerHeight = parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
const firstDayColumn = document.querySelector(
".dailycal .content .days td",
) as HTMLElement;
if (!firstDayColumn) return;
const baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${baseContainerHeight}px`
})
const timeColumn = document.querySelector(".times")
(td as HTMLElement).style.height = `${baseContainerHeight}px`;
});
const timeColumn = document.querySelector(".times");
if (timeColumn) {
const times = timeColumn.querySelectorAll(".time")
const timeHeight = baseContainerHeight / times.length
const times = timeColumn.querySelectorAll(".time");
const timeHeight = baseContainerHeight / times.length;
times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px`
})
(time as HTMLElement).style.height = `${timeHeight}px`;
});
}
const lessons = document.querySelectorAll(".dailycal .lesson")
const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement
const originalHeight = lessonEl.getAttribute('data-original-height')
const lessonEl = lesson as HTMLElement;
const originalHeight = lessonEl.getAttribute("data-original-height");
if (originalHeight) {
lessonEl.style.height = `${originalHeight}px`
lessonEl.style.height = `${originalHeight}px`;
}
})
});
const entries = document.querySelectorAll(".entry")
const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
entryEl.style.opacity = '1'
})
const entryEl = entry as HTMLElement;
entryEl.style.opacity = "1";
});
const zoomControls = document.querySelector('.timetable-zoom-controls')
const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) {
const handlers = zoomHandlers.get(zoomControls)
const handlers = zoomHandlers.get(zoomControls);
if (handlers) {
const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)')
const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)')
if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn)
if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut)
zoomHandlers.delete(zoomControls)
const zoomIn = zoomControls.querySelector(".timetable-zoom:nth-child(2)");
const zoomOut = zoomControls.querySelector(
".timetable-zoom:nth-child(1)",
);
if (zoomIn) zoomIn.removeEventListener("click", handlers.zoomIn);
if (zoomOut) zoomOut.removeEventListener("click", handlers.zoomOut);
zoomHandlers.delete(zoomControls);
}
}
}
async function handleTimetable(): Promise<void> {
await waitForElm(".time", true, 10)
await waitForElm(".time", true, 10);
// Store original heights when timetable loads
const lessons = document.querySelectorAll(".dailycal .lesson")
const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement
const lessonEl = lesson as HTMLElement;
lessonEl.setAttribute(
"data-original-height",
lessonEl.offsetHeight.toString(),
)
})
);
});
// Existing time format code
if (settingsState.timeFormat == "12") {
const times = document.querySelectorAll(".timetablepage .times .time")
const times = document.querySelectorAll(".timetablepage .times .time");
for (const time of times) {
if (!time.textContent) continue
time.textContent = convertTo12HourFormat(time.textContent, true)
if (!time.textContent) continue;
time.textContent = convertTo12HourFormat(time.textContent, true);
}
}
handleTimetableZoom()
handleTimetableAssessmentHide()
handleTimetableZoom();
handleTimetableAssessmentHide();
}
function handleTimetableZoom(): void {
console.log("Initializing timetable zoom controls")
console.log("Initializing timetable zoom controls");
// Lazy initialize state variables only when function is first called
let timetableZoomLevel = 1
let baseContainerHeight: number | null = null
let timetableZoomLevel = 1;
let baseContainerHeight: number | null = null;
const originalEntryPositions = new Map<
Element,
{ topRatio: number; heightRatio: number }
>()
>();
// Create zoom controls
const zoomControls = document.createElement("div")
zoomControls.className = "timetable-zoom-controls"
const zoomControls = document.createElement("div");
zoomControls.className = "timetable-zoom-controls";
const zoomIn = document.createElement("button")
zoomIn.className = "uiButton timetable-zoom iconFamily"
zoomIn.innerHTML = "&#xed93;" // Unicode for zoom in icon (custom iconfamily)
const zoomIn = document.createElement("button");
zoomIn.className = "uiButton timetable-zoom iconFamily";
zoomIn.innerHTML = "&#xed93;"; // Unicode for zoom in icon (custom iconfamily)
const zoomOut = document.createElement("button")
zoomOut.className = "uiButton timetable-zoom iconFamily"
zoomOut.innerHTML = "&#xed94;" // Unicode for zoom out icon (custom iconfamily)
const zoomOut = document.createElement("button");
zoomOut.className = "uiButton timetable-zoom iconFamily";
zoomOut.innerHTML = "&#xed94;"; // Unicode for zoom out icon (custom iconfamily)
zoomControls.appendChild(zoomOut)
zoomControls.appendChild(zoomIn)
zoomControls.appendChild(zoomOut);
zoomControls.appendChild(zoomIn);
const toolbar = document.getElementById("toolbar")
toolbar?.appendChild(zoomControls)
const toolbar = document.getElementById("toolbar");
toolbar?.appendChild(zoomControls);
// Store event listener references
const zoomInHandler = () => {
if (timetableZoomLevel < 2) {
timetableZoomLevel += 0.2
updateZoom()
timetableZoomLevel += 0.2;
updateZoom();
}
}
};
const zoomOutHandler = () => {
if (timetableZoomLevel > 0.6) {
timetableZoomLevel -= 0.2
updateZoom()
timetableZoomLevel -= 0.2;
updateZoom();
}
}
};
zoomIn.addEventListener("click", zoomInHandler)
zoomOut.addEventListener("click", zoomOutHandler)
zoomIn.addEventListener("click", zoomInHandler);
zoomOut.addEventListener("click", zoomOutHandler);
// Store references for cleanup
zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler })
zoomHandlers.set(zoomControls, {
zoomIn: zoomInHandler,
zoomOut: zoomOutHandler,
});
const initializePositions = () => {
// Get the base container height from the first TD
const firstDayColumn = document.querySelector(
".dailycal .content .days td",
) as HTMLElement
if (!firstDayColumn) return false
) as HTMLElement;
if (!firstDayColumn) return false;
baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
// Store original ratios
const entries = document.querySelectorAll(".entriesWrapper .entry")
const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
const entryEl = entry as HTMLElement;
// Calculate ratios relative to detected base height
if (baseContainerHeight === null) return
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight
if (baseContainerHeight === null) return;
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight;
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight;
originalEntryPositions.set(entry, { topRatio, heightRatio })
})
originalEntryPositions.set(entry, { topRatio, heightRatio });
});
return true
}
return true;
};
const updateZoom = () => {
// Initialize positions if not already done
if (baseContainerHeight === null && !initializePositions()) {
console.error("Failed to initialize positions")
return
console.error("Failed to initialize positions");
return;
}
console.debug(`Updating zoom level to: ${timetableZoomLevel}`)
console.debug(`Updating zoom level to: ${timetableZoomLevel}`);
// Calculate new container height
if (baseContainerHeight === null) return
const newContainerHeight = baseContainerHeight * timetableZoomLevel
if (baseContainerHeight === null) return;
const newContainerHeight = baseContainerHeight * timetableZoomLevel;
// Update all day columns (TDs)
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${newContainerHeight}px`
})
(td as HTMLElement).style.height = `${newContainerHeight}px`;
});
// Update all entries using stored ratios
const entries = document.querySelectorAll(".entriesWrapper .entry")
const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
const originalRatios = originalEntryPositions.get(entry)
const entryEl = entry as HTMLElement;
const originalRatios = originalEntryPositions.get(entry);
if (originalRatios) {
// Calculate new positions from original ratios
const newTop = originalRatios.topRatio * newContainerHeight
const newHeight = originalRatios.heightRatio * newContainerHeight
const newTop = originalRatios.topRatio * newContainerHeight;
const newHeight = originalRatios.heightRatio * newContainerHeight;
// Apply new values
entryEl.style.top = `${Math.round(newTop)}px`
entryEl.style.height = `${Math.round(newHeight)}px`
entryEl.style.top = `${Math.round(newTop)}px`;
entryEl.style.height = `${Math.round(newHeight)}px`;
}
})
});
// Update time column to match
const timeColumn = document.querySelector(".times")
const timeColumn = document.querySelector(".times");
if (timeColumn) {
const times = timeColumn.querySelectorAll(".time")
const timeHeight = newContainerHeight / times.length
const times = timeColumn.querySelectorAll(".time");
const timeHeight = newContainerHeight / times.length;
times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px`
})
(time as HTMLElement).style.height = `${timeHeight}px`;
});
}
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
behavior: "instant",
block: "center",
})
}
});
};
}
function handleTimetableAssessmentHide(): void {
const hideControls = document.createElement("div")
hideControls.className = "timetable-hide-controls"
const hideControls = document.createElement("div");
hideControls.className = "timetable-hide-controls";
const hideOn = document.createElement("button")
hideOn.className = "uiButton timetable-hide iconFamily"
hideOn.innerHTML = "&#128065;"
const hideOn = document.createElement("button");
hideOn.className = "uiButton timetable-hide iconFamily";
hideOn.innerHTML = "&#128065;";
hideControls.appendChild(hideOn)
hideControls.appendChild(hideOn);
const toolbar = document.getElementById("toolbar")
toolbar?.appendChild(hideControls)
const toolbar = document.getElementById("toolbar");
toolbar?.appendChild(hideControls);
function hideElements(): void {
const entries = document.querySelectorAll(".entry")
const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement
const entryEl = entry as HTMLElement;
if (!entryEl.classList.contains("assessment")) {
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3"
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3";
}
})
});
}
hideOn.addEventListener("click", hideElements)
hideOn.addEventListener("click", hideElements);
}
export default timetablePlugin;
export default timetablePlugin;
+109 -62
View File
@@ -1,7 +1,16 @@
import type { EventsAPI, Plugin, PluginAPI, PluginSettings, SEQTAAPI, SettingsAPI, SettingValue, StorageAPI } from './types';
import { eventManager } from '@/seqta/utils/listeners/EventManager';
import ReactFiber from '@/seqta/utils/ReactFiber';
import browser from 'webextension-polyfill';
import type {
EventsAPI,
Plugin,
PluginAPI,
PluginSettings,
SEQTAAPI,
SettingsAPI,
SettingValue,
StorageAPI,
} from "./types";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
import ReactFiber from "@/seqta/utils/ReactFiber";
import browser from "webextension-polyfill";
function createSEQTAAPI(): SEQTAAPI {
return {
@@ -11,41 +20,46 @@ function createSEQTAAPI(): SEQTAAPI {
{
customCheck: (element) => element.matches(selector),
},
callback
callback,
);
},
getFiber: (selector) => {
return ReactFiber.find(selector);
},
getCurrentPage: () => {
const path = window.location.hash.split('?page=/')[1] || '';
return path.split('/')[0];
const path = window.location.hash.split("?page=/")[1] || "";
return path.split("/")[0];
},
onPageChange: (callback) => {
const handler = () => {
const page = window.location.hash.split('?page=/')[1] || '';
callback(page.split('/')[0]);
const page = window.location.hash.split("?page=/")[1] || "";
callback(page.split("/")[0]);
};
window.addEventListener('hashchange', handler);
window.addEventListener("hashchange", handler);
// Return an unregister function
return {
unregister: () => {
window.removeEventListener('hashchange', handler);
}
window.removeEventListener("hashchange", handler);
},
};
}
},
};
}
function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): SettingsAPI<T> & { loaded: Promise<void> } {
function createSettingsAPI<T extends PluginSettings>(
plugin: Plugin<T>,
): SettingsAPI<T> & { loaded: Promise<void> } {
const storageKey = `plugin.${plugin.id}.settings`;
const listeners = new Map<keyof T, Set<(value: any) => void>>();
// Initialize with default values
const settingsWithMeta: any = {
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
onChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => {
if (!listeners.has(key)) {
listeners.set(key, new Set());
}
@@ -53,13 +67,16 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return {
unregister: () => {
listeners.get(key)!.delete(callback);
}
},
};
},
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => {
offChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => {
listeners.get(key)?.delete(callback);
},
loaded: Promise.resolve() // will be replaced below
loaded: Promise.resolve(), // will be replaced below
};
// Fill with defaults first
@@ -71,33 +88,45 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
const loaded = (async () => {
try {
const stored = await browser.storage.local.get(storageKey);
const storedSettings = stored[storageKey] as Partial<Record<keyof T, any>>;
const storedSettings = stored[storageKey] as Partial<
Record<keyof T, any>
>;
if (storedSettings) {
for (const key in storedSettings) {
if (key in settingsWithMeta) {
settingsWithMeta[key] = storedSettings[key];
listeners.get(key as keyof T)?.forEach(cb => cb(storedSettings[key]));
listeners
.get(key as keyof T)
?.forEach((cb) => cb(storedSettings[key]));
}
}
}
} catch (error) {
console.error(`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`, error);
console.error(
`[BetterSEQTA+] Error loading settings for plugin ${plugin.id}:`,
error,
);
}
})();
settingsWithMeta.loaded = loaded;
// Listen for storage changes and update settingsWithMeta
const handleStorageChange = (changes: { [key: string]: browser.Storage.StorageChange }, area: string) => {
if (area !== 'local' || !(storageKey in changes)) return;
const handleStorageChange = (
changes: { [key: string]: browser.Storage.StorageChange },
area: string,
) => {
if (area !== "local" || !(storageKey in changes)) return;
const newValue = changes[storageKey].newValue as Partial<Record<keyof T, any>> | undefined;
const newValue = changes[storageKey].newValue as
| Partial<Record<keyof T, any>>
| undefined;
if (!newValue) return;
for (const key in newValue) {
const typedKey = key as keyof T;
settingsWithMeta[typedKey] = newValue[typedKey];
listeners.get(typedKey)?.forEach(cb => cb(newValue[typedKey]));
listeners.get(typedKey)?.forEach((cb) => cb(newValue[typedKey]));
}
};
@@ -108,7 +137,8 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
return target[prop];
},
set(target, prop, value) {
if (['onChange', 'offChange', 'loaded'].includes(prop as string)) return false;
if (["onChange", "offChange", "loaded"].includes(prop as string))
return false;
target[prop] = value;
@@ -120,25 +150,29 @@ function createSettingsAPI<T extends PluginSettings>(plugin: Plugin<T>): Setting
browser.storage.local.set({ [storageKey]: dataToStore });
listeners.get(prop as keyof T)?.forEach(cb => cb(value));
listeners.get(prop as keyof T)?.forEach((cb) => cb(value));
return true;
}
},
}) as SettingsAPI<T> & { loaded: Promise<void> };
return proxy;
}
function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in keyof T]: T[K] } {
function createStorageAPI<T = any>(
pluginId: string,
): StorageAPI<T> & { [K in keyof T]: T[K] } {
const prefix = `plugin.${pluginId}.storage.`;
const cache: Record<string, any> = {};
const listeners = new Map<string, Set<(value: any) => void>>();
const storageListeners = new Set<(changes: { [key: string]: any }, area: string) => void>();
const storageListeners = new Set<
(changes: { [key: string]: any }, area: string) => void
>();
// Load all existing storage values for this plugin
const loadStoragePromise = (async () => {
try {
const allStorage = await browser.storage.local.get(null);
// Filter for this plugin's storage keys and populate cache
Object.entries(allStorage).forEach(([key, value]) => {
if (key.startsWith(prefix)) {
@@ -147,31 +181,39 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
}
});
} catch (error) {
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error);
console.error(
`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`,
error,
);
}
})();
// Listen for storage changes
const handleStorageChange = (changes: { [key: string]: any }, area: string) => {
if (area === 'local') {
const handleStorageChange = (
changes: { [key: string]: any },
area: string,
) => {
if (area === "local") {
Object.entries(changes).forEach(([key, change]) => {
if (key.startsWith(prefix)) {
const shortKey = key.slice(prefix.length);
cache[shortKey] = change.newValue;
// Notify listeners
listeners.get(shortKey)?.forEach(callback => callback(change.newValue));
listeners
.get(shortKey)
?.forEach((callback) => callback(change.newValue));
}
});
}
};
browser.storage.onChanged.addListener(handleStorageChange);
storageListeners.add(handleStorageChange);
// Create the proxy for direct property access
return new Proxy(cache, {
get(target, prop: string) {
if (prop === 'onChange') {
if (prop === "onChange") {
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
if (!listeners.has(key as string)) {
listeners.set(key as string, new Set());
@@ -180,79 +222,84 @@ function createStorageAPI<T = any>(pluginId: string): StorageAPI<T> & { [K in ke
return {
unregister: () => {
listeners.get(key as string)?.delete(callback);
}
},
};
};
}
if (prop === 'offChange') {
if (prop === "offChange") {
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
listeners.get(key as string)?.delete(callback);
};
}
if (prop === 'loaded') {
if (prop === "loaded") {
return loadStoragePromise;
}
// Direct property access
return target[prop];
},
set(target, prop: string, value: any) {
if (['onChange', 'offChange', 'loaded'].includes(prop)) {
if (["onChange", "offChange", "loaded"].includes(prop)) {
return false;
}
// Update cache and store in browser storage
target[prop] = value;
browser.storage.local.set({ [prefix + prop]: value });
// Notify listeners
listeners.get(prop)?.forEach(callback => callback(value));
listeners.get(prop)?.forEach((callback) => callback(value));
return true;
}
},
}) as StorageAPI<T> & { [K in keyof T]: T[K] };
}
function createEventsAPI(pluginId: string): EventsAPI {
const prefix = `plugin.${pluginId}.`;
const eventListeners = new Map<string, Set<{ callback: (...args: any[]) => void, listener: EventListener }>>();
const eventListeners = new Map<
string,
Set<{ callback: (...args: any[]) => void; listener: EventListener }>
>();
return {
on: (event, callback) => {
const fullEventName = prefix + event;
const listener = ((e: CustomEvent) => {
callback(...(e.detail || []));
}) as EventListener;
document.addEventListener(fullEventName, listener);
if (!eventListeners.has(event)) {
eventListeners.set(event, new Set());
}
eventListeners.get(event)!.add({ callback, listener });
return {
unregister: () => {
document.removeEventListener(fullEventName, listener);
eventListeners.get(event)?.delete({ callback, listener });
}
},
};
},
emit: (event, ...args) => {
document.dispatchEvent(
new CustomEvent(prefix + event, {
detail: args.length > 0 ? args : null
})
detail: args.length > 0 ? args : null,
}),
);
},
};
}
export function createPluginAPI<T extends PluginSettings, S = any>(plugin: Plugin<T, S>): PluginAPI<T, S> {
export function createPluginAPI<T extends PluginSettings, S = any>(
plugin: Plugin<T, S>,
): PluginAPI<T, S> {
return {
seqta: createSEQTAAPI(),
settings: createSettingsAPI(plugin),
storage: createStorageAPI<S>(plugin.id),
events: createEventsAPI(plugin.id),
};
}
}
+108 -74
View File
@@ -1,6 +1,13 @@
import type { BooleanSetting, NumberSetting, Plugin, PluginSettings, SelectSetting, StringSetting } from './types';
import { createPluginAPI } from './createAPI';
import browser from 'webextension-polyfill';
import type {
BooleanSetting,
NumberSetting,
Plugin,
PluginSettings,
SelectSetting,
StringSetting,
} from "./types";
import { createPluginAPI } from "./createAPI";
import browser from "webextension-polyfill";
interface PluginSettingsStorage {
enabled?: boolean;
@@ -34,7 +41,7 @@ export class PluginManager {
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
const fullEventName = `plugin.${pluginId}.${event}`;
// Dispatch plugin event if it's running otherwise queue it
if (this.runningPlugins.get(pluginId)) {
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
@@ -49,7 +56,7 @@ export class PluginManager {
private async processBackloggedEvents(pluginId: string) {
for (const [key, argsList] of this.eventBacklog.entries()) {
const [eventPluginId, event] = key.split(':');
const [eventPluginId, event] = key.split(":");
if (eventPluginId === pluginId) {
for (const args of argsList) {
this.dispatchPluginEvent(pluginId, event, args);
@@ -59,7 +66,9 @@ export class PluginManager {
}
}
public registerPlugin<T extends PluginSettings, S>(plugin: Plugin<T, S>): void {
public registerPlugin<T extends PluginSettings, S>(
plugin: Plugin<T, S>,
): void {
if (this.plugins.has(plugin.id)) {
throw new Error(`Plugin with id "${plugin.id}" is already registered`);
}
@@ -79,53 +88,60 @@ export class PluginManager {
try {
const api = createPluginAPI(plugin);
// Check if plugin is enabled before starting
if (plugin.disableToggle) {
const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`);
const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined;
const enabled = pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
const settings = await browser.storage.local.get(
`plugin.${pluginId}.settings`,
);
const pluginSettings = settings[`plugin.${pluginId}.settings`] as
| PluginSettingsStorage
| undefined;
const enabled =
pluginSettings?.enabled ?? plugin.defaultEnabled ?? true;
if (!enabled) {
console.info(`Plugin "${pluginId}" is disabled, skipping initialization`);
console.info(
`Plugin "${pluginId}" is disabled, skipping initialization`,
);
return;
}
}
// Inject plugin styles if provided
if (plugin.styles) {
const styleElement = document.createElement('style');
const styleElement = document.createElement("style");
styleElement.textContent = plugin.styles;
document.head.appendChild(styleElement);
this.styleElements.set(pluginId, styleElement);
}
// Wait for both settings and storage to be loaded before starting the plugin
await Promise.all([
(api.settings as any).loaded,
api.storage.loaded
]);
await Promise.all([(api.settings as any).loaded, api.storage.loaded]);
const result = await plugin.run(api);
if (typeof result === 'function') {
if (typeof result === "function") {
this.cleanupFunctions.set(plugin.id, result);
}
this.runningPlugins.set(pluginId, true);
console.info(`Plugin "${pluginId}" started successfully`);
// Process any backlogged events
await this.processBackloggedEvents(pluginId);
} catch (error) {
console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error);
console.error(
`[BetterSEQTA+] Failed to start plugin ${pluginId}:`,
error,
);
throw error;
}
}
public async startAllPlugins(): Promise<void> {
const startPromises = Array.from(this.plugins.keys()).map(id =>
this.startPlugin(id).catch(error => {
const startPromises = Array.from(this.plugins.keys()).map((id) =>
this.startPlugin(id).catch((error) => {
console.error(`Failed to start plugin "${id}":`, error);
return Promise.reject(error);
})
}),
);
await Promise.allSettled(startPromises);
@@ -146,11 +162,11 @@ export class PluginManager {
}
this.runningPlugins.set(pluginId, false);
console.info(`Plugin "${pluginId}" stopped`);
this.emit('plugin.stopped', pluginId);
this.emit("plugin.stopped", pluginId);
}
public stopAllPlugins(): void {
Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id));
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
}
public getPlugin(pluginId: string): Plugin | undefined {
@@ -166,40 +182,49 @@ export class PluginManager {
name: string;
description: string;
settings: {
[key: string]: (Omit<BooleanSetting, 'type'> & { type: 'boolean', id: string }) |
(Omit<StringSetting, 'type'> & { type: 'string', id: string }) |
(Omit<NumberSetting, 'type'> & { type: 'number', id: string }) |
(Omit<SelectSetting<string>, 'type'> & { type: 'select', id: string, options: Array<{ value: string, label: string }> });
}
[key: string]:
| (Omit<BooleanSetting, "type"> & { type: "boolean"; id: string })
| (Omit<StringSetting, "type"> & { type: "string"; id: string })
| (Omit<NumberSetting, "type"> & { type: "number"; id: string })
| (Omit<SelectSetting<string>, "type"> & {
type: "select";
id: string;
options: Array<{ value: string; label: string }>;
});
};
}> {
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
const settingsEntries = Object.entries(plugin.settings).map(([key, setting]) => {
const settingObj = setting as any;
// Create a copy of the setting object without any functions
const result: any = Object.fromEntries(
Object.entries(settingObj)
.filter(([_, value]) => typeof value !== 'function')
);
// Ensure required properties are present
result.id = key;
result.title = result.title || key;
result.description = result.description || '';
result.defaultEnabled = plugin.defaultEnabled ?? true;
return [key, result];
});
const settingsEntries = Object.entries(plugin.settings).map(
([key, setting]) => {
const settingObj = setting as any;
// Create a copy of the setting object without any functions
const result: any = Object.fromEntries(
Object.entries(settingObj).filter(
([_, value]) => typeof value !== "function",
),
);
// Ensure required properties are present
result.id = key;
result.title = result.title || key;
result.description = result.description || "";
result.defaultEnabled = plugin.defaultEnabled ?? true;
return [key, result];
},
);
if (plugin.disableToggle) {
settingsEntries.push([
'enabled', {
id: 'enabled',
"enabled",
{
id: "enabled",
title: plugin.name,
description: plugin.description,
type: 'boolean',
default: plugin.defaultEnabled ?? true
}
])
type: "boolean",
default: plugin.defaultEnabled ?? true,
},
]);
}
return {
pluginId: id,
@@ -218,7 +243,7 @@ export class PluginManager {
private emit(event: string, ...args: any[]): void {
const listeners = this.listeners.get(event);
if (listeners) {
listeners.forEach(listener => listener(...args));
listeners.forEach((listener) => listener(...args));
}
}
@@ -237,7 +262,10 @@ export class PluginManager {
}
// Add handler for plugin enable/disable state changes
private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise<void> {
private async handlePluginStateChange(
pluginId: string,
enabled: boolean,
): Promise<void> {
if (enabled) {
await this.startPlugin(pluginId);
} else {
@@ -247,24 +275,30 @@ export class PluginManager {
// Add listener for plugin settings changes
private setupPluginStateListener(): void {
browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => {
if (area !== 'local') return;
for (const [key, change] of Object.entries(changes)) {
const match = key.match(/^plugin\.(.+)\.settings$/);
if (!match) continue;
const pluginId = match[1];
const plugin = this.plugins.get(pluginId);
if (!plugin?.disableToggle) continue;
const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true;
const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? plugin.defaultEnabled ?? true;
if (enabled !== wasEnabled) {
this.handlePluginStateChange(pluginId, enabled);
browser.storage.onChanged.addListener(
(changes: { [key: string]: StorageChange }, area: string) => {
if (area !== "local") return;
for (const [key, change] of Object.entries(changes)) {
const match = key.match(/^plugin\.(.+)\.settings$/);
if (!match) continue;
const pluginId = match[1];
const plugin = this.plugins.get(pluginId);
if (!plugin?.disableToggle) continue;
const enabled =
(change.newValue as PluginSettingsStorage)?.enabled ?? true;
const wasEnabled =
(change.oldValue as PluginSettingsStorage)?.enabled ??
plugin.defaultEnabled ??
true;
if (enabled !== wasEnabled) {
this.handlePluginStateChange(pluginId, enabled);
}
}
}
});
},
);
}
}
}
+6 -6
View File
@@ -1,14 +1,14 @@
import type { PluginSettings } from './types';
import type { PluginSettings } from "./types";
export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => {
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
Object.defineProperty(proto, 'settings', {
if (!proto.hasOwnProperty("settings")) {
Object.defineProperty(proto, "settings", {
value: {},
writable: true,
configurable: true,
enumerable: true
enumerable: true,
});
}
@@ -27,7 +27,7 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
// Copy settings from the prototype to the instance
// This ensures that each instance has its own settings object
// IMPORTANT: Ensure the prototype actually HAS settings before copying
if (this.constructor.prototype.hasOwnProperty('settings')) {
if (this.constructor.prototype.hasOwnProperty("settings")) {
// Deep clone might be safer if settings objects become complex,
// but a shallow clone is usually fine for this structure.
this.settings = { ...this.constructor.prototype.settings } as T;
@@ -36,4 +36,4 @@ export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
this.settings = {} as T;
}
}
}
}
+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 {
type: 'number',
...options
type: "number",
...options,
};
}
export function booleanSetting(options: Omit<BooleanSetting, 'type'>): BooleanSetting {
export function booleanSetting(
options: Omit<BooleanSetting, "type">,
): BooleanSetting {
return {
type: 'boolean',
...options
type: "boolean",
...options,
};
}
export function stringSetting(options: Omit<StringSetting, 'type'>): StringSetting {
export function stringSetting(
options: Omit<StringSetting, "type">,
): StringSetting {
return {
type: 'string',
...options
type: "string",
...options,
};
}
export function selectSetting<T extends string>(options: Omit<SelectSetting<T>, 'type'>): SelectSetting<T> {
export function selectSetting<T extends string>(
options: Omit<SelectSetting<T>, "type">,
): SelectSetting<T> {
return {
type: 'select',
...options
type: "select",
...options,
};
}
@@ -35,16 +48,15 @@ export function defineSettings<T extends Record<string, any>>(settings: T): T {
export function Setting(settingDef: any): PropertyDecorator {
return (target, propertyKey) => {
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
Object.defineProperty(proto, 'settings', {
if (!proto.hasOwnProperty("settings")) {
Object.defineProperty(proto, "settings", {
value: {},
writable: true,
configurable: true,
enumerable: true
enumerable: true,
});
}
proto.settings[propertyKey] = settingDef;
};
}
+53 -26
View File
@@ -1,14 +1,14 @@
import ReactFiber from '@/seqta/utils/ReactFiber';
import ReactFiber from "@/seqta/utils/ReactFiber";
export interface BooleanSetting {
type: 'boolean';
type: "boolean";
default: boolean;
title: string;
description?: string;
}
export interface StringSetting {
type: 'string';
type: "string";
default: string;
title: string;
description?: string;
@@ -17,7 +17,7 @@ export interface StringSetting {
}
export interface NumberSetting {
type: 'number';
type: "number";
default: number;
title: string;
description?: string;
@@ -27,47 +27,69 @@ export interface NumberSetting {
}
export interface SelectSetting<T extends string> {
type: 'select';
type: "select";
options: readonly T[];
default: T;
title: string;
description?: string;
}
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting | SelectSetting<string>;
export type PluginSetting =
| BooleanSetting
| StringSetting
| NumberSetting
| SelectSetting<string>;
export type PluginSettings = {
[key: string]: PluginSetting;
}
};
// Helper type to extract the actual value type from a setting
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
T extends StringSetting ? string :
T extends NumberSetting ? number :
T extends SelectSetting<infer O> ? O :
never;
export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
? boolean
: T extends StringSetting
? string
: T extends NumberSetting
? number
: T extends SelectSetting<infer O>
? O
: never;
export type SettingsAPI<T extends PluginSettings> = {
[K in keyof T]: SettingValue<T[K]>;
} & {
onChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => { unregister: () => void };
offChange: <K extends keyof T>(key: K, callback: (value: SettingValue<T[K]>) => void) => void;
onChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => { unregister: () => void };
offChange: <K extends keyof T>(
key: K,
callback: (value: SettingValue<T[K]>) => void,
) => void;
loaded: Promise<void>;
}
};
export interface SEQTAAPI {
onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void };
onMount: (
selector: string,
callback: (element: Element) => void,
) => { unregister: () => void };
getFiber: (selector: string) => ReactFiber;
getCurrentPage: () => string;
onPageChange: (callback: (page: string) => void) => { unregister: () => void };
onPageChange: (callback: (page: string) => void) => {
unregister: () => void;
};
}
export interface StorageAPI<T = any> {
/**
* Register a callback to be called when a storage value changes
*/
onChange: <K extends keyof T>(key: K, callback: (value: T[K]) => void) => { unregister: () => void };
onChange: <K extends keyof T>(
key: K,
callback: (value: T[K]) => void,
) => { unregister: () => void };
/**
* Promise that resolves when storage values are loaded
*/
@@ -76,10 +98,13 @@ export interface StorageAPI<T = any> {
export type TypedStorageAPI<T> = StorageAPI<T> & {
[K in keyof T]: T[K];
}
};
export interface EventsAPI {
on: (event: string, callback: (...args: any[]) => void) => { unregister: () => void };
on: (
event: string,
callback: (...args: any[]) => void,
) => { unregister: () => void };
emit: (event: string, ...args: any[]) => void;
}
@@ -96,8 +121,10 @@ export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
description: string;
version: string;
settings: T;
styles?: string; // Optional CSS styles for the plugin
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
}
styles?: string; // Optional CSS styles for the plugin
disableToggle?: boolean; // Optional flag to show/hide the plugin's enable/disable toggle in settings
defaultEnabled?: boolean; // Optional flag to set the plugin's default enabled state
run: (
api: PluginAPI<T, S>,
) => void | Promise<void> | (() => void) | Promise<() => void>;
}
+9 -9
View File
@@ -1,12 +1,12 @@
import { PluginManager } from './core/manager';
import { PluginManager } from "./core/manager";
// plugins
import timetablePlugin from './built-in/timetable';
import notificationCollectorPlugin from './built-in/notificationCollector';
import themesPlugin from './built-in/themes';
import animatedBackgroundPlugin from './built-in/animatedBackground';
import assessmentsAveragePlugin from './built-in/assessmentsAverage';
import globalSearchPlugin from './built-in/globalSearch/src/core';
import timetablePlugin from "./built-in/timetable";
import notificationCollectorPlugin from "./built-in/notificationCollector";
import themesPlugin from "./built-in/themes";
import animatedBackgroundPlugin from "./built-in/animatedBackground";
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
import globalSearchPlugin from "./built-in/globalSearch/src/core";
//import testPlugin from './built-in/test';
// Initialize plugin manager
@@ -21,7 +21,7 @@ pluginManager.registerPlugin(timetablePlugin);
pluginManager.registerPlugin(globalSearchPlugin);
//pluginManager.registerPlugin(testPlugin);
export { init as Monofile } from './monofile';
export { init as Monofile } from "./monofile";
export async function initializePlugins(): Promise<void> {
await pluginManager.startAllPlugins();
@@ -31,4 +31,4 @@ export { pluginManager };
export function getAllPluginSettings() {
return pluginManager.getAllPluginSettings();
}
}
+247 -244
View File
@@ -1,135 +1,134 @@
// Third-party libraries
import browser from "webextension-polyfill"
import { animate, stagger } from "motion"
import browser from "webextension-polyfill";
import { animate, stagger } from "motion";
// Internal utilities and functions
import { ChangeMenuItemPositions, MenuOptionsOpen } from "@/seqta/utils/Openers/OpenMenuOptions"
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour"
import { waitForElm } from "@/seqta/utils/waitForElm"
import { delay } from "@/seqta/utils/delay"
import stringToHTML from "@/seqta/utils/stringToHTML"
import { MessageHandler } from "@/seqta/utils/listeners/MessageListener"
import {
settingsState,
} from "@/seqta/utils/listeners/SettingsState"
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges"
import { eventManager } from "@/seqta/utils/listeners/EventManager"
ChangeMenuItemPositions,
MenuOptionsOpen,
} from "@/seqta/utils/Openers/OpenMenuOptions";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { waitForElm } from "@/seqta/utils/waitForElm";
import { delay } from "@/seqta/utils/delay";
import stringToHTML from "@/seqta/utils/stringToHTML";
import { MessageHandler } from "@/seqta/utils/listeners/MessageListener";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges";
import { eventManager } from "@/seqta/utils/listeners/EventManager";
// UI and theme management
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners"
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements"
import { updateAllColors } from "@/seqta/ui/colors/Manager"
import loading from "@/seqta/ui/Loading"
import { SendNewsPage } from "@/seqta/utils/SendNewsPage"
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew"
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners";
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements";
import { updateAllColors } from "@/seqta/ui/colors/Manager";
import loading from "@/seqta/ui/Loading";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
// JSON content
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json"
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
// Icons and fonts
import IconFamily from "@/resources/fonts/IconFamily.woff"
import IconFamily from "@/resources/fonts/IconFamily.woff";
// Stylesheets
import iframeCSS from "@/css/iframe.scss?raw"
import iframeCSS from "@/css/iframe.scss?raw";
function SetDisplayNone(ElementName: string) {
return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}`
return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}`;
}
async function HideMenuItems(): Promise<void> {
try {
let stylesheetInnerText: string = ""
let stylesheetInnerText: string = "";
for (const [menuItem, { toggle }] of Object.entries(
settingsState.menuitems,
)) {
if (!toggle) {
stylesheetInnerText += SetDisplayNone(menuItem)
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`)
stylesheetInnerText += SetDisplayNone(menuItem);
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
}
}
const menuItemStyle: HTMLStyleElement = document.createElement("style")
menuItemStyle.innerText = stylesheetInnerText
document.head.appendChild(menuItemStyle)
const menuItemStyle: HTMLStyleElement = document.createElement("style");
menuItemStyle.innerText = stylesheetInnerText;
document.head.appendChild(menuItemStyle);
} catch (error) {
console.error("[BetterSEQTA+] An error occurred:", error)
console.error("[BetterSEQTA+] An error occurred:", error);
}
}
export function hideSideBar() {
const sidebar = document.getElementById("menu") // The sidebar element to be closed
const main = document.getElementById("main") // The main content element that must be resized to fill the page
const sidebar = document.getElementById("menu"); // The sidebar element to be closed
const main = document.getElementById("main"); // The main content element that must be resized to fill the page
const currentMenuWidth = window.getComputedStyle(sidebar!).width // Get the styles of the different elements
const currentContentPosition = window.getComputedStyle(main!).position
const currentMenuWidth = window.getComputedStyle(sidebar!).width; // Get the styles of the different elements
const currentContentPosition = window.getComputedStyle(main!).position;
if (currentMenuWidth != "0") {
// Actually modify it to collapse the sidebar
sidebar!.style.width = "0"
sidebar!.style.width = "0";
} else {
sidebar!.style.width = "100%"
sidebar!.style.width = "100%";
}
if (currentContentPosition != "relative") {
main!.style.position = "relative"
main!.style.position = "relative";
} else {
main!.style.position = "absolute"
main!.style.position = "absolute";
}
}
export async function finishLoad() {
try {
document.querySelector(".legacy-root")?.classList.remove("hidden")
document.querySelector(".legacy-root")?.classList.remove("hidden");
const loadingbk = document.getElementById("loading")
loadingbk?.classList.add("closeLoading")
await delay(501)
loadingbk?.remove()
const loadingbk = document.getElementById("loading");
loadingbk?.classList.add("closeLoading");
await delay(501);
loadingbk?.remove();
} catch (err) {
console.error("Error during loading cleanup:", err)
console.error("Error during loading cleanup:", err);
}
if (settingsState.justupdated && !document.getElementById("whatsnewbk")) {
OpenWhatsNewPopup()
OpenWhatsNewPopup();
}
}
export function GetCSSElement(file: string) {
const cssFile = browser.runtime.getURL(file)
const fileref = document.createElement("link")
fileref.setAttribute("rel", "stylesheet")
fileref.setAttribute("type", "text/css")
fileref.setAttribute("href", cssFile)
const cssFile = browser.runtime.getURL(file);
const fileref = document.createElement("link");
fileref.setAttribute("rel", "stylesheet");
fileref.setAttribute("type", "text/css");
fileref.setAttribute("href", cssFile);
return fileref
return fileref;
}
function removeThemeTagsFromNotices() {
// Grabs an array of the notice iFrames
const userHTMLArray = document.getElementsByClassName("userHTML")
const userHTMLArray = document.getElementsByClassName("userHTML");
// Iterates through the array, applying the iFrame css
for (const item of userHTMLArray) {
// Grabs the HTML of the body tag
const item1 = item as HTMLIFrameElement
const body = item1.contentWindow!.document.querySelectorAll("body")[0]
const item1 = item as HTMLIFrameElement;
const body = item1.contentWindow!.document.querySelectorAll("body")[0];
if (body) {
// Replaces the theme tag with nothing
const bodyText = body.innerHTML
const bodyText = body.innerHTML;
body.innerHTML = bodyText
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/ +/, " ")
.replace(/ +/, " ");
}
}
}
async function updateIframesWithDarkMode(): Promise<void> {
const cssLink = document.createElement("style")
cssLink.classList.add("iframecss")
const cssContent = document.createTextNode(iframeCSS)
cssLink.appendChild(cssContent)
const cssLink = document.createElement("style");
cssLink.classList.add("iframecss");
const cssContent = document.createTextNode(iframeCSS);
cssLink.appendChild(cssContent);
eventManager.register(
"iframeAdded",
@@ -139,63 +138,63 @@ async function updateIframesWithDarkMode(): Promise<void> {
!element.classList.contains("iframecss"),
},
(element) => {
const iframe = element as HTMLIFrameElement
const iframe = element as HTMLIFrameElement;
try {
applyDarkModeToIframe(iframe, cssLink)
applyDarkModeToIframe(iframe, cssLink);
if (element.classList.contains("cke_wysiwyg_frame")) {
(async () => {
await delay(100)
iframe.contentDocument?.body.setAttribute("spellcheck", "true")
})()
await delay(100);
iframe.contentDocument?.body.setAttribute("spellcheck", "true");
})();
}
} catch (error) {
console.error("Error applying dark mode:", error)
console.error("Error applying dark mode:", error);
}
},
)
);
}
function applyDarkModeToIframe(
iframe: HTMLIFrameElement,
cssLink: HTMLStyleElement,
): void {
const iframeDocument = iframe.contentDocument
if (!iframeDocument) return
const iframeDocument = iframe.contentDocument;
if (!iframeDocument) return;
iframe.onload = () => {
applyDarkModeToIframe(iframe, cssLink)
}
applyDarkModeToIframe(iframe, cssLink);
};
if (settingsState.DarkMode) {
iframeDocument.documentElement.classList.add("dark")
iframeDocument.documentElement.classList.add("dark");
}
const head = iframeDocument.head
const head = iframeDocument.head;
if (head && !head.innerHTML.includes("iframecss")) {
head.innerHTML += cssLink.outerHTML
head.innerHTML += cssLink.outerHTML;
}
}
function SortMessagePageItems(messagesParentElement: any) {
try {
let filterbutton = document.createElement("div")
filterbutton.classList.add("messages-filterbutton")
filterbutton.innerText = "Filter"
let filterbutton = document.createElement("div");
filterbutton.classList.add("messages-filterbutton");
filterbutton.innerText = "Filter";
let header = document.querySelector(
"[class*='MessageList__MessageList___']",
) as HTMLElement
header.append(filterbutton)
messagesParentElement
) as HTMLElement;
header.append(filterbutton);
messagesParentElement;
} catch (error) {
console.error("Error sorting message page items:", error)
console.error("Error sorting message page items:", error);
}
}
async function LoadPageElements(): Promise<void> {
await AddBetterSEQTAElements()
const sublink: string | undefined = window.location.href.split("/")[4]
await AddBetterSEQTAElements();
const sublink: string | undefined = window.location.href.split("/")[4];
eventManager.register(
"messagesAdded",
@@ -204,7 +203,7 @@ async function LoadPageElements(): Promise<void> {
className: "messages",
},
handleMessages,
)
);
eventManager.register(
"noticesAdded",
@@ -213,7 +212,7 @@ async function LoadPageElements(): Promise<void> {
className: "notices",
},
CheckNoticeTextColour,
)
);
eventManager.register(
"dashboardAdded",
@@ -222,7 +221,7 @@ async function LoadPageElements(): Promise<void> {
className: "dashboard",
},
handleDashboard,
)
);
eventManager.register(
"documentsAdded",
@@ -231,7 +230,7 @@ async function LoadPageElements(): Promise<void> {
className: "documents",
},
handleDocuments,
)
);
eventManager.register(
"reportsAdded",
@@ -240,7 +239,7 @@ async function LoadPageElements(): Promise<void> {
className: "reports",
},
handleReports,
)
);
/* eventManager.register(
"timetableAdded",
@@ -258,21 +257,21 @@ async function LoadPageElements(): Promise<void> {
className: "notice",
},
handleNotices,
)
);
RegisterClickListeners()
RegisterClickListeners();
await handleSublink(sublink)
await handleSublink(sublink);
}
async function handleNotices(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
node.style.opacity = "0"
node.style.opacity = "0";
// get index of node in relation to parent
const index = Array.from(node.parentElement!.children).indexOf(node)
const index = Array.from(node.parentElement!.children).indexOf(node);
animate(
node,
@@ -283,71 +282,73 @@ async function handleNotices(node: Element): Promise<void> {
stiffness: 250,
damping: 20,
},
)
);
}
async function handleSublink(sublink: string | undefined): Promise<void> {
switch (sublink) {
case "news":
await handleNewsPage()
break
await handleNewsPage();
break;
case undefined:
window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`)
if (settingsState.defaultPage === "home") loadHomePage()
window.location.replace(
`${location.origin}/#?page=/${settingsState.defaultPage}`,
);
if (settingsState.defaultPage === "home") loadHomePage();
if (settingsState.defaultPage === "documents")
handleDocuments(document.querySelector(".documents")!)
handleDocuments(document.querySelector(".documents")!);
if (settingsState.defaultPage === "reports")
handleReports(document.querySelector(".reports")!)
handleReports(document.querySelector(".reports")!);
if (settingsState.defaultPage === "messages")
handleMessages(document.querySelector(".messages")!)
handleMessages(document.querySelector(".messages")!);
finishLoad()
break
finishLoad();
break;
case "home":
window.location.replace(`${location.origin}/#?page=/home`)
console.info("[BetterSEQTA+] Started Init")
if (settingsState.onoff) loadHomePage()
finishLoad()
break
window.location.replace(`${location.origin}/#?page=/home`);
console.info("[BetterSEQTA+] Started Init");
if (settingsState.onoff) loadHomePage();
finishLoad();
break;
default:
await handleDefault()
break
await handleDefault();
break;
}
}
async function handleNewsPage(): Promise<void> {
console.info("[BetterSEQTA+] Started Init")
console.info("[BetterSEQTA+] Started Init");
if (settingsState.onoff) {
SendNewsPage()
finishLoad()
SendNewsPage();
finishLoad();
}
}
async function handleDefault(): Promise<void> {
finishLoad()
finishLoad();
}
async function handleMessages(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!(node instanceof HTMLElement)) return;
const element = document.getElementById("title")!.firstChild as HTMLElement
element.innerText = "Direct Messages"
document.title = "Direct Messages ― SEQTA Learn"
SortMessagePageItems(node)
if (!settingsState.animations) return
const element = document.getElementById("title")!.firstChild as HTMLElement;
element.innerText = "Direct Messages";
document.title = "Direct Messages ― SEQTA Learn";
SortMessagePageItems(node);
if (!settingsState.animations) return;
// Hides messages on page load
const style = document.createElement("style")
style.classList.add("messageHider")
style.innerHTML = "[data-message]{opacity: 0 !important;}"
document.head.append(style)
const style = document.createElement("style");
style.classList.add("messageHider");
style.innerHTML = "[data-message]{opacity: 0 !important;}";
document.head.append(style);
await waitForElm("[data-message]", true, 10)
await waitForElm("[data-message]", true, 10);
const messages = Array.from(
document.querySelectorAll("[data-message]"),
).slice(0, 35)
).slice(0, 35);
animate(
messages,
{ opacity: [0, 1], y: [10, 0] },
@@ -356,21 +357,21 @@ async function handleMessages(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
document.head.querySelector("style.messageHider")?.remove()
document.head.querySelector("style.messageHider")?.remove();
}
async function handleDashboard(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
const style = document.createElement("style")
style.classList.add("dashboardHider")
style.innerHTML = ".dashboard{opacity: 0 !important;}"
document.head.append(style)
const style = document.createElement("style");
style.classList.add("dashboardHider");
style.innerHTML = ".dashboard{opacity: 0 !important;}";
document.head.append(style);
await waitForElm(".dashlet", true, 10)
await waitForElm(".dashlet", true, 10);
animate(
".dashboard > *",
{ opacity: [0, 1], y: [10, 0] },
@@ -379,16 +380,16 @@ async function handleDashboard(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
document.head.querySelector("style.dashboardHider")?.remove()
document.head.querySelector("style.dashboardHider")?.remove();
}
async function handleDocuments(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
await waitForElm(".document", true, 10)
await waitForElm(".document", true, 10);
animate(
".documents tbody tr.document",
{ opacity: [0, 1], y: [10, 0] },
@@ -397,14 +398,14 @@ async function handleDocuments(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
}
async function handleReports(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return
if (!settingsState.animations) return
if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return;
await waitForElm(".report", true, 10)
await waitForElm(".report", true, 10);
animate(
".reports .item",
{ opacity: [0, 1], y: [10, 0] },
@@ -413,7 +414,7 @@ async function handleReports(node: Element): Promise<void> {
duration: 0.5,
ease: [0.22, 0.03, 0.26, 1],
},
)
);
}
function CheckNoticeTextColour(notice: any) {
@@ -425,132 +426,134 @@ function CheckNoticeTextColour(notice: any) {
parentElement: notice,
},
(node) => {
var hex = (node as HTMLElement).style.cssText.split(" ")[1]
var hex = (node as HTMLElement).style.cssText.split(" ")[1];
if (hex) {
const hex1 = hex.slice(0, -1)
var threshold = GetThresholdOfColor(hex1)
const hex1 = hex.slice(0, -1);
var threshold = GetThresholdOfColor(hex1);
if (settingsState.DarkMode && threshold < 100) {
(node as HTMLElement).style.cssText = "--color: undefined;"
(node as HTMLElement).style.cssText = "--color: undefined;";
}
}
},
)
);
}
export function tryLoad() {
waitForElm(".login").then(() => {
finishLoad()
})
finishLoad();
});
waitForElm(".day-container").then(() => {
finishLoad()
})
finishLoad();
});
waitForElm("[data-key=welcome]").then((elm: any) => {
elm.classList.remove("active")
})
elm.classList.remove("active");
});
waitForElm(".code", true, 50).then((elm: any) => {
if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements()
})
if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements();
});
updateIframesWithDarkMode()
updateIframesWithDarkMode();
// Waits for page to call on load, run scripts
document.addEventListener(
"load",
function () {
removeThemeTagsFromNotices()
removeThemeTagsFromNotices();
},
true,
)
);
}
function ReplaceMenuSVG(element: HTMLElement, svg: string) {
let item = element.firstChild as HTMLElement
item!.firstChild!.remove()
let item = element.firstChild as HTMLElement;
item!.firstChild!.remove();
item.innerHTML = `<span>${item.innerHTML}</span>`
item.innerHTML = `<span>${item.innerHTML}</span>`;
let newsvg = stringToHTML(svg).firstChild
item.insertBefore(newsvg as Node, item.firstChild)
let newsvg = stringToHTML(svg).firstChild;
item.insertBefore(newsvg as Node, item.firstChild);
}
const processedSymbol = Symbol('processed')
const processedSymbol = Symbol("processed");
export async function ObserveMenuItemPosition() {
await waitForElm("#menu > ul > li")
await waitForElm("#menu > ul > li");
eventManager.register(
"menuList",
{
parentElement: document.querySelector("#menu")!.firstChild as Element,
},
(element: Element) => {
const node = element as HTMLElement
const node = element as HTMLElement;
// Only process top-level menu items and skip everything else
if (!node.classList.contains('item') ||
node.nodeName !== 'LI' ||
node.parentElement?.parentElement?.id !== 'menu') {
return
if (
!node.classList.contains("item") ||
node.nodeName !== "LI" ||
node.parentElement?.parentElement?.id !== "menu"
) {
return;
}
// Early exit if already processed
if ((element as any)[processedSymbol]) {
return
return;
}
if (!node?.dataset?.checked && !MenuOptionsOpen) {
const key =
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey]
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
if (key) {
ReplaceMenuSVG(
node,
MenuitemSVGKey[node.dataset.key as keyof typeof MenuitemSVGKey],
)
);
} else if (node?.firstChild?.nodeName === "LABEL") {
const label = node.firstChild as HTMLElement
let textNode = label.lastChild as HTMLElement
const label = node.firstChild as HTMLElement;
let textNode = label.lastChild as HTMLElement;
if (
textNode.nodeType === 3 &&
textNode.parentNode &&
textNode.parentNode.nodeName !== "SPAN"
) {
const span = document.createElement("span")
span.textContent = textNode.nodeValue
const span = document.createElement("span");
span.textContent = textNode.nodeValue;
label.replaceChild(span, textNode)
label.replaceChild(span, textNode);
}
}
ChangeMenuItemPositions(settingsState.menuorder);
(element as any)[processedSymbol] = true
(element as any)[processedSymbol] = true;
}
},
)
);
}
export function showConflictPopup() {
if (document.getElementById("conflict-popup")) return
document.body.classList.remove("hidden")
if (document.getElementById("conflict-popup")) return;
document.body.classList.remove("hidden");
const background = document.createElement("div")
background.id = "conflict-popup"
background.classList.add("whatsnewBackground")
background.style.zIndex = "10000000"
const background = document.createElement("div");
background.id = "conflict-popup";
background.classList.add("whatsnewBackground");
background.style.zIndex = "10000000";
const container = document.createElement("div")
container.classList.add("whatsnewContainer")
container.style.height = "auto"
const container = document.createElement("div");
container.classList.add("whatsnewContainer");
container.style.height = "auto";
const headerHTML = /* html */ `
<div class="whatsnewHeader">
<h1>Extension Conflict Detected</h1>
<p>Legacy BetterSEQTA Installed</p>
</div>
`
const header = stringToHTML(headerHTML).firstChild
`;
const header = stringToHTML(headerHTML).firstChild;
const textHTML = /* html */ `
<div class="whatsnewTextContainer" style="overflow-y: auto; font-size: 1.3rem;">
@@ -562,91 +565,91 @@ export function showConflictPopup() {
Please remove the older BetterSEQTA extension to ensure that BetterSEQTA+ works correctly.
</p>
</div>
`
const text = stringToHTML(textHTML).firstChild
`;
const text = stringToHTML(textHTML).firstChild;
const exitButton = document.createElement("div")
exitButton.id = "whatsnewclosebutton"
const exitButton = document.createElement("div");
exitButton.id = "whatsnewclosebutton";
if (header) container.append(header)
if (text) container.append(text)
container.append(exitButton)
if (header) container.append(header);
if (text) container.append(text);
container.append(exitButton);
background.append(container)
background.append(container);
document.getElementById("container")?.append(background)
document.getElementById("container")?.append(background);
if (settingsState.animations) {
animate([background as HTMLElement], { opacity: [0, 1] })
animate([background as HTMLElement], { opacity: [0, 1] });
}
background.addEventListener("click", (event) => {
if (event.target === background) {
background.remove()
background.remove();
}
})
});
exitButton.addEventListener("click", () => {
background.remove()
})
background.remove();
});
}
export function init() {
const handleDisabled = () => {
waitForElm(".code", true, 50).then(AppendElementsToDisabledPage)
}
waitForElm(".code", true, 50).then(AppendElementsToDisabledPage);
};
if (settingsState.onoff) {
console.info("[BetterSEQTA+] Enabled")
console.info("[BetterSEQTA+] Enabled");
if (settingsState.DarkMode) document.documentElement.classList.add("dark");
document.querySelector(".legacy-root")?.classList.add("hidden")
document.querySelector(".legacy-root")?.classList.add("hidden");
ObserveMenuItemPosition();
new StorageChangeHandler()
new MessageHandler()
new StorageChangeHandler();
new MessageHandler();
updateAllColors()
loading()
InjectCustomIcons()
HideMenuItems()
tryLoad()
updateAllColors();
loading();
InjectCustomIcons();
HideMenuItems();
tryLoad();
setTimeout(() => {
const legacyElement = document.querySelector(
".outside-container .bottom-container",
)
);
if (legacyElement) {
console.log("Legacy extension detected")
showConflictPopup()
console.log("Legacy extension detected");
showConflictPopup();
}
}, 1000)
}, 1000);
} else {
handleDisabled()
window.addEventListener("load", handleDisabled)
handleDisabled();
window.addEventListener("load", handleDisabled);
}
}
function InjectCustomIcons() {
console.info("[BetterSEQTA+] Injecting Icons")
console.info("[BetterSEQTA+] Injecting Icons");
const style = document.createElement("style")
style.setAttribute("type", "text/css")
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.innerHTML = `
@font-face {
font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal;
font-style: normal;
}`
document.head.appendChild(style)
}`;
document.head.appendChild(style);
}
export function AppendElementsToDisabledPage() {
console.info("[BetterSEQTA+] Appending elements to disabled page")
AddBetterSEQTAElements()
console.info("[BetterSEQTA+] Appending elements to disabled page");
AddBetterSEQTAElements();
let settingsStyle = document.createElement("style")
let settingsStyle = document.createElement("style");
settingsStyle.innerHTML = /* css */ `
.addedButton {
position: absolute !important;
@@ -671,6 +674,6 @@ export function AppendElementsToDisabledPage() {
box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6);
transform-origin: 70% 0;
}
`
document.head.append(settingsStyle)
}
`;
document.head.append(settingsStyle);
}
+2 -3
View File
@@ -2,6 +2,5 @@ module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
},
};
+4 -3
View File
@@ -1,6 +1,7 @@
@font-face {
font-family: 'IconFamily';
src: local('IconFamily') local('Icon Family') url('/fonts/IconFamily.woff') format('woff');
font-family: "IconFamily";
src: local("IconFamily") local("Icon Family") url("/fonts/IconFamily.woff")
format("woff");
font-weight: 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;
}
small {
font-size: .66666667em;
font-size: 0.66666667em;
}
a {
color: #e74c3c;
text-decoration: none;
}
a:hover, a:focus {
a:hover,
a:focus {
box-shadow: 0 1px #e74c3c;
}
.bshadow0, input {
.bshadow0,
input {
box-shadow: inset 0 -2px #e7e7e7;
}
input:hover {
box-shadow: inset 0 -2px #ccc;
}
input, fieldset {
input,
fieldset {
font-family: sans-serif;
font-size: 1em;
margin: 0;
@@ -38,7 +41,7 @@ input {
color: inherit;
line-height: 1.5;
height: 1.5em;
padding: .25em 0;
padding: 0.25em 0;
}
input:focus {
outline: none;
@@ -77,19 +80,22 @@ p {
margin-bottom: 1em;
}
.mvm {
margin-top: .75em;
margin-bottom: .75em;
margin-top: 0.75em;
margin-bottom: 0.75em;
}
.mtn {
margin-top: 0;
}
.mtl, .mal {
.mtl,
.mal {
margin-top: 1.5em;
}
.mbl, .mal {
.mbl,
.mal {
margin-bottom: 1.5em;
}
.mal, .mhl {
.mal,
.mhl {
margin-left: 1.5em;
margin-right: 1.5em;
}
@@ -98,16 +104,18 @@ p {
margin-right: 1em;
}
.mls {
margin-left: .25em;
margin-left: 0.25em;
}
.ptl {
padding-top: 1.5em;
}
.pbs, .pvs {
padding-bottom: .25em;
.pbs,
.pvs {
padding-bottom: 0.25em;
}
.pvs, .pts {
padding-top: .25em;
.pvs,
.pts {
padding-top: 0.25em;
}
.unit {
float: left;
@@ -121,7 +129,8 @@ p {
.size1of1 {
width: 100%;
}
.clearfix:before, .clearfix:after {
.clearfix:before,
.clearfix:after {
content: " ";
display: table;
}
@@ -134,7 +143,7 @@ p {
.textbox0 {
width: 3em;
background: #f1f1f1;
padding: .25em .5em;
padding: 0.25em 0.5em;
line-height: 1.5;
height: 1.5em;
}
@@ -149,4 +158,3 @@ p {
.fs1 {
font-size: 16px;
}
+27 -25
View File
@@ -1,30 +1,32 @@
if (!('boxShadow' in document.body.style)) {
document.body.setAttribute('class', 'noBoxShadow');
if (!("boxShadow" in document.body.style)) {
document.body.setAttribute("class", "noBoxShadow");
}
document.body.addEventListener("click", function(e) {
var target = e.target;
if (target.tagName === "INPUT" &&
target.getAttribute('class').indexOf('liga') === -1) {
target.select();
}
document.body.addEventListener("click", function (e) {
var target = e.target;
if (
target.tagName === "INPUT" &&
target.getAttribute("class").indexOf("liga") === -1
) {
target.select();
}
});
(function() {
var fontSize = document.getElementById('fontSize'),
testDrive = document.getElementById('testDrive'),
testText = document.getElementById('testText');
function updateTest() {
testDrive.innerHTML = testText.value || String.fromCharCode(160);
if (window.icomoonLiga) {
window.icomoonLiga(testDrive);
}
(function () {
var fontSize = document.getElementById("fontSize"),
testDrive = document.getElementById("testDrive"),
testText = document.getElementById("testText");
function updateTest() {
testDrive.innerHTML = testText.value || String.fromCharCode(160);
if (window.icomoonLiga) {
window.icomoonLiga(testDrive);
}
function updateSize() {
testDrive.style.fontSize = fontSize.value + 'px';
}
fontSize.addEventListener('change', updateSize, false);
testText.addEventListener('input', updateTest, false);
testText.addEventListener('change', updateTest, false);
updateSize();
}());
}
function updateSize() {
testDrive.style.fontSize = fontSize.value + "px";
}
fontSize.addEventListener("change", updateSize, false);
testText.addEventListener("input", updateTest, false);
testText.addEventListener("change", updateTest, false);
updateSize();
})();
+10 -8
View File
@@ -1,21 +1,23 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?biv4go');
src: url('fonts/icomoon.eot?biv4go#iefix') format('embedded-opentype'),
url('IconFamily.woff') format('woff'),
url('fonts/icomoon.svg?biv4go#icomoon') format('svg');
font-family: "icomoon";
src: url("fonts/icomoon.eot?biv4go");
src:
url("fonts/icomoon.eot?biv4go#iefix") format("embedded-opentype"),
url("IconFamily.woff") format("woff"),
url("fonts/icomoon.svg?biv4go#icomoon") format("svg");
font-weight: normal;
font-style: normal;
font-display: block;
}
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 */
font-family: 'icomoon' !important;
font-family: "icomoon" !important;
speak: never;
font-style: normal;
font-weight: normal;
+2 -2
View File
@@ -1,5 +1,5 @@
export default /* html */`
export default /* html */ `
<svg style="width:24px;height:24px;border-radius:0;" viewBox="0 0 24 24">
<path fill="currentColor" d="M6 20H13V22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H18C19.11 2 20 2.9 20 4V12.54L18.5 11.72L18 12V4H13V12L10.5 9.75L8 12V4H6V20M24 17L18.5 14L13 17L18.5 20L24 17M15 19.09V21.09L18.5 23L22 21.09V19.09L18.5 21L15 19.09Z"></path>
</svg>
`;
`;
+2 -2
View File
@@ -1,5 +1,5 @@
export default /* html */`
export default /* html */ `
<svg style="width:24px;height:24px;border-radius:0;" viewBox="0 0 24 24">
<path fill="currentColor" d="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z"></path>
</svg>
`;
`;
+17 -19
View File
@@ -1,42 +1,40 @@
// Third-party libraries
import browser from "webextension-polyfill"
import browser from "webextension-polyfill";
// Internal utilities and functions
import {
settingsState,
} from "@/seqta/utils/listeners/SettingsState"
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
// UI and theme management
import pageState from "@/pageState.js?url"
import pageState from "@/pageState.js?url";
// Stylesheets
import injectedCSS from "@/css/injected.scss?inline"
import injectedCSS from "@/css/injected.scss?inline";
export async function main() {
return new Promise(async (resolve, reject) => {
try {
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
if (import.meta.env.MODE === "development") {
import("../css/injected.scss")
import("../css/injected.scss");
} else {
const injectedStyle = document.createElement("style")
injectedStyle.textContent = injectedCSS
document.head.appendChild(injectedStyle)
const injectedStyle = document.createElement("style");
injectedStyle.textContent = injectedCSS;
document.head.appendChild(injectedStyle);
}
}
resolve(true)
resolve(true);
} catch (error: any) {
console.error(error)
reject(error)
console.error(error);
reject(error);
}
})
});
}
function injectPageState() {
const mainScript = document.createElement("script")
mainScript.src = browser.runtime.getURL(pageState)
document.head.appendChild(mainScript)
}
const mainScript = document.createElement("script");
mainScript.src = browser.runtime.getURL(pageState);
document.head.appendChild(mainScript);
}
+117 -99
View File
@@ -3,7 +3,6 @@ import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { appendBackgroundToUI } from "./ImageBackgrounds";
import stringToHTML from "@/seqta/utils/stringToHTML";
@@ -15,15 +14,15 @@ let cachedUserInfo: any = null;
async function getUserInfo() {
if (cachedUserInfo) return cachedUserInfo;
try {
const response = await fetch(`${location.origin}/seqta/student/login`, {
method: 'POST',
method: "POST",
headers: {
'Content-Type': 'application/json; charset=utf-8',
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({
mode: 'normal',
mode: "normal",
query: null,
redirect_url: location.origin,
}),
@@ -33,36 +32,36 @@ async function getUserInfo() {
cachedUserInfo = responseData.payload;
return cachedUserInfo;
} catch (error) {
console.error('Error fetching user info:', error);
console.error("Error fetching user info:", error);
throw error;
}
}
export async function AddBetterSEQTAElements() {
if (settingsState.onoff) {
if (settingsState.onoff) {
if (settingsState.DarkMode) {
document.documentElement.classList.add('dark');
document.documentElement.classList.add("dark");
}
const fragment = document.createDocumentFragment();
const menu = document.getElementById('menu')!;
const menu = document.getElementById("menu")!;
const menuList = menu.firstChild as HTMLElement;
createHomeButton(fragment, menuList);
createNewsButton(fragment, menu);
menuList.insertBefore(fragment, menuList.firstChild);
try {
await Promise.all([
appendBackgroundToUI(),
handleUserInfo(),
handleStudentData()
handleStudentData(),
]);
} catch (error) {
console.error('Error initializing UI elements:', error);
console.error("Error initializing UI elements:", error);
}
setupEventListeners();
await addDarkLightToggle();
customizeMenuToggle();
@@ -74,12 +73,14 @@ export async function AddBetterSEQTAElements() {
}
function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) {
const container = document.getElementById('content')!;
const div = document.createElement('div');
div.classList.add('titlebar');
const container = document.getElementById("content")!;
const div = document.createElement("div");
div.classList.add("titlebar");
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) {
fragment.appendChild(NewButton.firstChild);
}
@@ -90,7 +91,7 @@ async function handleUserInfo() {
const info = await getUserInfo();
updateUserInfo(info);
} catch (error) {
console.error('Error fetching and processing student data:', error);
console.error("Error fetching and processing student data:", error);
}
}
@@ -112,17 +113,17 @@ function updateUserInfo(info: {
userDesc: string | null;
userName: string | null;
}) {
const titlebar = document.getElementsByClassName('titlebar')[0];
const userInfo = stringToHTML(/* html */`
const titlebar = document.getElementsByClassName("titlebar")[0];
const userInfo = stringToHTML(/* html */ `
<div class="userInfosvgdiv tooltip">
<svg class="userInfosvg" viewBox="0 0 24 24"><path fill="var(--text-primary)" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path></svg>
<div class="tooltiptext topmenutooltip" id="logouttooltip"></div>
</div>
`).firstChild;
titlebar.append(userInfo!);
const userinfo = stringToHTML(/* html */`
const userinfo = stringToHTML(/* html */ `
<div class="userInfo">
<div class="userInfoText">
<div style="display: flex; align-items: center;">
@@ -134,27 +135,30 @@ function updateUserInfo(info: {
</div>
`).firstChild;
titlebar.append(userinfo!);
var logoutbutton = document.getElementsByClassName('logout')[0];
var userInfosvgdiv = document.getElementById('logouttooltip')!;
var logoutbutton = document.getElementsByClassName("logout")[0];
var userInfosvgdiv = document.getElementById("logouttooltip")!;
userInfosvgdiv.appendChild(logoutbutton);
}
async function handleStudentData() {
try {
const response = await fetch(`${location.origin}/seqta/student/load/message/people`, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
const response = await fetch(
`${location.origin}/seqta/student/load/message/people`,
{
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
body: JSON.stringify({ mode: "student" }),
},
body: JSON.stringify({ mode: 'student' }),
});
);
const responseData = await response.json();
let students = responseData.payload;
await updateStudentInfo(students);
} catch (error) {
console.error('Error fetching and processing student data:', error);
console.error("Error fetching and processing student data:", error);
}
}
@@ -162,20 +166,21 @@ async function updateStudentInfo(students: any) {
const info = await getUserInfo();
var index = students.findIndex(function (person: any) {
return (
person.firstname == info.userDesc.split(' ')[0] &&
person.surname == info.userDesc.split(' ')[1]
person.firstname == info.userDesc.split(" ")[0] &&
person.surname == info.userDesc.split(" ")[1]
);
});
let houseelement1 = document.getElementsByClassName('userInfohouse')[0];
let houseelement1 = document.getElementsByClassName("userInfohouse")[0];
const houseelement = houseelement1 as HTMLElement;
if (students[index]?.house) {
if (students[index]?.house_colour) {
houseelement.style.background = students[index].house_colour;
try {
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;
} catch (error) {
houseelement.innerText = students[index].house;
@@ -184,120 +189,133 @@ async function updateStudentInfo(students: any) {
} else {
try {
houseelement.innerText = students[index].year;
} catch(err) {
houseelement.innerText = 'N/A';
} catch (err) {
houseelement.innerText = "N/A";
}
}
}
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);
if (NewsButton.firstChild) {
fragment.appendChild(NewsButton.firstChild);
}
let iconCover = document.createElement('div');
iconCover.classList.add('icon-cover');
iconCover.id = 'icon-cover';
let iconCover = document.createElement("div");
iconCover.classList.add("icon-cover");
iconCover.id = "icon-cover";
menu.appendChild(iconCover);
}
function setupEventListeners() {
const menuCover = document.querySelector('#icon-cover');
const homebutton = document.getElementById('homebutton');
const newsbutton = document.getElementById('newsbutton');
homebutton?.addEventListener('click', function() {
if (!homebutton.classList.contains('draggable') && !homebutton.classList.contains('active')) {
const menuCover = document.querySelector("#icon-cover");
const homebutton = document.getElementById("homebutton");
const newsbutton = document.getElementById("newsbutton");
homebutton?.addEventListener("click", function () {
if (
!homebutton.classList.contains("draggable") &&
!homebutton.classList.contains("active")
) {
loadHomePage();
}
});
newsbutton?.addEventListener('click', function() {
if (!newsbutton.classList.contains('draggable') && !newsbutton.classList.contains('active')) {
newsbutton?.addEventListener("click", function () {
if (
!newsbutton.classList.contains("draggable") &&
!newsbutton.classList.contains("active")
) {
SendNewsPage();
}
});
menuCover?.addEventListener('click', function() {
location.href = '../#?page=/home';
menuCover?.addEventListener("click", function () {
location.href = "../#?page=/home";
loadHomePage();
(document.getElementById('menu')!.firstChild! as HTMLElement).classList.remove('noscroll');
(
document.getElementById("menu")!.firstChild! as HTMLElement
).classList.remove("noscroll");
});
}
async function createSettingsButton() {
let SettingsButton = stringToHTML( /* html */`
let SettingsButton = stringToHTML(/* html */ `
<button class="addedButton tooltip" id="AddedSettings">
<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>
</svg>
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ''}
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
</button>
`);
let ContentDiv = document.getElementById('content');
let ContentDiv = document.getElementById("content");
ContentDiv!.append(SettingsButton.firstChild!);
}
function GetLightDarkModeString() {
function GetLightDarkModeString() {
if (settingsState.DarkMode) {
return 'Switch to light theme'
return "Switch to light theme";
} else {
return 'Switch to dark theme'
return "Switch to dark theme";
}
}
async function addDarkLightToggle() {
const tooltipString = GetLightDarkModeString();
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_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 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_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 */ `
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
<svg xmlns="http://www.w3.org/2000/svg">${svgContent}</svg>
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div>
</button>
`);
let ContentDiv = document.getElementById('content');
let ContentDiv = document.getElementById("content");
ContentDiv!.append(LightDarkModeButton.firstChild!);
updateAllColors();
document.getElementById('LightDarkModeButton')!.addEventListener('click', async () => {
const darklightText = document.getElementById('darklighttooliptext');
if (settingsState.originalDarkMode != undefined && settingsState.selectedTheme) {
darklightText!.innerText = 'Locked by current theme';
document
.getElementById("LightDarkModeButton")!
.addEventListener("click", async () => {
const darklightText = document.getElementById("darklighttooliptext");
await delay(1000)
if (
settingsState.originalDarkMode != undefined &&
settingsState.selectedTheme
) {
darklightText!.innerText = "Locked by current theme";
await delay(1000);
darklightText!.innerText = GetLightDarkModeString();
return;
}
settingsState.DarkMode = !settingsState.DarkMode;
updateAllColors();
darklightText!.innerText = GetLightDarkModeString();
return
}
settingsState.DarkMode = !settingsState.DarkMode;
updateAllColors();
darklightText!.innerText = GetLightDarkModeString();
});
});
}
function customizeMenuToggle() {
const menuToggle = document.getElementById('menuToggle');
const menuToggle = document.getElementById("menuToggle");
if (menuToggle) {
menuToggle.innerHTML = '';
menuToggle.innerHTML = "";
}
for (let i = 0; i < 3; i++) {
const line = document.createElement('div');
line.className = 'hamburger-line';
const line = document.createElement("div");
line.className = "hamburger-line";
menuToggle!.appendChild(line);
}
}
}
+29 -25
View File
@@ -1,15 +1,18 @@
import { getDataById, isIndexedDBSupported } from '@/interface/hooks/BackgroundDataLoader';
import {
getDataById,
isIndexedDBSupported,
} from "@/interface/hooks/BackgroundDataLoader";
export async function appendBackgroundToUI() {
const parent = document.getElementById('container');
const parent = document.getElementById("container");
if (!parent) return;
const backgroundContainer = document.createElement('div');
backgroundContainer.classList.add('imageBackground');
backgroundContainer.setAttribute('excludeDarkCheck', 'true');
const backgroundContainer = document.createElement("div");
backgroundContainer.classList.add("imageBackground");
backgroundContainer.setAttribute("excludeDarkCheck", "true");
const mediaContainer = document.createElement('div');
mediaContainer.id = 'media-container';
const mediaContainer = document.createElement("div");
mediaContainer.id = "media-container";
backgroundContainer.appendChild(mediaContainer);
parent.appendChild(backgroundContainer);
@@ -24,9 +27,9 @@ export async function loadBackground() {
}
try {
const selectedBackgroundId = localStorage.getItem('selectedBackground');
const selectedBackgroundId = localStorage.getItem("selectedBackground");
if (!selectedBackgroundId) {
const backgroundContainer = document.querySelector('.imageBackground');
const backgroundContainer = document.querySelector(".imageBackground");
if (backgroundContainer) {
backgroundContainer.remove();
}
@@ -36,35 +39,36 @@ export async function loadBackground() {
const background = await getDataById(selectedBackgroundId);
if (!background) return;
let backgroundContainer = document.querySelector('.imageBackground');
let backgroundContainer = document.querySelector(".imageBackground");
if (!backgroundContainer) {
backgroundContainer = document.createElement('div');
backgroundContainer.classList.add('imageBackground');
backgroundContainer.setAttribute('excludeDarkCheck', 'true');
const parent = document.getElementById('container');
backgroundContainer = document.createElement("div");
backgroundContainer.classList.add("imageBackground");
backgroundContainer.setAttribute("excludeDarkCheck", "true");
const parent = document.getElementById("container");
if (parent) {
parent.appendChild(backgroundContainer);
}
}
let mediaContainer = document.getElementById('media-container');
let mediaContainer = document.getElementById("media-container");
if (!mediaContainer) {
mediaContainer = document.createElement('div');
mediaContainer.id = 'media-container';
mediaContainer = document.createElement("div");
mediaContainer.id = "media-container";
backgroundContainer.appendChild(mediaContainer);
}
mediaContainer = document.getElementById('media-container');
mediaContainer = document.getElementById("media-container");
if (!mediaContainer) return;
mediaContainer.innerHTML = '';
mediaContainer.innerHTML = "";
const mediaElement = background.type === 'video'
? document.createElement('video')
: document.createElement('img');
const mediaElement =
background.type === "video"
? document.createElement("video")
: document.createElement("img");
mediaElement.src = URL.createObjectURL(background.blob);
mediaElement.classList.add('background');
mediaElement.classList.add("background");
if (mediaElement instanceof HTMLVideoElement) {
mediaElement.loop = true;
@@ -74,6 +78,6 @@ export async function loadBackground() {
mediaContainer.appendChild(mediaElement);
} catch (error) {
console.error('Error loading background:', error);
console.error("Error loading background:", error);
}
}
}
+10 -13
View File
File diff suppressed because one or more lines are too long
+8 -5
View File
@@ -6,21 +6,24 @@ import { debounce } from "lodash";
export class SettingsResizer {
constructor() {
this.adjustPopupHeight();
window.addEventListener('resize', debounce(this.adjustPopupHeight, 250) as EventListener);
document.addEventListener('DOMContentLoaded', this.adjustPopupHeight);
window.addEventListener(
"resize",
debounce(this.adjustPopupHeight, 250) as EventListener,
);
document.addEventListener("DOMContentLoaded", this.adjustPopupHeight);
}
private adjustPopupHeight() {
const iframePopup = document.getElementById('ExtensionPopup');
const iframePopup = document.getElementById("ExtensionPopup");
if (!iframePopup) return;
const viewportHeight = window.innerHeight;
const idealHeight = viewportHeight - 80 - 15; // -80px for the top of the popup
if (idealHeight > 600) {
iframePopup.style.height = '600px';
iframePopup.style.height = "600px";
} else {
iframePopup.style.height = `${idealHeight}px`;
}
}
}
}

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