mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
Compare commits
198 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 048ccb248e | |||
| 363fbfa3c8 | |||
| 0bf4ed8157 | |||
| 3dc77dd398 | |||
| e7c5357c64 | |||
| 7a80dc2cc3 | |||
| 68e8c89b35 | |||
| 77582a4d00 | |||
| 3f97049451 | |||
| ebc7baaacc | |||
| 35ca292c04 | |||
| e928399066 | |||
| a4033862c9 | |||
| 22ddb4bc41 | |||
| b8d8b108c3 | |||
| aeaf5d9e59 | |||
| 1acda4f399 | |||
| 121888c1c3 | |||
| 647a32fbac | |||
| 19cc1a5600 | |||
| e3f4b59d9c | |||
| a07323499c | |||
| 600456f28e | |||
| 3ecd7205ed | |||
| 6147e96cc9 | |||
| 09855c9ef5 | |||
| 9542cb13f5 | |||
| d19f573093 | |||
| 7af6acaf38 | |||
| c4c50f2c30 | |||
| a33f4f3f00 | |||
| 1f023574b8 | |||
| dc4499e8a2 | |||
| ad2ad4d456 | |||
| 5413286f56 | |||
| f0c5b1dace | |||
| ad14dc3aa5 | |||
| 64bf1d88e8 | |||
| 7196a85f7d | |||
| f2b594a13b | |||
| a17a9a50c1 | |||
| 207832640f | |||
| b76999cb13 | |||
| fc0e491ea7 | |||
| 68159ddd0e | |||
| 4696529964 | |||
| a9e198ea68 | |||
| 620d168d28 | |||
| 1c63c06b72 | |||
| 7a76d3f4eb | |||
| 8e34db4a67 | |||
| 9fc24767ec | |||
| 331c9a9d81 | |||
| 74e92ddb53 | |||
| 1a6dc9ebb9 | |||
| be54816d83 | |||
| b644dbbbc7 | |||
| d06356101a | |||
| 7eacf345d0 | |||
| 9a71a5241a | |||
| f4ae9098d8 | |||
| 325f6c5f9b | |||
| ea46ab41ce | |||
| e6f36edabf | |||
| 587aa5eb89 | |||
| da3a680455 | |||
| 77c3761947 | |||
| 6fb4ea5372 | |||
| 5c0044a4d4 | |||
| dba688d3cd | |||
| 75446c6855 | |||
| fe2fa87cb5 | |||
| 9f7b46d2ad | |||
| ef890ee776 | |||
| d42dc79415 | |||
| e072b3f5c8 | |||
| e32218bf07 | |||
| 286375c662 | |||
| f2d197e8f0 | |||
| 85beb62a37 | |||
| 0b908cb251 | |||
| c9f0f9cf16 | |||
| 3c65e6d6c5 | |||
| 2cb607c5a9 | |||
| 695357a639 | |||
| 8cb052f2ff | |||
| 6b39f60db7 | |||
| 1638dd4989 | |||
| ca7e6b9137 | |||
| 1263c1c8ef | |||
| 5eb92bc87a | |||
| ecff10a991 | |||
| 4745df7ace | |||
| c7bdd86967 | |||
| f920980948 | |||
| 8c2f36033f | |||
| 75e687f934 | |||
| 5cd0f47fe5 | |||
| 84cfaccded | |||
| 0c55098bc7 | |||
| 50157f24fd | |||
| b77e2b2247 | |||
| 0c0fabe661 | |||
| f39bfce5c3 | |||
| 2d26f729e3 | |||
| d7b541c814 | |||
| 41bb5996df | |||
| d3d7a1199f | |||
| 3277b02dfb | |||
| 4703d68bac | |||
| 696043e01a | |||
| 24ef85c39e | |||
| 35fc996e37 | |||
| edb0a0f929 | |||
| 5051d04451 | |||
| ffc695f022 | |||
| ca5d232e47 | |||
| 44325f0d49 | |||
| c446217916 | |||
| a51049154b | |||
| f41da95f7e | |||
| 3e405cc453 | |||
| d3ae21b7fa | |||
| 6247e17d70 | |||
| 550f2cab54 | |||
| ddb94e6b07 | |||
| 12270d28b9 | |||
| 639d35b2f5 | |||
| 410bd0e54e | |||
| c1bc3d3d22 | |||
| 17b093b5ea | |||
| c8330091ca | |||
| 2a00344243 | |||
| cd2c98bd65 | |||
| 81b690ec9a | |||
| 13095cef19 | |||
| 36ecbd37ed | |||
| 2cc5ce3f1a | |||
| 14aa511198 | |||
| 4e397e3c57 | |||
| af311d9b3e | |||
| 3af28f574b | |||
| 9f1c3e3bc8 | |||
| e7df2abc6d | |||
| c7ae2e1ab6 | |||
| a0888eb091 | |||
| e8d9dc7a6b | |||
| 32934593d8 | |||
| 855d979b7f | |||
| 083dfad5c2 | |||
| 9de863be02 | |||
| 9d7dab84f1 | |||
| a6999051c4 | |||
| c35855559b | |||
| 8972a5a8bf | |||
| a321a482cc | |||
| 5f561f516c | |||
| 395ec3291e | |||
| 96b17c7eeb | |||
| fad50e6eba | |||
| f74ad97c0a | |||
| 7f4e6cf5ec | |||
| 677f17c418 | |||
| e58584a55a | |||
| 59444dc904 | |||
| 178c4fdef4 | |||
| cdaaceade7 | |||
| d65bfa8c46 | |||
| 694d11477d | |||
| 61e1bcdae9 | |||
| 23a09004d8 | |||
| 3ce075cd47 | |||
| 92a51daf36 | |||
| 479b2878a9 | |||
| 18ffa1b47d | |||
| 6098cf9608 | |||
| 5fde2a3660 | |||
| e4d5f7fd3f | |||
| 31b069056d | |||
| 3e5ebe8ef4 | |||
| 338292ac15 | |||
| 187c484901 | |||
| 24d0616110 | |||
| 260ac4aaea | |||
| 4311a8fe76 | |||
| 251e09941b | |||
| bb1541ab2d | |||
| 1c6ec3ee91 | |||
| 0ef0078fb7 | |||
| 834b8b41af | |||
| f1512ba6e1 | |||
| 13fc077686 | |||
| 7cf765121c | |||
| 4e393f14bb | |||
| 98347e038d | |||
| f2bdb22ea8 | |||
| 4afab2c52a | |||
| 4c6b43d7c7 |
@@ -0,0 +1,414 @@
|
||||
/** @type {import('dependency-cruiser').IConfiguration} */
|
||||
module.exports = {
|
||||
forbidden: [
|
||||
{
|
||||
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) ',
|
||||
from: {},
|
||||
to: {
|
||||
circular: true
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
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
|
||||
]
|
||||
},
|
||||
to: {},
|
||||
},
|
||||
{
|
||||
name: 'no-deprecated-core',
|
||||
comment:
|
||||
'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',
|
||||
from: {},
|
||||
to: {
|
||||
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$'
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'deprecated'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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 " +
|
||||
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
|
||||
"in your package.json.",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'npm-no-pkg',
|
||||
'npm-unknown'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
from: {},
|
||||
to: {
|
||||
couldNotResolve: true
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
from: {},
|
||||
to: {
|
||||
moreThanOneDependencyType: true,
|
||||
// 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"]
|
||||
}
|
||||
},
|
||||
|
||||
/* rules you might want to tweak for your specific situation: */
|
||||
|
||||
{
|
||||
name: 'not-to-spec',
|
||||
comment:
|
||||
'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',
|
||||
from: {},
|
||||
to: {
|
||||
path: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
|
||||
}
|
||||
},
|
||||
{
|
||||
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 ' +
|
||||
"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',
|
||||
from: {
|
||||
path: '^(src)',
|
||||
pathNot: '[.](?:spec|test)[.](?:js|mjs|cjs|jsx|ts|mts|cts|tsx)$'
|
||||
},
|
||||
to: {
|
||||
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/'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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. " +
|
||||
"If you're using an optional dependency here by design - add an exception to your" +
|
||||
"dependency-cruiser configuration.",
|
||||
from: {},
|
||||
to: {
|
||||
dependencyTypes: [
|
||||
'npm-optional'
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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',
|
||||
from: {},
|
||||
to: {
|
||||
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']
|
||||
},
|
||||
|
||||
/* Which modules to exclude */
|
||||
// exclude : {
|
||||
// /* path: an array of regular expressions in strings to match against */
|
||||
// path: '',
|
||||
// },
|
||||
|
||||
/* Which modules to exclusively include (array of regular expressions in strings)
|
||||
dependency-cruiser will skip everything not matching this pattern
|
||||
*/
|
||||
// includeOnly : [''],
|
||||
|
||||
/* List of module systems to cruise.
|
||||
When left out dependency-cruiser will fall back to the list of _all_
|
||||
module systems it knows of. It's the default because it's the safe option
|
||||
It might come at a performance penalty, though.
|
||||
moduleSystems: ['amd', 'cjs', 'es6', 'tsd']
|
||||
|
||||
As in practice only commonjs ('cjs') and ecmascript modules ('es6')
|
||||
are widely used, you can limit the moduleSystems to those.
|
||||
*/
|
||||
|
||||
// moduleSystems: ['cjs', 'es6'],
|
||||
|
||||
/*
|
||||
false: don't look at JSDoc imports (the default)
|
||||
true: dependency-cruiser will detect dependencies in JSDoc-style
|
||||
import statements. Implies "parser": "tsc", so the dependency-cruiser
|
||||
will use the typescript parser for JavaScript files.
|
||||
|
||||
For this to work the typescript compiler will need to be installed in the
|
||||
same spot as you're running dependency-cruiser from.
|
||||
*/
|
||||
// detectJSDocImports: true,
|
||||
|
||||
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/main/'
|
||||
to open it on your online repo or `vscode://file/${process.cwd()}/` to
|
||||
open it in visual studio code),
|
||||
*/
|
||||
// prefix: `vscode://file/${process.cwd()}/`,
|
||||
|
||||
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
|
||||
true: also detect dependencies that only exist before typescript-to-javascript compilation
|
||||
"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.
|
||||
*/
|
||||
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
|
||||
|
||||
/* if true combines the package.jsons found from the module up to the base
|
||||
folder the cruise is initiated from. Useful for how (some) mono-repos
|
||||
manage dependencies & dependency definitions.
|
||||
*/
|
||||
// combinedDependencies: false,
|
||||
|
||||
/* if true leave symlinks untouched, otherwise use the realpath */
|
||||
// preserveSymlinks: false,
|
||||
|
||||
/* TypeScript project file ('tsconfig.json') to use for
|
||||
(1) compilation and
|
||||
(2) resolution (e.g. with the paths property)
|
||||
|
||||
The (optional) fileName attribute specifies which file to take (relative to
|
||||
dependency-cruiser's current working directory). When not provided
|
||||
defaults to './tsconfig.json'.
|
||||
*/
|
||||
tsConfig: {
|
||||
fileName: 'tsconfig.json'
|
||||
},
|
||||
|
||||
/* Webpack configuration to use to get resolve options from.
|
||||
|
||||
The (optional) fileName attribute specifies which file to take (relative
|
||||
to dependency-cruiser's current working directory. When not provided defaults
|
||||
to './webpack.conf.js'.
|
||||
|
||||
The (optional) `env` and `arguments` attributes contain the parameters
|
||||
to be passed if your webpack config is a function and takes them (see
|
||||
webpack documentation for details)
|
||||
*/
|
||||
// webpackConfig: {
|
||||
// fileName: 'webpack.config.js',
|
||||
// env: {},
|
||||
// arguments: {}
|
||||
// },
|
||||
|
||||
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
|
||||
for compilation
|
||||
*/
|
||||
// babelConfig: {
|
||||
// fileName: '.babelrc',
|
||||
// },
|
||||
|
||||
/* List of strings you have in use in addition to cjs/ es6 requires
|
||||
& imports to declare module dependencies. Use this e.g. if you've
|
||||
re-declared require, use a require-wrapper or use window.require as
|
||||
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
|
||||
|
||||
If you use webpack: you can also set these in webpack.conf.js. The set
|
||||
there will override the ones specified here.
|
||||
*/
|
||||
enhancedResolveOptions: {
|
||||
/* 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.
|
||||
*/
|
||||
conditionNames: ["import", "require", "node", "default", "types"],
|
||||
/* The extensions, by default are the same as the ones dependency-cruiser
|
||||
can access (run `npx depcruise --info` to see which ones that are in
|
||||
_your_ environment). If that list is larger than you need you can pass
|
||||
the extensions you actually use (e.g. [".js", ".jsx"]). This can speed
|
||||
up module resolution, which is the most expensive step.
|
||||
*/
|
||||
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
|
||||
/* What to consider a 'main' field in package.json */
|
||||
mainFields: ["module", "main", "types", "typings"],
|
||||
/* A list of alias fields in package.jsons
|
||||
|
||||
See [this specification](https://github.com/defunctzombie/package-browser-field-spec) and
|
||||
the webpack [resolve.alias](https://webpack.js.org/configuration/resolve/#resolvealiasfields)
|
||||
documentation.
|
||||
|
||||
Defaults to an empty array (= don't use alias fields).
|
||||
*/
|
||||
// aliasFields: ["browser"],
|
||||
},
|
||||
|
||||
/* skipAnalysisNotInRules will make dependency-cruiser execute
|
||||
analysis strictly necessary for checking the rule set only.
|
||||
|
||||
See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules
|
||||
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: {
|
||||
add: [
|
||||
"bun",
|
||||
"bun:ffi",
|
||||
"bun:jsc",
|
||||
"bun:sqlite",
|
||||
"bun:test",
|
||||
"bun:wrap",
|
||||
"detect-libc",
|
||||
"undici",
|
||||
"ws"
|
||||
]
|
||||
},
|
||||
|
||||
reporterOptions: {
|
||||
dot: {
|
||||
/* pattern of modules that can be consolidated in the detailed
|
||||
graphical dependency graph. The default pattern in this configuration
|
||||
collapses everything in node_modules to one folder deep so you see
|
||||
the external modules, but their innards.
|
||||
*/
|
||||
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
|
||||
for details and some examples. If you don't specify a theme
|
||||
dependency-cruiser falls back to a built-in one.
|
||||
*/
|
||||
// theme: {
|
||||
// graph: {
|
||||
// /* splines: "ortho" gives straight lines, but is slow on big graphs
|
||||
// splines: "true" gives bezier curves (fast, not as nice as ortho)
|
||||
// */
|
||||
// splines: "true"
|
||||
// },
|
||||
// }
|
||||
},
|
||||
archi: {
|
||||
/* pattern of modules that can be consolidated in the high level
|
||||
graphical dependency graph. If you use the high level graphical
|
||||
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/(?:@[^/]+/[^/]+|[^/]+)',
|
||||
|
||||
/* 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
|
||||
dot section above and otherwise use the default one.
|
||||
*/
|
||||
// theme: { },
|
||||
},
|
||||
"text": {
|
||||
"highlightFocused": true
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
// generated: dependency-cruiser@16.10.0 on 2025-02-16T22:32:01.621Z
|
||||
@@ -7,21 +7,17 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
**Bug Description**
|
||||
Please provide a clear and concise description of the bug.
|
||||
|
||||
**To Reproduce**
|
||||
Please indicate how did you make this happen.
|
||||
**Steps to Reproduce**
|
||||
Please list the actions that caused the issue.
|
||||
|
||||
**Expected behaviuor**
|
||||
Please add a clear and concise description of what you expected to happen.
|
||||
**Expected Behavior**
|
||||
Please describe how you think the program should have behaved, making sure to be as clear and concise as possible.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
If applicable, please provide screenshots. This will help us to reproduce the issue and better understand what we are looking for.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- If using Windows, the build number. Find this by using ```winver``` and copying down the build id.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
**Additional Context**
|
||||
Feel free to provide any additional, applicable context or information that is relevant to the problem.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: BetterSEQTA Community Support
|
||||
url: https://discord.gg/YzmbnCDkat
|
||||
about: Join our discord for community updates, discussion, and more!
|
||||
@@ -7,14 +7,8 @@ assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
**Is your feature request related to a problem? if so, Please describe the issue or link to a pre-existing bug report**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
A clear and concise description of what you want to happen. Provide reference art/pictures if poccible
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Vulnerability
|
||||
about: Report a vulnerability in this extension.
|
||||
title: "[VUL] "
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**What is the vulnerability?**
|
||||
Describe the vulnerability in concise language.
|
||||
|
||||
**Where is the vulnerability found?**
|
||||
Describe where the vulnerability is found.
|
||||
|
||||
**What does this affect?**
|
||||
Explain what it affects. E.G: It opens up my school email to the world. etc.
|
||||
@@ -0,0 +1,14 @@
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
@@ -9,6 +9,8 @@ yarn.lock
|
||||
.env
|
||||
.env.submit
|
||||
|
||||
dependency-graph.svg
|
||||
|
||||
# Build
|
||||
extension.zip
|
||||
build/
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"plugins": {
|
||||
"tailwindcss": {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,21 @@
|
||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||
email, or any other method with the owners of this repository before making a change.
|
||||
|
||||
## Community
|
||||
|
||||
Join our community channels to discuss the project, get help, and connect with other contributors:
|
||||
|
||||
- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta)
|
||||
- **GitHub Discussions**: For longer-form conversations
|
||||
- **GitHub Issues**: For bug reports and feature requests
|
||||
|
||||
## Creating Plugins
|
||||
|
||||
If you're interested in creating plugins for BetterSEQTA+, check out our plugin development guides:
|
||||
|
||||
- [Creating Your First Plugin](./docs/plugins/creating-plugins.md)
|
||||
- [Plugin API Reference](./docs/advanced/plugin-api.md)
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
1. It is recommended to start by opening an issue to discuss the change you wish to make. This will allow us to discuss the change and ensure it is a good fit for the project.
|
||||
|
||||
@@ -43,23 +43,19 @@
|
||||
- Easier Access Notices
|
||||
- Assessments
|
||||
- Options to remove certain items from the side menu
|
||||
- Fully customisable themes and an offical theme store
|
||||
- Grades calculator
|
||||
- Fully customisable themes and an official theme store
|
||||
- Notification for next lesson (sent 5 minutes before end of the lesson)
|
||||
- Browser Support
|
||||
- Chrome Supported
|
||||
- Edge Supported
|
||||
- Brave Supported
|
||||
- Opera Supported
|
||||
- Vivaldi Supported
|
||||
- Chromium-based browsers are supported
|
||||
- Firefox (Experimental - available [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)
|
||||
- Safari (Experimental - only available via compilation)
|
||||
- Chrome, Edge, Brave, Opera and other Chromium-Based browsers are supported
|
||||
- Firefox Supported: [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)!
|
||||
- Safari (Experimental and not recommended - only available via compilation)
|
||||
|
||||
## 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).
|
||||
|
||||
Don't worry- if you get stuck feel free to ask around in the discord. We're open and happy to help out! Happy creating :)
|
||||
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 :)
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -69,20 +65,43 @@ Don't worry- if you get stuck feel free to ask around in the discord. We're open
|
||||
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
|
||||
```
|
||||
|
||||
### Running Development
|
||||
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
npm install --legacy-peer-deps # Only NPM supported
|
||||
```
|
||||
### Running Development
|
||||
2. Run the dev script (it updates as you save files)
|
||||
|
||||
```
|
||||
npm run dev
|
||||
npm run dev # or use your perferred package manager
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Building for production
|
||||
|
||||
2. Run the build script
|
||||
|
||||
```
|
||||
npm run build # or use your perferred package manager
|
||||
```
|
||||
|
||||
2.1. Package it up (optional)
|
||||
|
||||
```
|
||||
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`
|
||||
@@ -90,33 +109,15 @@ npm run dev
|
||||
- Click `Load unpacked`
|
||||
- Select the `dist` folder
|
||||
|
||||
Just remember, in order to update changes to the extension, you need to click the refresh button on the extension in `chrome://extensions` whenever anything's changed.
|
||||
|
||||
### Building for production
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```
|
||||
npm install # or your preferred package manager like pnpm or yarn
|
||||
```
|
||||
|
||||
2. Run the build script
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. Package it up (optional)
|
||||
|
||||
```
|
||||
npm run zip # This requires 7-Zip to be installed in order to work
|
||||
```
|
||||
Just remember, in order to update changes to the extension if you are running in developer mode, you need to click the refresh button on the extension in `chrome://extensions` whenever anything's changed.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
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.
|
||||
|
||||
@@ -135,4 +136,4 @@ This extension was initially developed by [Nulkem](https://github.com/Nulkem/bet
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#sethburkart123/EvenBetterSEQTA&Date)
|
||||
[](https://star-history.com/#BetterSEQTA/BetterSEQTA-Plus&Date)
|
||||
|
||||
+3
-4
@@ -6,11 +6,10 @@ Below here is the supported versions of BetterSEQTA+. Anything older than this i
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.4.0 | :white_check_mark: |
|
||||
| <= 3.3 | :x: |
|
||||
| 3.4.3 | ✅ |
|
||||
| < 3.4.3 | :x: |
|
||||
|
||||
`*` May not work on other devices.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find vulnerabilities, REPORT IT IMMEDIATELY. Make an issue and use the template provided for vulnerabilities.
|
||||
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
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# BetterSEQTA+ Documentation
|
||||
|
||||
🚧 DOCS UNDER CONSTRUCTION! 🚧
|
||||
|
||||
Welcome to the BetterSEQTA+ documentation! This documentation will help you understand how BetterSEQTA+ works and how to extend it with plugins and new features.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
### Getting Started
|
||||
- [Project Overview](./README.md) - This file
|
||||
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
|
||||
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
|
||||
|
||||
### Plugin System
|
||||
- [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
|
||||
|
||||
## Core Concepts
|
||||
|
||||
BetterSEQTA+ is built around several core concepts:
|
||||
|
||||
1. **Plugin System**: BetterSEQTA+ uses a plugin system to extend SEQTA with new features. Plugins are self-contained pieces of code that can be enabled or disabled by the user. Check out our [plugin guide](./plugins/README.md) to learn how to create your own!
|
||||
|
||||
2. **Type-Safe Settings**: Each plugin can define settings that are type-safe and automatically rendered in the settings UI. The settings system uses TypeScript decorators to make it easy to define settings with proper typing.
|
||||
|
||||
3. **Storage API**: Plugins can use the Storage API to persist data between sessions. The Storage API is also type-safe, ensuring that plugins can only access their own data.
|
||||
|
||||
4. **SEQTA Integration**: BetterSEQTA+ integrates with SEQTA Learn by injecting code into the page. This allows plugins to modify the SEQTA UI and add new features.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you need help with BetterSEQTA+, you can:
|
||||
|
||||
- [Open an Issue](https://github.com/SeqtaLearning/betterseqta-plus/issues) - Report bugs or request features
|
||||
- [Join the Discord](https://discord.gg/YzmbnCDkat) - Chat with the community
|
||||
- [Email the Maintainers](mailto:betterseqta.plus@gmail.com) - Contact the maintainers directly
|
||||
|
||||
## Contributing to the Documentation
|
||||
|
||||
We welcome contributions to the documentation! If you find something unclear or missing, please open an issue or submit a pull request.
|
||||
|
||||
To contribute to the documentation:
|
||||
|
||||
1. Fork the repository
|
||||
2. Make your changes to the documentation files
|
||||
3. Submit a pull request with a clear description of your changes
|
||||
|
||||
## License
|
||||
|
||||
BetterSEQTA+ is licensed under the [MIT License](../LICENSE).
|
||||
@@ -0,0 +1,262 @@
|
||||
# Contributing to BetterSEQTA+
|
||||
|
||||
Thank you for your interest in contributing to BetterSEQTA+! This document provides guidelines and instructions for contributing to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [Getting Started](#getting-started)
|
||||
- [Setting Up Your Development Environment](#setting-up-your-development-environment)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Contributing Code](#contributing-code)
|
||||
- [Branching Strategy](#branching-strategy)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Coding Standards](#coding-standards)
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Suggesting Features](#suggesting-features)
|
||||
- [Writing Documentation](#writing-documentation)
|
||||
- [Community](#community)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
BetterSEQTA+ is committed to providing a welcoming and inclusive environment for all contributors. We expect all participants to adhere to our Code of Conduct, which promotes respectful and harassment-free interaction.
|
||||
|
||||
Key points:
|
||||
- Be respectful and inclusive
|
||||
- Focus on what is best for the community
|
||||
- Show empathy towards other community members
|
||||
- Be open to constructive feedback
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setting Up Your Development Environment
|
||||
|
||||
1. **Fork the Repository**
|
||||
|
||||
Start by forking the BetterSEQTA+ repository to your GitHub account.
|
||||
|
||||
2. **Clone Your Fork**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yourusername/betterseqta-plus.git
|
||||
cd betterseqta-plus
|
||||
```
|
||||
|
||||
3. **Install Dependencies**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
4. **Set Up Development Environment**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
5. **Install in Chrome/Firefox**
|
||||
|
||||
Follow the [installation instructions](./installation.md#development-installation) to load the development version into your browser.
|
||||
|
||||
### Project Structure
|
||||
|
||||
Understanding the project structure will help you navigate the codebase:
|
||||
|
||||
```
|
||||
betterseqta-plus/
|
||||
├── src/ # Source code
|
||||
│ ├── plugins/ # Plugin system
|
||||
│ │ ├── built-in/ # Built-in plugins
|
||||
│ │ ├── core/ # Plugin core functionality
|
||||
│ ├── settings/ # Settings system
|
||||
│ ├── utils/ # Utility functions
|
||||
│ ├── extension/ # Browser extension code
|
||||
├── docs/ # Documentation
|
||||
├── test/ # Test files
|
||||
├── dist/ # Build output (generated)
|
||||
├── package.json # Project dependencies
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── README.md # Project README
|
||||
```
|
||||
|
||||
## Contributing Code
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
We follow a simple branching strategy:
|
||||
|
||||
- `main` - The main development branch
|
||||
- `feature/*` - Feature branches
|
||||
- `bugfix/*` - Bug fix branches
|
||||
- `docs/*` - Documentation branches
|
||||
|
||||
Always create a new branch for your changes:
|
||||
|
||||
```bash
|
||||
git checkout -b feature/my-new-feature
|
||||
```
|
||||
|
||||
### Pull Request Process
|
||||
|
||||
1. **Keep PRs Focused**
|
||||
|
||||
Each pull request should address a single concern. If you're working on multiple features, create separate PRs for each.
|
||||
|
||||
2. **Write Clear Commit Messages**
|
||||
|
||||
Follow the conventional commits format:
|
||||
```
|
||||
feat: add new feature
|
||||
fix: resolve bug with timetable
|
||||
docs: update installation instructions
|
||||
```
|
||||
|
||||
3. **Update Documentation**
|
||||
|
||||
If your changes require documentation updates, include them in the same PR.
|
||||
|
||||
4. **Run Tests**
|
||||
|
||||
Make sure all tests pass before submitting your PR:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
5. **Submit Your PR**
|
||||
|
||||
When you're ready, push your branch and create a pull request on GitHub.
|
||||
|
||||
6. **Code Review**
|
||||
|
||||
All PRs will be reviewed by maintainers. Be responsive to feedback and make requested changes.
|
||||
|
||||
7. **Merge**
|
||||
|
||||
Once approved, a maintainer will merge your PR.
|
||||
|
||||
### Coding Standards
|
||||
|
||||
We follow TypeScript best practices and have a consistent code style:
|
||||
|
||||
1. **Use TypeScript**
|
||||
|
||||
All new code should be written in TypeScript with proper typing.
|
||||
|
||||
2. **Follow Existing Patterns**
|
||||
|
||||
Match the coding style of the existing codebase.
|
||||
|
||||
3. **Write Tests**
|
||||
|
||||
Add tests for new features and bug fixes.
|
||||
|
||||
4. **Document Your Code**
|
||||
|
||||
Add comments for complex logic and JSDoc comments for functions.
|
||||
|
||||
5. **Use Linters**
|
||||
|
||||
We use ESLint and Prettier. Run them before submitting your PR:
|
||||
```bash
|
||||
npm run lint
|
||||
npm run format
|
||||
```
|
||||
|
||||
## Reporting Bugs
|
||||
|
||||
If you find a bug, please report it by creating an issue on GitHub:
|
||||
|
||||
1. **Search Existing Issues**
|
||||
|
||||
Check if the bug has already been reported.
|
||||
|
||||
2. **Use the Bug Report Template**
|
||||
|
||||
Fill in all sections of the bug report template:
|
||||
- Description
|
||||
- Steps to reproduce
|
||||
- Expected behavior
|
||||
- Actual behavior
|
||||
- Screenshots (if applicable)
|
||||
- Environment (browser, OS, etc.)
|
||||
|
||||
3. **Be Specific**
|
||||
|
||||
The more details you provide, the easier it will be to fix the bug.
|
||||
|
||||
## Suggesting Features
|
||||
|
||||
We welcome feature suggestions! To suggest a new feature:
|
||||
|
||||
1. **Search Existing Suggestions**
|
||||
|
||||
Check if your idea has already been suggested.
|
||||
|
||||
2. **Use the Feature Request Template**
|
||||
|
||||
Fill in all sections of the feature request template:
|
||||
- Description
|
||||
- Use case
|
||||
- Potential implementation
|
||||
- Alternatives considered
|
||||
|
||||
3. **Be Patient**
|
||||
|
||||
Feature requests are evaluated based on alignment with project goals, feasibility, and maintainer bandwidth.
|
||||
|
||||
## Writing Documentation
|
||||
|
||||
Good documentation is crucial for the project. To contribute to documentation:
|
||||
|
||||
1. **Identify Gaps**
|
||||
|
||||
Look for areas where documentation is missing or unclear.
|
||||
|
||||
2. **Follow Documentation Style**
|
||||
|
||||
Maintain a consistent style and format.
|
||||
|
||||
3. **Use Clear Language**
|
||||
|
||||
Write in simple, clear English. Avoid jargon when possible.
|
||||
|
||||
4. **Include Examples**
|
||||
|
||||
Code examples and screenshots help users understand.
|
||||
|
||||
5. **Submit a PR**
|
||||
|
||||
Follow the same process as code contributions, but create a branch with a `docs/` prefix.
|
||||
|
||||
## Community
|
||||
|
||||
Join our community channels to discuss the project, get help, and connect with other contributors:
|
||||
|
||||
- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta)
|
||||
- **GitHub Discussions**: For longer-form conversations
|
||||
- **GitHub Issues**: For bug reports and feature requests
|
||||
|
||||
## Creating Plugins
|
||||
|
||||
If you're interested in creating plugins for BetterSEQTA+, check out our plugin development guides:
|
||||
|
||||
- [Creating Your First Plugin](./plugins/creating-plugins.md)
|
||||
- [Plugin API Reference](./advanced/plugin-api.md)
|
||||
|
||||
## Recognition
|
||||
|
||||
Contributors are recognized in several ways:
|
||||
|
||||
1. **CONTRIBUTORS.md**: All contributors are listed in this file
|
||||
2. **Release Notes**: Significant contributions are highlighted in release notes
|
||||
3. **Community Recognition**: Regular shout-outs in community channels
|
||||
|
||||
## Questions?
|
||||
|
||||
If you have any questions about contributing, please:
|
||||
|
||||
1. Check the documentation
|
||||
2. Ask in the Discord server
|
||||
3. Open a GitHub Discussion
|
||||
|
||||
Thank you for contributing to BetterSEQTA+! Your efforts help make SEQTA better for students and teachers everywhere.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Installing BetterSEQTA+
|
||||
|
||||
This guide will walk you through the process of installing and setting up BetterSEQTA+ for development or usage.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, make sure you have the following installed:
|
||||
|
||||
- [npm](https://www.npmjs.com/) (v7 or higher) or [Bun](https://bun.sh/) (recommended)
|
||||
- A modern web browser (Chrome, Firefox, Edge, etc.)
|
||||
|
||||
## Installation Methods
|
||||
|
||||
There are two ways to install BetterSEQTA+:
|
||||
|
||||
1. **For Users**: Install the browser extension
|
||||
2. **For Developers**: Clone the repository and set up the development environment
|
||||
|
||||
## For Users: Installing the Browser Extension
|
||||
|
||||
BetterSEQTA+ is available as a browser extension for Chrome, Firefox, and Edge.
|
||||
|
||||
### Chrome/Edge
|
||||
|
||||
1. Visit the [Chrome Web Store page for BetterSEQTA+](https://chrome.google.com/webstore/detail/betterseqta)
|
||||
2. Click the "Add to Chrome" button
|
||||
3. Confirm the installation when prompted
|
||||
4. The extension will be installed and ready to use
|
||||
|
||||
### Firefox
|
||||
|
||||
1. Visit the [Firefox Add-ons page for BetterSEQTA+](https://addons.mozilla.org/en-US/firefox/addon/betterseqta)
|
||||
2. Click the "Add to Firefox" button
|
||||
3. Confirm the installation when prompted
|
||||
4. The extension will be installed and ready to use
|
||||
|
||||
## For Developers: Setting Up the Development Environment
|
||||
|
||||
If you want to develop for BetterSEQTA+ or modify the code, follow these steps:
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/SeqtaLearning/betterseqta-plus.git
|
||||
cd betterseqta-plus
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
Using npm:
|
||||
|
||||
```bash
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
Using Bun (recommended):
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
### 3. Set Up Environment Variables - Only required for pushing to extension stores from the command line
|
||||
|
||||
Copy the example environment file:
|
||||
|
||||
```bash
|
||||
cp .env.submit.example .env
|
||||
```
|
||||
|
||||
Edit the `.env` file with your SEQTA credentials and settings.
|
||||
|
||||
### 4. Start the Development Server
|
||||
|
||||
Using npm:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Using Bun:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
This will start a development server and build the extension in watch mode.
|
||||
|
||||
### 5. Load the Extension in Your Browser
|
||||
|
||||
#### Chrome/Edge
|
||||
|
||||
1. Open Chrome/Edge and navigate to `chrome://extensions` or `edge://extensions`
|
||||
2. Enable "Developer mode" using the toggle in the top right
|
||||
3. Click "Load unpacked" and select the `dist` folder in your BetterSEQTA+ directory
|
||||
4. The extension should now appear in your extensions list
|
||||
|
||||
#### Firefox
|
||||
|
||||
1. Open Firefox and navigate to `about:debugging#/runtime/this-firefox`
|
||||
2. Click "Load Temporary Add-on..."
|
||||
3. Select the `manifest.json` file in the `dist` folder
|
||||
4. The extension should now appear in your add-ons list
|
||||
|
||||
### 6. Test Your Changes
|
||||
|
||||
After making changes to the code, the development server will automatically rebuild the extension. However, you may need to reload the extension in your browser to see the changes:
|
||||
|
||||
1. Go to the extensions page in your browser
|
||||
2. Find BetterSEQTA+ and click the reload icon
|
||||
3. Refresh any SEQTA Learn pages you have open
|
||||
|
||||
## Troubleshooting Installation
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "Cannot find module" errors
|
||||
|
||||
If you see errors about missing modules, try:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
```
|
||||
|
||||
Or with Bun:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
bun install
|
||||
```
|
||||
|
||||
#### Extension not appearing in SEQTA
|
||||
|
||||
Make sure:
|
||||
- You're visiting a SEQTA Learn page
|
||||
- The extension is enabled
|
||||
- You've refreshed the page after installing the extension
|
||||
|
||||
#### Development build not updating
|
||||
|
||||
Try:
|
||||
1. Stopping the development server
|
||||
2. Clearing your browser cache
|
||||
3. Removing the extension from your browser
|
||||
4. Rebuilding the extension
|
||||
5. Loading it again
|
||||
|
||||
## Updating BetterSEQTA+
|
||||
|
||||
### For Users
|
||||
|
||||
Browser extensions update automatically, but you can manually check for updates:
|
||||
|
||||
- **Chrome/Edge**: Go to `chrome://extensions` or `edge://extensions`, enable Developer mode, and click "Update"
|
||||
- **Firefox**: Go to `about:addons`, click the gear icon, and select "Check for Updates"
|
||||
|
||||
### For Developers
|
||||
|
||||
If you're working on the code, pull the latest changes and reinstall dependencies:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Or with Bun:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
bun install
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that you have BetterSEQTA+ installed, you can:
|
||||
|
||||
- [Getting Started with Plugins](./plugins/getting-started.md)
|
||||
- [Contribute to the project](../CONTRIBUTING.md)
|
||||
@@ -0,0 +1,257 @@
|
||||
# Creating Plugins for BetterSEQTA+
|
||||
|
||||
Hey there! 👋 So you want to create a plugin for BetterSEQTA+? That's awesome! This guide will walk you through everything you need to know, from the very basics to more advanced features. Don't worry if you're new to this - we'll explain everything step by step.
|
||||
|
||||
## 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
|
||||
- Collects notifications in a better way
|
||||
- Really, anything you can imagine!
|
||||
|
||||
## Your First Plugin
|
||||
|
||||
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';
|
||||
|
||||
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',
|
||||
|
||||
// This tells BetterSEQTA+ that users can turn our plugin on/off
|
||||
disableToggle: true,
|
||||
|
||||
// This is where the magic happens!
|
||||
run: async (api) => {
|
||||
// Wait for the homepage to load
|
||||
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';
|
||||
|
||||
// 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');
|
||||
message?.remove();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default myFirstPlugin;
|
||||
```
|
||||
|
||||
Let's break down what's happening here:
|
||||
|
||||
1. First, we import the `Plugin` type that tells TypeScript what a plugin should look like
|
||||
2. We create our plugin object with some basic information:
|
||||
- `id`: A unique name for your plugin (use lowercase and dashes)
|
||||
- `name`: A friendly name that users will see
|
||||
- `description`: Explain what your plugin does
|
||||
- `version`: Your plugin's version number
|
||||
3. We set `disableToggle: true` so users can turn our plugin on/off in settings
|
||||
4. The `run` function is where we put our plugin's code
|
||||
5. We use `api.seqta.onMount` to wait for the homepage to load
|
||||
6. We create and style a message element
|
||||
7. We return a cleanup function that removes our changes when the plugin is disabled
|
||||
|
||||
## The Plugin API
|
||||
|
||||
When your plugin runs, it gets access to a powerful API that lets you do all sorts of things. Let's look at what you can do:
|
||||
|
||||
### SEQTA API (`api.seqta`)
|
||||
|
||||
This helps you interact with SEQTA's pages:
|
||||
|
||||
```typescript
|
||||
// Wait for an element to appear on the page
|
||||
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);
|
||||
});
|
||||
|
||||
// Get the current page
|
||||
const currentPage = api.seqta.getCurrentPage();
|
||||
```
|
||||
|
||||
### Settings API (`api.settings`)
|
||||
|
||||
Want to let users customize your plugin? Use settings!
|
||||
|
||||
```typescript
|
||||
import { BasePlugin } from '@/plugins/core/settings';
|
||||
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
|
||||
|
||||
// Define your settings
|
||||
const settings = defineSettings({
|
||||
showMessage: booleanSetting({
|
||||
default: true,
|
||||
title: "Show Welcome Message",
|
||||
description: "Show a friendly message on the homepage",
|
||||
})
|
||||
});
|
||||
|
||||
// Create a class for your plugin
|
||||
class MyPluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.showMessage)
|
||||
showMessage!: boolean;
|
||||
}
|
||||
|
||||
// Create your plugin
|
||||
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) => {
|
||||
if (newValue) {
|
||||
// Show the message
|
||||
} else {
|
||||
// Hide the message
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Storage API (`api.storage`)
|
||||
|
||||
Need to save some data? The storage API has got you covered:
|
||||
|
||||
```typescript
|
||||
// Save some data
|
||||
await api.storage.set('lastVisit', new Date().toISOString());
|
||||
|
||||
// Get it back later
|
||||
const lastVisit = await api.storage.get('lastVisit');
|
||||
|
||||
// Listen for changes
|
||||
api.storage.onChange('lastVisit', (newValue) => {
|
||||
console.log('Last visit updated:', newValue);
|
||||
});
|
||||
```
|
||||
|
||||
### Events API (`api.events`)
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// Send an event
|
||||
api.events.emit('myCustomEvent', { some: 'data' });
|
||||
```
|
||||
|
||||
## Adding Styles
|
||||
|
||||
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 {
|
||||
background: linear-gradient(135deg, #6e8efb, #a777e3);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px;
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from { transform: translateY(-20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
`,
|
||||
|
||||
run: async (api) => {
|
||||
// Your plugin code here
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
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');
|
||||
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
|
||||
|
||||
5. **Make It User-Friendly**:
|
||||
- Add clear settings with good descriptions
|
||||
- Use `disableToggle: true` so users can turn it off if needed
|
||||
- Add helpful error messages if something goes wrong
|
||||
|
||||
## 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
|
||||
- [assessmentsAverage](../../src/plugins/built-in/assessmentsAverage/index.ts): Shows how to add new features to existing pages
|
||||
|
||||
## 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)
|
||||
@@ -0,0 +1,314 @@
|
||||
# Plugin API Reference
|
||||
|
||||
This document provides detailed technical information about BetterSEQTA+'s plugin APIs. For a beginner-friendly introduction, see [Creating Your First Plugin](./README.md).
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
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';
|
||||
|
||||
// First, define your settings
|
||||
const settings = defineSettings({
|
||||
enabled: booleanSetting({
|
||||
default: true,
|
||||
title: "Enable Feature",
|
||||
description: "Turn this feature on or off",
|
||||
})
|
||||
});
|
||||
|
||||
// Create a class to handle your settings
|
||||
class MyPluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.enabled)
|
||||
enabled!: boolean;
|
||||
}
|
||||
|
||||
// Create an instance of your settings
|
||||
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',
|
||||
settings: settingsInstance.settings,
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
console.log('Plugin is running!');
|
||||
|
||||
// Do stuff when settings change
|
||||
api.settings.onChange('enabled', (enabled) => {
|
||||
if (enabled) {
|
||||
console.log('Feature enabled!');
|
||||
}
|
||||
});
|
||||
|
||||
// Return a cleanup function
|
||||
return () => {
|
||||
console.log('Plugin cleanup');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default myPlugin;
|
||||
```
|
||||
|
||||
## SEQTA API
|
||||
|
||||
The SEQTA API helps you interact with SEQTA's pages:
|
||||
|
||||
```typescript
|
||||
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',
|
||||
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);
|
||||
});
|
||||
|
||||
// Track page changes
|
||||
const { unregister: pageUnregister } = api.seqta.onPageChange((page) => {
|
||||
console.log('User went to:', page);
|
||||
});
|
||||
|
||||
// Clean up when disabled
|
||||
return () => {
|
||||
timetableUnregister();
|
||||
pageUnregister();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default seqtaPlugin;
|
||||
```
|
||||
|
||||
## Settings API
|
||||
|
||||
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';
|
||||
|
||||
// Define your settings
|
||||
const settings = defineSettings({
|
||||
darkMode: booleanSetting({
|
||||
default: false,
|
||||
title: "Dark Mode",
|
||||
description: "Enable dark mode"
|
||||
}),
|
||||
userName: stringSetting({
|
||||
default: "",
|
||||
title: "User Name",
|
||||
description: "Your display name",
|
||||
placeholder: "Enter your name..."
|
||||
}),
|
||||
theme: selectSetting({
|
||||
default: "light",
|
||||
title: "Theme",
|
||||
description: "Choose your theme",
|
||||
options: [
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" }
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
// Create your settings class
|
||||
class ThemePluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.darkMode)
|
||||
darkMode!: boolean;
|
||||
|
||||
@Setting(settings.userName)
|
||||
userName!: string;
|
||||
|
||||
@Setting(settings.theme)
|
||||
theme!: string;
|
||||
}
|
||||
|
||||
// Create the plugin
|
||||
const themePlugin: Plugin<typeof settings> = {
|
||||
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');
|
||||
}
|
||||
|
||||
// Listen for changes
|
||||
const { unregister } = api.settings.onChange('darkMode', (enabled) => {
|
||||
document.body.classList.toggle('dark', enabled);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregister();
|
||||
document.body.classList.remove('dark');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default themePlugin;
|
||||
```
|
||||
|
||||
## Storage API
|
||||
|
||||
Here's how to use storage in your plugin:
|
||||
|
||||
```typescript
|
||||
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',
|
||||
settings: {},
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
// Wait for storage to be ready
|
||||
await api.storage.loaded;
|
||||
|
||||
// Save some data
|
||||
await api.storage.set('lastVisit', new Date().toISOString());
|
||||
|
||||
// Get saved data
|
||||
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);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregister();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default storagePlugin;
|
||||
```
|
||||
|
||||
## Events API
|
||||
|
||||
Here's how to use events in your plugin:
|
||||
|
||||
```typescript
|
||||
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',
|
||||
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);
|
||||
});
|
||||
|
||||
// Listen for notifications
|
||||
const { unregister: notifyListener } = api.events.on('notification.new', (notification) => {
|
||||
console.log('New notification:', notification);
|
||||
});
|
||||
|
||||
// Clean up listeners
|
||||
return () => {
|
||||
themeListener();
|
||||
notifyListener();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default eventsPlugin;
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
Here's how to write efficient plugins:
|
||||
|
||||
```typescript
|
||||
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',
|
||||
settings: {},
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
// ✅ Good: Use onMount
|
||||
const { unregister } = api.seqta.onMount('.timetable', (el) => {
|
||||
el.classList.add('enhanced');
|
||||
});
|
||||
|
||||
// ❌ Bad: Don't use intervals
|
||||
// const interval = setInterval(() => {
|
||||
// const el = document.querySelector('.timetable');
|
||||
// if (el) el.classList.add('enhanced');
|
||||
// }, 100);
|
||||
|
||||
// ✅ Good: Cache DOM elements
|
||||
const header = document.querySelector('.header');
|
||||
if (header) {
|
||||
// Reuse header instead of querying again
|
||||
}
|
||||
|
||||
// ✅ Good: Batch DOM updates
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const div = document.createElement('div');
|
||||
fragment.appendChild(div);
|
||||
}
|
||||
document.body.appendChild(fragment);
|
||||
|
||||
return () => {
|
||||
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
|
||||
4. Create the plugin object with proper type annotation
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
// ref: https://stackoverflow.com/a/76920975
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export default function ClosePlugin(): Plugin {
|
||||
return {
|
||||
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)
|
||||
} else {
|
||||
console.log('Build ended')
|
||||
}
|
||||
},
|
||||
|
||||
// use this to catch the end of a build without errors
|
||||
closeBundle() {
|
||||
console.log('Bundle closed')
|
||||
process.exit(0)
|
||||
},
|
||||
}
|
||||
}
|
||||
+24
-10
@@ -25,17 +25,31 @@ export function updateManifestPlugin(): PluginOption {
|
||||
console.log('** updated **');
|
||||
}
|
||||
|
||||
fs.watchFile(manifestPath, () => {
|
||||
console.log('** watchFile ** ');
|
||||
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
if (manifestContents.web_accessible_resources.some((resource: any) => resource.use_dynamic_url)) {
|
||||
const updated = forceDisableUseDynamicUrl();
|
||||
if (updated) {
|
||||
server.ws.send({ type: 'full-reload' });
|
||||
console.log('** updated **');
|
||||
}
|
||||
// Implement retry mechanism for file watching
|
||||
const watchWithRetry = () => {
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
console.log('Manifest not found, retrying in 1 second...');
|
||||
setTimeout(watchWithRetry, 1000);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
fs.watchFile(manifestPath, () => {
|
||||
try {
|
||||
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
if (manifestContents.web_accessible_resources?.some((resource: any) => resource.use_dynamic_url)) {
|
||||
const updated = forceDisableUseDynamicUrl();
|
||||
if (updated) {
|
||||
server.ws.send({ type: 'full-reload' });
|
||||
console.log('** updated **');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error reading manifest, will retry on next change:', error.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watchWithRetry();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
+24
-6
@@ -5,26 +5,44 @@ const path = require('path');
|
||||
|
||||
function getLatestVersion(files) {
|
||||
console.log('Files passed to getLatestVersion:', files);
|
||||
|
||||
const versions = files.map(file => {
|
||||
const match = file.match(/@(\d+\.\d+\.\d+)-/);
|
||||
const match = file.match(/@([\d\.]+)-/);
|
||||
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
|
||||
return match ? match[1] : 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
|
||||
|
||||
return { fullVersion, semverVersion };
|
||||
}).filter(Boolean);
|
||||
|
||||
console.log('Extracted versions:', versions);
|
||||
const latestVersion = semver.maxSatisfying(versions, '*');
|
||||
console.log('Latest version:', latestVersion);
|
||||
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);
|
||||
|
||||
// Get the full version that matches the latest SemVer version
|
||||
const latestVersion = versions.find(v => v.semverVersion === latestSemver)?.fullVersion || null;
|
||||
|
||||
console.log('Final selected latest version:', latestVersion);
|
||||
return latestVersion;
|
||||
}
|
||||
|
||||
function getLatestFiles(browser) {
|
||||
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
||||
console.log('Glob pattern:', pattern);
|
||||
|
||||
const files = glob.sync(pattern);
|
||||
console.log('Files found for browser', browser, ':', files);
|
||||
|
||||
const latestVersion = getLatestVersion(files);
|
||||
|
||||
const latestFile = files.find(file => file.includes(latestVersion));
|
||||
// Find the exact file by matching the original full version
|
||||
const latestFile = files.find(file => file.includes(`@${latestVersion}-`));
|
||||
|
||||
console.log('Latest file for browser', browser, ':', latestFile);
|
||||
return latestFile;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import fs from 'fs';
|
||||
|
||||
export default function touchGlobalCSSPlugin() {
|
||||
return {
|
||||
name: 'touch-global-css',
|
||||
handleHotUpdate({ modules }) {
|
||||
// log all of the staticImportedUrls
|
||||
const importers = modules[0]._clientModule.importers
|
||||
importers.forEach((importer) => {
|
||||
if (importer.file.includes('.css')) {
|
||||
console.log("touching", importer.file)
|
||||
fs.utimesSync(importer.file, new Date(), new Date())
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
+52
-50
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "betterseqtaplus",
|
||||
"version": "3.4.1",
|
||||
"version": "3.4.6",
|
||||
"type": "module",
|
||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development, while incorporating a plethora of new and improved features!",
|
||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"scripts": {
|
||||
"dev": "cross-env MODE=chrome vite dev",
|
||||
@@ -11,7 +11,9 @@
|
||||
"build:chrome": "cross-env MODE=chrome vite build",
|
||||
"build:firefox": "cross-env MODE=firefox vite build",
|
||||
"build:safari": "cross-env MODE=safari vite build",
|
||||
"build:dev": "cross-env MODE=chrome SOURCEMAP=true vite build && cross-env MODE=firefox SOURCEMAP=true vite build",
|
||||
"convert:safari": "xcrun safari-web-extension-converter dist/safari --project-location . --app-name $npm_package_name-safari",
|
||||
"dependency-graph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg",
|
||||
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
|
||||
"publish": "bun lib/publish.js --b",
|
||||
"zip": "bedframe zip"
|
||||
@@ -31,69 +33,69 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||
"@babel/runtime": "^7.26.9",
|
||||
"@bedframe/cli": "^0.0.91",
|
||||
"@crxjs/vite-plugin": "2.0.0-beta.25",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"glob": "^11.0.0",
|
||||
"dependency-cruiser": "^16.10.0",
|
||||
"eslint": "9.22.0",
|
||||
"glob": "^11.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier": "^3.5.3",
|
||||
"process": "^0.11.10",
|
||||
"sass": "^1.78.0",
|
||||
"sass-loader": "^13.3.3",
|
||||
"semver": "^7.6.3",
|
||||
"publish-browser-extension": "^3.0.0",
|
||||
"sass": "^1.85.1",
|
||||
"sass-loader": "^16.0.5",
|
||||
"semver": "^7.7.1",
|
||||
"tailwindcss": "3",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bedframe/cli": "^0.0.85",
|
||||
"@codemirror/lang-css": "^6.3.0",
|
||||
"@codemirror/lang-less": "^6.0.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@tailwindcss/forms": "^0.5.9",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/language": "^6.10.8",
|
||||
"@codemirror/search": "^6.5.10",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/chrome": "^0.0.270",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/react": "17",
|
||||
"@types/react-dom": "17",
|
||||
"@types/chrome": "^0.0.308",
|
||||
"@types/color": "^4.2.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@types/webextension-polyfill": "^0.10.7",
|
||||
"@uiw/codemirror-extensions-color": "^4.23.3",
|
||||
"@uiw/codemirror-theme-github": "^4.23.3",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"caniuse-lite": "^1.0.30001684",
|
||||
"classnames": "^2.5.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/webextension-polyfill": "^0.12.3",
|
||||
"@uiw/codemirror-extensions-color": "^4.23.10",
|
||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"codemirror": "^6.0.1",
|
||||
"color": "^4.2.3",
|
||||
"dompurify": "^3.1.6",
|
||||
"embla-carousel-autoplay": "^8.3.1",
|
||||
"embla-carousel-svelte": "^8.3.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"idb": "^8.0.0",
|
||||
"kolorist": "^1.8.0",
|
||||
"color": "^5.0.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-svelte": "^8.5.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"idb": "^8.0.2",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"million": "^3.1.11",
|
||||
"motion": "^11.12.0",
|
||||
"postcss": "^8.4.45",
|
||||
"publish-browser-extension": "^2.2.1",
|
||||
"motion": "^12.4.12",
|
||||
"postcss": "^8.5.3",
|
||||
"react": "17",
|
||||
"react-best-gradient-color-picker": "^3.0.10",
|
||||
"react-best-gradient-color-picker": "3.0.11",
|
||||
"react-dom": "17",
|
||||
"sortablejs": "^1.15.3",
|
||||
"svelte": "^5.1.9",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
"vite": "^5.4.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
"rss-parser": "^3.13.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"svelte": "^5.22.6",
|
||||
"typescript": "^5.8.2",
|
||||
"uuid": "^11.1.0",
|
||||
"vite": "^6.2.1",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
--- a/Users/sethburkart/Documents/Coding/betterseqta-plus/src/plugins/core/settings.ts
|
||||
+++ b/Users/sethburkart/Documents/Coding/betterseqta-plus/src/plugins/core/settings.ts
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Base interfaces for our settings
|
||||
interface BaseSettingOptions {
|
||||
- title: string;
|
||||
+ readonly title: string; // Mark as readonly where appropriate
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -11,21 +11,21 @@
|
||||
}
|
||||
|
||||
interface StringSettingOptions extends BaseSettingOptions {
|
||||
- default: string;
|
||||
+ readonly default: string;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
interface NumberSettingOptions extends BaseSettingOptions {
|
||||
- default: number;
|
||||
+ readonly default: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
interface SelectSettingOptions<T extends string> extends BaseSettingOptions {
|
||||
- default: T;
|
||||
- options: readonly T[];
|
||||
+ readonly default: T;
|
||||
+ readonly options: readonly T[];
|
||||
}
|
||||
|
||||
// The actual decorators
|
||||
@@ -34,14 +34,16 @@
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
- proto.settings = {};
|
||||
+ // Initialize with a base type that can be extended
|
||||
+ Object.defineProperty(proto, 'settings', {
|
||||
+ value: {},
|
||||
+ writable: true, // Allows adding properties
|
||||
+ configurable: true,
|
||||
+ enumerable: true
|
||||
+ });
|
||||
}
|
||||
-
|
||||
+
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'boolean' as const,
|
||||
...options
|
||||
};
|
||||
- };
|
||||
-}
|
||||
-
|
||||
-export function StringSetting(options: StringSettingOptions): PropertyDecorator {
|
||||
- return (target: Object, propertyKey: string | symbol) => {
|
||||
- // Ensure the settings property exists on the constructor's prototype
|
||||
- const proto = target.constructor.prototype;
|
||||
- if (!proto.hasOwnProperty('settings')) {
|
||||
- proto.settings = {};
|
||||
- }
|
||||
-
|
||||
- // Add the setting to the prototype's settings object with const assertion
|
||||
- proto.settings[propertyKey] = {
|
||||
- type: 'string' as const,
|
||||
- ...options
|
||||
- };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,14 +52,16 @@
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
- proto.settings = {};
|
||||
+ Object.defineProperty(proto, 'settings', {
|
||||
+ value: {},
|
||||
+ writable: true,
|
||||
+ configurable: true,
|
||||
+ enumerable: true
|
||||
+ });
|
||||
}
|
||||
-
|
||||
+
|
||||
// Add the setting to the prototype's settings object with const assertion
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'number' as const,
|
||||
...options
|
||||
};
|
||||
- };
|
||||
-}
|
||||
-
|
||||
-export function SelectSetting<T extends string>(options: SelectSettingOptions<T>): PropertyDecorator {
|
||||
- return (target: Object, propertyKey: string | symbol) => {
|
||||
- // Ensure the settings property exists on the constructor's prototype
|
||||
- const proto = target.constructor.prototype;
|
||||
- if (!proto.hasOwnProperty('settings')) {
|
||||
- proto.settings = {};
|
||||
- }
|
||||
-
|
||||
- // Add the setting to the prototype's settings object with const assertion
|
||||
- proto.settings[propertyKey] = {
|
||||
- type: 'select' as const,
|
||||
- ...options
|
||||
- };
|
||||
};
|
||||
}
|
||||
|
||||
// Base plugin class that handles settings
|
||||
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||
// The settings property will be populated by decorators
|
||||
- settings!: T;
|
||||
-
|
||||
+ // Keep the instance property and constructor logic as is,
|
||||
+ // as changing it would require changing animated-background/index.ts
|
||||
+ settings!: T; // Use definite assignment assertion
|
||||
+
|
||||
constructor() {
|
||||
// Copy settings from the prototype to the instance
|
||||
// This ensures that each instance has its own settings object
|
||||
+34
-2763
File diff suppressed because it is too large
Load Diff
+89
-153
@@ -1,61 +1,6 @@
|
||||
import browser from 'webextension-polyfill'
|
||||
import type { SettingsState } from "@/types/storage";
|
||||
|
||||
export const openDB = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('MyDatabase', 1);
|
||||
|
||||
request.onupgradeneeded = (event: any) => {
|
||||
const db = event.target.result;
|
||||
db.createObjectStore('backgrounds', { keyPath: 'id' });
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve(request.result);
|
||||
};
|
||||
|
||||
request.onerror = (event: any) => {
|
||||
reject('Error opening database: ' + event.target.errorCode);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const writeData = async (type: any, data: any) => {
|
||||
const db: any = await openDB();
|
||||
|
||||
const tx = db.transaction('backgrounds', 'readwrite');
|
||||
const store = tx.objectStore('backgrounds');
|
||||
const request = await store.put({ id: 'customBackground', type, data });
|
||||
|
||||
return request.result;
|
||||
};
|
||||
|
||||
export const readData = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
openDB()
|
||||
.then((db: any) => {
|
||||
const tx = db.transaction('backgrounds', 'readonly');
|
||||
const store = tx.objectStore('backgrounds');
|
||||
|
||||
// Retrieve the custom background
|
||||
const getRequest = store.get('customBackground');
|
||||
|
||||
// Attach success and error event handlers
|
||||
getRequest.onsuccess = function(event: any) {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
getRequest.onerror = function(event: any) {
|
||||
console.error('An error occurred:', event);
|
||||
reject(event);
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('An error occurred:', error);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
import { fetchNews } from './background/news';
|
||||
|
||||
function reloadSeqtaPages() {
|
||||
const result = browser.tabs.query({})
|
||||
@@ -69,70 +14,50 @@ function reloadSeqtaPages() {
|
||||
result.then(open, console.error)
|
||||
}
|
||||
|
||||
// Main message listener
|
||||
browser.runtime.onMessage.addListener((request: any, _sender: any, sendResponse: any) => {
|
||||
// @ts-ignore
|
||||
browser.runtime.onMessage.addListener((request: any, _: any, sendResponse: (response?: any) => void) => {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
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 'githubTab':
|
||||
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' });
|
||||
break;
|
||||
case 'reloadTabs':
|
||||
reloadSeqtaPages();
|
||||
break;
|
||||
|
||||
case 'setDefaultStorage':
|
||||
SetStorageValue(DefaultValues);
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
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':
|
||||
const date = new Date();
|
||||
|
||||
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`;
|
||||
|
||||
GetNews(sendResponse, url);
|
||||
return true;
|
||||
case 'githubTab':
|
||||
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' });
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown request type');
|
||||
}
|
||||
});
|
||||
case 'setDefaultStorage':
|
||||
SetStorageValue(DefaultValues);
|
||||
break;
|
||||
|
||||
function GetNews(sendResponse: any, url: string) {
|
||||
fetch(url)
|
||||
.then((result) => result.json())
|
||||
.then((response) => {
|
||||
if (response.code == 'rateLimited') {
|
||||
GetNews(sendResponse, url += '%00');
|
||||
} else {
|
||||
sendResponse({ news: response });
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'sendNews':
|
||||
fetchNews(request.source ?? 'australia', sendResponse);
|
||||
return true;
|
||||
|
||||
default:
|
||||
console.log('Unknown request type');
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const DefaultValues: SettingsState = {
|
||||
onoff: true,
|
||||
@@ -140,7 +65,6 @@ const DefaultValues: SettingsState = {
|
||||
bksliderinput: "50",
|
||||
transparencyEffects: false,
|
||||
lessonalert: true,
|
||||
notificationcollector: true,
|
||||
defaultmenuorder: [],
|
||||
menuitems: {
|
||||
assessments: { toggle: true },
|
||||
@@ -167,6 +91,7 @@ const DefaultValues: SettingsState = {
|
||||
originalSelectedColor: '',
|
||||
DarkMode: true,
|
||||
animations: true,
|
||||
assessmentsAverage: true,
|
||||
defaultPage: 'home',
|
||||
shortcuts: [
|
||||
{
|
||||
@@ -219,6 +144,8 @@ const DefaultValues: SettingsState = {
|
||||
},
|
||||
],
|
||||
customshortcuts: [],
|
||||
lettergrade: false,
|
||||
newsSource: 'australia',
|
||||
};
|
||||
|
||||
function SetStorageValue(object: any) {
|
||||
@@ -227,54 +154,63 @@ function SetStorageValue(object: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function UpdateCurrentValues() {
|
||||
try {
|
||||
const items = await browser.storage.local.get();
|
||||
const CurrentValues = items;
|
||||
function convertBksliderToSpeed(bksliderinput: number): number {
|
||||
const minBase = 50;
|
||||
const maxBase = 150;
|
||||
|
||||
const NewValue = Object.assign({}, DefaultValues, CurrentValues);
|
||||
const scaledValue = 2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
|
||||
const baseSpeed = 3;
|
||||
|
||||
function CheckInnerElement(element: any) {
|
||||
for (let i in element) {
|
||||
if (typeof element[i] === 'object') {
|
||||
// @ts-expect-error
|
||||
if (!Array.isArray(DefaultValues[i])) {
|
||||
// @ts-expect-error
|
||||
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
const length = DefaultValues[i].length;
|
||||
// @ts-expect-error
|
||||
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
|
||||
let NewArray = [];
|
||||
for (let j = 0; j < length; j++) {
|
||||
NewArray.push(NewValue[i][j]);
|
||||
}
|
||||
NewValue[i] = NewArray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const speed = baseSpeed / scaledValue;
|
||||
return speed;
|
||||
}
|
||||
|
||||
CheckInnerElement(DefaultValues);
|
||||
async function migrateLegacySettings() {
|
||||
const storage = await browser.storage.local.get(null) as unknown as SettingsState;
|
||||
|
||||
if (items['customshortcuts']) {
|
||||
NewValue['customshortcuts'] = items['customshortcuts'];
|
||||
}
|
||||
|
||||
SetStorageValue(NewValue);
|
||||
console.log('[BetterSEQTA+] Values updated successfully');
|
||||
} catch (error) {
|
||||
console.error('[BetterSEQTA+] Error updating values:', error);
|
||||
// Animated Background Migration
|
||||
if ('animatedbk' in storage || 'bksliderinput' in storage) {
|
||||
const animatedSettings = {
|
||||
enabled: storage.animatedbk ?? true,
|
||||
speed: storage.bksliderinput ? convertBksliderToSpeed(parseFloat(storage.bksliderinput)) : 1
|
||||
};
|
||||
await browser.storage.local.set({ 'plugin.animated-background.settings': animatedSettings });
|
||||
}
|
||||
|
||||
// Assessments Average Migration
|
||||
if ('assessmentsAverage' in storage || 'lettergrade' in storage) {
|
||||
const assessmentsSettings = {
|
||||
enabled: storage.assessmentsAverage ?? true,
|
||||
lettergrade: storage.lettergrade ?? false
|
||||
};
|
||||
await browser.storage.local.set({ 'plugin.assessments-average.settings': assessmentsSettings });
|
||||
}
|
||||
|
||||
if ('selectedTheme' in storage) {
|
||||
const themesSettings = { enabled: true };
|
||||
await browser.storage.local.set({ 'plugin.themes.settings': themesSettings });
|
||||
}
|
||||
if (storage.notificationCollector !== false) {
|
||||
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: true } });
|
||||
} else {
|
||||
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: false } });
|
||||
}
|
||||
|
||||
const keysToRemove = [
|
||||
'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']);
|
||||
|
||||
UpdateCurrentValues();
|
||||
if ( event.reason == 'install', event.reason == 'update' ) {
|
||||
if ( event.reason == 'install' || event.reason == 'update' ) {
|
||||
browser.storage.local.set({ justupdated: true });
|
||||
migrateLegacySettings();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
} else {
|
||||
sendResponse({ news: response });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rssFeedsByCountry: Record<string, string[]> = {
|
||||
usa: [
|
||||
"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
||||
"https://www.huffpost.com/section/front-page/feed",
|
||||
"https://www.npr.org/rss/rss.php",
|
||||
],
|
||||
taiwan: [
|
||||
"https://focustaiwan.tw/rss",
|
||||
"https://www.taipeitimes.com/rss/all.xml",
|
||||
"https://international.thenewslens.com/rss",
|
||||
],
|
||||
hong_kong: [
|
||||
"https://news.rthk.hk/rthk/en/rss.htm",
|
||||
"https://www.scmp.com/rss/91/feed",
|
||||
],
|
||||
panama: [
|
||||
"http://www.panama-guide.com/backend.php",
|
||||
],
|
||||
canada: [
|
||||
"https://www.cbc.ca/cmlink/rss-topstories",
|
||||
"https://www.theglobeandmail.com/?service=rss",
|
||||
],
|
||||
singapore: [
|
||||
"https://www.straitstimes.com/news/singapore/rss.xml",
|
||||
"https://www.channelnewsasia.com/rssfeeds/8395986",
|
||||
],
|
||||
uk: [
|
||||
"http://feeds.bbci.co.uk/news/rss.xml",
|
||||
"https://www.theguardian.com/uk/rss",
|
||||
],
|
||||
japan: [
|
||||
"https://www.japantimes.co.jp/feed/topstories.xml",
|
||||
"https://www3.nhk.or.jp/nhkworld/en/news/feeds/",
|
||||
],
|
||||
netherlands: [
|
||||
"https://www.dutchnews.nl/feed/",
|
||||
"http://feeds.nos.nl/nosnieuwsalgemeen",
|
||||
],
|
||||
};
|
||||
|
||||
export async function fetchNews(source: string, sendResponse: any) {
|
||||
const parser = new Parser();
|
||||
let feeds: string[];
|
||||
console.log('fetchNews', source)
|
||||
|
||||
if (source === "australia") {
|
||||
const date = new Date();
|
||||
|
||||
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);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (rssFeedsByCountry[source.toLowerCase()]) {
|
||||
// If the source is a country, fetch from predefined feeds
|
||||
feeds = rssFeedsByCountry[source.toLowerCase()];
|
||||
} else if (source.startsWith("http")) {
|
||||
// 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.");
|
||||
}
|
||||
|
||||
const articlesPromises = feeds.map(async (feedUrl) => {
|
||||
try {
|
||||
const response = await fetch(feedUrl);
|
||||
const feedString = await response.text();
|
||||
const feed = await parser.parseString(feedString);
|
||||
|
||||
return feed.items.map((item) => ({
|
||||
title: item.title || "",
|
||||
description: item.contentSnippet || "",
|
||||
url: item.link || "",
|
||||
urlToImage: null,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch RSS feed: ${feedUrl}`, error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
const articlesArray = await Promise.all(articlesPromises);
|
||||
const articles = articlesArray.flat();
|
||||
|
||||
sendResponse({ news: { articles } });
|
||||
}
|
||||
+406
-179
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ html.transparencyEffects:not(.dark) {
|
||||
|
||||
html.transparencyEffects {
|
||||
/* Background Fixes */
|
||||
.notifications__item___2ErJN,
|
||||
[class*="notifications__item___"],
|
||||
#shortcuts {
|
||||
backdrop-filter: unset !important;
|
||||
}
|
||||
@@ -24,21 +24,21 @@ html.transparencyEffects {
|
||||
/* Blurs */
|
||||
.draggable,
|
||||
.notice,
|
||||
.BasicPanel__BasicPanel___1GP6s,
|
||||
[class*="BasicPanel__BasicPanel___"],
|
||||
.message.addMessage,
|
||||
.singleSelect,
|
||||
.uiFileHandlerPanel,
|
||||
.Module__wrapper___2sbOo,
|
||||
.notifications__list___rp2L2,
|
||||
[class*="Module__wrapper___"],
|
||||
[class*="notifications__list___"],
|
||||
.thread,
|
||||
.calendar,
|
||||
.navigator,
|
||||
#title,
|
||||
.LabelList__selected___3Egk7,
|
||||
[class*="LabelList__selected___"],
|
||||
.buttonChecklist,
|
||||
.pane,
|
||||
.legacy-root button, .legacy-root a,
|
||||
.MessageList__MessageList___3DxoC {
|
||||
[class*="MessageList__MessageList___"] {
|
||||
backdrop-filter: blur(80px);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ html.transparencyEffects {
|
||||
}
|
||||
|
||||
.whatsnewContainer,
|
||||
.Message__Message___3oJaU {
|
||||
[class*="Message__Message___"] {
|
||||
backdrop-filter: blur(50px);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@ declare module '*.woff';
|
||||
declare module '*.scss';
|
||||
declare module '*.png';
|
||||
declare module '*.html';
|
||||
declare module '*.svelte';
|
||||
|
||||
declare module "*.png?base64" {
|
||||
const value: string;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
let editor = $state<HTMLDivElement | null>(null)
|
||||
let view: EditorView | null = null;
|
||||
let editorTheme = new Compartment();
|
||||
let { value, onChange } = $props<{value: string, onChange: (value: string) => void}>()
|
||||
let { value, onChange, className } = $props<{value: string, onChange: (value: string) => void, className?: string}>()
|
||||
|
||||
function createEditorState(initialContents: string) {
|
||||
let extensions = [
|
||||
@@ -91,4 +91,4 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg text-[13px] overflow-clip w-full bg-white dark:bg-zinc-900" bind:this={editor}></div>
|
||||
<div class={`rounded-lg text-[13px] overflow-clip w-full bg-white dark:bg-zinc-900 ${className}`} bind:this={editor}></div>
|
||||
@@ -8,6 +8,13 @@ div:has(> #rbgcp-wrapper) {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#rbgcp-inputs-wrap #rbgcp-hex-input,
|
||||
#rbgcp-inputs-wrap #rbgcp-input {
|
||||
color: white !important;
|
||||
background-color: #37373b !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
div:has(> #rbgcp-solid-btn),
|
||||
div:has(> #rbgcp-advanced-btn),
|
||||
#rbgcp-color-model-btn > div,
|
||||
|
||||
@@ -93,7 +93,7 @@ export default function Picker({
|
||||
<ColorPicker
|
||||
disableDarkMode={true}
|
||||
presets={presets}
|
||||
hideInputs={true}
|
||||
hideInputs={customOnChange ? false : true}
|
||||
value={customThemeColor ?? ""}
|
||||
onChange={(color: string) => {
|
||||
if (customOnChange) {
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script lang="ts">
|
||||
let { state, onChange } = $props<{ state: number, onChange: (value: number) => void }>();
|
||||
let percentage = $derived((state / 100) * 100);
|
||||
let { state, onChange, min = 0, max = 100, step = 1 } = $props<{
|
||||
state: number,
|
||||
onChange: (value: number) => void,
|
||||
min?: number,
|
||||
max?: number,
|
||||
step?: number
|
||||
}>();
|
||||
let percentage = $derived(((state - min) / (max - min)) * 100);
|
||||
</script>
|
||||
|
||||
<div class="relative w-full max-w-lg mx-auto">
|
||||
<div class="relative mx-auto w-full max-w-lg">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
bind:value={state}
|
||||
style={`background: linear-gradient(to right, #30D259 ${percentage}%, #dddddd ${percentage}%)`}
|
||||
onchange={(e) => onChange(Number(e.currentTarget.value))}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
|
||||
let activeTab = $state(0);
|
||||
let hoveredTab = $state<number | null>(null);
|
||||
let containerRef: HTMLElement | null = null;
|
||||
let tabWidth = $state(0);
|
||||
|
||||
@@ -24,10 +23,6 @@
|
||||
return 0;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
calcXPos(hoveredTab);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
updateTabWidth();
|
||||
|
||||
@@ -45,26 +40,24 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full">
|
||||
<div bind:this={containerRef} class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 tab-width-container">
|
||||
<div class="relative flex">
|
||||
<div class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 px-2 tab-width-container">
|
||||
<div bind:this={containerRef} class="flex relative">
|
||||
<MotionDiv
|
||||
class="absolute top-0 left-0 z-0 h-full bg-[#DDDDDD] dark:bg-[#38373D] rounded-full opacity-40 tab-width"
|
||||
animate={{ x: calcXPos(hoveredTab) }}
|
||||
animate={{ x: calcXPos(activeTab) }}
|
||||
transition={springTransition}
|
||||
/>
|
||||
{#each tabs as { title }, index}
|
||||
<button
|
||||
class="relative z-10 flex-1 px-4 py-2 focus-visible:outline-none"
|
||||
onclick={() => activeTab = index}
|
||||
onmouseenter={() => hoveredTab = index}
|
||||
onmouseleave={() => hoveredTab = null}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full px-4 overflow-hidden">
|
||||
<div class="overflow-hidden px-4 h-full">
|
||||
<MotionDiv
|
||||
class="h-full"
|
||||
animate={{ x: `${-activeTab * 100}%` }}
|
||||
@@ -80,4 +73,4 @@
|
||||
</div>
|
||||
</MotionDiv>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader';
|
||||
import { setTheme } from '@/seqta/ui/themes/setTheme';
|
||||
import Spinner from '../Spinner.svelte';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
import Fuse from 'fuse.js';
|
||||
import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'
|
||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
type Background = { id: string; category: string; type: string; lowResUrl: string; highResUrl: string; name: string; description: string; featured?: boolean };
|
||||
let { searchTerm } = $props<{ searchTerm: string }>();
|
||||
@@ -170,13 +172,13 @@
|
||||
|
||||
function selectNoBackground() {
|
||||
selectedBackground = null;
|
||||
setTheme('');
|
||||
themeManager.setTheme('');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full">
|
||||
<!-- Sidebar -->
|
||||
<div class="w-64 h-full p-4 border-r border-zinc-200 dark:border-zinc-700">
|
||||
<div class="p-4 w-64 h-full border-r border-zinc-200 dark:border-zinc-700">
|
||||
<div class="mb-8">
|
||||
<h2 class="mb-4 text-lg font-semibold">Categories</h2>
|
||||
<nav class="space-y-2">
|
||||
@@ -208,15 +210,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 overflow-auto">
|
||||
<div class="overflow-auto flex-1">
|
||||
<!-- Header -->
|
||||
<div class="sticky top-0 z-10 p-4 border-b bg-[#F1F1F3] dark:bg-zinc-900 dark:border-zinc-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h1 class="text-2xl font-bold">Explore Backgrounds {searchTerm ? `- "${searchTerm}"` : ''}</h1>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex gap-4 items-center">
|
||||
<select
|
||||
bind:value={sortBy}
|
||||
class="p-2 border rounded-lg border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
class="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800"
|
||||
>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="name">Name</option>
|
||||
@@ -230,7 +232,7 @@
|
||||
<button
|
||||
class={`px-4 py-2 text-sm font-medium transition-colors rounded-full
|
||||
${activeTab === tab.toLowerCase() ? 'bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700' :
|
||||
'bg-zinc-100 dark:bg-transparent dark:outline dark:outline-1 dark:outline-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700/20'}`}
|
||||
'bg-zinc-100 dark:bg-transparent dark:outline dark:outline-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700/20'}`}
|
||||
onclick={() => activeTab = tab.toLowerCase() as typeof activeTab}
|
||||
>
|
||||
{tab}
|
||||
@@ -244,15 +246,15 @@
|
||||
{#if isLoading}
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{#each Array(9) as _}
|
||||
<div class="relative overflow-hidden rounded-lg animate-pulse">
|
||||
<div class="overflow-hidden relative rounded-lg animate-pulse">
|
||||
<!-- Image placeholder -->
|
||||
<div class="w-full h-48 bg-zinc-200 dark:bg-zinc-800"></div>
|
||||
<!-- Gradient overlay -->
|
||||
<div class="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-zinc-300 dark:from-zinc-700 to-transparent">
|
||||
<div class="absolute right-0 bottom-0 left-0 h-16 to-transparent bg-linear-to-t from-zinc-300 dark:from-zinc-700">
|
||||
<!-- Title placeholder -->
|
||||
<div class="absolute bottom-2 left-2 right-2">
|
||||
<div class="absolute right-2 bottom-2 left-2">
|
||||
<div class="w-2/3 h-4 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>
|
||||
<div class="w-1/2 h-3 mt-2 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>
|
||||
<div class="mt-2 w-1/2 h-3 rounded-full bg-zinc-200 dark:bg-zinc-800"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +273,7 @@
|
||||
return true;
|
||||
}) as background (background.id)}
|
||||
<div
|
||||
class="relative overflow-hidden rounded-lg shadow-lg cursor-pointer group"
|
||||
class="overflow-hidden relative rounded-lg shadow-lg cursor-pointer group"
|
||||
onclick={() => toggleBackgroundInstallation(background)}
|
||||
onkeydown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
@@ -286,7 +288,7 @@
|
||||
{:else}
|
||||
<video src={background.lowResUrl} class="object-cover w-full h-48" muted loop autoplay></video>
|
||||
{/if}
|
||||
<div class="absolute inset-0 flex items-center justify-center transition-opacity duration-300 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100">
|
||||
<div class={`flex absolute inset-0 justify-center items-center opacity-0 transition-opacity duration-300 bg-black/50 group-hover:opacity-100 ${installingBackgrounds.has(background.id) ? 'opacity-100' : ''}`}>
|
||||
{#if installingBackgrounds.has(background.id)}
|
||||
<Spinner />
|
||||
{:else if savedBackgrounds.includes(background.id)}
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
</script>
|
||||
|
||||
{#if coverThemes.length > 0}
|
||||
<div class="relative w-full transition-opacity rounded-xl overflow-clip" transition:fade>
|
||||
<div class="relative w-full overflow-clip rounded-xl transition-opacity" transition:fade>
|
||||
<div
|
||||
class="w-full aspect-[8/3]"
|
||||
class="w-full aspect-8/3"
|
||||
use:emblaCarouselSvelte={{ options, plugins }}
|
||||
onemblaInit={onInit}
|
||||
>
|
||||
@@ -47,20 +47,20 @@
|
||||
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
|
||||
<p class='text-lg text-white'>{theme.description}</p>
|
||||
</div>
|
||||
<div class='absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-black/80 to-transparent'></div>
|
||||
<div class='absolute bottom-0 left-0 w-full h-1/2 to-transparent bg-linear-to-t from-black/80'></div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
<div class='absolute z-10 flex gap-2 bottom-2 right-2'>
|
||||
<button aria-label="Previous" onclick={slidePrev} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
|
||||
<div class='flex absolute right-2 bottom-2 z-10 gap-2'>
|
||||
<button aria-label="Previous" onclick={slidePrev} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button aria-label="Next" onclick={slideNext} class='flex items-center justify-center w-8 h-8 text-white bg-black bg-opacity-50 rounded-full dark:bg-zinc-800 dark:bg-opacity-50'>
|
||||
<button aria-label="Next" onclick={slideNext} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width={1.5} stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Background } from './types';
|
||||
|
||||
export let filteredBackgrounds: Background[];
|
||||
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
let filters = $state({
|
||||
@@ -13,9 +9,9 @@
|
||||
orientation: [] as string[]
|
||||
});
|
||||
|
||||
$: {
|
||||
$effect(() => {
|
||||
dispatch('filter', filters);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleFilter(category: keyof typeof filters, value: string) {
|
||||
if (filters[category].includes(value)) {
|
||||
@@ -42,21 +38,19 @@
|
||||
<h3 class="mb-2 font-medium">Type</h3>
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" checked={filters.type.includes('image')} on:change={() => toggleFilter('type', 'image')}>
|
||||
<input type="checkbox" checked={filters.type.includes('image')} onchange={() => toggleFilter('type', 'image')}>
|
||||
<span class="ml-2">Image</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" checked={filters.type.includes('video')} on:change={() => toggleFilter('type', 'video')}>
|
||||
<input type="checkbox" checked={filters.type.includes('video')} onchange={() => toggleFilter('type', 'video')}>
|
||||
<span class="ml-2">Video</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add similar sections for color, resolution, and orientation -->
|
||||
|
||||
|
||||
<button
|
||||
class="px-4 py-2 mt-4 text-white bg-red-500 rounded hover:bg-red-600"
|
||||
on:click={clearFilters}
|
||||
onclick={clearFilters}
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</script>
|
||||
|
||||
<header class="fixed top-0 z-50 w-full h-[4.25rem] bg-white border-b shadow-md border-b-white/10 dark:bg-zinc-950/90 backdrop-blur-xl dark:text-white">
|
||||
<div class="flex items-center justify-between px-4 py-1">
|
||||
<div class="flex gap-4 cursor-pointer place-items-center" onkeydown={(e) => { if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0">
|
||||
<div class="flex justify-between items-center px-4 py-1">
|
||||
<div class="flex gap-4 place-items-center cursor-pointer" onkeydown={(e) => { if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0">
|
||||
<img src={browser.runtime.getURL(logo)} class="h-14 {darkMode ? 'hidden' : ''}" alt="Logo" />
|
||||
<img src={browser.runtime.getURL(logoDark)} class="h-14 {darkMode ? '' : 'hidden'}" alt="Dark Logo" />
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="relative flex gap-2">
|
||||
<div class="flex relative gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search themes..."
|
||||
@@ -49,7 +49,7 @@
|
||||
oninput={(e: any) => setSearchTerm(e.target.value)}
|
||||
class="px-4 py-2 pl-10 text-lg transition bg-gray-100/80 rounded-lg ring-0 focus:bg-gray-100/0 dark:focus:bg-zinc-700/50 focus:ring-[1px] ring-zinc-200 dark:ring-zinc-600 dark:bg-zinc-700/80 dark:text-gray-100 focus:outline-none focus:border-transparent" />
|
||||
<svg
|
||||
class="absolute w-5 h-5 text-gray-400 transform -translate-y-1/2 left-3 top-1/2 dark:text-gray-200"
|
||||
class="absolute left-3 top-1/2 w-5 h-5 text-gray-400 transform -translate-y-1/2 dark:text-gray-200"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Theme } from '@/interface/types/Theme'
|
||||
|
||||
let { theme, onClick } = $props<{ theme: Theme; onClick: () => void }>();
|
||||
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -6,12 +8,12 @@
|
||||
|
||||
<div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={onClick}>
|
||||
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border" transition:fade>
|
||||
<div class="absolute z-10 mb-1 text-xl font-bold text-white bottom-1 left-3">
|
||||
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white">
|
||||
{theme.name}
|
||||
</div>
|
||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent'></div>
|
||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-linear-to-t to-transparent from-black/80'></div>
|
||||
<div class='w-full'>
|
||||
<img src={theme.coverImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
|
||||
<img src={theme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-70"
|
||||
class="flex fixed inset-0 z-50 justify-center items-end bg-black/70"
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget) hideModal();
|
||||
}}
|
||||
@@ -79,12 +79,12 @@
|
||||
<h2 class="mb-4 text-2xl font-bold">
|
||||
{theme.name}
|
||||
</h2>
|
||||
<img src={theme.marqueeImage} alt="Theme Cover" class="object-cover w-full mb-4 rounded-md" />
|
||||
<img src={theme.marqueeImage} alt="Theme Cover" class="object-cover mb-4 w-full rounded-md" />
|
||||
<p class="mb-4 text-gray-700 dark:text-gray-300">
|
||||
{theme.description}
|
||||
</p>
|
||||
{#if currentThemes.includes(theme.id)}
|
||||
<button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="relative flex items-center justify-center w-32 px-4 py-2 mt-4 ml-auto text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||
<button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||
{#if installing}
|
||||
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
|
||||
@@ -93,7 +93,7 @@
|
||||
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
|
||||
</button>
|
||||
{:else}
|
||||
<button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="relative flex items-center justify-center w-32 px-4 py-2 mt-4 ml-auto text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||
<button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
|
||||
{#if installing}
|
||||
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
|
||||
@@ -112,11 +112,11 @@
|
||||
{#each getRelatedThemes() as relatedTheme (relatedTheme.id)}
|
||||
<button onclick={() => { hideModal(relatedTheme) }} class="w-full cursor-pointer">
|
||||
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
|
||||
<div class="absolute z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
|
||||
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5">
|
||||
{relatedTheme.name}
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent"></div>
|
||||
<img src={relatedTheme.coverImage} alt="Theme Preview" class="object-cover w-full h-48" />
|
||||
<div class="absolute bottom-0 z-0 w-full h-3/4 to-transparent from-black/80 bg-linear-to-t"></div>
|
||||
<img src={relatedTheme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-48" />
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
onkeydown={onClick}
|
||||
tabindex="-1"
|
||||
role="button"
|
||||
class="relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-zinc-500/50 ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-4 ring-4' : 'ring-0'}"
|
||||
class="relative w-16 h-16 cursor-pointer rounded-xl transition ring-3 dark:ring-zinc-500/50 ring-zinc-300 {isEditMode ? 'animate-shake' : ''} {isSelected ? 'dark:ring-4 ring-4' : 'ring-0'}"
|
||||
>
|
||||
{#if isEditMode}
|
||||
<div
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
||||
import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { OpenThemeCreator } from '@/seqta/ui/ThemeCreator'
|
||||
import shareTheme from '@/seqta/ui/themes/shareTheme'
|
||||
import { InstallTheme } from '@/seqta/ui/themes/downloadTheme'
|
||||
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
|
||||
import { setTheme } from '@/seqta/ui/themes/setTheme'
|
||||
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
|
||||
import { OpenThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
||||
import { OpenStorePage } from '@/seqta/ui/renderStore'
|
||||
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
|
||||
import { closeExtensionPopup } from '@/SEQTA'
|
||||
import { closeExtensionPopup } from '@/seqta/utils/Closers/closeExtensionPopup'
|
||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
let themes = $state<ThemeList | null>(null);
|
||||
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
||||
@@ -20,10 +17,10 @@
|
||||
const handleThemeClick = async (theme: CustomTheme) => {
|
||||
if (isEditMode) return;
|
||||
if (theme.id === themes?.selectedTheme) {
|
||||
await disableTheme();
|
||||
await themeManager.disableTheme();
|
||||
themes.selectedTheme = '';
|
||||
} else {
|
||||
await setTheme(theme.id);
|
||||
await themeManager.setTheme(theme.id);
|
||||
if (!themes) return;
|
||||
themes.selectedTheme = theme.id;
|
||||
}
|
||||
@@ -31,13 +28,13 @@
|
||||
|
||||
const handleThemeDelete = async (themeId: string) => {
|
||||
try {
|
||||
await deleteTheme(themeId);
|
||||
await themeManager.deleteTheme(themeId);
|
||||
if (!themes) return;
|
||||
|
||||
themes.themes = themes.themes.filter(theme => theme.id !== themeId);
|
||||
if (themeId === themes.selectedTheme) {
|
||||
themes.selectedTheme = '';
|
||||
await disableTheme();
|
||||
await themeManager.disableTheme();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting theme:', error);
|
||||
@@ -46,7 +43,7 @@
|
||||
|
||||
const handleShareTheme = async (theme: CustomTheme) => {
|
||||
try {
|
||||
await shareTheme(theme.id);
|
||||
await themeManager.shareTheme(theme.id);
|
||||
} catch (error) {
|
||||
console.error('Error sharing theme:', error);
|
||||
}
|
||||
@@ -72,9 +69,10 @@
|
||||
try {
|
||||
const result = JSON.parse(event.target?.result as string);
|
||||
tempTheme = result;
|
||||
await InstallTheme(result);
|
||||
await themeManager.installTheme(result);
|
||||
await fetchThemes();
|
||||
} catch (error) {
|
||||
console.error('Error parsing file:', error);
|
||||
alert('Error parsing file. Please upload a valid JSON theme file.');
|
||||
}
|
||||
tempTheme = null;
|
||||
@@ -83,7 +81,10 @@
|
||||
}
|
||||
|
||||
const fetchThemes = async () => {
|
||||
themes = await getAvailableThemes();
|
||||
themes = {
|
||||
themes: await themeManager.getAvailableThemes(),
|
||||
selectedTheme: themeManager.getSelectedThemeId() || '',
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@@ -98,7 +99,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full pt-5 mb-1"
|
||||
class="pt-5 mb-1 w-full"
|
||||
role="list"
|
||||
tabindex="-1"
|
||||
ondragover={handleDragOver}
|
||||
@@ -106,9 +107,9 @@
|
||||
ondrop={handleDrop}
|
||||
>
|
||||
<div class="{isDragging ? 'opacity-100' : 'opacity-0'} transition pointer-events-none absolute w-full p-2 z-50">
|
||||
<div class="sticky w-full h-64 bg-white shadow-xl dark:bg-zinc-900 top-5 dark:text-white rounded-xl outline-dashed outline-4 outline-zinc-200 dark:outline-zinc-700">
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="sticky top-5 w-full h-64 bg-white rounded-xl shadow-xl dark:bg-zinc-900 dark:text-white outline-dashed outline-4 outline-zinc-200 dark:outline-zinc-700">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<svg height="48" width="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="currentColor">
|
||||
<path d="M44,31a1,1,0,0,0-1,1v8a3,3,0,0,1-3,3H8a3,3,0,0,1-3-3V32a1,1,0,0,0-2,0v8a5.006,5.006,0,0,0,5,5H40a5.006,5.006,0,0,0,5-5V32A1,1,0,0,0,44,31Z" fill="currentColor"/>
|
||||
@@ -130,7 +131,7 @@
|
||||
>
|
||||
{#if isEditMode}
|
||||
<div
|
||||
class="absolute z-20 flex w-6 h-6 p-2 text-white bg-red-600 rounded-full opacity-100 right-2 place-items-center top-2"
|
||||
class="flex absolute top-2 right-2 z-20 place-items-center p-2 w-6 h-6 text-white bg-red-600 rounded-full opacity-100"
|
||||
onclick={(event) => { event.stopPropagation(); handleThemeDelete(theme.id) }}
|
||||
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleThemeDelete(theme.id) }}
|
||||
role="button"
|
||||
@@ -152,7 +153,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute z-20 flex w-8 h-8 p-2 text-center transition-all -translate-y-1/2 rounded-full opacity-0 text-white/80 top-1/4 right-12 bg-black/50 place-items-center group-hover:opacity-100 group-hover:top-1/2"
|
||||
class="flex absolute right-12 top-1/4 z-20 place-items-center p-2 w-8 h-8 text-center rounded-full opacity-0 transition-all -translate-y-1/2 text-white/80 bg-black/50 group-hover:opacity-100 group-hover:top-1/2"
|
||||
onclick={(event) => { event.stopPropagation(); handleShareTheme(theme) }}
|
||||
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleShareTheme(theme) }}
|
||||
role="button"
|
||||
@@ -167,7 +168,7 @@
|
||||
<img
|
||||
src={typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}
|
||||
alt={theme.name}
|
||||
class="absolute inset-0 z-0 object-cover w-full h-full pointer-events-none"
|
||||
class="object-cover absolute inset-0 z-0 w-full h-full pointer-events-none"
|
||||
/>
|
||||
{/if}
|
||||
{#if !theme.hideThemeName}
|
||||
@@ -179,7 +180,7 @@
|
||||
{/if}
|
||||
|
||||
{#if tempTheme}
|
||||
<div class="flex justify-center w-full bg-gray-200 rounded-xl dark:bg-zinc-700/50 place-items-center aspect-theme animate-pulse">
|
||||
<div class="flex justify-center place-items-center w-full bg-gray-200 rounded-xl animate-pulse dark:bg-zinc-700/50 aspect-theme">
|
||||
<svg class="w-5 h-5 text-white animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
@@ -193,7 +194,7 @@
|
||||
|
||||
<button
|
||||
onclick={() => OpenStorePage()}
|
||||
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
<span class="ml-2">Theme Store</span>
|
||||
@@ -201,7 +202,7 @@
|
||||
|
||||
<button
|
||||
onclick={() => { OpenThemeCreator(); closeExtensionPopup() }}
|
||||
class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white"
|
||||
>
|
||||
<span class="text-xl font-IconFamily"></span>
|
||||
<span class="ml-2">Create your own</span>
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
button {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -48,5 +42,9 @@ input {
|
||||
.cm-editor {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
max-height: 400px;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.editorHeight {
|
||||
height: calc(100vh - 58px);
|
||||
}
|
||||
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BetterSEQTA+ Settings</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<body class="h-[600px]">
|
||||
<div id="app" style="height: 100%;"></div>
|
||||
<script type="module" src="./index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-19
@@ -1,25 +1,8 @@
|
||||
import "./index.css"
|
||||
import { mount } from "svelte"
|
||||
import type { ComponentType } from "svelte"
|
||||
import Settings from "./pages/settings.svelte"
|
||||
import IconFamily from '@/resources/fonts/IconFamily.woff'
|
||||
import browser from "webextension-polyfill"
|
||||
|
||||
export default function renderSvelte(
|
||||
Component: ComponentType | any,
|
||||
mountPoint: ShadowRoot | HTMLElement,
|
||||
props: Record<string, any> = {},
|
||||
) {
|
||||
const app = mount(Component, {
|
||||
target: mountPoint,
|
||||
props: {
|
||||
standalone: true,
|
||||
...props,
|
||||
},
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
import renderSvelte from "./main"
|
||||
|
||||
function InjectCustomIcons() {
|
||||
console.info('[BetterSEQTA+] Injecting Icons')
|
||||
@@ -43,4 +26,4 @@ if (!mountPoint) {
|
||||
}
|
||||
|
||||
InjectCustomIcons()
|
||||
renderSvelte(Settings, mountPoint)
|
||||
renderSvelte(Settings, mountPoint, { standalone: true })
|
||||
@@ -1,6 +1,6 @@
|
||||
import styles from "./index.css?inline"
|
||||
import { mount } from "svelte"
|
||||
import type { ComponentType } from "svelte"
|
||||
import style from './index.css?inline'
|
||||
|
||||
export default function renderSvelte(
|
||||
Component: ComponentType | any,
|
||||
@@ -15,10 +15,9 @@ export default function renderSvelte(
|
||||
},
|
||||
})
|
||||
|
||||
const style = document.createElement("style")
|
||||
style.setAttribute("type", "text/css")
|
||||
style.innerHTML = styles
|
||||
mountPoint.appendChild(style)
|
||||
const styleElement = document.createElement('style')
|
||||
styleElement.textContent = style
|
||||
mountPoint.appendChild(styleElement)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
import { onMount } from 'svelte'
|
||||
import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
|
||||
import { closeExtensionPopup, OpenAboutPage, OpenWhatsNewPopup } from "@/SEQTA"
|
||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
||||
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage"
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew"
|
||||
|
||||
import ColourPicker from '../components/ColourPicker.svelte'
|
||||
import { settingsPopup } from '../hooks/SettingsPopup'
|
||||
|
||||
@@ -56,13 +59,14 @@
|
||||
|
||||
if (!standalone) return;
|
||||
initializeSettingsState();
|
||||
console.log('settingsState', $settingsState);
|
||||
StandaloneStore.setStandalone(true);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode ? 'dark' : ''} { standalone ? 'h-[600px]' : 'h-full rounded-xl' } overflow-clip">
|
||||
<div class="relative flex flex-col h-full gap-2 bg-white overflow-clip dark:bg-zinc-800 dark:text-white">
|
||||
<div class="grid border-b border-b-zinc-200/40 place-items-center">
|
||||
<div class="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white">
|
||||
<div class="grid place-items-center border-b border-b-zinc-200/40">
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" onclick={handleDevModeToggle} />
|
||||
@@ -71,8 +75,8 @@
|
||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" onclick={handleDevModeToggle} />
|
||||
|
||||
{#if !standalone}
|
||||
<button onclick={openChangelog} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-1 bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
|
||||
<button onclick={openAbout} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-10 bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</button>
|
||||
<button onclick={openChangelog} class="absolute top-1 right-1 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
|
||||
<button onclick={openAbout} class="absolute top-1 right-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,17 +5,78 @@
|
||||
import Select from "@/interface/components/Select.svelte"
|
||||
|
||||
import browser from "webextension-polyfill"
|
||||
|
||||
|
||||
import type { SettingsList } from "@/interface/types/SettingsProps"
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
||||
|
||||
import { getAllPluginSettings } from "@/plugins"
|
||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting } from "@/plugins/core/types"
|
||||
|
||||
// Union type representing all possible settings
|
||||
type SettingType =
|
||||
(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: string[]
|
||||
});
|
||||
|
||||
interface Plugin {
|
||||
pluginId: string;
|
||||
name: string;
|
||||
description: string;
|
||||
settings: Record<string, SettingType>;
|
||||
}
|
||||
|
||||
const pluginSettings = getAllPluginSettings() as Plugin[];
|
||||
const pluginSettingsValues = $state<Record<string, Record<string, any>>>({});
|
||||
|
||||
async function loadPluginSettings() {
|
||||
for (const plugin of pluginSettings) {
|
||||
if (Object.keys(plugin.settings).length === 0) continue;
|
||||
|
||||
const storageKey = `plugin.${plugin.pluginId}.settings`;
|
||||
const stored = await browser.storage.local.get(storageKey);
|
||||
|
||||
pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {};
|
||||
|
||||
for (const [key, setting] of Object.entries(plugin.settings)) {
|
||||
if (pluginSettingsValues[plugin.pluginId][key] === undefined) {
|
||||
pluginSettingsValues[plugin.pluginId][key] = setting.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePluginSetting(pluginId: string, key: string, value: any) {
|
||||
const storageKey = `plugin.${pluginId}.settings`;
|
||||
|
||||
if (!pluginSettingsValues[pluginId]) {
|
||||
pluginSettingsValues[pluginId] = {};
|
||||
}
|
||||
pluginSettingsValues[pluginId][key] = value;
|
||||
|
||||
const stored = await browser.storage.local.get(storageKey);
|
||||
const currentSettings = (stored[storageKey] || {}) as Record<string, any>;
|
||||
|
||||
currentSettings[key] = value;
|
||||
|
||||
await browser.storage.local.set({ [storageKey]: currentSettings });
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadPluginSettings();
|
||||
})
|
||||
|
||||
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
||||
</script>
|
||||
|
||||
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
||||
<div class="flex items-center justify-between px-4 py-3">
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<div class="pr-4">
|
||||
<h2 class="text-sm font-bold">{title}</h2>
|
||||
<p class="text-xs">{description}</p>
|
||||
@@ -38,26 +99,6 @@
|
||||
onChange: (isOn: boolean) => settingsState.transparencyEffects = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Animated Background",
|
||||
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
|
||||
id: 2,
|
||||
Component: Switch,
|
||||
props: {
|
||||
state: $settingsState.animatedbk,
|
||||
onChange: (isOn: boolean) => settingsState.animatedbk = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Animated Background Speed",
|
||||
description: "Controls the speed of the animated background.",
|
||||
id: 3,
|
||||
Component: Slider,
|
||||
props: {
|
||||
state: $settingsState.bksliderinput,
|
||||
onChange: (value: number) => settingsState.bksliderinput = `${value}`
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Custom Theme Colour",
|
||||
description: "Customise the overall theme colour of SEQTA Learn.",
|
||||
@@ -87,36 +128,6 @@
|
||||
onChange: (isOn: boolean) => settingsState.animations = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Notification Collector",
|
||||
description: "Uncaps the 9+ limit for notifications, showing the real number.",
|
||||
id: 7,
|
||||
Component: Switch,
|
||||
props: {
|
||||
state: $settingsState.notificationcollector,
|
||||
onChange: (isOn: boolean) => settingsState.notificationcollector = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Assessment Average",
|
||||
description: "Shows your subject average for assessments.",
|
||||
id: 8,
|
||||
Component: Switch,
|
||||
props: {
|
||||
state: $settingsState.assessmentsAverage,
|
||||
onChange: (isOn: boolean) => settingsState.assessmentsAverage = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Lesson Alerts",
|
||||
description: "Sends a native browser notification ~5 minutes prior to lessons.",
|
||||
id: 8,
|
||||
Component: Switch,
|
||||
props: {
|
||||
state: $settingsState.lessonalert,
|
||||
onChange: (isOn: boolean) => settingsState.lessonalert = isOn
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "12 Hour Time",
|
||||
description: "Prefer 12 hour time format for SEQTA",
|
||||
@@ -147,18 +158,108 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "BetterSEQTA+",
|
||||
description: "Enables BetterSEQTA+ features",
|
||||
title: "News Feed Source",
|
||||
description: "Choose sources of your news feed.",
|
||||
id: 11,
|
||||
Component: Switch,
|
||||
Component: Select,
|
||||
props: {
|
||||
state: $settingsState.onoff,
|
||||
onChange: (isOn: boolean) => settingsState.onoff = isOn
|
||||
state: $settingsState.newsSource,
|
||||
onChange: (value: string) => settingsState.newsSource = value,
|
||||
options: [
|
||||
{ value: "australia", label: "Australia" },
|
||||
{ value: "usa", label: "USA" },
|
||||
{ value: "taiwan", label: "Taiwan" },
|
||||
{ value: "hong_kong", label: "Hong Kong" },
|
||||
{ value: "panama", label: "Panama" },
|
||||
{ value: "canada", label: "Canada" },
|
||||
{ value: "singapore", label: "Singapore" },
|
||||
{ value: "uk", label: "UK" },
|
||||
{ value: "japan", label: "Japan" },
|
||||
{ value: "netherlands", label: "Netherlands" }
|
||||
]
|
||||
}
|
||||
}
|
||||
] as option}
|
||||
{@render Setting(option)}
|
||||
{/each}
|
||||
|
||||
{#each pluginSettings as plugin}
|
||||
<div>
|
||||
<!-- Always show enable toggle if disableToggle is true -->
|
||||
{#if (plugin as any).disableToggle}
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<div class="pr-4">
|
||||
<h2 class="text-sm font-bold">Enable {plugin.name}</h2>
|
||||
<p class="text-xs">{plugin.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Switch
|
||||
state={pluginSettingsValues[plugin.pluginId]?.enabled ?? true}
|
||||
onChange={(value) => updatePluginSetting(plugin.pluginId, 'enabled', value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Only show other settings if plugin is enabled or has no disableToggle -->
|
||||
{#if !((plugin as any).disableToggle) || (pluginSettingsValues[plugin.pluginId]?.enabled ?? true)}
|
||||
{#each Object.entries(plugin.settings) as [key, setting]}
|
||||
<!-- Skip the 'enabled' setting if it's part of the settings object -->
|
||||
{#if key !== 'enabled'}
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<div class="pr-4">
|
||||
<h2 class="text-sm font-bold">{setting.title || key}</h2>
|
||||
<p class="text-xs">{setting.description || ''}</p>
|
||||
</div>
|
||||
<div>
|
||||
{#if setting.type === 'boolean'}
|
||||
<Switch
|
||||
state={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||
/>
|
||||
{:else if setting.type === 'number'}
|
||||
<Slider
|
||||
state={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||
min={setting.min}
|
||||
max={setting.max}
|
||||
step={setting.step}
|
||||
/>
|
||||
{:else if setting.type === 'string'}
|
||||
<input
|
||||
type="text"
|
||||
class="px-2 py-1 text-sm rounded-md dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white"
|
||||
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||
oninput={(e) => updatePluginSetting(plugin.pluginId, key, e.currentTarget.value)}
|
||||
/>
|
||||
{:else if setting.type === 'select'}
|
||||
<Select
|
||||
state={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||
options={(setting.options as string[]).map(opt => ({
|
||||
value: opt,
|
||||
label: opt.charAt(0).toUpperCase() + opt.slice(1)
|
||||
}))}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{@render Setting({
|
||||
title: "BetterSEQTA+",
|
||||
description: "Enables BetterSEQTA+ features",
|
||||
id: 12,
|
||||
Component: Switch,
|
||||
props: {
|
||||
state: $settingsState.onoff,
|
||||
onChange: (isOn: boolean) => settingsState.onoff = isOn
|
||||
}
|
||||
})}
|
||||
|
||||
{#if $settingsState.devMode}
|
||||
<div class="flex items-center justify-between px-4 py-3 mt-4 pt-[1.75rem]">
|
||||
@@ -170,7 +271,7 @@
|
||||
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-4 py-3">
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<div class="pr-4">
|
||||
<h2 class="text-sm font-bold">Sensitive Hider</h2>
|
||||
<p class="text-xs">Replace sensitive content with mock data</p>
|
||||
@@ -183,4 +284,4 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,16 +9,15 @@
|
||||
import type { Theme } from '../types/Theme'
|
||||
import browser from 'webextension-polyfill'
|
||||
import ThemeModal from '../components/store/ThemeModal.svelte'
|
||||
import { StoreDownloadTheme } from '@/seqta/ui/themes/downloadTheme'
|
||||
import { setTheme } from '@/seqta/ui/themes/setTheme'
|
||||
import Header from '../components/store/Header.svelte'
|
||||
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
|
||||
import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'
|
||||
import { themeUpdates } from '../hooks/ThemeUpdates'
|
||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||
|
||||
import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
|
||||
import Backgrounds from '../components/store/Backgrounds.svelte'
|
||||
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
// State variables
|
||||
let searchTerm = $state('');
|
||||
let themes = $state<Theme[]>([]);
|
||||
@@ -33,8 +32,8 @@
|
||||
let selectedBackground = $state<string | null>(null);
|
||||
|
||||
const fetchCurrentThemes = async () => {
|
||||
const themes = await getAvailableThemes();
|
||||
currentThemes = themes.themes.filter(theme => theme !== null).map(theme => theme.id);
|
||||
const themes = await themeManager.getAvailableThemes();
|
||||
currentThemes = themes.filter(theme => theme !== null).map(theme => theme.id);
|
||||
};
|
||||
|
||||
const setDisplayTheme = (theme: Theme | null) => {
|
||||
@@ -123,8 +122,8 @@
|
||||
{setDisplayTheme}
|
||||
onInstall={async () => {
|
||||
if (displayTheme) {
|
||||
await StoreDownloadTheme({themeContent: displayTheme})
|
||||
setTheme(displayTheme.id);
|
||||
await themeManager.downloadTheme(displayTheme);
|
||||
await themeManager.setTheme(displayTheme.id);
|
||||
themeUpdates.triggerUpdate();
|
||||
await fetchCurrentThemes();
|
||||
}
|
||||
@@ -132,7 +131,7 @@
|
||||
onRemove={async () => {
|
||||
if (displayTheme?.id) {
|
||||
console.debug('deleting theme', displayTheme.id);
|
||||
deleteTheme(displayTheme.id)
|
||||
await themeManager.deleteTheme(displayTheme.id);
|
||||
themeUpdates.triggerUpdate();
|
||||
await fetchCurrentThemes();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { type LoadedCustomTheme } from '@/types/CustomThemes'
|
||||
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
import { getTheme } from '@/seqta/ui/themes/getTheme'
|
||||
|
||||
import Divider from '@/interface/components/themeCreator/divider.svelte'
|
||||
import Switch from '@/interface/components/Switch.svelte'
|
||||
@@ -22,13 +21,13 @@
|
||||
handleImageVariableChange,
|
||||
handleCoverImageUpload
|
||||
} from '../utils/themeImageHandlers';
|
||||
import { ClearThemePreview, UpdateThemePreview } from '@/seqta/ui/themes/UpdateThemePreview'
|
||||
import { saveTheme } from '@/seqta/ui/themes/saveTheme'
|
||||
import { CloseThemeCreator } from '@/seqta/ui/ThemeCreator'
|
||||
import { CloseThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
||||
import { themeUpdates } from '../hooks/ThemeUpdates'
|
||||
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
|
||||
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||
|
||||
const { themeID } = $props<{ themeID: string }>()
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
let theme = $state<LoadedCustomTheme>({
|
||||
id: uuidv4(),
|
||||
name: '',
|
||||
@@ -45,8 +44,19 @@
|
||||
})
|
||||
let closedAccordions = $state<string[]>([])
|
||||
let themeLoaded = $state(false);
|
||||
let codeEditorFullscreen = $state(false);
|
||||
|
||||
function toggleCodeEditorFullscreen(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
codeEditorFullscreen = !codeEditorFullscreen;
|
||||
}
|
||||
|
||||
function toggleAccordion(title: string, e: MouseEvent | KeyboardEvent) {
|
||||
// if the target is the fullscreen button return
|
||||
if (e.target instanceof HTMLButtonElement && e.target.classList.contains('fullscreen-toggle')) {
|
||||
return;
|
||||
}
|
||||
|
||||
function toggleAccordion(title: string) {
|
||||
if (closedAccordions.includes(title)) {
|
||||
closedAccordions = closedAccordions.filter(t => t !== title);
|
||||
} else {
|
||||
@@ -55,10 +65,10 @@
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
disableTheme();
|
||||
await themeManager.disableTheme();
|
||||
|
||||
if (themeID) {
|
||||
const tempTheme = await getTheme(themeID)
|
||||
const tempTheme = await themeManager.getTheme(themeID)
|
||||
|
||||
if (!tempTheme) return
|
||||
|
||||
@@ -66,16 +76,12 @@
|
||||
const loadedTheme = {
|
||||
...tempTheme,
|
||||
CustomImages: tempTheme.CustomImages.map(image => ({
|
||||
...image,
|
||||
url: image.blob ? URL.createObjectURL(image.blob) : null
|
||||
})),
|
||||
coverImageUrl: tempTheme.coverImage ? URL.createObjectURL(tempTheme.coverImage) : undefined
|
||||
...image
|
||||
}))
|
||||
}
|
||||
|
||||
if (tempTheme) {
|
||||
theme = loadedTheme
|
||||
themeLoaded = true
|
||||
}
|
||||
theme = loadedTheme
|
||||
themeLoaded = true
|
||||
} else {
|
||||
themeLoaded = true
|
||||
}
|
||||
@@ -99,7 +105,7 @@
|
||||
theme = await handleCoverImageUpload(event, theme);
|
||||
}
|
||||
|
||||
function submitTheme() {
|
||||
async function submitTheme() {
|
||||
const themeClone = JSON.parse(JSON.stringify(theme));
|
||||
|
||||
// re-insert blobs into themeClone
|
||||
@@ -109,14 +115,17 @@
|
||||
}))
|
||||
themeClone.coverImage = theme.coverImage
|
||||
|
||||
ClearThemePreview();
|
||||
saveTheme(themeClone);
|
||||
themeManager.clearPreview();
|
||||
await themeManager.saveTheme(themeClone);
|
||||
await themeManager.setTheme(themeClone.id);
|
||||
themeUpdates.triggerUpdate();
|
||||
CloseThemeCreator();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
UpdateThemePreview(theme);
|
||||
if (themeLoaded) {
|
||||
void themeManager.updatePreviewDebounced(theme);
|
||||
}
|
||||
});
|
||||
|
||||
type SettingType = 'switch' | 'button' | 'slider' | 'colourPicker' | 'select' | 'codeEditor' | 'imageUpload' | 'conditional' | 'lightDarkToggle';
|
||||
@@ -156,8 +165,8 @@
|
||||
<div class="flex justify-between {item.direction === 'vertical' ? 'flex-col items-start' : 'items-center'} py-3">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
onclick={() => { item.direction === 'vertical' && toggleAccordion(item.title) }}
|
||||
onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title) }}
|
||||
onclick={(e) => { item.direction === 'vertical' && toggleAccordion(item.title, e) }}
|
||||
onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title, e) }}
|
||||
class="flex justify-between pr-4 {item.direction === 'vertical' ? 'cursor-pointer w-full select-none' : ''}">
|
||||
|
||||
<div>
|
||||
@@ -166,7 +175,14 @@
|
||||
</div>
|
||||
|
||||
{#if item.direction === 'vertical'}
|
||||
<div class="flex items-center justify-center h-full text-xl font-light text-zinc-500 dark:text-zinc-300">
|
||||
<div class="flex justify-center items-center h-full text-xl font-light text-zinc-500 dark:text-zinc-300">
|
||||
{#if item.type === 'codeEditor'}
|
||||
<!-- Fullscreen toggle button -->
|
||||
<button onclick={toggleCodeEditorFullscreen} class="px-2 mr-2 text-lg font-IconFamily fullscreen-toggle">
|
||||
{'\uebdb'}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<span class='font-IconFamily transition-transform duration-300 {closedAccordions.includes(item.title) ? 'rotate-180' : ''}'>{'\ue9e6'}</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -185,21 +201,24 @@
|
||||
<ColourPicker savePresets={false} standalone={true} {...(item.props)} />
|
||||
{/key}
|
||||
{:else if item.type === 'codeEditor'}
|
||||
{#key themeLoaded}
|
||||
<CodeEditor {...(item.props as CodeEditorProps)} />
|
||||
{/key}
|
||||
{#if !codeEditorFullscreen}
|
||||
{#key themeLoaded}
|
||||
<!-- Only render inline if not fullscreen -->
|
||||
<CodeEditor className="h-[400px]" {...(item.props as CodeEditorProps)} />
|
||||
{/key}
|
||||
{/if}
|
||||
{:else if item.type === 'imageUpload'}
|
||||
{#each theme.CustomImages as image (image.id)}
|
||||
<div class="flex items-center h-16 gap-2 px-2 py-2 mb-4 bg-white rounded-lg shadow-lg dark:bg-zinc-700">
|
||||
<div class="h-full ">
|
||||
<img src={image.url} alt={image.variableName} class="object-contain h-full rounded" />
|
||||
<div class="flex gap-2 items-center px-2 py-2 mb-4 h-16 bg-white rounded-lg shadow-lg dark:bg-zinc-700">
|
||||
<div class="h-full">
|
||||
<img src={URL.createObjectURL(image.blob)} alt={image.variableName} class="object-contain h-full rounded" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={image.variableName}
|
||||
oninput={(e) => onImageVariableChange(image.id, e.currentTarget.value)}
|
||||
placeholder="CSS Variable Name"
|
||||
class="flex-grow flex-[3] w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-600/50 focus:bg-zinc-300/50 dark:focus:bg-zinc-600"
|
||||
class="p-2 w-full rounded-lg border-0 transition grow flex-3 dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-600/50 focus:bg-zinc-300/50 dark:focus:bg-zinc-600"
|
||||
/>
|
||||
<button onclick={() => onRemoveImage(image.id)} class="p-2 transition dark:text-white">
|
||||
<span class='text-xl font-IconFamily'>{'\ued8c'}</span>
|
||||
@@ -207,14 +226,14 @@
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="relative flex justify-center w-full h-8 gap-1 overflow-hidden transition rounded-lg place-items-center bg-zinc-200 dark:bg-zinc-700">
|
||||
<div class="flex overflow-hidden relative gap-1 justify-center place-items-center w-full h-8 rounded-lg transition bg-zinc-200 dark:bg-zinc-700">
|
||||
<span class='font-IconFamily'>{'\uec60'}</span>
|
||||
<span class='dark:text-white'>Add image</span>
|
||||
<input type="file" accept='image/*' onchange={onImageUpload} class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
</div>
|
||||
{:else if item.type === 'lightDarkToggle'}
|
||||
<button
|
||||
class="relative px-4 py-1 overflow-hidden text-xl font-medium transition rounded-lg bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 font-IconFamily"
|
||||
class="overflow-hidden relative px-4 py-1 text-xl font-medium rounded-lg transition bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 font-IconFamily"
|
||||
onclick={() => (item.props as LightDarkToggleProps).onChange(!(item.props as LightDarkToggleProps).state)}
|
||||
>
|
||||
{#key (item.props as LightDarkToggleProps).state}
|
||||
@@ -236,10 +255,23 @@
|
||||
{/snippet}
|
||||
|
||||
<div class='h-screen overflow-y-scroll {$settingsState.DarkMode && "dark"} no-scrollbar'>
|
||||
<div class='flex flex-col w-full min-h-screen p-2 bg-zinc-100 dark:bg-zinc-800 dark:text-white'>
|
||||
{#if codeEditorFullscreen}
|
||||
<div class="absolute inset-0 bg-white z-[10000] dark:bg-zinc-900 dark:text-white">
|
||||
<div class="sticky top-0 px-2 h-screen">
|
||||
<div class="flex justify-between items-center my-4">
|
||||
<h2 class="text-xl font-bold">Custom CSS</h2>
|
||||
<button onclick={toggleCodeEditorFullscreen} class="pr-14 text-xl font-IconFamily">{'\uec06'}</button>
|
||||
</div>
|
||||
<CodeEditor className="editorHeight" value={theme.CustomCSS} onChange={(value: string) => { theme = { ...theme, CustomCSS: value } }} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class='flex relative flex-col p-2 w-full min-h-screen bg-zinc-100 dark:bg-zinc-800 dark:text-white'>
|
||||
|
||||
|
||||
<h1 class='text-xl font-semibold'>Theme Creator</h1>
|
||||
<a href='https://betterseqta.gitbook.io/betterseqta-docs' target='_blank' class='text-sm font-light text-zinc-500 dark:text-zinc-400'>
|
||||
<span class='no-underline font-IconFamily pr-0.5'>{'\ueb44'}</span>
|
||||
<span class='pr-0.5 no-underline font-IconFamily'>{'\ueb44'}</span>
|
||||
<span class='underline'>
|
||||
Need help? Check out the docs!
|
||||
</span>
|
||||
@@ -254,7 +286,7 @@
|
||||
type='text'
|
||||
placeholder='What is your theme called?'
|
||||
bind:value={theme.name}
|
||||
class='w-full p-2 mb-4 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600' />
|
||||
class='p-2 mb-4 w-full rounded-lg border-0 transition dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -263,23 +295,23 @@
|
||||
id='themeDescription'
|
||||
placeholder="Don't worry, this one's optional!"
|
||||
bind:value={theme.description}
|
||||
class='w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:outline-none focus:ring-1 focus:ring-zinc-100 dark:focus:ring-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600'></textarea>
|
||||
class='p-2 w-full rounded-lg border-0 transition dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:outline-none focus:ring-1 focus:ring-zinc-100 dark:focus:ring-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600'></textarea>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div class="relative flex justify-center w-full gap-1 overflow-hidden transition rounded-lg aspect-theme group place-items-center bg-zinc-200 dark:bg-zinc-700">
|
||||
<div class="flex overflow-hidden relative gap-1 justify-center place-items-center w-full rounded-lg transition aspect-theme group bg-zinc-200 dark:bg-zinc-700">
|
||||
<div class={`transition pointer-events-none z-30 font-IconFamily ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>
|
||||
{'\uec60'}
|
||||
</div>
|
||||
<span class={`dark:text-white pointer-events-none z-30 transition ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>{theme.coverImage ? 'Change' : 'Add'} cover image</span>
|
||||
<input type="file" accept='image/*' onchange={onCoverImageUpload} class="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer" />
|
||||
{#if !theme.hideThemeName && theme.coverImage}
|
||||
<div class="absolute z-30 transition-opacity opacity-100 pointer-events-none group-hover:opacity-0">{theme.name}</div>
|
||||
<div class="absolute z-30 opacity-100 transition-opacity pointer-events-none group-hover:opacity-0">{theme.name}</div>
|
||||
{/if}
|
||||
{#if theme.coverImage}
|
||||
<div class="absolute z-20 w-full h-full transition-opacity opacity-0 pointer-events-none group-hover:opacity-100 bg-black/20"></div>
|
||||
<img src={theme.coverImageUrl} alt='Cover' class="absolute z-0 object-cover w-full h-full rounded" />
|
||||
<div class="absolute z-20 w-full h-full opacity-0 transition-opacity pointer-events-none group-hover:opacity-100 bg-black/20"></div>
|
||||
<img src="{typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}" alt='Cover' class="object-cover absolute z-0 w-full h-full rounded" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promi
|
||||
const variableName = `custom-image-${theme.CustomImages.length}`;
|
||||
resolve({
|
||||
...theme,
|
||||
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: URL.createObjectURL(imageBlob) }],
|
||||
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: null }],
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
@@ -51,7 +51,7 @@ export function handleCoverImageUpload(event: Event, theme: LoadedCustomTheme):
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
|
||||
resolve({ ...theme, coverImage: imageBlob, coverImageUrl: URL.createObjectURL(imageBlob) });
|
||||
resolve({ ...theme, coverImage: imageBlob });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createManifest } from '../../lib/createManifest'
|
||||
import baseManifest from './manifest.json'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
export const brave = createManifest(baseManifest, 'brave')
|
||||
export const brave = createManifest({
|
||||
...baseManifest,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
}, 'brave')
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createManifest } from '../../lib/createManifest'
|
||||
import baseManifest from './manifest.json'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
export const chrome = createManifest(baseManifest, 'chrome')
|
||||
export const chrome = createManifest({
|
||||
...baseManifest,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
}, 'chrome')
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createManifest } from '../../lib/createManifest'
|
||||
import baseManifest from './manifest.json'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
export const edge = createManifest(baseManifest, 'edge')
|
||||
export const edge = createManifest({
|
||||
...baseManifest,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
}, 'edge')
|
||||
|
||||
@@ -4,6 +4,8 @@ import pkg from '../../package.json'
|
||||
|
||||
const updatedFirefoxManifest = {
|
||||
...baseManifest,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
background: {
|
||||
scripts: [baseManifest.background.service_worker],
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "BetterSEQTA+",
|
||||
"version": "3.4.1",
|
||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
||||
"icons": {
|
||||
"32": "resources/icons/icon-32.png",
|
||||
"48": "resources/icons/icon-48.png",
|
||||
@@ -34,11 +32,11 @@
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["*://*/*"],
|
||||
"resources": ["*/*"],
|
||||
"matches": ["*://*/*"]
|
||||
},
|
||||
{
|
||||
"resources": ["resources/icons/*"],
|
||||
"resources": ["resources/*"],
|
||||
"matches": ["*://*/*"]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { createManifest } from '../../lib/createManifest'
|
||||
import baseManifest from './manifest.json'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
export const opera = createManifest(baseManifest, 'opera')
|
||||
export const opera = createManifest({
|
||||
...baseManifest,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
}, 'opera')
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createManifest } from '../../lib/createManifest'
|
||||
import baseManifest from './manifest.json'
|
||||
import pkg from '../../package.json'
|
||||
|
||||
const updatedSafariManifest = {
|
||||
...baseManifest,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
browser_specific_settings: {
|
||||
safari: {
|
||||
strict_min_version: '15.4',
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
class ReactFiber {
|
||||
constructor(selector, options = {}) {
|
||||
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));
|
||||
|
||||
if (this.debug) {
|
||||
console.log("Selected Nodes:", this.nodes);
|
||||
console.log("🔍 Found Fibers:", this.fibers);
|
||||
console.log("🛠 Found Components:", this.components);
|
||||
}
|
||||
}
|
||||
|
||||
static find(selector, options = {}) {
|
||||
return new ReactFiber(selector, options);
|
||||
}
|
||||
|
||||
getFiberNode(node) {
|
||||
if (!node) return null;
|
||||
const fiberKey = Object.getOwnPropertyNames(node).find(name =>
|
||||
name.startsWith('__reactFiber') || name.startsWith('__reactInternalInstance')
|
||||
);
|
||||
return fiberKey ? node[fiberKey] : null;
|
||||
}
|
||||
|
||||
getOwnerComponent(fiberNode) {
|
||||
let current = fiberNode;
|
||||
while (current) {
|
||||
if (current.stateNode && (current.stateNode.setState || current.stateNode.forceUpdate)) {
|
||||
return current.stateNode;
|
||||
}
|
||||
current = current.return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getState(key) {
|
||||
if (!this.components.length) return null;
|
||||
const state = this.components[0]?.state || null;
|
||||
|
||||
if (key === undefined) {
|
||||
return state;
|
||||
} else if (typeof key === 'string') {
|
||||
return state?.[key];
|
||||
} else if (Array.isArray(key)) {
|
||||
const filteredState = {};
|
||||
for (const k of key) {
|
||||
if (state && Object.hasOwn(state, k)) {
|
||||
filteredState[k] = state[k];
|
||||
}
|
||||
}
|
||||
return filteredState;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setState(update) {
|
||||
this.components.forEach(component => {
|
||||
if (component?.setState) {
|
||||
if (typeof update === 'function') {
|
||||
// Functional update
|
||||
component.setState(prevState => {
|
||||
const newState = update(prevState);
|
||||
if (this.debug) console.log("✅ Updated State (Functional):", newState);
|
||||
return newState;
|
||||
});
|
||||
} else {
|
||||
// Object update (merge with existing state)
|
||||
component.setState(prevState => {
|
||||
const newState = {
|
||||
...prevState,
|
||||
...update
|
||||
};
|
||||
if (this.debug) console.log("✅ Updated State (Object Merge):", newState);
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
getProp(propName) {
|
||||
if (!this.fibers.length) return null;
|
||||
|
||||
if (propName === undefined) {
|
||||
return this.fibers[0]?.memoizedProps;
|
||||
}
|
||||
|
||||
return this.fibers[0]?.memoizedProps?.[propName];
|
||||
}
|
||||
|
||||
setProp(propName) {
|
||||
this.fibers.forEach(fiber => {
|
||||
if (fiber?.memoizedProps) {
|
||||
fiber.memoizedProps[propName] = value;
|
||||
}
|
||||
});
|
||||
return this; // Enable chaining
|
||||
}
|
||||
|
||||
forceUpdate() {
|
||||
this.components.forEach(component => {
|
||||
if (component?.forceUpdate) {
|
||||
component.forceUpdate();
|
||||
if (this.debug) console.log("🔄 Forced React Re-render");
|
||||
}
|
||||
});
|
||||
return this; // Enable chaining
|
||||
}
|
||||
}
|
||||
|
||||
function makeSerializable(obj) {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => makeSerializable(item));
|
||||
}
|
||||
|
||||
const serializableObj = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
let value = obj[key];
|
||||
|
||||
if (typeof value === 'function') {
|
||||
value = '[Function]';
|
||||
} else if (value instanceof HTMLElement) {
|
||||
value = {
|
||||
type: 'HTMLElement',
|
||||
id: value.id,
|
||||
tagName: value.tagName
|
||||
}; // Replace DOM node with ID/tag info
|
||||
} else if (typeof value === 'symbol') {
|
||||
value = value.toString();
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
value = makeSerializable(value);
|
||||
}
|
||||
|
||||
serializableObj[key] = value;
|
||||
}
|
||||
}
|
||||
return serializableObj;
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === "reactFiberRequest") {
|
||||
const {
|
||||
selector,
|
||||
action,
|
||||
payload,
|
||||
debug,
|
||||
messageId
|
||||
} = event.data;
|
||||
const fiberInstance = ReactFiber.find(selector, {
|
||||
debug
|
||||
});
|
||||
|
||||
let response;
|
||||
switch (action) {
|
||||
case "getState":
|
||||
response = fiberInstance.getState(payload.key);
|
||||
break;
|
||||
case "setState":
|
||||
// Handle both function and object updates
|
||||
if (payload.updateFn) {
|
||||
const updateFn = eval(`(${payload.updateFn})`);
|
||||
fiberInstance.setState(updateFn);
|
||||
} else {
|
||||
fiberInstance.setState(payload.updateObject);
|
||||
}
|
||||
response = {};
|
||||
break;
|
||||
|
||||
case "getProp":
|
||||
response = fiberInstance.getProp(payload.propName);
|
||||
break;
|
||||
case "setProp":
|
||||
fiberInstance.setProp(payload.propName, payload.value);
|
||||
response = {};
|
||||
break;
|
||||
case "forceUpdate":
|
||||
fiberInstance.forceUpdate();
|
||||
response = {};
|
||||
break;
|
||||
default:
|
||||
console.warn(`[pageState] Unknown action: ${action}`);
|
||||
response = null;
|
||||
}
|
||||
|
||||
if (response !== null && typeof response === 'object') {
|
||||
response = makeSerializable(response);
|
||||
}
|
||||
|
||||
window.postMessage({
|
||||
type: "reactFiberResponse",
|
||||
response,
|
||||
messageId,
|
||||
}, "*");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
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({
|
||||
default: 1,
|
||||
title: "Animation Speed",
|
||||
description: "Controls how fast the background moves",
|
||||
min: 0.1,
|
||||
max: 2,
|
||||
step: 0.05
|
||||
})
|
||||
});
|
||||
|
||||
class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.speed)
|
||||
speed!: number;
|
||||
}
|
||||
|
||||
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',
|
||||
disableToggle: true,
|
||||
styles: styles,
|
||||
settings: instance.settings,
|
||||
|
||||
run: async (api) => {
|
||||
// Create the background elements
|
||||
const container = document.getElementById("container");
|
||||
const menu = document.getElementById("menu");
|
||||
|
||||
if (!container || !menu) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const backgrounds = [
|
||||
{ classes: ["bg"] },
|
||||
{ classes: ["bg", "bg2"] },
|
||||
{ classes: ["bg", "bg3"] }
|
||||
];
|
||||
|
||||
backgrounds.forEach(({ classes }) => {
|
||||
const bk = document.createElement("div");
|
||||
classes.forEach(cls => bk.classList.add(cls));
|
||||
container.insertBefore(bk, menu);
|
||||
});
|
||||
|
||||
// Set initial speed
|
||||
updateAnimationSpeed(api.settings.speed);
|
||||
|
||||
// Listen for speed changes
|
||||
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());
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function updateAnimationSpeed(speed: number) {
|
||||
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;
|
||||
@@ -0,0 +1,31 @@
|
||||
.bg {
|
||||
animation: slide 3s ease-in-out infinite alternate;
|
||||
background: var(--better-main);
|
||||
bottom: 0;
|
||||
left: -50%;
|
||||
opacity: 0.5;
|
||||
position: fixed;
|
||||
right: -50%;
|
||||
top: 0;
|
||||
z-index: 0 !important;
|
||||
overflow: hidden;
|
||||
scale: 1.5;
|
||||
}
|
||||
|
||||
.bg2 {
|
||||
animation-direction: alternate-reverse;
|
||||
animation-duration: 4s;
|
||||
}
|
||||
|
||||
.bg3 {
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform: translate(50%) rotate(-60deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(5%) rotate(-60deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export function CreateBackground() {
|
||||
const bkCheck = document.getElementsByClassName("bg");
|
||||
if (bkCheck.length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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"] }
|
||||
];
|
||||
|
||||
backgrounds.forEach(({ classes }) => {
|
||||
const bk = document.createElement("div");
|
||||
classes.forEach(cls => bk.classList.add(cls));
|
||||
container.insertBefore(bk, menu);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +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());
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { BasePlugin } from "@/plugins/core/settings";
|
||||
import { booleanSetting, defineSettings, Setting } from "@/plugins/core/settingsHelpers";
|
||||
import { type Plugin } from "@/plugins/core/types";
|
||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
|
||||
const settings = defineSettings({
|
||||
lettergrade: booleanSetting({
|
||||
default: false,
|
||||
title: "Letter Grades",
|
||||
description: "Display the average as a letter instead of a percentage"
|
||||
}),
|
||||
});
|
||||
|
||||
class AssessmentsAveragePluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.lettergrade)
|
||||
lettergrade!: boolean;
|
||||
}
|
||||
|
||||
const instance = new AssessmentsAveragePluginClass();
|
||||
|
||||
const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
id: "assessments-average",
|
||||
name: "Assessment Averages",
|
||||
description: "Adds an average grade to the Assessments page",
|
||||
version: "1.0.0",
|
||||
disableToggle: true,
|
||||
settings: instance.settings,
|
||||
|
||||
run: async (api) => {
|
||||
api.seqta.onMount(".assessmentsWrapper", async () => {
|
||||
// Wait for any assessment item to load first
|
||||
await waitForElm(
|
||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
||||
true,
|
||||
10,
|
||||
1000
|
||||
);
|
||||
|
||||
// Helper function to find actual class names by their base pattern
|
||||
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] : '';
|
||||
};
|
||||
|
||||
// Find actual class names from the DOM
|
||||
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___');
|
||||
|
||||
// Get Thermoscore classes
|
||||
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___');
|
||||
|
||||
// Find assessment list
|
||||
const assessmentsList = document.querySelector("#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']");
|
||||
if (!assessmentsList) return;
|
||||
|
||||
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,
|
||||
F: 0,
|
||||
};
|
||||
|
||||
function parseGrade(text: string): number {
|
||||
const str = text.trim().toUpperCase();
|
||||
if (str.includes("/")) {
|
||||
const [raw, max] = str.split("/").map(n => parseFloat(n));
|
||||
return (raw / max) * 100;
|
||||
}
|
||||
if (str.includes("%")) {
|
||||
return parseFloat(str.replace("%", "")) || 0;
|
||||
}
|
||||
return letterToNumber[str] ?? 0;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
let count = 0;
|
||||
gradeElements.forEach((el) => {
|
||||
const grade = parseGrade(el.textContent || "");
|
||||
if (grade > 0) {
|
||||
total += grade;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
if (!count) return;
|
||||
|
||||
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 letterAvg = numberToLetter[rounded] ?? "N/A";
|
||||
const display = api.settings.lettergrade ? letterAvg : `${avg.toFixed(2)}%`;
|
||||
|
||||
// Prevent duplicate
|
||||
const existing = assessmentsList.querySelector(`[class*='AssessmentItem__title___']`);
|
||||
if (existing?.textContent === "Subject Average") return;
|
||||
|
||||
// Use the dynamic class names in the HTML template
|
||||
const averageElement = stringToHTML(/* html */ `
|
||||
<div class="${assessmentItemClass}">
|
||||
<div class="${metaContainerClass}">
|
||||
<div class="${metaClass}">
|
||||
<div class="${simpleResultClass}">
|
||||
<div class="${titleClass}">Subject Average</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="${thermoscoreClass}">
|
||||
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
|
||||
<div class="${textClass}" title="${display}">${display}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
|
||||
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default assessmentsAveragePlugin;
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Plugin } from '../../core/types';
|
||||
|
||||
interface NotificationCollectorStorage {
|
||||
lastNotificationCount: number;
|
||||
lastCheckedTime: string;
|
||||
}
|
||||
|
||||
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||
id: 'notificationCollector',
|
||||
name: 'Notification Collector',
|
||||
description: 'Collects and displays SEQTA notifications',
|
||||
version: '1.0.0',
|
||||
settings: {},
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
let pollInterval: number | null = null;
|
||||
|
||||
// Store last notification count in storage
|
||||
if (!api.storage.lastNotificationCount) {
|
||||
api.storage.lastNotificationCount = 0;
|
||||
}
|
||||
|
||||
const checkNotifications = async () => {
|
||||
try {
|
||||
const alertDiv = document.querySelector("[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'
|
||||
},
|
||||
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 {
|
||||
console.info("[BetterSEQTA+] No notifications currently");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[BetterSEQTA+] Error fetching notifications:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
if (pollInterval) return; // Already polling
|
||||
checkNotifications();
|
||||
pollInterval = window.setInterval(checkNotifications, 30000);
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollInterval) {
|
||||
window.clearInterval(pollInterval);
|
||||
pollInterval = null;
|
||||
const alertDiv = document.querySelector("[class*='notifications__bubble___']") as HTMLElement;
|
||||
if (alertDiv) {
|
||||
if (api.storage.lastNotificationCount > 9) {
|
||||
alertDiv.textContent = "9+";
|
||||
} else {
|
||||
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
api.seqta.onMount("[class*='notifications__bubble___']", (_) => {
|
||||
startPolling();
|
||||
});
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default notificationCollectorPlugin;
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Plugin } from '@/plugins/core/types';
|
||||
import { BasePlugin } from '@/plugins/core/settings';
|
||||
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
|
||||
|
||||
// Step 1: Define settings with proper typing
|
||||
const settings = defineSettings({
|
||||
someSetting: booleanSetting({
|
||||
default: true,
|
||||
title: "Test Plugin",
|
||||
description: "Some random setting",
|
||||
})
|
||||
});
|
||||
|
||||
// Step 2: Create the plugin class with @Setting decorators
|
||||
class TestPluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.someSetting)
|
||||
someSetting!: boolean;
|
||||
}
|
||||
|
||||
// Step 3: Instantiate and plug it in
|
||||
const settingsInstance = new TestPluginClass();
|
||||
|
||||
const testPlugin: Plugin<typeof settings> = {
|
||||
id: 'test',
|
||||
name: 'Test Plugin',
|
||||
description: 'A test plugin for BetterSEQTA+',
|
||||
version: '1.0.0',
|
||||
settings: settingsInstance.settings,
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
console.log('Test plugin running');
|
||||
|
||||
const { unregister } = api.seqta.onPageChange((page) => {
|
||||
console.log('Page changed to', page);
|
||||
|
||||
console.log('Current setting value:', api.settings.someSetting);
|
||||
});
|
||||
|
||||
return () => {
|
||||
console.log('Test plugin stopped');
|
||||
unregister();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default testPlugin;
|
||||
@@ -1,9 +1,11 @@
|
||||
import renderSvelte from "@/interface/main"
|
||||
import themeCreator from "@/interface/pages/themeCreator.svelte"
|
||||
import { unmount } from "svelte"
|
||||
import { ClearThemePreview } from "./themes/UpdateThemePreview"
|
||||
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager"
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
|
||||
let themeCreatorSvelteApp: any = null
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
|
||||
/**
|
||||
* Open the Theme Creator sidebar, it is an embedded page loaded similar to the extension popup
|
||||
@@ -13,6 +15,12 @@ let themeCreatorSvelteApp: any = null
|
||||
export function OpenThemeCreator(themeID: string = "") {
|
||||
CloseThemeCreator()
|
||||
|
||||
// Only store original color if we're not editing an existing theme
|
||||
localStorage.setItem('themeCreatorOpen', 'true');
|
||||
if (!themeID) {
|
||||
localStorage.setItem('originalPreviewColor', settingsState.selectedColor);
|
||||
}
|
||||
|
||||
const width = "310px"
|
||||
|
||||
const themeCreatorDiv: HTMLDivElement = document.createElement("div")
|
||||
@@ -33,7 +41,7 @@ export function OpenThemeCreator(themeID: string = "") {
|
||||
closeButton.textContent = "×"
|
||||
closeButton.addEventListener("click", () => {
|
||||
CloseThemeCreator()
|
||||
ClearThemePreview()
|
||||
themeManager.clearPreview()
|
||||
})
|
||||
|
||||
document.body.appendChild(closeButton)
|
||||
@@ -55,7 +63,7 @@ export function OpenThemeCreator(themeID: string = "") {
|
||||
const mouseMoveHandler = (e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
const windowWidth = window.innerWidth
|
||||
const newWidth = Math.min(Math.max(310, windowWidth - e.clientX), 600)
|
||||
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`
|
||||
@@ -82,6 +90,9 @@ export function OpenThemeCreator(themeID: string = "") {
|
||||
* @returns void
|
||||
*/
|
||||
export function CloseThemeCreator() {
|
||||
// Remove the stored flag
|
||||
localStorage.removeItem('themeCreatorOpen');
|
||||
|
||||
const themeCreator = document.getElementById("themeCreator")
|
||||
const closeButton = document.querySelector(
|
||||
".themeCloseButton",
|
||||
@@ -0,0 +1,17 @@
|
||||
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',
|
||||
settings: {},
|
||||
|
||||
run: async (_) => {
|
||||
const themeManager = ThemeManager.getInstance();
|
||||
await themeManager.initialize();
|
||||
}
|
||||
};
|
||||
|
||||
export default themesPlugin;
|
||||
@@ -0,0 +1,738 @@
|
||||
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;
|
||||
name: string;
|
||||
coverImage?: string; // base64, optional
|
||||
description: string;
|
||||
defaultColour?: string;
|
||||
CanChangeColour?: boolean;
|
||||
CustomCSS?: string;
|
||||
hideThemeName?: boolean;
|
||||
forceDark?: boolean;
|
||||
images: { id: string, variableName: string, data: string }[]; // data: base64
|
||||
};
|
||||
|
||||
export class ThemeManager {
|
||||
private static instance: ThemeManager;
|
||||
private currentTheme: CustomTheme | null = null;
|
||||
private styleElement: HTMLStyleElement | null = null;
|
||||
private previewStyleElement: HTMLStyleElement | null = null;
|
||||
private previousImageVariableNames: string[] = [];
|
||||
private originalPreviewColor: string | null = null;
|
||||
private originalPreviewTheme: boolean | null = null;
|
||||
private imageUrlCache: Map<string, string> = new Map();
|
||||
|
||||
private constructor() {
|
||||
console.debug('[ThemeManager] Initializing...');
|
||||
}
|
||||
|
||||
public static getInstance(): ThemeManager {
|
||||
if (!ThemeManager.instance) {
|
||||
ThemeManager.instance = new ThemeManager();
|
||||
}
|
||||
return ThemeManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active theme
|
||||
*/
|
||||
public getCurrentTheme(): CustomTheme | null {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a theme by ID from storage
|
||||
*/
|
||||
public async getTheme(themeId: string): Promise<CustomTheme | null> {
|
||||
console.debug('[ThemeManager] Getting theme:', themeId);
|
||||
try {
|
||||
const theme = await localforage.getItem(themeId) as CustomTheme;
|
||||
return theme;
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error getting theme:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the currently selected theme
|
||||
*/
|
||||
public getSelectedThemeId(): string {
|
||||
return settingsState.selectedTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the current theme without deleting it
|
||||
*/
|
||||
public async disableTheme(): Promise<void> {
|
||||
console.debug('[ThemeManager] Disabling current theme');
|
||||
try {
|
||||
if (!this.currentTheme) {
|
||||
console.debug('[ThemeManager] No theme to disable');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.removeTheme(this.currentTheme);
|
||||
this.currentTheme = null;
|
||||
settingsState.selectedTheme = '';
|
||||
console.debug('[ThemeManager] Theme disabled successfully');
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error disabling theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the theme system and restore previous state
|
||||
*/
|
||||
public async initialize(): Promise<void> {
|
||||
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');
|
||||
this.clearPreview();
|
||||
// Clean up the flag
|
||||
localStorage.removeItem('themeCreatorOpen');
|
||||
}
|
||||
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up theme system resources
|
||||
*/
|
||||
public async cleanup(): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set and apply a theme by ID
|
||||
*/
|
||||
public async setTheme(themeId: string): Promise<void> {
|
||||
console.debug('[ThemeManager] Setting theme:', themeId);
|
||||
try {
|
||||
const theme = await localforage.getItem(themeId) as CustomTheme;
|
||||
if (!theme) {
|
||||
console.error('[ThemeManager] Theme not found:', themeId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store original settings before applying new theme
|
||||
if (!settingsState.selectedTheme) {
|
||||
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');
|
||||
|
||||
await this.removeTheme(this.currentTheme);
|
||||
}
|
||||
|
||||
// Apply new theme
|
||||
await this.applyTheme(theme);
|
||||
this.currentTheme = theme;
|
||||
settingsState.selectedTheme = themeId;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error setting theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme components (CSS, images, settings)
|
||||
*/
|
||||
private async applyTheme(theme: CustomTheme): Promise<void> {
|
||||
console.debug('[ThemeManager] Applying theme:', theme.name);
|
||||
try {
|
||||
// Apply custom CSS
|
||||
if (theme.CustomCSS) {
|
||||
console.debug('[ThemeManager] Applying custom CSS');
|
||||
this.applyCustomCSS(theme.CustomCSS);
|
||||
}
|
||||
|
||||
// Apply custom images
|
||||
if (theme.CustomImages) {
|
||||
console.debug('[ThemeManager] Applying custom images');
|
||||
theme.CustomImages.forEach((image) => {
|
||||
const imageUrl = URL.createObjectURL(image.blob);
|
||||
document.documentElement.style.setProperty('--' + image.variableName, `url(${imageUrl})`);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply theme settings
|
||||
if (theme.forceDark !== undefined) {
|
||||
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);
|
||||
settingsState.selectedColor = theme.selectedColor;
|
||||
} else if (theme.defaultColour) {
|
||||
console.debug('[ThemeManager] Using default color:', theme.defaultColour);
|
||||
settingsState.selectedColor = theme.defaultColour;
|
||||
}
|
||||
|
||||
} catch (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);
|
||||
try {
|
||||
// Remove custom CSS
|
||||
if (this.styleElement) {
|
||||
console.debug('[ThemeManager] Removing custom CSS');
|
||||
this.styleElement.remove();
|
||||
this.styleElement = null;
|
||||
}
|
||||
|
||||
// Remove custom images
|
||||
if (theme.CustomImages) {
|
||||
console.debug('[ThemeManager] Removing custom images');
|
||||
theme.CustomImages.forEach((image) => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.currentTheme) {
|
||||
// Store the current color with the theme before removing it
|
||||
await localforage.setItem(this.currentTheme.id, {
|
||||
...this.currentTheme,
|
||||
selectedColor: settingsState.selectedColor
|
||||
});
|
||||
}
|
||||
|
||||
// Restore original settings
|
||||
if (settingsState.originalSelectedColor) {
|
||||
console.debug('[ThemeManager] Restoring original color:', settingsState.originalSelectedColor);
|
||||
settingsState.selectedColor = settingsState.originalSelectedColor;
|
||||
}
|
||||
|
||||
if (settingsState.originalDarkMode !== undefined) {
|
||||
console.debug('[ThemeManager] Restoring original dark mode:', settingsState.originalDarkMode);
|
||||
settingsState.DarkMode = settingsState.originalDarkMode;
|
||||
settingsState.originalDarkMode = undefined;
|
||||
}
|
||||
|
||||
this.currentTheme = null;
|
||||
if (clearSelectedTheme) {
|
||||
settingsState.selectedTheme = '';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error removing theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply custom CSS to the document
|
||||
*/
|
||||
private applyCustomCSS(css: string): void {
|
||||
console.debug('[ThemeManager] Applying custom CSS');
|
||||
try {
|
||||
if (!this.styleElement) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available themes
|
||||
*/
|
||||
public async getAvailableThemes(): Promise<CustomTheme[]> {
|
||||
console.debug('[ThemeManager] Getting available themes');
|
||||
try {
|
||||
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 themes.filter(theme => theme !== null);
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error getting available themes:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update a theme
|
||||
*/
|
||||
public async saveTheme(theme: LoadedCustomTheme): Promise<void> {
|
||||
console.debug('[ThemeManager] Saving theme:', theme.name);
|
||||
try {
|
||||
await localforage.setItem(theme.id, theme);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
await localforage.setItem('customThemes', [theme.id]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error saving theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a theme
|
||||
*/
|
||||
public async deleteTheme(themeId: string): Promise<void> {
|
||||
console.debug('[ThemeManager] Deleting theme:', themeId);
|
||||
try {
|
||||
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;
|
||||
if (themeIds) {
|
||||
const updatedThemeIds = themeIds.filter(id => id !== themeId);
|
||||
await localforage.setItem('customThemes', updatedThemeIds);
|
||||
}
|
||||
}
|
||||
} catch (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);
|
||||
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;
|
||||
|
||||
await this.installTheme(themeData);
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error downloading theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a theme from theme data
|
||||
*/
|
||||
public async installTheme(themeData: ThemeContent): Promise<void> {
|
||||
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)');
|
||||
}
|
||||
|
||||
// Handle cover image (optional)
|
||||
let coverImageBlob = null;
|
||||
if (themeData.coverImage) {
|
||||
try {
|
||||
const strippedCoverImage = this.stripBase64Prefix(themeData.coverImage);
|
||||
coverImageBlob = this.base64ToBlob(strippedCoverImage);
|
||||
} catch (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) ?? [];
|
||||
|
||||
// Create theme with defaults for optional fields
|
||||
const theme: LoadedCustomTheme = {
|
||||
id: themeData.id,
|
||||
name: themeData.name,
|
||||
description: themeData.description || '',
|
||||
webURL: themeData.id,
|
||||
coverImage: coverImageBlob,
|
||||
CustomImages: images,
|
||||
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
|
||||
};
|
||||
|
||||
await this.saveTheme(theme);
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error installing theme:', error);
|
||||
throw error; // Re-throw to handle in UI
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a theme by exporting it
|
||||
*/
|
||||
public async shareTheme(themeId: string): Promise<void> {
|
||||
console.debug('[ThemeManager] Sharing theme:', themeId);
|
||||
try {
|
||||
const theme = await localforage.getItem(themeId) as LoadedCustomTheme;
|
||||
if (!theme) {
|
||||
console.error('[ThemeManager] Theme not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract only the fields we want to share
|
||||
const {
|
||||
CustomImages = [],
|
||||
coverImage,
|
||||
webURL,
|
||||
isEditable,
|
||||
selectedColor,
|
||||
allowBackgrounds,
|
||||
...themeBasics
|
||||
} = theme;
|
||||
|
||||
// Convert images to base64
|
||||
const finalImages = await Promise.all(CustomImages.map(async (image) => ({
|
||||
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;
|
||||
|
||||
// Create shareable theme data with only necessary fields
|
||||
const shareableTheme = {
|
||||
...themeBasics,
|
||||
images: finalImages,
|
||||
coverImage: coverImageBase64
|
||||
};
|
||||
|
||||
// Save theme file
|
||||
this.saveThemeFile(shareableTheme, theme.name || 'Unnamed_Theme');
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error sharing theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview a theme without applying it
|
||||
*/
|
||||
public async previewTheme(theme: LoadedCustomTheme): Promise<void> {
|
||||
console.debug('[ThemeManager] Previewing theme:', theme.name);
|
||||
try {
|
||||
const { CustomCSS, CustomImages, defaultColour, forceDark } = theme;
|
||||
|
||||
// Store original settings only if this is a new theme
|
||||
if (!theme.webURL) {
|
||||
if (this.originalPreviewColor === null) {
|
||||
this.originalPreviewColor = settingsState.selectedColor;
|
||||
localStorage.setItem('originalPreviewColor', settingsState.selectedColor);
|
||||
}
|
||||
if (this.originalPreviewTheme === null) {
|
||||
this.originalPreviewTheme = settingsState.DarkMode;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply custom CSS
|
||||
if (CustomCSS) {
|
||||
this.applyPreviewCSS(CustomCSS);
|
||||
}
|
||||
|
||||
// Apply custom images
|
||||
const newImageVariableNames = CustomImages.map(image => image.variableName);
|
||||
|
||||
// Remove old preview images
|
||||
this.previousImageVariableNames.forEach(variableName => {
|
||||
if (!newImageVariableNames.includes(variableName)) {
|
||||
this.removeImageFromDocument(variableName);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply new images
|
||||
CustomImages.forEach((image) => {
|
||||
const imageUrl = URL.createObjectURL(image.blob);
|
||||
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${imageUrl})`);
|
||||
});
|
||||
|
||||
// Update previousImageVariableNames
|
||||
this.previousImageVariableNames = newImageVariableNames;
|
||||
|
||||
// Apply theme settings
|
||||
if (forceDark !== undefined) {
|
||||
settingsState.DarkMode = forceDark;
|
||||
}
|
||||
|
||||
if (defaultColour) {
|
||||
settingsState.selectedColor = defaultColour;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error previewing theme:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
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)
|
||||
if (!theme.webURL) {
|
||||
if (this.originalPreviewColor === null) {
|
||||
this.originalPreviewColor = settingsState.selectedColor;
|
||||
}
|
||||
if (this.originalPreviewTheme === null) {
|
||||
this.originalPreviewTheme = settingsState.DarkMode;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply CSS if changed
|
||||
if (theme.CustomCSS !== undefined) {
|
||||
this.applyPreviewCSS(theme.CustomCSS);
|
||||
}
|
||||
|
||||
// Handle images if present
|
||||
if (theme.CustomImages) {
|
||||
const newImageVariableNames = theme.CustomImages.map(image => image.variableName);
|
||||
|
||||
// Remove old preview images that are no longer present
|
||||
this.previousImageVariableNames.forEach(variableName => {
|
||||
if (!newImageVariableNames.includes(variableName)) {
|
||||
this.removeImageFromDocument(variableName);
|
||||
// Clean up cached URL
|
||||
this.imageUrlCache.delete(variableName);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply or update images
|
||||
theme.CustomImages.forEach((image) => {
|
||||
const existingUrl = this.imageUrlCache.get(image.variableName);
|
||||
if (!existingUrl) {
|
||||
// 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})`);
|
||||
} else {
|
||||
// Reuse existing URL
|
||||
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${existingUrl})`);
|
||||
}
|
||||
});
|
||||
|
||||
this.previousImageVariableNames = newImageVariableNames;
|
||||
}
|
||||
|
||||
// Always apply dark mode setting
|
||||
if (theme.forceDark !== undefined) {
|
||||
settingsState.DarkMode = theme.forceDark;
|
||||
}
|
||||
|
||||
// Only apply color if this is a new theme
|
||||
if (!theme.webURL && theme.defaultColour) {
|
||||
settingsState.selectedColor = theme.defaultColour;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error updating theme preview:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Clear theme preview
|
||||
*/
|
||||
public clearPreview(): void {
|
||||
console.debug('[ThemeManager] Clearing theme preview');
|
||||
try {
|
||||
// Remove preview images and revoke URLs
|
||||
this.previousImageVariableNames.forEach(variableName => {
|
||||
this.removeImageFromDocument(variableName);
|
||||
});
|
||||
// Clear all cached URLs
|
||||
this.imageUrlCache.forEach(url => URL.revokeObjectURL(url));
|
||||
this.imageUrlCache.clear();
|
||||
this.previousImageVariableNames = [];
|
||||
|
||||
// Remove preview CSS
|
||||
if (this.previewStyleElement) {
|
||||
this.previewStyleElement.remove();
|
||||
this.previewStyleElement = null;
|
||||
}
|
||||
|
||||
// Restore original settings
|
||||
const storedColor = localStorage.getItem('originalPreviewColor');
|
||||
|
||||
if (storedColor) {
|
||||
settingsState.selectedColor = storedColor;
|
||||
localStorage.removeItem('originalPreviewColor');
|
||||
} else if (this.originalPreviewColor !== null) {
|
||||
console.debug('[ThemeManager] Restoring color from memory:', this.originalPreviewColor);
|
||||
settingsState.selectedColor = this.originalPreviewColor;
|
||||
console.debug('[ThemeManager] Color after restore:', settingsState.selectedColor);
|
||||
} else {
|
||||
console.debug('[ThemeManager] No color to restore found');
|
||||
}
|
||||
this.originalPreviewColor = null;
|
||||
|
||||
if (this.originalPreviewTheme !== null) {
|
||||
console.debug('[ThemeManager] Restoring dark mode:', this.originalPreviewTheme);
|
||||
settingsState.DarkMode = this.originalPreviewTheme;
|
||||
this.originalPreviewTheme = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ThemeManager] Error clearing preview:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
private stripBase64Prefix(base64String: string): string {
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
private base64ToBlob(base64: string): Blob {
|
||||
try {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64String = reader.result as string;
|
||||
const base64Data = base64String.split(',')[1];
|
||||
resolve(base64Data);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
private saveThemeFile(data: object, fileName: string): void {
|
||||
try {
|
||||
const fileData = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([fileData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private removeImageFromDocument(variableName: string): void {
|
||||
try {
|
||||
const value = document.documentElement.style.getPropertyValue('--' + variableName);
|
||||
if (value) {
|
||||
const url = this.imageUrlCache.get(variableName);
|
||||
if (url) {
|
||||
URL.revokeObjectURL(url);
|
||||
this.imageUrlCache.delete(variableName);
|
||||
}
|
||||
}
|
||||
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');
|
||||
try {
|
||||
if (!this.previewStyleElement) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
import type { Plugin } from '../../core/types';
|
||||
import { convertTo12HourFormat } from '@/seqta/utils/convertTo12HourFormat';
|
||||
import { waitForElm } from '@/seqta/utils/waitForElm';
|
||||
|
||||
const timetablePlugin: Plugin<{}, {}> = {
|
||||
id: 'timetable',
|
||||
name: 'Timetable Enhancer',
|
||||
description: 'Adds extra features to the timetable view',
|
||||
version: '1.0.0',
|
||||
settings: {},
|
||||
disableToggle: true,
|
||||
|
||||
run: async (api) => {
|
||||
const { unregister } = api.seqta.onMount('.timetablepage', handleTimetable)
|
||||
|
||||
return () => {
|
||||
// Call the unregister function to remove the mount listener
|
||||
unregister();
|
||||
|
||||
const timetablePage = document.querySelector('.timetablepage')
|
||||
if (timetablePage) {
|
||||
const zoomControls = document.querySelector('.timetable-zoom-controls')
|
||||
if (zoomControls) zoomControls.remove()
|
||||
|
||||
const hideControls = document.querySelector('.timetable-hide-controls')
|
||||
if (hideControls) hideControls.remove()
|
||||
|
||||
resetTimetableStyles()
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Store event handlers globally for cleanup
|
||||
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")
|
||||
dayColumns.forEach((td: Element) => {
|
||||
(td as HTMLElement).style.height = `${baseContainerHeight}px`
|
||||
})
|
||||
|
||||
const timeColumn = document.querySelector(".times")
|
||||
if (timeColumn) {
|
||||
const times = timeColumn.querySelectorAll(".time")
|
||||
const timeHeight = baseContainerHeight / times.length
|
||||
times.forEach((time: Element) => {
|
||||
(time as HTMLElement).style.height = `${timeHeight}px`
|
||||
})
|
||||
}
|
||||
|
||||
const lessons = document.querySelectorAll(".dailycal .lesson")
|
||||
lessons.forEach((lesson: Element) => {
|
||||
const lessonEl = lesson as HTMLElement
|
||||
const originalHeight = lessonEl.getAttribute('data-original-height')
|
||||
if (originalHeight) {
|
||||
lessonEl.style.height = `${originalHeight}px`
|
||||
}
|
||||
})
|
||||
|
||||
const entries = document.querySelectorAll(".entry")
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement
|
||||
entryEl.style.opacity = '1'
|
||||
})
|
||||
|
||||
const zoomControls = document.querySelector('.timetable-zoom-controls')
|
||||
if (zoomControls) {
|
||||
const handlers = zoomHandlers.get(zoomControls)
|
||||
if (handlers) {
|
||||
const zoomIn = zoomControls.querySelector('.timetable-zoom:nth-child(2)')
|
||||
const zoomOut = zoomControls.querySelector('.timetable-zoom:nth-child(1)')
|
||||
if (zoomIn) zoomIn.removeEventListener('click', handlers.zoomIn)
|
||||
if (zoomOut) zoomOut.removeEventListener('click', handlers.zoomOut)
|
||||
zoomHandlers.delete(zoomControls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTimetable(): Promise<void> {
|
||||
await waitForElm(".time", true, 10)
|
||||
|
||||
// Store original heights when timetable loads
|
||||
const lessons = document.querySelectorAll(".dailycal .lesson")
|
||||
lessons.forEach((lesson: Element) => {
|
||||
const lessonEl = lesson as HTMLElement
|
||||
lessonEl.setAttribute(
|
||||
"data-original-height",
|
||||
lessonEl.offsetHeight.toString(),
|
||||
)
|
||||
})
|
||||
|
||||
// Existing time format code
|
||||
if (settingsState.timeFormat == "12") {
|
||||
const times = document.querySelectorAll(".timetablepage .times .time")
|
||||
for (const time of times) {
|
||||
if (!time.textContent) continue
|
||||
time.textContent = convertTo12HourFormat(time.textContent, true)
|
||||
}
|
||||
}
|
||||
|
||||
handleTimetableZoom()
|
||||
handleTimetableAssessmentHide()
|
||||
}
|
||||
|
||||
function handleTimetableZoom(): void {
|
||||
console.log("Initializing timetable zoom controls")
|
||||
|
||||
// Lazy initialize state variables only when function is first called
|
||||
let timetableZoomLevel = 1
|
||||
let baseContainerHeight: number | null = null
|
||||
const originalEntryPositions = new Map<
|
||||
Element,
|
||||
{ topRatio: number; heightRatio: number }
|
||||
>()
|
||||
|
||||
// Create zoom controls
|
||||
const zoomControls = document.createElement("div")
|
||||
zoomControls.className = "timetable-zoom-controls"
|
||||
|
||||
const zoomIn = document.createElement("button")
|
||||
zoomIn.className = "uiButton timetable-zoom iconFamily"
|
||||
zoomIn.innerHTML = "" // Unicode for zoom in icon (custom iconfamily)
|
||||
|
||||
const zoomOut = document.createElement("button")
|
||||
zoomOut.className = "uiButton timetable-zoom iconFamily"
|
||||
zoomOut.innerHTML = "" // Unicode for zoom out icon (custom iconfamily)
|
||||
|
||||
zoomControls.appendChild(zoomOut)
|
||||
zoomControls.appendChild(zoomIn)
|
||||
|
||||
const toolbar = document.getElementById("toolbar")
|
||||
toolbar?.appendChild(zoomControls)
|
||||
|
||||
// Store event listener references
|
||||
const zoomInHandler = () => {
|
||||
if (timetableZoomLevel < 2) {
|
||||
timetableZoomLevel += 0.2
|
||||
updateZoom()
|
||||
}
|
||||
}
|
||||
|
||||
const zoomOutHandler = () => {
|
||||
if (timetableZoomLevel > 0.6) {
|
||||
timetableZoomLevel -= 0.2
|
||||
updateZoom()
|
||||
}
|
||||
}
|
||||
|
||||
zoomIn.addEventListener("click", zoomInHandler)
|
||||
zoomOut.addEventListener("click", zoomOutHandler)
|
||||
|
||||
// Store references for cleanup
|
||||
zoomHandlers.set(zoomControls, { zoomIn: zoomInHandler, zoomOut: zoomOutHandler })
|
||||
|
||||
const initializePositions = () => {
|
||||
// Get the base container height from the first TD
|
||||
const firstDayColumn = document.querySelector(
|
||||
".dailycal .content .days td",
|
||||
) as HTMLElement
|
||||
if (!firstDayColumn) return false
|
||||
|
||||
baseContainerHeight =
|
||||
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight
|
||||
|
||||
// Store original ratios
|
||||
const entries = document.querySelectorAll(".entriesWrapper .entry")
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement
|
||||
|
||||
// Calculate ratios relative to detected base height
|
||||
if (baseContainerHeight === null) return
|
||||
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight
|
||||
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight
|
||||
|
||||
originalEntryPositions.set(entry, { topRatio, heightRatio })
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const updateZoom = () => {
|
||||
// Initialize positions if not already done
|
||||
if (baseContainerHeight === null && !initializePositions()) {
|
||||
console.error("Failed to initialize positions")
|
||||
return
|
||||
}
|
||||
|
||||
console.debug(`Updating zoom level to: ${timetableZoomLevel}`)
|
||||
|
||||
// Calculate new container height
|
||||
if (baseContainerHeight === null) return
|
||||
const newContainerHeight = baseContainerHeight * timetableZoomLevel
|
||||
|
||||
// Update all day columns (TDs)
|
||||
const dayColumns = document.querySelectorAll(".dailycal .content .days td")
|
||||
dayColumns.forEach((td: Element) => {
|
||||
(td as HTMLElement).style.height = `${newContainerHeight}px`
|
||||
})
|
||||
|
||||
// Update all entries using stored ratios
|
||||
const entries = document.querySelectorAll(".entriesWrapper .entry")
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement
|
||||
const originalRatios = originalEntryPositions.get(entry)
|
||||
|
||||
if (originalRatios) {
|
||||
// Calculate new positions from original ratios
|
||||
const newTop = originalRatios.topRatio * newContainerHeight
|
||||
const newHeight = originalRatios.heightRatio * newContainerHeight
|
||||
|
||||
// Apply new values
|
||||
entryEl.style.top = `${Math.round(newTop)}px`
|
||||
entryEl.style.height = `${Math.round(newHeight)}px`
|
||||
}
|
||||
})
|
||||
|
||||
// Update time column to match
|
||||
const timeColumn = document.querySelector(".times")
|
||||
if (timeColumn) {
|
||||
const times = timeColumn.querySelectorAll(".time")
|
||||
const timeHeight = newContainerHeight / times.length
|
||||
times.forEach((time: Element) => {
|
||||
(time as HTMLElement).style.height = `${timeHeight}px`
|
||||
})
|
||||
}
|
||||
|
||||
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "center",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function handleTimetableAssessmentHide(): void {
|
||||
const hideControls = document.createElement("div")
|
||||
hideControls.className = "timetable-hide-controls"
|
||||
|
||||
const hideOn = document.createElement("button")
|
||||
hideOn.className = "uiButton timetable-hide iconFamily"
|
||||
hideOn.innerHTML = "👁"
|
||||
|
||||
hideControls.appendChild(hideOn)
|
||||
|
||||
const toolbar = document.getElementById("toolbar")
|
||||
toolbar?.appendChild(hideControls)
|
||||
|
||||
function hideElements(): void {
|
||||
const entries = document.querySelectorAll(".entry")
|
||||
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement
|
||||
if (!entryEl.classList.contains("assessment")) {
|
||||
entryEl.style.opacity = entryEl.style.opacity === "0.3" ? "1" : "0.3"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hideOn.addEventListener("click", hideElements)
|
||||
}
|
||||
|
||||
export default timetablePlugin;
|
||||
@@ -0,0 +1,258 @@
|
||||
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 {
|
||||
onMount: (selector, callback) => {
|
||||
return eventManager.register(
|
||||
`${selector}Added`,
|
||||
{
|
||||
customCheck: (element) => element.matches(selector),
|
||||
},
|
||||
callback
|
||||
);
|
||||
},
|
||||
getFiber: (selector) => {
|
||||
return ReactFiber.find(selector);
|
||||
},
|
||||
getCurrentPage: () => {
|
||||
const path = window.location.hash.split('?page=/')[1] || '';
|
||||
return path.split('/')[0];
|
||||
},
|
||||
onPageChange: (callback) => {
|
||||
const handler = () => {
|
||||
const page = window.location.hash.split('?page=/')[1] || '';
|
||||
callback(page.split('/')[0]);
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', handler);
|
||||
|
||||
// Return an unregister function
|
||||
return {
|
||||
unregister: () => {
|
||||
window.removeEventListener('hashchange', handler);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set());
|
||||
}
|
||||
listeners.get(key)!.add(callback);
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key)!.delete(callback);
|
||||
}
|
||||
};
|
||||
},
|
||||
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
|
||||
};
|
||||
|
||||
// Fill with defaults first
|
||||
for (const key in plugin.settings) {
|
||||
settingsWithMeta[key] = plugin.settings[key].default;
|
||||
}
|
||||
|
||||
// Load stored settings and override defaults
|
||||
const loaded = (async () => {
|
||||
try {
|
||||
const stored = await browser.storage.local.get(storageKey);
|
||||
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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (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 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]));
|
||||
}
|
||||
};
|
||||
|
||||
browser.storage.onChanged.addListener(handleStorageChange);
|
||||
|
||||
const proxy = new Proxy(settingsWithMeta, {
|
||||
get(target, prop) {
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop, value) {
|
||||
if (['onChange', 'offChange', 'loaded'].includes(prop as string)) return false;
|
||||
|
||||
target[prop] = value;
|
||||
|
||||
// Reconstruct just the data keys for storage (excluding metadata methods)
|
||||
const dataToStore: any = {};
|
||||
for (const key in plugin.settings) {
|
||||
dataToStore[key] = target[key];
|
||||
}
|
||||
|
||||
browser.storage.local.set({ [storageKey]: dataToStore });
|
||||
|
||||
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] } {
|
||||
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>();
|
||||
|
||||
// Load all existing storage values for this plugin
|
||||
const loadStoragePromise = (async () => {
|
||||
try {
|
||||
const allStorage = await browser.storage.local.get(null);
|
||||
|
||||
// Filter for this plugin's storage keys and populate cache
|
||||
Object.entries(allStorage).forEach(([key, value]) => {
|
||||
if (key.startsWith(prefix)) {
|
||||
const shortKey = key.slice(prefix.length);
|
||||
cache[shortKey] = value;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Error loading storage for plugin ${pluginId}:`, error);
|
||||
}
|
||||
})();
|
||||
|
||||
// Listen for storage changes
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
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') {
|
||||
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
|
||||
if (!listeners.has(key as string)) {
|
||||
listeners.set(key as string, new Set());
|
||||
}
|
||||
listeners.get(key as string)!.add(callback);
|
||||
return {
|
||||
unregister: () => {
|
||||
listeners.get(key as string)?.delete(callback);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
if (prop === 'offChange') {
|
||||
return (key: keyof T, callback: (value: T[keyof T]) => void) => {
|
||||
listeners.get(key as string)?.delete(callback);
|
||||
};
|
||||
}
|
||||
if (prop === 'loaded') {
|
||||
return loadStoragePromise;
|
||||
}
|
||||
|
||||
// Direct property access
|
||||
return target[prop];
|
||||
},
|
||||
set(target, prop: string, value: any) {
|
||||
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));
|
||||
|
||||
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 }>>();
|
||||
|
||||
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
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import type { BooleanSetting, NumberSetting, Plugin, PluginSettings, SelectSetting, StringSetting } from './types';
|
||||
import { createPluginAPI } from './createAPI';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
interface PluginSettingsStorage {
|
||||
enabled?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface StorageChange<T = any> {
|
||||
oldValue?: T;
|
||||
newValue?: T;
|
||||
}
|
||||
|
||||
export class PluginManager {
|
||||
private static instance: PluginManager;
|
||||
private plugins: Map<string, Plugin<any, any>> = new Map();
|
||||
private runningPlugins: Map<string, boolean> = new Map();
|
||||
private eventBacklog: Map<string, any[]> = new Map();
|
||||
private cleanupFunctions: Map<string, () => void> = new Map();
|
||||
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||
private styleElements: Map<string, HTMLStyleElement> = new Map();
|
||||
|
||||
private constructor() {
|
||||
this.setupPluginStateListener();
|
||||
}
|
||||
|
||||
public static getInstance(): PluginManager {
|
||||
if (!PluginManager.instance) {
|
||||
PluginManager.instance = new PluginManager();
|
||||
}
|
||||
return PluginManager.instance;
|
||||
}
|
||||
|
||||
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
||||
const fullEventName = `plugin.${pluginId}.${event}`;
|
||||
|
||||
// Dispatch plugin event if it's running otherwise queue it
|
||||
if (this.runningPlugins.get(pluginId)) {
|
||||
document.dispatchEvent(new CustomEvent(fullEventName, { detail: args }));
|
||||
} else {
|
||||
const key = `${pluginId}:${event}`;
|
||||
if (!this.eventBacklog.has(key)) {
|
||||
this.eventBacklog.set(key, []);
|
||||
}
|
||||
this.eventBacklog.get(key)!.push(args);
|
||||
}
|
||||
}
|
||||
|
||||
private async processBackloggedEvents(pluginId: string) {
|
||||
for (const [key, argsList] of this.eventBacklog.entries()) {
|
||||
const [eventPluginId, event] = key.split(':');
|
||||
if (eventPluginId === pluginId) {
|
||||
for (const args of argsList) {
|
||||
this.dispatchPluginEvent(pluginId, event, args);
|
||||
}
|
||||
this.eventBacklog.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public registerPlugin<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`);
|
||||
}
|
||||
this.plugins.set(plugin.id, plugin);
|
||||
}
|
||||
|
||||
public async startPlugin(pluginId: string): Promise<void> {
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin) {
|
||||
throw new Error(`Plugin "${pluginId}" not found`);
|
||||
}
|
||||
|
||||
if (this.runningPlugins.get(pluginId)) {
|
||||
console.warn(`Plugin "${pluginId}" is already running`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const api = createPluginAPI(plugin);
|
||||
|
||||
// Check if plugin is enabled before starting
|
||||
if (plugin.disableToggle) {
|
||||
const settings = await browser.storage.local.get(`plugin.${pluginId}.settings`);
|
||||
const pluginSettings = settings[`plugin.${pluginId}.settings`] as PluginSettingsStorage | undefined;
|
||||
const enabled = pluginSettings?.enabled ?? true;
|
||||
if (!enabled) {
|
||||
console.info(`Plugin "${pluginId}" is disabled, skipping initialization`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject plugin styles if provided
|
||||
if (plugin.styles) {
|
||||
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
|
||||
]);
|
||||
|
||||
const result = await plugin.run(api);
|
||||
if (typeof result === 'function') {
|
||||
this.cleanupFunctions.set(plugin.id, result);
|
||||
}
|
||||
this.runningPlugins.set(pluginId, true);
|
||||
console.info(`Plugin "${pluginId}" started successfully`);
|
||||
|
||||
// Process any backlogged events
|
||||
await this.processBackloggedEvents(pluginId);
|
||||
} catch (error) {
|
||||
console.error(`[BetterSEQTA+] Failed to start plugin ${pluginId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async startAllPlugins(): Promise<void> {
|
||||
const startPromises = Array.from(this.plugins.keys()).map(id =>
|
||||
this.startPlugin(id).catch(error => {
|
||||
console.error(`Failed to start plugin "${id}":`, error);
|
||||
return Promise.reject(error);
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.allSettled(startPromises);
|
||||
}
|
||||
|
||||
public async stopPlugin(pluginId: string): Promise<void> {
|
||||
// Remove plugin styles
|
||||
const styleElement = this.styleElements.get(pluginId);
|
||||
if (styleElement) {
|
||||
styleElement.remove();
|
||||
this.styleElements.delete(pluginId);
|
||||
}
|
||||
|
||||
const cleanup = this.cleanupFunctions.get(pluginId);
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
this.cleanupFunctions.delete(pluginId);
|
||||
}
|
||||
this.runningPlugins.set(pluginId, false);
|
||||
console.info(`Plugin "${pluginId}" stopped`);
|
||||
this.emit('plugin.stopped', pluginId);
|
||||
}
|
||||
|
||||
public stopAllPlugins(): void {
|
||||
Array.from(this.plugins.keys()).forEach(id => this.stopPlugin(id));
|
||||
}
|
||||
|
||||
public getPlugin(pluginId: string): Plugin | undefined {
|
||||
return this.plugins.get(pluginId);
|
||||
}
|
||||
|
||||
public getAllPlugins(): Plugin[] {
|
||||
return Array.from(this.plugins.values());
|
||||
}
|
||||
|
||||
public getAllPluginSettings(): Array<{
|
||||
pluginId: string;
|
||||
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 }> });
|
||||
}
|
||||
}> {
|
||||
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 || '';
|
||||
|
||||
return [key, result];
|
||||
});
|
||||
|
||||
if (plugin.disableToggle) {
|
||||
settingsEntries.push([
|
||||
'enabled', {
|
||||
id: 'enabled',
|
||||
title: plugin.name,
|
||||
description: plugin.description,
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
])
|
||||
}
|
||||
return {
|
||||
pluginId: id,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
settings: Object.fromEntries(settingsEntries),
|
||||
disableToggle: plugin.disableToggle
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public isPluginRunning(pluginId: string): boolean {
|
||||
return this.runningPlugins.get(pluginId) || false;
|
||||
}
|
||||
|
||||
private emit(event: string, ...args: any[]): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => listener(...args));
|
||||
}
|
||||
}
|
||||
|
||||
public on(event: string, callback: (...args: any[]) => void): void {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, new Set());
|
||||
}
|
||||
this.listeners.get(event)!.add(callback);
|
||||
}
|
||||
|
||||
public off(event: string, callback: (...args: any[]) => void): void {
|
||||
const listeners = this.listeners.get(event);
|
||||
if (listeners) {
|
||||
listeners.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Add handler for plugin enable/disable state changes
|
||||
private async handlePluginStateChange(pluginId: string, enabled: boolean): Promise<void> {
|
||||
if (enabled) {
|
||||
await this.startPlugin(pluginId);
|
||||
} else {
|
||||
await this.stopPlugin(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
// Add listener for plugin settings changes
|
||||
private setupPluginStateListener(): void {
|
||||
browser.storage.onChanged.addListener((changes: { [key: string]: StorageChange }, area: string) => {
|
||||
if (area !== 'local') return;
|
||||
|
||||
for (const [key, change] of Object.entries(changes)) {
|
||||
const match = key.match(/^plugin\.(.+)\.settings$/);
|
||||
if (!match) continue;
|
||||
|
||||
const pluginId = match[1];
|
||||
const plugin = this.plugins.get(pluginId);
|
||||
if (!plugin?.disableToggle) continue;
|
||||
|
||||
const enabled = (change.newValue as PluginSettingsStorage)?.enabled ?? true;
|
||||
const wasEnabled = (change.oldValue as PluginSettingsStorage)?.enabled ?? true;
|
||||
|
||||
if (enabled !== wasEnabled) {
|
||||
this.handlePluginStateChange(pluginId, enabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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', {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
proto.settings[propertyKey] = settingDef;
|
||||
};
|
||||
}
|
||||
|
||||
// Base plugin class that handles settings
|
||||
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
|
||||
// The settings property will be populated by decorators
|
||||
// Keep the instance property and constructor logic as is,
|
||||
// as changing it would require changing animated-background/index.ts
|
||||
settings!: T; // Use definite assignment assertion
|
||||
|
||||
constructor() {
|
||||
// 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')) {
|
||||
// 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;
|
||||
} else {
|
||||
// Fallback if decorators somehow didn't run or add the property
|
||||
this.settings = {} as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { BooleanSetting, NumberSetting, SelectSetting, StringSetting } from './types';
|
||||
|
||||
export function numberSetting(options: Omit<NumberSetting, 'type'>): NumberSetting {
|
||||
return {
|
||||
type: 'number',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function booleanSetting(options: Omit<BooleanSetting, 'type'>): BooleanSetting {
|
||||
return {
|
||||
type: 'boolean',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function stringSetting(options: Omit<StringSetting, 'type'>): StringSetting {
|
||||
return {
|
||||
type: 'string',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSetting<T extends string>(options: Omit<SelectSetting<T>, 'type'>): SelectSetting<T> {
|
||||
return {
|
||||
type: 'select',
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function Setting(settingDef: any): PropertyDecorator {
|
||||
return (target, propertyKey) => {
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
Object.defineProperty(proto, 'settings', {
|
||||
value: {},
|
||||
writable: true,
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
|
||||
proto.settings[propertyKey] = settingDef;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import ReactFiber from '@/seqta/utils/ReactFiber';
|
||||
|
||||
export interface BooleanSetting {
|
||||
type: 'boolean';
|
||||
default: boolean;
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface StringSetting {
|
||||
type: 'string';
|
||||
default: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
maxLength?: number;
|
||||
pattern?: string;
|
||||
}
|
||||
|
||||
export interface NumberSetting {
|
||||
type: 'number';
|
||||
default: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export interface SelectSetting<T extends string> {
|
||||
type: 'select';
|
||||
options: readonly T[];
|
||||
default: T;
|
||||
title: string;
|
||||
description?: 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 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;
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
|
||||
export interface SEQTAAPI {
|
||||
onMount: (selector: string, callback: (element: Element) => void) => { unregister: () => void };
|
||||
getFiber: (selector: string) => ReactFiber;
|
||||
getCurrentPage: () => string;
|
||||
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 };
|
||||
|
||||
/**
|
||||
* Promise that resolves when storage values are loaded
|
||||
*/
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
|
||||
export type TypedStorageAPI<T> = StorageAPI<T> & {
|
||||
[K in keyof T]: T[K];
|
||||
}
|
||||
|
||||
export interface EventsAPI {
|
||||
on: (event: string, callback: (...args: any[]) => void) => { unregister: () => void };
|
||||
emit: (event: string, ...args: any[]) => void;
|
||||
}
|
||||
|
||||
export interface PluginAPI<T extends PluginSettings, S = any> {
|
||||
seqta: SEQTAAPI;
|
||||
settings: SettingsAPI<T>;
|
||||
storage: TypedStorageAPI<S>;
|
||||
events: EventsAPI;
|
||||
}
|
||||
|
||||
export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
|
||||
id: string;
|
||||
name: string;
|
||||
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
|
||||
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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';
|
||||
// Initialize plugin manager
|
||||
const pluginManager = PluginManager.getInstance();
|
||||
|
||||
// Register built-in plugins
|
||||
pluginManager.registerPlugin(themesPlugin);
|
||||
pluginManager.registerPlugin(animatedBackgroundPlugin);
|
||||
pluginManager.registerPlugin(assessmentsAveragePlugin);
|
||||
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||
pluginManager.registerPlugin(timetablePlugin);
|
||||
//pluginManager.registerPlugin(testPlugin);
|
||||
|
||||
export { init as Monofile } from './monofile';
|
||||
|
||||
export async function initializePlugins(): Promise<void> {
|
||||
await pluginManager.startAllPlugins();
|
||||
}
|
||||
|
||||
export { pluginManager };
|
||||
|
||||
export function getAllPluginSettings() {
|
||||
return pluginManager.getAllPluginSettings();
|
||||
}
|
||||
@@ -0,0 +1,661 @@
|
||||
// Third-party libraries
|
||||
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"
|
||||
|
||||
// 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"
|
||||
|
||||
// JSON content
|
||||
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json"
|
||||
|
||||
// Icons and fonts
|
||||
import IconFamily from "@/resources/fonts/IconFamily.woff"
|
||||
|
||||
// Stylesheets
|
||||
import iframeCSS from "@/css/iframe.scss?raw"
|
||||
|
||||
function SetDisplayNone(ElementName: string) {
|
||||
return `li[data-key=${ElementName}]{display:var(--menuHidden) !important; transition: 1s;}`
|
||||
}
|
||||
|
||||
async function HideMenuItems(): Promise<void> {
|
||||
try {
|
||||
let stylesheetInnerText: string = ""
|
||||
for (const [menuItem, { toggle }] of Object.entries(
|
||||
settingsState.menuitems,
|
||||
)) {
|
||||
if (!toggle) {
|
||||
stylesheetInnerText += SetDisplayNone(menuItem)
|
||||
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`)
|
||||
}
|
||||
}
|
||||
|
||||
const menuItemStyle: HTMLStyleElement = document.createElement("style")
|
||||
menuItemStyle.innerText = stylesheetInnerText
|
||||
document.head.appendChild(menuItemStyle)
|
||||
} catch (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 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"
|
||||
} else {
|
||||
sidebar!.style.width = "100%"
|
||||
}
|
||||
|
||||
if (currentContentPosition != "relative") {
|
||||
main!.style.position = "relative"
|
||||
} else {
|
||||
main!.style.position = "absolute"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function finishLoad() {
|
||||
try {
|
||||
document.querySelector(".legacy-root")?.classList.remove("hidden")
|
||||
|
||||
const loadingbk = document.getElementById("loading")
|
||||
loadingbk?.classList.add("closeLoading")
|
||||
await delay(501)
|
||||
loadingbk?.remove()
|
||||
} catch (err) {
|
||||
console.error("Error during loading cleanup:", err)
|
||||
}
|
||||
|
||||
if (settingsState.justupdated && !document.getElementById("whatsnewbk")) {
|
||||
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)
|
||||
|
||||
return fileref
|
||||
}
|
||||
|
||||
function removeThemeTagsFromNotices() {
|
||||
// Grabs an array of the notice iFrames
|
||||
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]
|
||||
if (body) {
|
||||
// Replaces the theme tag with nothing
|
||||
const bodyText = body.innerHTML
|
||||
body.innerHTML = bodyText
|
||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||
.replace(/ +/, " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateIframesWithDarkMode(): Promise<void> {
|
||||
const cssLink = document.createElement("style")
|
||||
cssLink.classList.add("iframecss")
|
||||
const cssContent = document.createTextNode(iframeCSS)
|
||||
cssLink.appendChild(cssContent)
|
||||
|
||||
eventManager.register(
|
||||
"iframeAdded",
|
||||
{
|
||||
elementType: "iframe",
|
||||
customCheck: (element: Element) =>
|
||||
!element.classList.contains("iframecss"),
|
||||
},
|
||||
(element) => {
|
||||
const iframe = element as HTMLIFrameElement
|
||||
try {
|
||||
applyDarkModeToIframe(iframe, cssLink)
|
||||
|
||||
if (element.classList.contains("cke_wysiwyg_frame")) {
|
||||
(async () => {
|
||||
await delay(100)
|
||||
iframe.contentDocument?.body.setAttribute("spellcheck", "true")
|
||||
})()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error applying dark mode:", error)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function applyDarkModeToIframe(
|
||||
iframe: HTMLIFrameElement,
|
||||
cssLink: HTMLStyleElement,
|
||||
): void {
|
||||
const iframeDocument = iframe.contentDocument
|
||||
if (!iframeDocument) return
|
||||
|
||||
iframe.onload = () => {
|
||||
applyDarkModeToIframe(iframe, cssLink)
|
||||
}
|
||||
|
||||
if (settingsState.DarkMode) {
|
||||
iframeDocument.documentElement.classList.add("dark")
|
||||
}
|
||||
|
||||
const head = iframeDocument.head
|
||||
if (head && !head.innerHTML.includes("iframecss")) {
|
||||
head.innerHTML += cssLink.outerHTML
|
||||
}
|
||||
}
|
||||
|
||||
function SortMessagePageItems(messagesParentElement: any) {
|
||||
try {
|
||||
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
|
||||
} catch (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]
|
||||
|
||||
eventManager.register(
|
||||
"messagesAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "messages",
|
||||
},
|
||||
handleMessages,
|
||||
)
|
||||
|
||||
eventManager.register(
|
||||
"noticesAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "notices",
|
||||
},
|
||||
CheckNoticeTextColour,
|
||||
)
|
||||
|
||||
eventManager.register(
|
||||
"dashboardAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "dashboard",
|
||||
},
|
||||
handleDashboard,
|
||||
)
|
||||
|
||||
eventManager.register(
|
||||
"documentsAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "documents",
|
||||
},
|
||||
handleDocuments,
|
||||
)
|
||||
|
||||
eventManager.register(
|
||||
"reportsAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "reports",
|
||||
},
|
||||
handleReports,
|
||||
)
|
||||
|
||||
/* eventManager.register(
|
||||
"timetableAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "timetablepage",
|
||||
},
|
||||
handleTimetable,
|
||||
) */
|
||||
|
||||
eventManager.register(
|
||||
"noticesAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "notice",
|
||||
},
|
||||
handleNotices,
|
||||
)
|
||||
|
||||
RegisterClickListeners()
|
||||
|
||||
await handleSublink(sublink)
|
||||
}
|
||||
|
||||
async function handleNotices(node: Element): Promise<void> {
|
||||
if (!(node instanceof HTMLElement)) return
|
||||
if (!settingsState.animations) return
|
||||
|
||||
node.style.opacity = "0"
|
||||
|
||||
// get index of node in relation to parent
|
||||
const index = Array.from(node.parentElement!.children).indexOf(node)
|
||||
|
||||
animate(
|
||||
node,
|
||||
{ opacity: [0, 1], y: [50, 0], scale: [0.99, 1] },
|
||||
{
|
||||
delay: 0.1 * index,
|
||||
type: "spring",
|
||||
stiffness: 250,
|
||||
damping: 20,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function handleSublink(sublink: string | undefined): Promise<void> {
|
||||
switch (sublink) {
|
||||
case "news":
|
||||
await handleNewsPage()
|
||||
break
|
||||
case undefined:
|
||||
window.location.replace(`${location.origin}/#?page=/${settingsState.defaultPage}`)
|
||||
if (settingsState.defaultPage === "home") loadHomePage()
|
||||
if (settingsState.defaultPage === "documents")
|
||||
handleDocuments(document.querySelector(".documents")!)
|
||||
if (settingsState.defaultPage === "reports")
|
||||
handleReports(document.querySelector(".reports")!)
|
||||
if (settingsState.defaultPage === "messages")
|
||||
handleMessages(document.querySelector(".messages")!)
|
||||
|
||||
finishLoad()
|
||||
break
|
||||
case "home":
|
||||
window.location.replace(`${location.origin}/#?page=/home`)
|
||||
console.info("[BetterSEQTA+] Started Init")
|
||||
if (settingsState.onoff) loadHomePage()
|
||||
finishLoad()
|
||||
break
|
||||
|
||||
default:
|
||||
await handleDefault()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNewsPage(): Promise<void> {
|
||||
console.info("[BetterSEQTA+] Started Init")
|
||||
if (settingsState.onoff) {
|
||||
SendNewsPage()
|
||||
finishLoad()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDefault(): Promise<void> {
|
||||
finishLoad()
|
||||
}
|
||||
|
||||
async function handleMessages(node: Element): Promise<void> {
|
||||
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
|
||||
|
||||
// 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)
|
||||
|
||||
await waitForElm("[data-message]", true, 10)
|
||||
const messages = Array.from(
|
||||
document.querySelectorAll("[data-message]"),
|
||||
).slice(0, 35)
|
||||
animate(
|
||||
messages,
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.03),
|
||||
duration: 0.5,
|
||||
ease: [0.22, 0.03, 0.26, 1],
|
||||
},
|
||||
)
|
||||
|
||||
document.head.querySelector("style.messageHider")?.remove()
|
||||
}
|
||||
|
||||
async function handleDashboard(node: Element): Promise<void> {
|
||||
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)
|
||||
|
||||
await waitForElm(".dashlet", true, 10)
|
||||
animate(
|
||||
".dashboard > *",
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.1),
|
||||
duration: 0.5,
|
||||
ease: [0.22, 0.03, 0.26, 1],
|
||||
},
|
||||
)
|
||||
|
||||
document.head.querySelector("style.dashboardHider")?.remove()
|
||||
}
|
||||
|
||||
async function handleDocuments(node: Element): Promise<void> {
|
||||
if (!(node instanceof HTMLElement)) return
|
||||
if (!settingsState.animations) return
|
||||
|
||||
await waitForElm(".document", true, 10)
|
||||
animate(
|
||||
".documents tbody tr.document",
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.05),
|
||||
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
|
||||
|
||||
await waitForElm(".report", true, 10)
|
||||
animate(
|
||||
".reports .item",
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.05, { startDelay: 0.2 }),
|
||||
duration: 0.5,
|
||||
ease: [0.22, 0.03, 0.26, 1],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function CheckNoticeTextColour(notice: any) {
|
||||
eventManager.register(
|
||||
"noticeAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "notice",
|
||||
parentElement: notice,
|
||||
},
|
||||
(node) => {
|
||||
var hex = (node as HTMLElement).style.cssText.split(" ")[1]
|
||||
if (hex) {
|
||||
const hex1 = hex.slice(0, -1)
|
||||
var threshold = GetThresholdOfColor(hex1)
|
||||
if (settingsState.DarkMode && threshold < 100) {
|
||||
(node as HTMLElement).style.cssText = "--color: undefined;"
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function tryLoad() {
|
||||
waitForElm(".login").then(() => {
|
||||
finishLoad()
|
||||
})
|
||||
|
||||
waitForElm(".day-container").then(() => {
|
||||
finishLoad()
|
||||
})
|
||||
|
||||
waitForElm("[data-key=welcome]").then((elm: any) => {
|
||||
elm.classList.remove("active")
|
||||
})
|
||||
|
||||
waitForElm(".code", true, 50).then((elm: any) => {
|
||||
if (!elm.innerText.includes("BetterSEQTA")) LoadPageElements()
|
||||
})
|
||||
|
||||
updateIframesWithDarkMode()
|
||||
// Waits for page to call on load, run scripts
|
||||
document.addEventListener(
|
||||
"load",
|
||||
function () {
|
||||
removeThemeTagsFromNotices()
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
function ReplaceMenuSVG(element: HTMLElement, svg: string) {
|
||||
let item = element.firstChild as HTMLElement
|
||||
item!.firstChild!.remove()
|
||||
|
||||
item.innerHTML = `<span>${item.innerHTML}</span>`
|
||||
|
||||
let newsvg = stringToHTML(svg).firstChild
|
||||
item.insertBefore(newsvg as Node, item.firstChild)
|
||||
}
|
||||
|
||||
export async function ObserveMenuItemPosition() {
|
||||
await waitForElm("#menu > ul > li")
|
||||
await delay(100)
|
||||
|
||||
eventManager.register(
|
||||
"menuList",
|
||||
{
|
||||
parentElement: document.querySelector("#menu")!.firstChild as Element,
|
||||
},
|
||||
(element: Element) => {
|
||||
const node = element as HTMLElement
|
||||
if (!node?.dataset?.checked && !MenuOptionsOpen) {
|
||||
const key =
|
||||
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
|
||||
|
||||
if (
|
||||
textNode.nodeType === 3 &&
|
||||
textNode.parentNode &&
|
||||
textNode.parentNode.nodeName !== "SPAN"
|
||||
) {
|
||||
const span = document.createElement("span")
|
||||
span.textContent = textNode.nodeValue
|
||||
|
||||
label.replaceChild(span, textNode)
|
||||
}
|
||||
}
|
||||
ChangeMenuItemPositions(settingsState.menuorder)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function showConflictPopup() {
|
||||
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 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 textHTML = /* html */ `
|
||||
<div class="whatsnewTextContainer" style="overflow-y: auto; font-size: 1.3rem;">
|
||||
<p>
|
||||
It appears that you have the legacy BetterSEQTA extension installed alongside BetterSEQTA+.
|
||||
This conflict may cause unexpected behavior. (and breaks the extension)
|
||||
</p>
|
||||
<p>
|
||||
Please remove the older BetterSEQTA extension to ensure that BetterSEQTA+ works correctly.
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
const text = stringToHTML(textHTML).firstChild
|
||||
|
||||
const exitButton = document.createElement("div")
|
||||
exitButton.id = "whatsnewclosebutton"
|
||||
|
||||
if (header) container.append(header)
|
||||
if (text) container.append(text)
|
||||
container.append(exitButton)
|
||||
|
||||
background.append(container)
|
||||
|
||||
document.getElementById("container")?.append(background)
|
||||
|
||||
if (settingsState.animations) {
|
||||
animate([background as HTMLElement], { opacity: [0, 1] })
|
||||
}
|
||||
|
||||
background.addEventListener("click", (event) => {
|
||||
if (event.target === background) {
|
||||
background.remove()
|
||||
}
|
||||
})
|
||||
|
||||
exitButton.addEventListener("click", () => {
|
||||
background.remove()
|
||||
})
|
||||
}
|
||||
|
||||
export function init() {
|
||||
const handleDisabled = () => {
|
||||
waitForElm(".code", true, 50).then(AppendElementsToDisabledPage)
|
||||
}
|
||||
|
||||
if (settingsState.onoff) {
|
||||
console.info("[BetterSEQTA+] Enabled")
|
||||
if (settingsState.DarkMode) document.documentElement.classList.add("dark")
|
||||
|
||||
document.querySelector(".legacy-root")?.classList.add("hidden")
|
||||
|
||||
new StorageChangeHandler()
|
||||
new MessageHandler()
|
||||
|
||||
updateAllColors()
|
||||
loading()
|
||||
InjectCustomIcons()
|
||||
HideMenuItems()
|
||||
tryLoad()
|
||||
|
||||
setTimeout(() => {
|
||||
const legacyElement = document.querySelector(
|
||||
".outside-container .bottom-container",
|
||||
)
|
||||
if (legacyElement) {
|
||||
console.log("Legacy extension detected")
|
||||
showConflictPopup()
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
handleDisabled()
|
||||
window.addEventListener("load", handleDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
function InjectCustomIcons() {
|
||||
console.info("[BetterSEQTA+] Injecting Icons")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function AppendElementsToDisabledPage() {
|
||||
console.info("[BetterSEQTA+] Appending elements to disabled page")
|
||||
AddBetterSEQTAElements()
|
||||
|
||||
let settingsStyle = document.createElement("style")
|
||||
settingsStyle.innerHTML = /* css */ `
|
||||
.addedButton {
|
||||
position: absolute !important;
|
||||
right: 50px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
padding: 6px !important;
|
||||
overflow: unset !important;
|
||||
border-radius: 50%;
|
||||
margin: 7px !important;
|
||||
cursor: pointer;
|
||||
color: white !important;
|
||||
}
|
||||
.addedButton svg {
|
||||
margin: 6px;
|
||||
}
|
||||
.outside-container {
|
||||
top: 48px !important;
|
||||
}
|
||||
#ExtensionPopup {
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0px 0px 20px -2px rgba(0, 0, 0, 0.6);
|
||||
transform-origin: 70% 0;
|
||||
}
|
||||
`
|
||||
document.head.append(settingsStyle)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,45 @@
|
||||
// Third-party libraries
|
||||
import browser from "webextension-polyfill"
|
||||
|
||||
// Internal utilities and functions
|
||||
import {
|
||||
initializeSettingsState,
|
||||
settingsState,
|
||||
} from "@/seqta/utils/listeners/SettingsState"
|
||||
|
||||
// UI and theme management
|
||||
import pageState from "@/pageState.js?url"
|
||||
|
||||
// Stylesheets
|
||||
import injectedCSS from "@/css/injected.scss?inline"
|
||||
|
||||
export async function main() {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
await initializeSettingsState()
|
||||
|
||||
if (settingsState.onoff) {
|
||||
injectPageState()
|
||||
|
||||
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
||||
if (import.meta.env.MODE === "development") {
|
||||
import("../css/injected.scss")
|
||||
} else {
|
||||
const injectedStyle = document.createElement("style")
|
||||
injectedStyle.textContent = injectedCSS
|
||||
document.head.appendChild(injectedStyle)
|
||||
}
|
||||
}
|
||||
resolve(true)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function injectPageState() {
|
||||
const mainScript = document.createElement("script")
|
||||
mainScript.src = browser.runtime.getURL(pageState)
|
||||
document.head.appendChild(mainScript)
|
||||
}
|
||||
@@ -1,26 +1,68 @@
|
||||
import { addExtensionSettings, enableAnimatedBackground, GetThresholdOfColor, loadHomePage, SendNewsPage, setupSettingsButton } from "@/SEQTA";
|
||||
import { updateBgDurations } from "./Animation";
|
||||
import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings";
|
||||
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";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { updateAllColors } from "./colors/Manager";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
|
||||
let cachedUserInfo: any = null;
|
||||
|
||||
async function getUserInfo() {
|
||||
if (cachedUserInfo) return cachedUserInfo;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${location.origin}/seqta/student/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mode: 'normal',
|
||||
query: null,
|
||||
redirect_url: location.origin,
|
||||
}),
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
cachedUserInfo = responseData.payload;
|
||||
return cachedUserInfo;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user info:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function AddBetterSEQTAElements() {
|
||||
if (settingsState.onoff) {
|
||||
initializeSettings();
|
||||
if (settingsState.DarkMode) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
createHomeButton();
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
const menu = document.getElementById('menu')!;
|
||||
const menuList = menu.firstChild as HTMLElement;
|
||||
|
||||
createHomeButton(fragment, menuList);
|
||||
createNewsButton(fragment, menu);
|
||||
|
||||
menuList.insertBefore(fragment, menuList.firstChild);
|
||||
|
||||
try {
|
||||
await appendBackgroundToUI();
|
||||
await Promise.all([
|
||||
appendBackgroundToUI(),
|
||||
handleUserInfo(),
|
||||
handleStudentData()
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error appending background to UI:', error);
|
||||
console.error('Error initializing UI elements:', error);
|
||||
}
|
||||
await handleUserInfo();
|
||||
handleStudentData();
|
||||
createNewsButton();
|
||||
|
||||
setupEventListeners();
|
||||
await addDarkLightToggle();
|
||||
customizeMenuToggle();
|
||||
@@ -28,27 +70,18 @@ export async function AddBetterSEQTAElements() {
|
||||
|
||||
addExtensionSettings();
|
||||
await createSettingsButton();
|
||||
|
||||
setupSettingsButton();
|
||||
}
|
||||
|
||||
function initializeSettings() {
|
||||
enableAnimatedBackground();
|
||||
updateBgDurations();
|
||||
}
|
||||
|
||||
function createHomeButton() {
|
||||
function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) {
|
||||
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 menu = document.getElementById('menu')!;
|
||||
const List = menu.firstChild! as HTMLElement;
|
||||
|
||||
if (NewButton.firstChild) {
|
||||
List.insertBefore(NewButton.firstChild, List.firstChild);
|
||||
fragment.appendChild(NewButton.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,28 +158,6 @@ async function handleStudentData() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserInfo() {
|
||||
try {
|
||||
const response = await fetch(`${location.origin}/seqta/student/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
mode: 'normal',
|
||||
query: null,
|
||||
redirect_url: location.origin,
|
||||
}),
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData.payload;
|
||||
} catch (error) {
|
||||
console.error('Error fetching user info:', error);
|
||||
throw error; // Rethrow the error after logging it
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStudentInfo(students: any) {
|
||||
const info = await getUserInfo();
|
||||
var index = students.findIndex(function (person: any) {
|
||||
@@ -179,41 +190,42 @@ async function updateStudentInfo(students: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function createNewsButton() {
|
||||
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 NewsButton = stringToHTML(NewsButtonStr);
|
||||
const menu = document.getElementById('menu')!;
|
||||
const List = menu.firstChild! as HTMLElement;
|
||||
|
||||
List!.appendChild(NewsButton.firstChild!);
|
||||
if (NewsButton.firstChild) {
|
||||
fragment.appendChild(NewsButton.firstChild);
|
||||
}
|
||||
|
||||
let a = document.createElement('div');
|
||||
a.classList.add('icon-cover');
|
||||
a.id = 'icon-cover';
|
||||
menu!.appendChild(a);
|
||||
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');
|
||||
menuCover!.addEventListener('click', function () {
|
||||
location.href = '../#?page=/home';
|
||||
loadHomePage();
|
||||
(document!.getElementById('menu')!.firstChild! as HTMLElement).classList.remove('noscroll');
|
||||
});
|
||||
|
||||
const homebutton = document.getElementById('homebutton');
|
||||
homebutton!.addEventListener('click', function () {
|
||||
if (!homebutton?.classList.contains('draggable') && !homebutton?.classList.contains('active')) {
|
||||
const newsbutton = document.getElementById('newsbutton');
|
||||
|
||||
homebutton?.addEventListener('click', function() {
|
||||
if (!homebutton.classList.contains('draggable') && !homebutton.classList.contains('active')) {
|
||||
loadHomePage();
|
||||
}
|
||||
});
|
||||
|
||||
const newsbutton = document.getElementById('newsbutton');
|
||||
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';
|
||||
loadHomePage();
|
||||
(document.getElementById('menu')!.firstChild! as HTMLElement).classList.remove('noscroll');
|
||||
});
|
||||
}
|
||||
|
||||
async function createSettingsButton() {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
|
||||
/**
|
||||
* Update the background animation durations based on the slider input.
|
||||
* @param {Object} item - The object containing the bksliderinput property.
|
||||
* @param {number} [minDuration=1] - The minimum animation duration in seconds.
|
||||
* @param {number} [maxDuration=10] - The maximum animation duration in seconds.
|
||||
*/
|
||||
export function updateBgDurations() {
|
||||
// Class names to look for
|
||||
const bgClasses = ['bg', 'bg2', 'bg3'];
|
||||
|
||||
// Function to calculate animation duration
|
||||
const calcDuration = (
|
||||
baseValue: number,
|
||||
offset = 0,
|
||||
minBase = 50,
|
||||
maxBase = 150,
|
||||
) => {
|
||||
const scaledValue = 2 + ((maxBase - baseValue) / (maxBase - minBase)) ** 4;
|
||||
return scaledValue + offset;
|
||||
};
|
||||
|
||||
// Iterate through each class name to update its animation duration
|
||||
bgClasses.forEach((className, index) => {
|
||||
const elements = document.getElementsByClassName(className);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = index * 0.05;
|
||||
const duration = calcDuration(parseInt(settingsState.bksliderinput), offset);
|
||||
(elements[0] as HTMLElement).style.animationDuration = `${duration}s`;
|
||||
(elements[0] as HTMLElement).style.animationDelay = `${offset * 5}s`;
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import browser from 'webextension-polyfill'
|
||||
import { GetThresholdOfColor } from '@/SEQTA';
|
||||
import { GetThresholdOfColor } from '@/seqta/ui/colors/getThresholdColour';
|
||||
import { lightenAndPaleColor } from './lightenAndPaleColor';
|
||||
import ColorLuminance from './ColorLuminance';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Color from "color"
|
||||
export function GetThresholdOfColor(color: any) {
|
||||
if (!color) return 0
|
||||
// Case-insensitive regular expression for matching RGBA colors
|
||||
const rgbaRegex = /rgba?\(([^)]+)\)/gi
|
||||
|
||||
// Check if the color string is a gradient (linear or radial)
|
||||
if (color.includes("gradient")) {
|
||||
let gradientThresholds = []
|
||||
|
||||
// Find and replace all instances of RGBA in the gradient
|
||||
let match
|
||||
while ((match = rgbaRegex.exec(color)) !== null) {
|
||||
// Extract the individual components (r, g, b, a)
|
||||
const rgbaString = match[1]
|
||||
const [r, g, b] = rgbaString.split(",").map((str) => str.trim())
|
||||
|
||||
// Compute the threshold using your existing algorithm
|
||||
const threshold = Math.sqrt(
|
||||
parseInt(r) ** 2 + parseInt(g) ** 2 + parseInt(b) ** 2,
|
||||
)
|
||||
|
||||
// Store the computed threshold
|
||||
gradientThresholds.push(threshold)
|
||||
}
|
||||
|
||||
// Calculate the average threshold
|
||||
const averageThreshold =
|
||||
gradientThresholds.reduce((acc, val) => acc + val, 0) /
|
||||
gradientThresholds.length
|
||||
|
||||
return averageThreshold
|
||||
} else {
|
||||
// Handle the color as a simple RGBA (or hex, or whatever the Color library supports)
|
||||
const rgb = Color.rgb(color).object()
|
||||
return Math.sqrt(rgb.r ** 2 + rgb.g ** 2 + rgb.b ** 2)
|
||||
}
|
||||
}
|
||||
@@ -77,26 +77,26 @@ const contentConfig: ContentConfig = {
|
||||
},
|
||||
|
||||
messageSubject: {
|
||||
selector: '.MessageList__subject___1NV5O',
|
||||
selector: '[class*="MessageList__subject___"]',
|
||||
action: (element) => { element.textContent = getRandomElement(mockData.messages.subjects); }
|
||||
},
|
||||
|
||||
messageSender: {
|
||||
selector: '.MessageList__value___1sN24',
|
||||
selector: '[class*="MessageList__value___"]',
|
||||
action: (element) => { element.textContent = getRandomElement(mockData.messages.sender); }
|
||||
},
|
||||
|
||||
messageRecipients: {
|
||||
selector: '.MessageList__recipients___3hqpE .MessageList__value___1sN24',
|
||||
selector: '[class*="MessageList__recipients___"] [class*="MessageList__value___"]',
|
||||
action: (element) => { element.textContent = 'Recipient(s) Redacted'; }
|
||||
},
|
||||
|
||||
messageDate: {
|
||||
selector: '.MessageList__date___7muMb',
|
||||
selector: '[class*="MessageList__date___"]',
|
||||
action: (element) => { element.textContent = getRandomDate().toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'long' }); }
|
||||
},
|
||||
avatarImage: {
|
||||
selector: '.Avatar__Avatar___gE5kx',
|
||||
selector: '[class*="Avatar__Avatar___"]',
|
||||
action: (element) => {
|
||||
if (element instanceof HTMLElement) {
|
||||
element.style.removeProperty('background-image');
|
||||
@@ -105,7 +105,7 @@ const contentConfig: ContentConfig = {
|
||||
}
|
||||
},
|
||||
notificationCount: {
|
||||
selector: '.notifications__bubble___1EkSQ',
|
||||
selector: '[class*="notifications__bubble___"]',
|
||||
action: (element) => { element.textContent = Math.floor(Math.random() * 100).toString(); }
|
||||
},
|
||||
schoolName: {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
export const imageData: Record<string, { url: string; variableName: string }> = {};
|
||||
|
||||
export function applyCustomCSS(customCSS: string) {
|
||||
let styleElement = document.getElementById('custom-theme');
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = 'custom-theme';
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
styleElement.textContent = customCSS;
|
||||
}
|
||||
|
||||
export function removeImageFromDocument(variableName: string) {
|
||||
document.documentElement.style.removeProperty('--' + variableName);
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import type { LoadedCustomTheme } from '@/types/CustomThemes';
|
||||
import { applyCustomCSS, removeImageFromDocument } from './Themes';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
let previousImageVariableNames: string[] = [];
|
||||
let originalColor: string | null = null;
|
||||
let originalTheme: boolean | null = null;
|
||||
|
||||
export const UpdateThemePreview = async (updatedTheme: LoadedCustomTheme) => {
|
||||
const { CustomCSS, CustomImages, defaultColour, forceDark } = updatedTheme;
|
||||
|
||||
// Update dark mode setting
|
||||
if (forceDark !== undefined) {
|
||||
// Store the original theme if it hasn't been stored yet
|
||||
if (originalTheme === null) {
|
||||
originalTheme = settingsState.DarkMode;
|
||||
}
|
||||
settingsState.DarkMode = forceDark;
|
||||
}
|
||||
|
||||
// Get the new image variable names
|
||||
const newImageVariableNames = CustomImages.map(image => image.variableName);
|
||||
|
||||
// Remove images that are no longer present
|
||||
previousImageVariableNames.forEach(variableName => {
|
||||
if (!newImageVariableNames.includes(variableName)) {
|
||||
removeImageFromDocument(variableName);
|
||||
}
|
||||
});
|
||||
|
||||
// Update or add new images
|
||||
CustomImages.forEach((image: any) => {
|
||||
document.documentElement.style.setProperty(`--${image.variableName}`, `url(${image.url})`);
|
||||
});
|
||||
|
||||
// Update the previousImageVariableNames for the next run
|
||||
previousImageVariableNames = newImageVariableNames;
|
||||
|
||||
// Apply custom CSS
|
||||
applyCustomCSS(CustomCSS);
|
||||
|
||||
// Apply default color
|
||||
if (defaultColour) {
|
||||
// Store the original color if it hasn't been stored yet
|
||||
if (originalColor === null) {
|
||||
originalColor = settingsState.selectedColor;
|
||||
}
|
||||
settingsState.selectedColor = defaultColour;
|
||||
}
|
||||
};
|
||||
|
||||
export const ClearThemePreview = () => {
|
||||
previousImageVariableNames.forEach(variableName => {
|
||||
removeImageFromDocument(variableName);
|
||||
});
|
||||
|
||||
previousImageVariableNames = [];
|
||||
|
||||
let styleElement = document.getElementById('custom-theme');
|
||||
if (styleElement) {
|
||||
styleElement.remove();
|
||||
}
|
||||
|
||||
// Reset the color to the original value
|
||||
if (originalColor !== null) {
|
||||
settingsState.selectedColor = originalColor;
|
||||
originalColor = null;
|
||||
}
|
||||
|
||||
// Reset the theme (dark/light mode) to the original value
|
||||
if (originalTheme !== null) {
|
||||
settingsState.DarkMode = originalTheme;
|
||||
originalTheme = null;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { CustomImage, CustomTheme } from '@/types/CustomThemes';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
import { applyCustomCSS } from './Themes';
|
||||
|
||||
|
||||
export const applyTheme = async (theme: CustomTheme, reEnable?: boolean) => {
|
||||
let CustomCSS = '';
|
||||
let CustomImages: CustomImage[] = [];
|
||||
|
||||
if (theme?.CustomCSS) CustomCSS = theme.CustomCSS;
|
||||
if (theme?.CustomImages) CustomImages = theme.CustomImages;
|
||||
if (theme?.forceDark != undefined) {
|
||||
if (!reEnable) settingsState.originalDarkMode = settingsState.DarkMode
|
||||
|
||||
settingsState.DarkMode = theme.forceDark
|
||||
}
|
||||
|
||||
// Apply custom CSS
|
||||
applyCustomCSS(CustomCSS);
|
||||
|
||||
// Apply custom images
|
||||
CustomImages.forEach((image) => {
|
||||
const imageUrl = URL.createObjectURL(image.blob);
|
||||
document.documentElement.style.setProperty('--' + image.variableName, `url(${imageUrl})`);
|
||||
});
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { CustomTheme } from '@/types/CustomThemes';
|
||||
import { removeTheme } from './removeTheme';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
|
||||
export const deleteTheme = async (themeId: string) => {
|
||||
try {
|
||||
const theme = await localforage.getItem(themeId) as CustomTheme;
|
||||
removeTheme(theme);
|
||||
|
||||
await localforage.removeItem(themeId);
|
||||
const themeIds = await localforage.getItem('customThemes') as string[] | null;
|
||||
if (themeIds) {
|
||||
const updatedThemeIds = themeIds.filter((id) => id !== themeId);
|
||||
await localforage.setItem('customThemes', updatedThemeIds);
|
||||
}
|
||||
|
||||
settingsState.selectedTheme = ''
|
||||
} catch (error) {
|
||||
console.error('Error deleting theme:', error);
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { CustomTheme } from '@/types/CustomThemes';
|
||||
import { removeTheme } from './removeTheme';
|
||||
import { Mutex } from '@/seqta/utils/mutex';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
const mutex = new Mutex();
|
||||
let isDisabling = false;
|
||||
|
||||
export const disableTheme = async () => {
|
||||
if (isDisabling) return;
|
||||
|
||||
if (!settingsState.selectedTheme || settingsState.selectedTheme === '') {
|
||||
console.debug('Theme is already disabled, exit early')
|
||||
// Theme is already disabled, exit early
|
||||
return;
|
||||
}
|
||||
isDisabling = true;
|
||||
const unlock = await mutex.lock();
|
||||
try {
|
||||
if (settingsState.selectedTheme) {
|
||||
console.debug('Disabling theme:', settingsState.selectedTheme);
|
||||
const theme = await localforage.getItem(settingsState.selectedTheme) as CustomTheme;
|
||||
if (theme) {
|
||||
await removeTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
settingsState.selectedTheme = ''
|
||||
} catch (error) {
|
||||
console.error('Error disabling theme:', error);
|
||||
} finally {
|
||||
unlock();
|
||||
isDisabling = false;
|
||||
}
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import base64ToBlob from '@/seqta/utils/base64ToBlob';
|
||||
|
||||
type Theme = {
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string;
|
||||
marqueeImage: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ThemeContent = {
|
||||
id: string;
|
||||
name: string;
|
||||
coverImage: string; // base64
|
||||
description: string;
|
||||
defaultColour: string;
|
||||
CanChangeColour: boolean;
|
||||
CustomCSS: string;
|
||||
hideThemeName: boolean;
|
||||
images: { id: string, variableName: string, data: string }[]; // data: base64
|
||||
};
|
||||
|
||||
function stripBase64Prefix(base64String: string): string {
|
||||
const prefixRegex = /^data:image\/\w+;base64,/;
|
||||
return base64String.replace(prefixRegex, '');
|
||||
}
|
||||
|
||||
export const StoreDownloadTheme = async (theme: { themeContent: Theme }) => {
|
||||
if (!theme.themeContent.id) return;
|
||||
|
||||
const themeContent = await fetch(`https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Themes/main/store/themes/${theme.themeContent.id}/theme.json`);
|
||||
const themeData = await themeContent.json() as ThemeContent;
|
||||
|
||||
await InstallTheme(themeData);
|
||||
};
|
||||
|
||||
export const InstallTheme = async (themeData: ThemeContent) => {
|
||||
const strippedCoverImage = stripBase64Prefix(themeData.coverImage);
|
||||
const coverImageBlob = base64ToBlob(strippedCoverImage);
|
||||
|
||||
const images = themeData.images.map((image) => ({
|
||||
...image,
|
||||
blob: base64ToBlob(image.data)
|
||||
}));
|
||||
|
||||
let availableThemes = await localforage.getItem('customThemes') as string[];
|
||||
if (availableThemes && !availableThemes.includes(themeData.id)) {
|
||||
availableThemes.push(themeData.id);
|
||||
} else if (!availableThemes) {
|
||||
availableThemes = [themeData.id];
|
||||
}
|
||||
await localforage.setItem('customThemes', availableThemes);
|
||||
|
||||
await localforage.setItem(themeData.id, {
|
||||
...themeData,
|
||||
webURL: themeData.id,
|
||||
coverImage: coverImageBlob,
|
||||
CustomImages: themeData.images.map((image) => {
|
||||
return {
|
||||
...image,
|
||||
blob: images.find((img) => image.id === img.id)?.blob
|
||||
};
|
||||
})
|
||||
});
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { CustomTheme } from '@/types/CustomThemes';
|
||||
import { applyTheme } from './applyTheme';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
|
||||
export const enableCurrentTheme = async () => {
|
||||
if (settingsState.selectedTheme) {
|
||||
const theme = await localforage.getItem(settingsState.selectedTheme) as CustomTheme;
|
||||
if (theme) {
|
||||
await applyTheme(theme, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { CustomTheme, ThemeList } from '@/types/CustomThemes';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
export const getAvailableThemes = async (): Promise<ThemeList> => {
|
||||
try {
|
||||
const themeIds = await localforage.getItem('customThemes') as string[] | null;
|
||||
if (themeIds) {
|
||||
const themes = await Promise.all(
|
||||
themeIds.map(async (id) => {
|
||||
const theme = await localforage.getItem(id) as CustomTheme;
|
||||
return theme;
|
||||
})
|
||||
);
|
||||
|
||||
return { themes, selectedTheme: settingsState.selectedTheme ? settingsState.selectedTheme : '' };
|
||||
}
|
||||
return {
|
||||
themes: [],
|
||||
selectedTheme: '',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting available themes:', error);
|
||||
return {
|
||||
themes: [],
|
||||
selectedTheme: ''
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { LoadedCustomTheme } from '@/types/CustomThemes';
|
||||
|
||||
|
||||
export const getTheme = async (themeId: string): Promise<LoadedCustomTheme | null> => {
|
||||
try {
|
||||
const theme = await localforage.getItem(themeId) as LoadedCustomTheme;
|
||||
|
||||
return theme;
|
||||
} catch (error) {
|
||||
console.error('Error getting theme:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { CustomTheme } from '@/types/CustomThemes';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
export const removeTheme = async (theme: CustomTheme) => {
|
||||
// Remove custom CSS
|
||||
const styleElement = document.getElementById('custom-theme');
|
||||
if (styleElement) {
|
||||
styleElement.parentNode?.removeChild(styleElement);
|
||||
}
|
||||
|
||||
const selectedTheme = await localforage.getItem(theme.id) as CustomTheme;
|
||||
localforage.setItem(theme.id, {
|
||||
...selectedTheme,
|
||||
selectedColor: settingsState.selectedColor
|
||||
})
|
||||
|
||||
// Reset default color
|
||||
if (settingsState.originalSelectedColor !== '') {
|
||||
settingsState.selectedColor = settingsState.originalSelectedColor
|
||||
}
|
||||
|
||||
if (settingsState.originalDarkMode !== undefined) {
|
||||
settingsState.DarkMode = settingsState.originalDarkMode
|
||||
settingsState.originalDarkMode = undefined
|
||||
}
|
||||
|
||||
// Remove custom images
|
||||
const customImageVariables = theme.CustomImages.map((image) => image.variableName);
|
||||
customImageVariables.forEach((variableName) => {
|
||||
const blobUrl = document.documentElement.style.getPropertyValue('--' + variableName);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
|
||||
document.documentElement.style.removeProperty('--' + variableName);
|
||||
});
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { LoadedCustomTheme } from '@/types/CustomThemes';
|
||||
import { disableTheme } from './disableTheme';
|
||||
import { themeUpdates } from '@/interface/hooks/ThemeUpdates';
|
||||
|
||||
|
||||
export const saveTheme = async (theme: LoadedCustomTheme) => {
|
||||
try {
|
||||
disableTheme();
|
||||
|
||||
console.debug('Theme to save:', theme);
|
||||
|
||||
await localforage.setItem(theme.id, theme);
|
||||
await localforage.getItem('customThemes').then((themes: unknown) => {
|
||||
const themeList = themes as string[] | null;
|
||||
if (themeList) {
|
||||
if (!themeList.includes(theme.id)) {
|
||||
themeList.push(theme.id);
|
||||
localforage.setItem('customThemes', themeList);
|
||||
}
|
||||
} else {
|
||||
localforage.setItem('customThemes', [theme.id]);
|
||||
}
|
||||
});
|
||||
console.debug('Theme saved successfully!');
|
||||
themeUpdates.triggerUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error saving theme:', error);
|
||||
}
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import localforage from 'localforage';
|
||||
import type { CustomTheme } from '@/types/CustomThemes';
|
||||
import { applyTheme } from './applyTheme';
|
||||
import { removeTheme } from './removeTheme';
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||
|
||||
|
||||
export const setTheme = async (themeId: string) => {
|
||||
try {
|
||||
const theme = await localforage.getItem(themeId) as CustomTheme;
|
||||
|
||||
console.debug('Loading theme', theme);
|
||||
|
||||
let originalSelectedColor = { selectedColor: '' };
|
||||
|
||||
const styleElement = document.getElementById('custom-theme');
|
||||
|
||||
// Remove the currently enabled theme
|
||||
if (settingsState.selectedTheme || styleElement) {
|
||||
const currentTheme = await localforage.getItem(settingsState.selectedTheme) as CustomTheme;
|
||||
if (currentTheme) {
|
||||
await removeTheme(currentTheme);
|
||||
}
|
||||
originalSelectedColor = { selectedColor: settingsState.originalSelectedColor };
|
||||
} else {
|
||||
originalSelectedColor = { selectedColor: settingsState.selectedColor };
|
||||
}
|
||||
|
||||
await applyTheme(theme);
|
||||
|
||||
settingsState.selectedTheme = themeId
|
||||
settingsState.selectedColor = theme.selectedColor ? theme.selectedColor : (theme.defaultColour !== '' ? theme.defaultColour : '#007bff')
|
||||
settingsState.originalSelectedColor = originalSelectedColor.selectedColor
|
||||
} catch (error) {
|
||||
console.error('Error setting theme:', error);
|
||||
}
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { getTheme } from './getTheme';
|
||||
|
||||
const saveThemeFile = (data: object, fileName: string) => {
|
||||
const fileData = JSON.stringify(data, null, 2);
|
||||
const blob = new Blob([fileData], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${fileName}.json.theme`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const shareTheme = async (themeID: string) => {
|
||||
try {
|
||||
// Use getTheme to retrieve the theme data
|
||||
const themeData = await getTheme(themeID);
|
||||
if (!themeData) {
|
||||
console.error('Failed to retrieve theme data');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract images and coverImage from themeData, if they exist
|
||||
const { CustomImages = [], coverImage, ...themeWithoutImages } = themeData;
|
||||
|
||||
// Helper function to convert Blob to Base64
|
||||
const blobToBase64 = (blob: Blob) => new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
// Convert cover image to Base64
|
||||
let coverImageBase64 = null;
|
||||
if (coverImage) {
|
||||
coverImageBase64 = await blobToBase64(coverImage);
|
||||
}
|
||||
|
||||
// Convert custom images to Base64
|
||||
const finalImages = await Promise.all(CustomImages.map(async (image) => {
|
||||
const imageBase64 = await blobToBase64(image.blob);
|
||||
return {
|
||||
id: image.id,
|
||||
variableName: image.variableName,
|
||||
data: imageBase64,
|
||||
};
|
||||
}));
|
||||
|
||||
// Prepare the non-file data for uploading
|
||||
const data = {
|
||||
...themeWithoutImages,
|
||||
images: finalImages.map((image) => ({
|
||||
id: image.id,
|
||||
variableName: image.variableName,
|
||||
data: image.data,
|
||||
})),
|
||||
coverImage: coverImageBase64,
|
||||
};
|
||||
|
||||
saveThemeFile(data, themeData.name || 'Unnamed_Theme');
|
||||
} catch (error) {
|
||||
console.error('Error sharing theme:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export default shareTheme;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { changeSettingsClicked, closeExtensionPopup, SettingsClicked } from "../Closers/closeExtensionPopup"
|
||||
import renderSvelte from "@/interface/main"
|
||||
import { SettingsResizer } from "@/seqta/ui/SettingsResizer"
|
||||
import Settings from "@/interface/pages/settings.svelte"
|
||||
|
||||
export function addExtensionSettings() {
|
||||
const extensionPopup = document.createElement("div")
|
||||
extensionPopup.classList.add("outside-container", "hide")
|
||||
extensionPopup.id = "ExtensionPopup"
|
||||
|
||||
const extensionContainer = document.querySelector(
|
||||
"#container",
|
||||
) as HTMLDivElement
|
||||
if (extensionContainer) extensionContainer.appendChild(extensionPopup)
|
||||
|
||||
// create shadow dom and render svelte app
|
||||
try {
|
||||
const shadow = extensionPopup.attachShadow({ mode: "open" })
|
||||
requestIdleCallback(() => renderSvelte(Settings, shadow))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
const container = document.getElementById("container")
|
||||
|
||||
new SettingsResizer()
|
||||
|
||||
container!.onclick = (event) => {
|
||||
if (!SettingsClicked) return
|
||||
|
||||
if (!(event.target as HTMLElement).closest("#AddedSettings")) {
|
||||
if (event.target == extensionPopup) return
|
||||
changeSettingsClicked(closeExtensionPopup())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import ShortcutLinks from "@/seqta/content/links.json"
|
||||
import stringToHTML from "../stringToHTML"
|
||||
|
||||
export function addShortcuts(shortcuts: any) {
|
||||
for (let i = 0; i < shortcuts.length; i++) {
|
||||
const currentShortcut = shortcuts[i]
|
||||
|
||||
if (currentShortcut?.enabled) {
|
||||
const Itemname = (currentShortcut?.name ?? "").replace(/\s/g, "")
|
||||
|
||||
const linkDetails =
|
||||
ShortcutLinks?.[Itemname as keyof typeof ShortcutLinks]
|
||||
if (linkDetails) {
|
||||
createNewShortcut(
|
||||
linkDetails.link,
|
||||
linkDetails.icon,
|
||||
linkDetails.viewBox,
|
||||
currentShortcut?.name,
|
||||
)
|
||||
} else {
|
||||
console.warn(`No link details found for '${Itemname}'`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
||||
// Creates the stucture and element information for each seperate shortcut
|
||||
let shortcut = document.createElement("a")
|
||||
shortcut.setAttribute("href", link)
|
||||
shortcut.setAttribute("target", "_blank")
|
||||
let shortcutdiv = document.createElement("div")
|
||||
shortcutdiv.classList.add("shortcut")
|
||||
|
||||
let image = stringToHTML(
|
||||
`<svg style="width:39px;height:39px" viewBox="${viewBox}"><path fill="currentColor" d="${icon}" /></svg>`,
|
||||
).firstChild
|
||||
;(image! as HTMLElement).classList.add("shortcuticondiv")
|
||||
let text = document.createElement("p")
|
||||
text.textContent = title
|
||||
shortcutdiv.append(image as HTMLElement)
|
||||
shortcutdiv.append(text)
|
||||
shortcut.append(shortcutdiv)
|
||||
|
||||
document.getElementById("shortcuts")!.appendChild(shortcut)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { animate } from "motion"
|
||||
|
||||
import { settingsPopup } from "@/interface/hooks/SettingsPopup"
|
||||
|
||||
export let SettingsClicked = false
|
||||
|
||||
export const closeExtensionPopup = (extensionPopup?: HTMLElement) => {
|
||||
if (!extensionPopup)
|
||||
extensionPopup = document.getElementById("ExtensionPopup")!
|
||||
|
||||
extensionPopup.classList.add("hide")
|
||||
if (settingsState.animations) {
|
||||
animate(1, 0, {
|
||||
onUpdate: (progress) => {
|
||||
extensionPopup.style.opacity = Math.max(0, progress).toString()
|
||||
extensionPopup.style.transform = `scale(${Math.max(0, progress)})`
|
||||
},
|
||||
type: "spring",
|
||||
stiffness: 520,
|
||||
damping: 20,
|
||||
})
|
||||
} else {
|
||||
extensionPopup.style.opacity = "0"
|
||||
extensionPopup.style.transform = "scale(0)"
|
||||
}
|
||||
|
||||
settingsPopup.triggerClose()
|
||||
return SettingsClicked = false
|
||||
}
|
||||
|
||||
export function changeSettingsClicked(newVal: boolean) {
|
||||
SettingsClicked = newVal
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user