mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 19:54:39 +00:00
Compare commits
156 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 |
@@ -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**
|
**Bug Description**
|
||||||
A clear and concise description of what the bug is.
|
Please provide a clear and concise description of the bug.
|
||||||
|
|
||||||
**To Reproduce**
|
**Steps to Reproduce**
|
||||||
Please indicate how did you make this happen.
|
Please list the actions that caused the issue.
|
||||||
|
|
||||||
**Expected behaviuor**
|
**Expected Behavior**
|
||||||
Please add a clear and concise description of what you expected to happen.
|
Please describe how you think the program should have behaved, making sure to be as clear and concise as possible.
|
||||||
|
|
||||||
**Screenshots**
|
**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):**
|
**Additional Context**
|
||||||
- OS: [e.g. iOS]
|
Feel free to provide any additional, applicable context or information that is relevant to the problem.
|
||||||
- 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.
|
|
||||||
|
|||||||
@@ -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 [...]
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen. Provide reference art/pictures if poccible
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|||||||
@@ -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
|
||||||
.env.submit
|
.env.submit
|
||||||
|
|
||||||
|
dependency-graph.svg
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
extension.zip
|
extension.zip
|
||||||
build/
|
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,
|
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.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -44,23 +44,18 @@
|
|||||||
- Assessments
|
- Assessments
|
||||||
- Options to remove certain items from the side menu
|
- Options to remove certain items from the side menu
|
||||||
- Grades calculator
|
- Grades calculator
|
||||||
- Fully customisable themes and an offical theme store
|
- Fully customisable themes and an official theme store
|
||||||
- Notification for next lesson (sent 5 minutes before end of the lesson)
|
- Notification for next lesson (sent 5 minutes before end of the lesson)
|
||||||
- Browser Support
|
- Browser Support
|
||||||
- Chrome Supported
|
- Chrome, Edge, Brave, Opera and other Chromium-Based browsers are supported
|
||||||
- Edge Supported
|
- Firefox Supported: [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)!
|
||||||
- Brave Supported
|
- Safari (Experimental and not recommended - only available via compilation)
|
||||||
- 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)
|
|
||||||
|
|
||||||
## Creating Custom Themes
|
## Creating Custom Themes
|
||||||
|
|
||||||
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/BetterSEQTA/BetterSEQTA-Theme-Generator).
|
If you are looking to create custom themes, I would recommend you start at the official documentation [here](https://betterseqta.gitbook.io/betterseqta-docs). You can see some premade examples along with a compilation script that can be used to allow for CSS frameworks and libraries such as SCSS to be used [here](https://github.com/BetterSEQTA/BetterSEQTA-Theme-Generator).
|
||||||
|
|
||||||
Don't worry- if you get stuck feel free to ask around in the discord. 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
|
## Getting started
|
||||||
|
|
||||||
@@ -70,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
|
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running Development
|
|
||||||
|
|
||||||
1. Install dependencies
|
1. Install dependencies
|
||||||
|
|
||||||
|
You may install the dependencies like below:
|
||||||
|
|
||||||
```
|
```
|
||||||
npm install # or your preferred package manager like pnpm or yarn
|
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)
|
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
|
3. Load the extension into chrome
|
||||||
|
|
||||||
- Go to `chrome://extensions`
|
- Go to `chrome://extensions`
|
||||||
@@ -91,33 +109,15 @@ npm run dev
|
|||||||
- Click `Load unpacked`
|
- Click `Load unpacked`
|
||||||
- Select the `dist` folder
|
- 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.
|
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.
|
||||||
|
|
||||||
### 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Folder Structure
|
## Folder Structure
|
||||||
|
|
||||||
The folder structure is as follows:
|
The folder structure is as follows:
|
||||||
|
|
||||||
- The `src` folder contains source files that are compiled to the build directory.
|
- 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.
|
- The `src/interface` folder contains source React & Svelte files that are required for the Settings page.
|
||||||
|
|
||||||
@@ -136,4 +136,4 @@ This extension was initially developed by [Nulkem](https://github.com/Nulkem/bet
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#sethburkart123/EvenBetterSEQTA&Date)
|
[](https://star-history.com/#BetterSEQTA/BetterSEQTA-Plus&Date)
|
||||||
|
|||||||
+1
-1
@@ -12,4 +12,4 @@ Below here is the supported versions of BetterSEQTA+. Anything older than this i
|
|||||||
`*` May not work on other devices.
|
`*` May not work on other devices.
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
If you find vulnerabilities, REPORT IT IMMEDIATELY. 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
|
||||||
@@ -34,7 +34,6 @@ export function updateManifestPlugin(): PluginOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.watchFile(manifestPath, () => {
|
fs.watchFile(manifestPath, () => {
|
||||||
console.log('** watchFile **');
|
|
||||||
try {
|
try {
|
||||||
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
if (manifestContents.web_accessible_resources?.some((resource: any) => resource.use_dynamic_url)) {
|
if (manifestContents.web_accessible_resources?.some((resource: any) => resource.use_dynamic_url)) {
|
||||||
|
|||||||
+24
-6
@@ -5,26 +5,44 @@ const path = require('path');
|
|||||||
|
|
||||||
function getLatestVersion(files) {
|
function getLatestVersion(files) {
|
||||||
console.log('Files passed to getLatestVersion:', files);
|
console.log('Files passed to getLatestVersion:', files);
|
||||||
|
|
||||||
const versions = files.map(file => {
|
const versions = files.map(file => {
|
||||||
const match = file.match(/@(\d+\.\d+\.\d+)-/);
|
const match = file.match(/@([\d\.]+)-/);
|
||||||
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
|
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
|
||||||
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);
|
}).filter(Boolean);
|
||||||
|
|
||||||
console.log('Extracted versions:', versions);
|
console.log('Extracted versions:', versions.map(v => v.semverVersion));
|
||||||
const latestVersion = semver.maxSatisfying(versions, '*');
|
|
||||||
console.log('Latest version:', latestVersion);
|
// 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;
|
return latestVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestFiles(browser) {
|
function getLatestFiles(browser) {
|
||||||
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
||||||
console.log('Glob pattern:', pattern);
|
console.log('Glob pattern:', pattern);
|
||||||
|
|
||||||
const files = glob.sync(pattern);
|
const files = glob.sync(pattern);
|
||||||
console.log('Files found for browser', browser, ':', files);
|
console.log('Files found for browser', browser, ':', files);
|
||||||
|
|
||||||
const latestVersion = getLatestVersion(files);
|
const latestVersion = getLatestVersion(files);
|
||||||
|
|
||||||
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);
|
console.log('Latest file for browser', browser, ':', latestFile);
|
||||||
return 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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+49
-41
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "betterseqtaplus",
|
"name": "betterseqtaplus",
|
||||||
"version": "3.4.4",
|
"version": "3.4.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more 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",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
@@ -11,7 +11,9 @@
|
|||||||
"build:chrome": "cross-env MODE=chrome vite build",
|
"build:chrome": "cross-env MODE=chrome vite build",
|
||||||
"build:firefox": "cross-env MODE=firefox vite build",
|
"build:firefox": "cross-env MODE=firefox vite build",
|
||||||
"build:safari": "cross-env MODE=safari 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",
|
"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",
|
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
|
||||||
"publish": "bun lib/publish.js --b",
|
"publish": "bun lib/publish.js --b",
|
||||||
"zip": "bedframe zip"
|
"zip": "bedframe zip"
|
||||||
@@ -31,63 +33,69 @@
|
|||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||||
"@babel/runtime": "^7.26.7",
|
"@babel/runtime": "^7.26.9",
|
||||||
"@bedframe/cli": "^0.0.85",
|
"@bedframe/cli": "^0.0.91",
|
||||||
"@crxjs/vite-plugin": "2.0.0-beta.25",
|
"@crxjs/vite-plugin": "2.0.0-beta.25",
|
||||||
"@types/mime-types": "^2.1.4",
|
"@types/mime-types": "^2.1.4",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
"@types/react": "^19.0.10",
|
||||||
|
"@types/react-dom": "^19.0.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.57.1",
|
"dependency-cruiser": "^16.10.0",
|
||||||
|
"eslint": "9.22.0",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"sass": "^1.83.4",
|
"publish-browser-extension": "^3.0.0",
|
||||||
"sass-loader": "^13.3.3",
|
"sass": "^1.85.1",
|
||||||
|
"sass-loader": "^16.0.5",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
|
"tailwindcss": "3",
|
||||||
"url": "^0.11.4"
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-css": "^6.3.0",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@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",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/chrome": "^0.0.270",
|
"@types/chrome": "^0.0.308",
|
||||||
"@types/color": "^3.0.6",
|
"@types/color": "^4.2.0",
|
||||||
"@types/dompurify": "^3.2.0",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/lodash": "^4.17.15",
|
"@types/node": "^22.13.10",
|
||||||
"@types/node": "^20.17.17",
|
|
||||||
"@types/react": "^17.0.83",
|
|
||||||
"@types/react-dom": "^17.0.26",
|
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/webextension-polyfill": "^0.10.7",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
"@uiw/codemirror-extensions-color": "^4.23.8",
|
"@uiw/codemirror-extensions-color": "^4.23.10",
|
||||||
"@uiw/codemirror-theme-github": "^4.23.8",
|
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"autoprefixer": "^10.4.21",
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"color": "^4.2.3",
|
"color": "^5.0.0",
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.2.4",
|
||||||
"embla-carousel-autoplay": "^8.3.1",
|
"embla-carousel-autoplay": "^8.5.2",
|
||||||
"embla-carousel-svelte": "^8.3.1",
|
"embla-carousel-svelte": "^8.5.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.1.0",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.2",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"million": "^3.1.11",
|
"million": "^3.1.11",
|
||||||
"motion": "^11.12.0",
|
"motion": "^12.4.12",
|
||||||
"postcss": "^8.4.45",
|
"postcss": "^8.5.3",
|
||||||
"react": "17",
|
"react": "17",
|
||||||
"react-best-gradient-color-picker": "^3.0.10",
|
"react-best-gradient-color-picker": "3.0.11",
|
||||||
"react-dom": "17",
|
"react-dom": "17",
|
||||||
"sortablejs": "^1.15.3",
|
"rss-parser": "^3.13.0",
|
||||||
"svelte": "^5.1.9",
|
"sortablejs": "^1.15.6",
|
||||||
"tailwindcss": "^3.4.11",
|
"svelte": "^5.22.6",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.8.2",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^11.1.0",
|
||||||
"vite": "^5.4.14",
|
"vite": "^6.2.1",
|
||||||
"webextension-polyfill": "^0.10.0"
|
"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
|
||||||
+29
-2961
File diff suppressed because it is too large
Load Diff
+54
-119
@@ -1,61 +1,6 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
import type { SettingsState } from "@/types/storage";
|
import type { SettingsState } from "@/types/storage";
|
||||||
|
import { fetchNews } from './background/news';
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function reloadSeqtaPages() {
|
function reloadSeqtaPages() {
|
||||||
const result = browser.tabs.query({})
|
const result = browser.tabs.query({})
|
||||||
@@ -69,8 +14,9 @@ function reloadSeqtaPages() {
|
|||||||
result.then(open, console.error)
|
result.then(open, console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main message listener
|
// @ts-ignore
|
||||||
browser.runtime.onMessage.addListener((request: any, _sender: any, sendResponse: any) => {
|
browser.runtime.onMessage.addListener((request: any, _: any, sendResponse: (response?: any) => void) => {
|
||||||
|
|
||||||
switch (request.type) {
|
switch (request.type) {
|
||||||
case 'reloadTabs':
|
case 'reloadTabs':
|
||||||
reloadSeqtaPages();
|
reloadSeqtaPages();
|
||||||
@@ -103,36 +49,15 @@ browser.runtime.onMessage.addListener((request: any, _sender: any, sendResponse:
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'sendNews':
|
case 'sendNews':
|
||||||
const date = new Date();
|
fetchNews(request.source ?? 'australia', sendResponse);
|
||||||
|
|
||||||
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;
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log('Unknown request type');
|
console.log('Unknown request type');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
function GetNews(sendResponse: any, url: string) {
|
return false;
|
||||||
fetch(url)
|
|
||||||
.then((result) => result.json())
|
|
||||||
.then((response) => {
|
|
||||||
if (response.code == 'rateLimited') {
|
|
||||||
GetNews(sendResponse, url += '%00');
|
|
||||||
} else {
|
|
||||||
sendResponse({ news: response });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultValues: SettingsState = {
|
const DefaultValues: SettingsState = {
|
||||||
onoff: true,
|
onoff: true,
|
||||||
@@ -140,7 +65,6 @@ const DefaultValues: SettingsState = {
|
|||||||
bksliderinput: "50",
|
bksliderinput: "50",
|
||||||
transparencyEffects: false,
|
transparencyEffects: false,
|
||||||
lessonalert: true,
|
lessonalert: true,
|
||||||
notificationcollector: true,
|
|
||||||
defaultmenuorder: [],
|
defaultmenuorder: [],
|
||||||
menuitems: {
|
menuitems: {
|
||||||
assessments: { toggle: true },
|
assessments: { toggle: true },
|
||||||
@@ -220,6 +144,8 @@ const DefaultValues: SettingsState = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
customshortcuts: [],
|
customshortcuts: [],
|
||||||
|
lettergrade: false,
|
||||||
|
newsSource: 'australia',
|
||||||
};
|
};
|
||||||
|
|
||||||
function SetStorageValue(object: any) {
|
function SetStorageValue(object: any) {
|
||||||
@@ -228,54 +154,63 @@ function SetStorageValue(object: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function UpdateCurrentValues() {
|
function convertBksliderToSpeed(bksliderinput: number): number {
|
||||||
try {
|
const minBase = 50;
|
||||||
const items = await browser.storage.local.get();
|
const maxBase = 150;
|
||||||
const CurrentValues = items;
|
|
||||||
|
|
||||||
const NewValue = Object.assign({}, DefaultValues, CurrentValues);
|
const scaledValue = 2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
|
||||||
|
const baseSpeed = 3;
|
||||||
|
|
||||||
function CheckInnerElement(element: any) {
|
const speed = baseSpeed / scaledValue;
|
||||||
for (let i in element) {
|
return speed;
|
||||||
if (typeof element[i] === 'object') {
|
}
|
||||||
// @ts-expect-error
|
|
||||||
if (!Array.isArray(DefaultValues[i])) {
|
async function migrateLegacySettings() {
|
||||||
// @ts-expect-error
|
const storage = await browser.storage.local.get(null) as unknown as SettingsState;
|
||||||
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
|
|
||||||
|
// 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 {
|
} else {
|
||||||
// @ts-expect-error
|
await browser.storage.local.set({ 'plugin.notificationCollector.settings': { enabled: false } });
|
||||||
const length = DefaultValues[i].length;
|
|
||||||
// @ts-expect-error
|
|
||||||
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
|
|
||||||
let NewArray = [];
|
|
||||||
for (let j = 0; j < length; j++) {
|
|
||||||
NewArray.push(NewValue[i][j]);
|
|
||||||
}
|
|
||||||
NewValue[i] = NewArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckInnerElement(DefaultValues);
|
const keysToRemove = [
|
||||||
|
'animatedbk',
|
||||||
if (items['customshortcuts']) {
|
'bksliderinput',
|
||||||
NewValue['customshortcuts'] = items['customshortcuts'];
|
'assessmentsAverage',
|
||||||
}
|
'lettergrade'
|
||||||
|
];
|
||||||
SetStorageValue(NewValue);
|
await browser.storage.local.remove(keysToRemove);
|
||||||
console.log('[BetterSEQTA+] Values updated successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[BetterSEQTA+] Error updating values:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
browser.runtime.onInstalled.addListener(function (event) {
|
browser.runtime.onInstalled.addListener(function (event) {
|
||||||
browser.storage.local.remove(['justupdated']);
|
browser.storage.local.remove(['justupdated']);
|
||||||
browser.storage.local.remove(['data']);
|
browser.storage.local.remove(['data']);
|
||||||
|
|
||||||
UpdateCurrentValues();
|
if ( event.reason == 'install' || event.reason == 'update' ) {
|
||||||
if ( event.reason == 'install', event.reason == 'update' ) {
|
|
||||||
browser.storage.local.set({ justupdated: true });
|
browser.storage.local.set({ justupdated: true });
|
||||||
|
migrateLegacySettings();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 } });
|
||||||
|
}
|
||||||
+279
-139
@@ -147,7 +147,7 @@ html {
|
|||||||
border-radius: 17px 17px 0px 0 !important;
|
border-radius: 17px 17px 0px 0 !important;
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
}
|
}
|
||||||
.LegacyModuleBody__LegacyModule___20YE2 {
|
[class*="LegacyModuleBody__LegacyModule___"] {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
#AddedSettings {
|
#AddedSettings {
|
||||||
@@ -192,17 +192,17 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.PillBox__PillBox___3GjAk {
|
[class*="PillBox__PillBox___"] {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.PillBox__active___3Qpi9 {
|
[class*="PillBox__active___"] {
|
||||||
background: rgba(0, 0, 0, 0.2) !important;
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .PillBox__active___3Qpi9 {
|
.dark [class*="PillBox__active___"] {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +261,10 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ais-btnSearch {
|
.ais-btnSearch {
|
||||||
transition: background 200ms, color 200ms, box-shadow 200ms;
|
transition:
|
||||||
|
background 200ms,
|
||||||
|
color 200ms,
|
||||||
|
box-shadow 200ms;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(0, 0, 0, 0.2) !important;
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
@@ -274,7 +277,7 @@ html {
|
|||||||
font-family: Rubik, sans-serif !important;
|
font-family: Rubik, sans-serif !important;
|
||||||
&::before {
|
&::before {
|
||||||
font-size: 18px !important;
|
font-size: 18px !important;
|
||||||
content: 'Search' !important;
|
content: "Search" !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,8 +386,7 @@ ul.magicDelete > li.deleting {
|
|||||||
width: 28px !important;
|
width: 28px !important;
|
||||||
height: 28px !important;
|
height: 28px !important;
|
||||||
}
|
}
|
||||||
.notifications__items___2hCdv,
|
[class*="notifications__items___"] {
|
||||||
#menu ul {
|
|
||||||
-ms-overflow-style: none !important;
|
-ms-overflow-style: none !important;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@@ -468,7 +470,7 @@ html {
|
|||||||
[data-type="student"] .header {
|
[data-type="student"] .header {
|
||||||
color: black !important;
|
color: black !important;
|
||||||
}
|
}
|
||||||
ol:has(.MessageList__avatar___2wxyb svg) {
|
ol:has([class*="MessageList__avatar___"] svg) {
|
||||||
transition-duration: 150ms !important;
|
transition-duration: 150ms !important;
|
||||||
transition-delay: 0ms !important;
|
transition-delay: 0ms !important;
|
||||||
}
|
}
|
||||||
@@ -490,7 +492,11 @@ ol:has(.MessageList__avatar___2wxyb svg) {
|
|||||||
.content [autocomplete="off"] {
|
.content [autocomplete="off"] {
|
||||||
background: var(--background-primary) !important;
|
background: var(--background-primary) !important;
|
||||||
}
|
}
|
||||||
.MessageList__MessageList___3DxoC .footer {
|
.coneqtMessage .body .wrapper .iframeWrapper {
|
||||||
|
background: var(--background-primary) !important;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
[class*="MessageList__MessageList___"] .footer {
|
||||||
background: var(--background-secondary) !important;
|
background: var(--background-secondary) !important;
|
||||||
}
|
}
|
||||||
.listWrapper {
|
.listWrapper {
|
||||||
@@ -752,15 +758,15 @@ ol > [data-label] {
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
.Message__Message___3oJaU > .uiFrameWrapper .iframeWrapper {
|
[class*="Message__Message___"] > .uiFrameWrapper .iframeWrapper {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.Viewer__newMessage___3ToUb {
|
[class*="Viewer__newMessage___"] {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
font-size: 12.8px !important;
|
font-size: 12.8px !important;
|
||||||
background: var(--background-primary) !important;
|
background: var(--background-primary) !important;
|
||||||
}
|
}
|
||||||
.MessageList__sender___32riy :last-child {
|
[class*="MessageList__sender___"] :last-child {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
[data-type="student"] [style="z-index: 30;"] .header:has(h1) {
|
[data-type="student"] [style="z-index: 30;"] .header:has(h1) {
|
||||||
@@ -859,7 +865,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.LabelList__LabelList___2RJFf > li {
|
[class*="LabelList__LabelList___"] > li {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -879,10 +885,10 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
.welcome
|
.welcome
|
||||||
> .portalPageView
|
> .portalPageView
|
||||||
> .powerPortalPage
|
> .powerPortalPage
|
||||||
> .Body__body___3pGxr
|
> [class*="Body__body___"]
|
||||||
> .Container__container___33GlY
|
> [class*="Container__container___"]
|
||||||
> .Document__document___1KJCG
|
> [class*="Document__document___"]
|
||||||
> .Canvas__canvas___OBdCZ {
|
> [class*="Canvas__canvas___"] {
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
background-image: unset !important;
|
background-image: unset !important;
|
||||||
background-size: unset;
|
background-size: unset;
|
||||||
@@ -892,7 +898,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
.Module__wrapper___2sbOo {
|
[class*="Module__wrapper___"] {
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
background: var(--background-primary) !important;
|
background: var(--background-primary) !important;
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
@@ -904,10 +910,10 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.composer
|
.composer
|
||||||
> .Body__body___3pGxr
|
> [class*="Body__body___"]
|
||||||
> .Container__container___33GlY
|
> [class*="Container__container___"]
|
||||||
> .Document__document___1KJCG
|
> [class*="Document__document___"]
|
||||||
> .Canvas__canvas___OBdCZ {
|
> [class*="Canvas__canvas___"] {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
background-image: unset !important;
|
background-image: unset !important;
|
||||||
color: white !important;
|
color: white !important;
|
||||||
@@ -1003,34 +1009,6 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
margin-right: 157.5px;
|
margin-right: 157.5px;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.home-root {
|
.home-root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1195,7 +1173,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
box-shadow: inset 0px 5px 20px 1px rgba(0, 0, 0, 0.3);
|
box-shadow: inset 0px 5px 20px 1px rgba(0, 0, 0, 0.3);
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.Empty__Empty___2F6rn {
|
[class*="Empty__Empty___"] {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.shortcut-container {
|
.shortcut-container {
|
||||||
@@ -1397,18 +1375,18 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
margin: 20px auto 0px;
|
margin: 20px auto 0px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.dark .notifications__detailsBody___2nU2k > .notifications__subtitle___1se8e {
|
.dark [class*="notifications__detailsBody___"] > [class*="notifications__subtitle___"] {
|
||||||
color: #c1bcbc;
|
color: #c1bcbc;
|
||||||
}
|
}
|
||||||
.notifications__detailsBody___2nU2k > .notifications__subtitle___1se8e {
|
[class*="notifications__detailsBody___"] > [class*="notifications__subtitle___"] {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.notifications__notifications___3mmLY.notifications__hasItems___gXxzx > button {
|
[class*="notifications__notifications___"] > button {
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 21 !important;
|
z-index: 21 !important;
|
||||||
color: var(--better-sub);
|
color: var(--better-sub);
|
||||||
}
|
}
|
||||||
.notifications__notifications___3mmLY > button {
|
[class*="notifications__notifications___"] > button {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
.legacy-root button > svg,
|
.legacy-root button > svg,
|
||||||
@@ -1416,9 +1394,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
height: 25px;
|
height: 25px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
.notifications__notifications___3mmLY
|
[class*="notifications__notifications___"] > button > [class*="notifications__bubble___"] {
|
||||||
> button
|
|
||||||
> .notifications__bubble___1EkSQ {
|
|
||||||
background: var(--better-alert-highlight);
|
background: var(--better-alert-highlight);
|
||||||
width: 25px;
|
width: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
@@ -1436,16 +1412,16 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
.legacy-root button:not([disabled]):focus {
|
.legacy-root button:not([disabled]):focus {
|
||||||
border-color: var(--better-sub);
|
border-color: var(--better-sub);
|
||||||
}
|
}
|
||||||
.notifications__list___rp2L2 {
|
[class*="notifications__list___"] {
|
||||||
border: 4px solid var(--auto-background);
|
border: 4px solid var(--auto-background);
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.notifications__item___2ErJN {
|
[class*="notifications__item___"] {
|
||||||
background: var(--background-primary) !important;
|
background: var(--background-primary) !important;
|
||||||
border-left: 4px solid var(--better-main) !important;
|
border-left: 4px solid var(--better-main) !important;
|
||||||
margin-bottom: 4px !important;
|
margin-bottom: 4px !important;
|
||||||
|
|
||||||
> .notifications__dismiss___zveKV {
|
> [class*="notifications__dismiss___"] {
|
||||||
background: rgba(0, 0, 0, 0.1) !important;
|
background: rgba(0, 0, 0, 0.1) !important;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
@@ -1470,7 +1446,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
#menu li:first-child {
|
#menu li:first-child {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
.notifications__actions___1UX7r {
|
[class*="notifications__actions___"] {
|
||||||
background: var(--auto-background);
|
background: var(--auto-background);
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -1478,27 +1454,27 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.notifications__items___2hCdv {
|
[class*="notifications__items___"] {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
height: 540px;
|
height: 540px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.notifications__details___193F4 {
|
[class*="notifications__details___"] {
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
.notifications__details___193F4 div {
|
[class*="notifications__details___"] div {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
#main > .messages {
|
#main > .messages {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.Overview__details___2Zlnr {
|
[class*="Overview__details___"] {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.Viewer__sidebar___1Btu4 {
|
[class*="Viewer__sidebar___"] {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-right: unset;
|
border-right: unset;
|
||||||
background: unset;
|
background: unset;
|
||||||
@@ -1507,14 +1483,14 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
background: unset;
|
background: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.MessageList__MessageList___3DxoC ::-webkit-scrollbar {
|
[class*="MessageList__MessageList___"] ::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
.MessageList__primary___1zTHa > :last-child {
|
[class*="MessageList__primary___"] > :last-child {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
.MessageList__MessageList___3DxoC ol .Button__Button___3SRFo::before {
|
[class*="MessageList__MessageList___"] ol [class*="Button__Button___"]::before {
|
||||||
// plus icon
|
// plus icon
|
||||||
content: "";
|
content: "";
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1523,7 +1499,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC ol .Button__Button___3SRFo {
|
[class*="MessageList__MessageList___"] ol [class*="Button__Button___"] {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
margin: 8px 16px;
|
margin: 8px 16px;
|
||||||
@@ -1532,21 +1508,21 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .MessageList__MessageList___3DxoC .Button__Button___3SRFo {
|
.dark [class*="MessageList__MessageList___"] [class*="Button__Button___"] {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC .Button__Button___3SRFo {
|
[class*="MessageList__MessageList___"] [class*="Button__Button___"] {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.MessageList__MessageList___3DxoC {
|
[class*="MessageList__MessageList___"] {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.Input__Input___3RSTI::before,
|
[class*="Input__Input___"]::before,
|
||||||
.ais-btnSearch::before {
|
.ais-btnSearch::before {
|
||||||
content: "";
|
content: "";
|
||||||
/* Unicode for the search icon */
|
/* Unicode for the search icon */
|
||||||
@@ -1558,7 +1534,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
font-family: "IconFamily";
|
font-family: "IconFamily";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.Input__Input___3RSTI {
|
[class*="Input__Input___"] {
|
||||||
transition:
|
transition:
|
||||||
background-color 0.5s,
|
background-color 0.5s,
|
||||||
border-color 0.5s;
|
border-color 0.5s;
|
||||||
@@ -1583,15 +1559,15 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
height: 180px;
|
height: 180px;
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.Avatar__Avatar___gE5kx.Avatar__staff___4gVLs {
|
[class*="Avatar__Avatar___"][class*="Avatar__staff___"] {
|
||||||
--person-colour: var(--better-light);
|
--person-colour: var(--better-light);
|
||||||
background: var(--person-colour, var(--navy));
|
background: var(--person-colour, var(--navy));
|
||||||
}
|
}
|
||||||
.LabelList__LabelList___2RJFf > li.LabelList__selected___3Egk7 {
|
[class*="LabelList__LabelList___"] > li[class*="LabelList__selected___"] {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.Message__Message___3oJaU {
|
[class*="Message__Message___"] {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
}
|
}
|
||||||
@@ -1611,29 +1587,31 @@ iframe.userHTML {
|
|||||||
background: var(--better-light);
|
background: var(--better-light);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
.Spinner__Spinner___CStEb > svg {
|
[class*="Spinner__Spinner___"] > svg {
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
.Spinner__Spinner___CStEb > svg > path {
|
[class*="Spinner__Spinner___"] > svg > path {
|
||||||
stroke: var(--text-primary) !important;
|
stroke: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
#main > .reports > .item > .report > .term {
|
#main > .reports > .item > .report > .term {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
background: var(--better-main);
|
background: var(--better-main);
|
||||||
}
|
}
|
||||||
.Collapsible__Collapsible___3O8P3 > .Collapsible__header___-Afvq {
|
[class*="Collapsible__Collapsible___"] > [class*="Collapsible__header__"] {
|
||||||
background: none;
|
background: none !important;
|
||||||
}
|
}
|
||||||
.Collapsible__Collapsible___3O8P3 > .Collapsible__content___2c6of.Collapsible__enterActive___3b2ow,
|
[class*="Collapsible__Collapsible___"]
|
||||||
.Collapsible__Collapsible___3O8P3 > .Collapsible__content___2c6of.Collapsible__exitActive___3rFL1 {
|
> [class*="Collapsible__content___"]
|
||||||
|
[class*="Collapsible__enterActive___"]
|
||||||
|
[class*="Collapsible__exitActive___"] {
|
||||||
animation-timing-function: ease-out !important;
|
animation-timing-function: ease-out !important;
|
||||||
}
|
}
|
||||||
.AssessmentList__AssessmentList___1GdCl
|
[class*="AssessmentList__AssessmentList___"]
|
||||||
> .AssessmentList__searchFilter___3N70o
|
> [class*="AssessmentList__searchFilter___"]
|
||||||
+ .AssessmentList__items___3LcmQ {
|
+ [class*="AssessmentList__items___"] {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.Thermoscore__Thermoscore___2tWMi {
|
[class*="Thermoscore__Thermoscore___"] {
|
||||||
background-image: unset;
|
background-image: unset;
|
||||||
background: var(--auto-background);
|
background: var(--auto-background);
|
||||||
}
|
}
|
||||||
@@ -1720,33 +1698,31 @@ ul {
|
|||||||
#userActions > .details > .code {
|
#userActions > .details > .code {
|
||||||
text-transform: initial;
|
text-transform: initial;
|
||||||
}
|
}
|
||||||
.SelectedAssessment__SelectedAssessment___3Bu5D {
|
[class*="SelectedAssessment__SelectedAssessment___"] {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.SelectedAssessment__SelectedAssessment___3Bu5D
|
[class*="SelectedAssessment__SelectedAssessment___"]
|
||||||
> .SelectedAssessment__meta___1gq_y
|
> [class*="SelectedAssessment__meta___"]
|
||||||
> .SelectedAssessment__clearBtn___21D85 {
|
> [class*="SelectedAssessment__clearBtn___"] {
|
||||||
background: var(--better-main);
|
background: var(--better-main);
|
||||||
}
|
}
|
||||||
.SelectedAssessment__SelectedAssessment___3Bu5D
|
[class*="SelectedAssessment__SelectedAssessment___"]
|
||||||
> .SelectedAssessment__meta___1gq_y {
|
> [class*="SelectedAssessment__meta___"] {
|
||||||
border-bottom: 1px solid var(--better-main);
|
border-bottom: 1px solid var(--better-main);
|
||||||
}
|
}
|
||||||
.TabSet__TabSet___Vo-SZ
|
[class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] > li[class*="TabSet__selected___"] {
|
||||||
> ol.TabSet__tabs___1RRZk
|
|
||||||
> li.TabSet__selected___1psfF {
|
|
||||||
border-bottom-color: var(--better-main);
|
border-bottom-color: var(--better-main);
|
||||||
}
|
}
|
||||||
.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk {
|
[class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
.TabSet__TabSet___Vo-SZ > ol.TabSet__tabs___1RRZk > li:hover {
|
[class*="TabSet__TabSet___"] > ol[class*="TabSet__tabs___"] > li:hover {
|
||||||
box-shadow: inset 0 -1px var(--better-main);
|
box-shadow: inset 0 -1px var(--better-main);
|
||||||
}
|
}
|
||||||
.TabSet__TabSet___Vo-SZ > .TabSet__tabContainer___3iIRe {
|
[class*="TabSet__TabSet___"] > [class*="TabSet__tabContainer___"] {
|
||||||
background: unset;
|
background: unset;
|
||||||
}
|
}
|
||||||
.BasicPanel__BasicPanel___1GP6s {
|
[class*="BasicPanel__BasicPanel___"] {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.back > svg {
|
.back > svg {
|
||||||
@@ -1770,25 +1746,25 @@ ul {
|
|||||||
}
|
}
|
||||||
.mediaWrapper,
|
.mediaWrapper,
|
||||||
.mediaRecorder,
|
.mediaRecorder,
|
||||||
.MediaRecorder__MediaRecorder___2c2_M {
|
[class*="MediaRecorder__MediaRecorder___"] {
|
||||||
border-top-left-radius: 16px;
|
border-top-left-radius: 16px;
|
||||||
border-top-right-radius: 16px;
|
border-top-right-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.MediaRecorder__MediaRecorder___2c2_M {
|
[class*="MediaRecorder__MediaRecorder___"] {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.legacy-root .uiFileHandler {
|
.legacy-root .uiFileHandler {
|
||||||
background: var(--auto-background);
|
background: var(--auto-background);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
.ResourceList__ResourceList___2z-c1 .legacy-root .uiFileHandler {
|
[class*="ResourceList__ResourceList___"] .legacy-root .uiFileHandler {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.legacy-root .uiFileHandler.dragTarget {
|
.legacy-root .uiFileHandler.dragTarget {
|
||||||
background: var(--better-main);
|
background: var(--better-main);
|
||||||
}
|
}
|
||||||
.MenuButton__MenuPanel___2q42B {
|
[class*="MenuButton__MenuPanel___"] {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -1916,11 +1892,13 @@ div.entry.class[style*="width: 46.5%"] {
|
|||||||
.sources .uiButton {
|
.sources .uiButton {
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
.MediaRecorder__preview___1hQqY,
|
[class*="MediaRecorder__preview___"] {
|
||||||
.MediaRecorder__actions___3Jjvp {
|
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
}
|
}
|
||||||
.Rubric__Rubric___2AAKS > .Rubric__line___JCC3Y {
|
[class*="MediaRecorder__actions___"] {
|
||||||
|
background: var(--background-primary);
|
||||||
|
}
|
||||||
|
[class*="Rubric__Rubric___"] > [class*="Rubric__line___"] {
|
||||||
background: unset;
|
background: unset;
|
||||||
}
|
}
|
||||||
#main > .course > .content > .header > .coverImage.blurred {
|
#main > .course > .content > .header > .coverImage.blurred {
|
||||||
@@ -1986,6 +1964,22 @@ div.bar.flat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cke_toolbox > .cke_toolbar > .cke_combo > .cke_combo_button {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cke_toolbox > .cke_toolbar > .cke_toolgroup > .cke_button {
|
||||||
|
&:last-child {
|
||||||
|
border-top-right-radius: 8px !important;
|
||||||
|
border-bottom-right-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 8px !important;
|
||||||
|
border-bottom-left-radius: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.formattedText > .wrapper > .cke > .cke_inner > .cke_contents {
|
.formattedText > .wrapper > .cke > .cke_inner > .cke_contents {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@@ -2022,8 +2016,8 @@ div.bar.flat {
|
|||||||
border-radius: 16px !important;
|
border-radius: 16px !important;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.document-width-micro .RootModule__root-module___2wT52,
|
.document-width-micro [class*="RootModule__root-module___"],
|
||||||
.document-width-nano .RootModule__root-module___2wT52 {
|
.document-width-nano [class*="RootModule__root-module___"] {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@@ -2064,24 +2058,48 @@ div.bar.flat {
|
|||||||
background: black !important;
|
background: black !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.quicktable {
|
.quicktable {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
.cke_toolbox > .cke_toolbar .cke_combo_on > .cke_combo_button,
|
.cke_toolbox > .cke_toolbar .cke_combo_on > .cke_combo_button,
|
||||||
.cke_toolbox > .cke_toolbar .cke_button_on {
|
.cke_toolbox > .cke_toolbar .cke_button_on {
|
||||||
background-color: #3d3d3e !important;
|
background-color: #3d3d3e !important;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: rgb(207, 207, 207) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.legacy-root input.singleSelect:focus {
|
}
|
||||||
|
|
||||||
|
.legacy-root input.singleSelect {
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
background: var(--auto-background);
|
background: var(--auto-background);
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.buttonMenu.depressed:hover {
|
||||||
|
border-radius: 100px;
|
||||||
|
border-bottom-left-radius: 100px !important;
|
||||||
|
border-bottom-right-radius: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.buttonMenu {
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
ul.singleSelect,
|
ul.singleSelect,
|
||||||
ul.buttonChecklist,
|
ul.buttonChecklist,
|
||||||
ul.buttonMenu,
|
ul.buttonMenu,
|
||||||
ul.colourButtonOptions,
|
ul.colourButtonOptions,
|
||||||
ul.uiSplitButtonList,
|
ul.uiSplitButtonList,
|
||||||
|
ul.buttonMenu,
|
||||||
.contactFormPanel {
|
.contactFormPanel {
|
||||||
background: var(--background-primary) !important;
|
background: var(--background-primary) !important;
|
||||||
border: solid 4px var(--background-primary);
|
border: solid 4px var(--background-primary);
|
||||||
@@ -2125,8 +2143,9 @@ body {
|
|||||||
> .entriesWrapper
|
> .entriesWrapper
|
||||||
> .entry {
|
> .entry {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
.Viewer__Viewer___32BH- {
|
[class*="Viewer__Viewer___"] {
|
||||||
background: unset;
|
background: unset;
|
||||||
}
|
}
|
||||||
.weekend {
|
.weekend {
|
||||||
@@ -2145,16 +2164,16 @@ body {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li.MessageList__unread___3imtO {
|
[class*="MessageList__unread___"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgb(228 225 225);
|
background: rgb(228 225 225);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark li.MessageList__unread___3imtO {
|
.dark [class*="MessageList__unread___"] {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC > ol > li:hover {
|
[class*="MessageList__MessageList___"] > ol > li:hover {
|
||||||
background: var(--theme-offset-bg-more);
|
background: var(--theme-offset-bg-more);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2162,18 +2181,17 @@ li.MessageList__unread___3imtO {
|
|||||||
border-radius: 1600px;
|
border-radius: 1600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC
|
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__selected___"]
|
||||||
> ol
|
[class*="MessageList__unread___"] {
|
||||||
> li.MessageList__selected___1SJNz.MessageList__unread___3imtO {
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Message__Message___3oJaU.Message__unread___23XIq > header {
|
[class*="Message__Message___"] [class*="Message__unread___"] > header {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC > ol > li.MessageList__unread___3imtO::before,
|
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__unread___"]::before,
|
||||||
.MessageList__MessageList___3DxoC > ol > li::before {
|
[class*="MessageList__MessageList___"] > ol > li::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -2184,9 +2202,7 @@ li.MessageList__unread___3imtO {
|
|||||||
transition: width 0.1s;
|
transition: width 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC
|
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__unread___"]::before {
|
||||||
> ol
|
|
||||||
> li.MessageList__unread___3imtO::before {
|
|
||||||
width: 3px;
|
width: 3px;
|
||||||
}
|
}
|
||||||
.connectedNotificationsWrapper > div > button {
|
.connectedNotificationsWrapper > div > button {
|
||||||
@@ -2261,13 +2277,13 @@ li.MessageList__unread___3imtO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark
|
.dark
|
||||||
.MessageList__MessageList___3DxoC
|
[class*="MessageList__MessageList___"]
|
||||||
> ol
|
> ol
|
||||||
> li.MessageList__selected___1SJNz {
|
> li[class*="MessageList__selected___"] {
|
||||||
background: var(--background-secondary);
|
background: var(--background-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC > ol > li.MessageList__selected___1SJNz {
|
[class*="MessageList__MessageList___"] > ol > li[class*="MessageList__selected___"] {
|
||||||
background: rgb(228 225 225);
|
background: rgb(228 225 225);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
@@ -2404,14 +2420,14 @@ li.MessageList__unread___3imtO {
|
|||||||
animation: spin 3s linear infinite;
|
animation: spin 3s linear infinite;
|
||||||
-moz-animation: spin 3s linear infinite;
|
-moz-animation: spin 3s linear infinite;
|
||||||
}
|
}
|
||||||
.dark .LabelList__name___-CHgq {
|
.dark [class*="LabelList__name___"] {
|
||||||
text-shadow: 0 0 5px black;
|
text-shadow: 0 0 5px black;
|
||||||
}
|
}
|
||||||
.LabelList__name___-CHgq {
|
[class*="LabelList__name___"] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
[data-label="inbox"] > .LabelList__name___-CHgq::before {
|
[data-label="inbox"] > [class*="LabelList__name___"]::before {
|
||||||
content: "\eb70";
|
content: "\eb70";
|
||||||
/* Unicode for the search icon */
|
/* Unicode for the search icon */
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
@@ -2421,7 +2437,7 @@ li.MessageList__unread___3imtO {
|
|||||||
font-family: "IconFamily";
|
font-family: "IconFamily";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
[data-label="outbox"] > .LabelList__name___-CHgq::before {
|
[data-label="outbox"] > [class*="LabelList__name___"]::before {
|
||||||
content: "\eca6";
|
content: "\eca6";
|
||||||
/* Unicode for the search icon */
|
/* Unicode for the search icon */
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
@@ -2431,7 +2447,7 @@ li.MessageList__unread___3imtO {
|
|||||||
font-family: "IconFamily";
|
font-family: "IconFamily";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
[data-label="starred"] > .LabelList__name___-CHgq::before {
|
[data-label="starred"] > [class*="LabelList__name___"]::before {
|
||||||
content: "\ece8";
|
content: "\ece8";
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -2440,7 +2456,7 @@ li.MessageList__unread___3imtO {
|
|||||||
font-family: "IconFamily";
|
font-family: "IconFamily";
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
[data-label="trash"] > .LabelList__name___-CHgq::before {
|
[data-label="trash"] > [class*="LabelList__name___"]::before {
|
||||||
content: "\ed2c";
|
content: "\ed2c";
|
||||||
/* Unicode for the search icon */
|
/* Unicode for the search icon */
|
||||||
color: currentColor;
|
color: currentColor;
|
||||||
@@ -2750,7 +2766,7 @@ li.MessageList__unread___3imtO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.MessageList__MessageList___3DxoC > header {
|
[class*="MessageList__MessageList___"] > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@@ -2815,7 +2831,9 @@ li.MessageList__unread___3imtO {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transition: 200ms, background-color 0s;
|
transition:
|
||||||
|
200ms,
|
||||||
|
background-color 0s;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
.dark .upcoming-items {
|
.dark .upcoming-items {
|
||||||
@@ -3059,7 +3077,6 @@ li.MessageList__unread___3imtO {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
animation-fill-mode: forwards;
|
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
.whatsnewHeader {
|
.whatsnewHeader {
|
||||||
@@ -3197,7 +3214,8 @@ li.MessageList__unread___3imtO {
|
|||||||
.loading {
|
.loading {
|
||||||
&.upcoming-items,
|
&.upcoming-items,
|
||||||
&.day-container {
|
&.day-container {
|
||||||
background: linear-gradient(90deg,
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
var(--background-primary) 0%,
|
var(--background-primary) 0%,
|
||||||
var(--background-secondary) 50%,
|
var(--background-secondary) 50%,
|
||||||
var(--background-primary) 100%
|
var(--background-primary) 100%
|
||||||
@@ -3210,3 +3228,125 @@ li.MessageList__unread___3imtO {
|
|||||||
height: 35em;
|
height: 35em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pane .formattedText > .wrapper {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto collapsing alignment toolbar
|
||||||
|
.cke_toolbar:has(.cke_button__seqta-align-left) {
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
.cke_toolgroup {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 32px;
|
||||||
|
|
||||||
|
.cke_button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
z-index: 100;
|
||||||
|
width: 32px;
|
||||||
|
margin: 0;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease-out,
|
||||||
|
visibility 0s linear,
|
||||||
|
background 0.3s ease,
|
||||||
|
border-radius 0.3s ease !important;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
visibility: visible !important;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cke_button_on {
|
||||||
|
visibility: visible;
|
||||||
|
position: absolute;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 101;
|
||||||
|
|
||||||
|
& + .cke_button:first-child {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button icons
|
||||||
|
.cke_button_icon {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// menu background
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
left: -4px;
|
||||||
|
right: -4px;
|
||||||
|
bottom: calc(-300% - 10px);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||||
|
background: var(--background-primary) !important;
|
||||||
|
z-index: 100;
|
||||||
|
transform: scale(0.65, 0.2);
|
||||||
|
transform-origin: 50% 6px;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropdown behavior on hover
|
||||||
|
&:hover {
|
||||||
|
&:hover:before {
|
||||||
|
transform: scale(1);
|
||||||
|
border-radius: 16px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
.cke_button {
|
||||||
|
visibility: visible;
|
||||||
|
transition-delay: 0s;
|
||||||
|
|
||||||
|
// Stack buttons in dropdown with spacing
|
||||||
|
&:first-child {
|
||||||
|
transform: translateY(0);
|
||||||
|
border-top-left-radius: 12px !important;
|
||||||
|
border-top-right-radius: 12px !important;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
transform: translateY(calc(100% + 2px));
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
transform: translateY(calc(200% + 4px));
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
transform: translateY(calc(300% + 6px));
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
transform: translateY(calc(400% + 6px));
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
transform: translateY(calc(500% + 6px));
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-left-radius: 12px !important;
|
||||||
|
border-bottom-right-radius: 12px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtle animation when closing dropdown
|
||||||
|
&:not(:hover)
|
||||||
|
.cke_button:not(.cke_button_on):not(
|
||||||
|
.cke_button__seqta-align-left:first-child
|
||||||
|
) {
|
||||||
|
transform: translateY(0);
|
||||||
|
visibility: hidden;
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
visibility 0s linear 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ html.transparencyEffects:not(.dark) {
|
|||||||
|
|
||||||
html.transparencyEffects {
|
html.transparencyEffects {
|
||||||
/* Background Fixes */
|
/* Background Fixes */
|
||||||
.notifications__item___2ErJN,
|
[class*="notifications__item___"],
|
||||||
#shortcuts {
|
#shortcuts {
|
||||||
backdrop-filter: unset !important;
|
backdrop-filter: unset !important;
|
||||||
}
|
}
|
||||||
@@ -24,21 +24,21 @@ html.transparencyEffects {
|
|||||||
/* Blurs */
|
/* Blurs */
|
||||||
.draggable,
|
.draggable,
|
||||||
.notice,
|
.notice,
|
||||||
.BasicPanel__BasicPanel___1GP6s,
|
[class*="BasicPanel__BasicPanel___"],
|
||||||
.message.addMessage,
|
.message.addMessage,
|
||||||
.singleSelect,
|
.singleSelect,
|
||||||
.uiFileHandlerPanel,
|
.uiFileHandlerPanel,
|
||||||
.Module__wrapper___2sbOo,
|
[class*="Module__wrapper___"],
|
||||||
.notifications__list___rp2L2,
|
[class*="notifications__list___"],
|
||||||
.thread,
|
.thread,
|
||||||
.calendar,
|
.calendar,
|
||||||
.navigator,
|
.navigator,
|
||||||
#title,
|
#title,
|
||||||
.LabelList__selected___3Egk7,
|
[class*="LabelList__selected___"],
|
||||||
.buttonChecklist,
|
.buttonChecklist,
|
||||||
.pane,
|
.pane,
|
||||||
.legacy-root button, .legacy-root a,
|
.legacy-root button, .legacy-root a,
|
||||||
.MessageList__MessageList___3DxoC {
|
[class*="MessageList__MessageList___"] {
|
||||||
backdrop-filter: blur(80px);
|
backdrop-filter: blur(80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ html.transparencyEffects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.whatsnewContainer,
|
.whatsnewContainer,
|
||||||
.Message__Message___3oJaU {
|
[class*="Message__Message___"] {
|
||||||
backdrop-filter: blur(50px);
|
backdrop-filter: blur(50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
let editor = $state<HTMLDivElement | null>(null)
|
let editor = $state<HTMLDivElement | null>(null)
|
||||||
let view: EditorView | null = null;
|
let view: EditorView | null = null;
|
||||||
let editorTheme = new Compartment();
|
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) {
|
function createEditorState(initialContents: string) {
|
||||||
let extensions = [
|
let extensions = [
|
||||||
@@ -91,4 +91,4 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</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;
|
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-solid-btn),
|
||||||
div:has(> #rbgcp-advanced-btn),
|
div:has(> #rbgcp-advanced-btn),
|
||||||
#rbgcp-color-model-btn > div,
|
#rbgcp-color-model-btn > div,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function Picker({
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
disableDarkMode={true}
|
disableDarkMode={true}
|
||||||
presets={presets}
|
presets={presets}
|
||||||
hideInputs={false}
|
hideInputs={customOnChange ? false : true}
|
||||||
value={customThemeColor ?? ""}
|
value={customThemeColor ?? ""}
|
||||||
onChange={(color: string) => {
|
onChange={(color: string) => {
|
||||||
if (customOnChange) {
|
if (customOnChange) {
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let { state, onChange } = $props<{ state: number, onChange: (value: number) => void }>();
|
let { state, onChange, min = 0, max = 100, step = 1 } = $props<{
|
||||||
let percentage = $derived((state / 100) * 100);
|
state: number,
|
||||||
|
onChange: (value: number) => void,
|
||||||
|
min?: number,
|
||||||
|
max?: number,
|
||||||
|
step?: number
|
||||||
|
}>();
|
||||||
|
let percentage = $derived(((state - min) / (max - min)) * 100);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative w-full max-w-lg mx-auto">
|
<div class="relative mx-auto w-full max-w-lg">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min={min}
|
||||||
max="100"
|
max={max}
|
||||||
|
step={step}
|
||||||
bind:value={state}
|
bind:value={state}
|
||||||
style={`background: linear-gradient(to right, #30D259 ${percentage}%, #dddddd ${percentage}%)`}
|
style={`background: linear-gradient(to right, #30D259 ${percentage}%, #dddddd ${percentage}%)`}
|
||||||
onchange={(e) => onChange(Number(e.currentTarget.value))}
|
onchange={(e) => onChange(Number(e.currentTarget.value))}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
|
let { tabs } = $props<{ tabs: { title: string, Content: any, props?: any }[] }>();
|
||||||
let activeTab = $state(0);
|
let activeTab = $state(0);
|
||||||
let hoveredTab = $state<number | null>(null);
|
|
||||||
let containerRef: HTMLElement | null = null;
|
let containerRef: HTMLElement | null = null;
|
||||||
let tabWidth = $state(0);
|
let tabWidth = $state(0);
|
||||||
|
|
||||||
@@ -24,10 +23,6 @@
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
calcXPos(hoveredTab);
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
updateTabWidth();
|
updateTabWidth();
|
||||||
|
|
||||||
@@ -45,26 +40,24 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full">
|
<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="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 px-2 tab-width-container">
|
||||||
<div class="relative flex">
|
<div bind:this={containerRef} class="flex relative">
|
||||||
<MotionDiv
|
<MotionDiv
|
||||||
class="absolute top-0 left-0 z-0 h-full bg-[#DDDDDD] dark:bg-[#38373D] rounded-full opacity-40 tab-width"
|
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}
|
transition={springTransition}
|
||||||
/>
|
/>
|
||||||
{#each tabs as { title }, index}
|
{#each tabs as { title }, index}
|
||||||
<button
|
<button
|
||||||
class="relative z-10 flex-1 px-4 py-2 focus-visible:outline-none"
|
class="relative z-10 flex-1 px-4 py-2 focus-visible:outline-none"
|
||||||
onclick={() => activeTab = index}
|
onclick={() => activeTab = index}
|
||||||
onmouseenter={() => hoveredTab = index}
|
|
||||||
onmouseleave={() => hoveredTab = null}
|
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full px-4 overflow-hidden">
|
<div class="overflow-hidden px-4 h-full">
|
||||||
<MotionDiv
|
<MotionDiv
|
||||||
class="h-full"
|
class="h-full"
|
||||||
animate={{ x: `${-activeTab * 100}%` }}
|
animate={{ x: `${-activeTab * 100}%` }}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader';
|
import { hasEnoughStorageSpace, isIndexedDBSupported, writeData, openDatabase, readAllData, deleteData } from '@/interface/hooks/BackgroundDataLoader';
|
||||||
import { setTheme } from '@/seqta/ui/themes/setTheme';
|
|
||||||
import Spinner from '../Spinner.svelte';
|
import Spinner from '../Spinner.svelte';
|
||||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||||
import Fuse from 'fuse.js';
|
import Fuse from 'fuse.js';
|
||||||
import { backgroundUpdates } from '@/interface/hooks/BackgroundUpdates'
|
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 };
|
type Background = { id: string; category: string; type: string; lowResUrl: string; highResUrl: string; name: string; description: string; featured?: boolean };
|
||||||
let { searchTerm } = $props<{ searchTerm: string }>();
|
let { searchTerm } = $props<{ searchTerm: string }>();
|
||||||
@@ -170,13 +172,13 @@
|
|||||||
|
|
||||||
function selectNoBackground() {
|
function selectNoBackground() {
|
||||||
selectedBackground = null;
|
selectedBackground = null;
|
||||||
setTheme('');
|
themeManager.setTheme('');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<!-- Sidebar -->
|
<!-- 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">
|
<div class="mb-8">
|
||||||
<h2 class="mb-4 text-lg font-semibold">Categories</h2>
|
<h2 class="mb-4 text-lg font-semibold">Categories</h2>
|
||||||
<nav class="space-y-2">
|
<nav class="space-y-2">
|
||||||
@@ -208,15 +210,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="flex-1 overflow-auto">
|
<div class="overflow-auto flex-1">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="sticky top-0 z-10 p-4 border-b bg-[#F1F1F3] dark:bg-zinc-900 dark:border-zinc-700">
|
<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>
|
<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
|
<select
|
||||||
bind:value={sortBy}
|
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="newest">Newest</option>
|
||||||
<option value="name">Name</option>
|
<option value="name">Name</option>
|
||||||
@@ -230,7 +232,7 @@
|
|||||||
<button
|
<button
|
||||||
class={`px-4 py-2 text-sm font-medium transition-colors rounded-full
|
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' :
|
${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}
|
onclick={() => activeTab = tab.toLowerCase() as typeof activeTab}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
@@ -244,15 +246,15 @@
|
|||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{#each Array(9) as _}
|
{#each Array(9) as _}
|
||||||
<div class="relative overflow-hidden rounded-lg animate-pulse">
|
<div class="overflow-hidden relative rounded-lg animate-pulse">
|
||||||
<!-- Image placeholder -->
|
<!-- Image placeholder -->
|
||||||
<div class="w-full h-48 bg-zinc-200 dark:bg-zinc-800"></div>
|
<div class="w-full h-48 bg-zinc-200 dark:bg-zinc-800"></div>
|
||||||
<!-- Gradient overlay -->
|
<!-- 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 -->
|
<!-- 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-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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +273,7 @@
|
|||||||
return true;
|
return true;
|
||||||
}) as background (background.id)}
|
}) as background (background.id)}
|
||||||
<div
|
<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)}
|
onclick={() => toggleBackgroundInstallation(background)}
|
||||||
onkeydown={(event) => {
|
onkeydown={(event) => {
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
@@ -286,7 +288,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<video src={background.lowResUrl} class="object-cover w-full h-48" muted loop autoplay></video>
|
<video src={background.lowResUrl} class="object-cover w-full h-48" muted loop autoplay></video>
|
||||||
{/if}
|
{/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)}
|
{#if installingBackgrounds.has(background.id)}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:else if savedBackgrounds.includes(background.id)}
|
{:else if savedBackgrounds.includes(background.id)}
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if coverThemes.length > 0}
|
{#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
|
<div
|
||||||
class="w-full aspect-[8/3]"
|
class="w-full aspect-8/3"
|
||||||
use:emblaCarouselSvelte={{ options, plugins }}
|
use:emblaCarouselSvelte={{ options, plugins }}
|
||||||
onemblaInit={onInit}
|
onemblaInit={onInit}
|
||||||
>
|
>
|
||||||
@@ -47,20 +47,20 @@
|
|||||||
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
|
<h2 class='text-4xl font-bold text-white'>{theme.name}</h2>
|
||||||
<p class='text-lg text-white'>{theme.description}</p>
|
<p class='text-lg text-white'>{theme.description}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation buttons -->
|
<!-- Navigation buttons -->
|
||||||
<div class='absolute z-10 flex gap-2 bottom-2 right-2'>
|
<div class='flex absolute right-2 bottom-2 z-10 gap-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'>
|
<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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 19.5-7.5-7.5 7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type { Background } from './types';
|
|
||||||
|
|
||||||
export let filteredBackgrounds: Background[];
|
|
||||||
|
|
||||||
let dispatch = createEventDispatcher();
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let filters = $state({
|
let filters = $state({
|
||||||
@@ -13,9 +9,9 @@
|
|||||||
orientation: [] as string[]
|
orientation: [] as string[]
|
||||||
});
|
});
|
||||||
|
|
||||||
$: {
|
$effect(() => {
|
||||||
dispatch('filter', filters);
|
dispatch('filter', filters);
|
||||||
}
|
});
|
||||||
|
|
||||||
function toggleFilter(category: keyof typeof filters, value: string) {
|
function toggleFilter(category: keyof typeof filters, value: string) {
|
||||||
if (filters[category].includes(value)) {
|
if (filters[category].includes(value)) {
|
||||||
@@ -42,21 +38,19 @@
|
|||||||
<h3 class="mb-2 font-medium">Type</h3>
|
<h3 class="mb-2 font-medium">Type</h3>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="flex items-center">
|
<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>
|
<span class="ml-2">Image</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex items-center">
|
<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>
|
<span class="ml-2">Video</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add similar sections for color, resolution, and orientation -->
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="px-4 py-2 mt-4 text-white bg-red-500 rounded hover:bg-red-600"
|
class="px-4 py-2 mt-4 text-white bg-red-500 rounded hover:bg-red-600"
|
||||||
on:click={clearFilters}
|
onclick={clearFilters}
|
||||||
>
|
>
|
||||||
Clear Filters
|
Clear Filters
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
</script>
|
</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">
|
<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 justify-between items-center 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 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(logo)} class="h-14 {darkMode ? 'hidden' : ''}" alt="Logo" />
|
||||||
<img src={browser.runtime.getURL(logoDark)} class="h-14 {darkMode ? '' : 'hidden'}" alt="Dark Logo" />
|
<img src={browser.runtime.getURL(logoDark)} class="h-14 {darkMode ? '' : 'hidden'}" alt="Dark Logo" />
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex gap-2">
|
<div class="flex relative gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search themes..."
|
placeholder="Search themes..."
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
oninput={(e: any) => setSearchTerm(e.target.value)}
|
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" />
|
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
|
<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"
|
fill="none"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white">
|
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white">
|
||||||
{theme.name}
|
{theme.name}
|
||||||
</div>
|
</div>
|
||||||
<div class='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t to-transparent from-black/80'></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'>
|
<div class='w-full'>
|
||||||
<img src={theme.marqueeImage} 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>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex fixed inset-0 z-50 justify-center items-end bg-black bg-opacity-70"
|
class="flex fixed inset-0 z-50 justify-center items-end bg-black/70"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
if (e.target === e.currentTarget) hideModal();
|
if (e.target === e.currentTarget) hideModal();
|
||||||
}}
|
}}
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<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">
|
<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}
|
{relatedTheme.name}
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t to-transparent from-black/80"></div>
|
<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" />
|
<img src={relatedTheme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-48" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
onkeydown={onClick}
|
onkeydown={onClick}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
role="button"
|
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}
|
{#if isEditMode}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
import type { CustomTheme, ThemeList } from '@/types/CustomThemes'
|
||||||
import { getAvailableThemes } from '@/seqta/ui/themes/getAvailableThemes'
|
|
||||||
import { onDestroy, onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
import { OpenThemeCreator } from '@/seqta/ui/ThemeCreator'
|
import { OpenThemeCreator } from '@/plugins/built-in/themes/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 { OpenStorePage } from '@/seqta/ui/renderStore'
|
import { OpenStorePage } from '@/seqta/ui/renderStore'
|
||||||
import { themeUpdates } from '@/interface/hooks/ThemeUpdates'
|
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 themes = $state<ThemeList | null>(null);
|
||||||
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
let { isEditMode } = $props<{ isEditMode: boolean }>();
|
||||||
@@ -20,10 +17,10 @@
|
|||||||
const handleThemeClick = async (theme: CustomTheme) => {
|
const handleThemeClick = async (theme: CustomTheme) => {
|
||||||
if (isEditMode) return;
|
if (isEditMode) return;
|
||||||
if (theme.id === themes?.selectedTheme) {
|
if (theme.id === themes?.selectedTheme) {
|
||||||
await disableTheme();
|
await themeManager.disableTheme();
|
||||||
themes.selectedTheme = '';
|
themes.selectedTheme = '';
|
||||||
} else {
|
} else {
|
||||||
await setTheme(theme.id);
|
await themeManager.setTheme(theme.id);
|
||||||
if (!themes) return;
|
if (!themes) return;
|
||||||
themes.selectedTheme = theme.id;
|
themes.selectedTheme = theme.id;
|
||||||
}
|
}
|
||||||
@@ -31,13 +28,13 @@
|
|||||||
|
|
||||||
const handleThemeDelete = async (themeId: string) => {
|
const handleThemeDelete = async (themeId: string) => {
|
||||||
try {
|
try {
|
||||||
await deleteTheme(themeId);
|
await themeManager.deleteTheme(themeId);
|
||||||
if (!themes) return;
|
if (!themes) return;
|
||||||
|
|
||||||
themes.themes = themes.themes.filter(theme => theme.id !== themeId);
|
themes.themes = themes.themes.filter(theme => theme.id !== themeId);
|
||||||
if (themeId === themes.selectedTheme) {
|
if (themeId === themes.selectedTheme) {
|
||||||
themes.selectedTheme = '';
|
themes.selectedTheme = '';
|
||||||
await disableTheme();
|
await themeManager.disableTheme();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting theme:', error);
|
console.error('Error deleting theme:', error);
|
||||||
@@ -46,7 +43,7 @@
|
|||||||
|
|
||||||
const handleShareTheme = async (theme: CustomTheme) => {
|
const handleShareTheme = async (theme: CustomTheme) => {
|
||||||
try {
|
try {
|
||||||
await shareTheme(theme.id);
|
await themeManager.shareTheme(theme.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sharing theme:', error);
|
console.error('Error sharing theme:', error);
|
||||||
}
|
}
|
||||||
@@ -72,9 +69,10 @@
|
|||||||
try {
|
try {
|
||||||
const result = JSON.parse(event.target?.result as string);
|
const result = JSON.parse(event.target?.result as string);
|
||||||
tempTheme = result;
|
tempTheme = result;
|
||||||
await InstallTheme(result);
|
await themeManager.installTheme(result);
|
||||||
await fetchThemes();
|
await fetchThemes();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error parsing file:', error);
|
||||||
alert('Error parsing file. Please upload a valid JSON theme file.');
|
alert('Error parsing file. Please upload a valid JSON theme file.');
|
||||||
}
|
}
|
||||||
tempTheme = null;
|
tempTheme = null;
|
||||||
@@ -83,7 +81,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchThemes = async () => {
|
const fetchThemes = async () => {
|
||||||
themes = await getAvailableThemes();
|
themes = {
|
||||||
|
themes: await themeManager.getAvailableThemes(),
|
||||||
|
selectedTheme: themeManager.getSelectedThemeId() || '',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
@@ -98,7 +99,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-full pt-5 mb-1"
|
class="pt-5 mb-1 w-full"
|
||||||
role="list"
|
role="list"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
ondragover={handleDragOver}
|
ondragover={handleDragOver}
|
||||||
@@ -106,9 +107,9 @@
|
|||||||
ondrop={handleDrop}
|
ondrop={handleDrop}
|
||||||
>
|
>
|
||||||
<div class="{isDragging ? 'opacity-100' : 'opacity-0'} transition pointer-events-none absolute w-full p-2 z-50">
|
<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="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 items-center justify-center h-full">
|
<div class="flex justify-center items-center h-full">
|
||||||
<div class="flex flex-col items-center justify-center">
|
<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">
|
<svg height="48" width="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g fill="currentColor">
|
<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"/>
|
<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}
|
{#if isEditMode}
|
||||||
<div
|
<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) }}
|
onclick={(event) => { event.stopPropagation(); handleThemeDelete(theme.id) }}
|
||||||
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleThemeDelete(theme.id) }}
|
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleThemeDelete(theme.id) }}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -152,7 +153,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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) }}
|
onclick={(event) => { event.stopPropagation(); handleShareTheme(theme) }}
|
||||||
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleShareTheme(theme) }}
|
onkeydown={(event) => { if (event.key === 'Enter' || event.key === ' ') handleShareTheme(theme) }}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -167,7 +168,7 @@
|
|||||||
<img
|
<img
|
||||||
src={typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}
|
src={typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}
|
||||||
alt={theme.name}
|
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}
|
||||||
{#if !theme.hideThemeName}
|
{#if !theme.hideThemeName}
|
||||||
@@ -179,7 +180,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if tempTheme}
|
{#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">
|
<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>
|
<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>
|
<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
|
<button
|
||||||
onclick={() => OpenStorePage()}
|
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="text-xl font-IconFamily"></span>
|
||||||
<span class="ml-2">Theme Store</span>
|
<span class="ml-2">Theme Store</span>
|
||||||
@@ -201,7 +202,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={() => { OpenThemeCreator(); closeExtensionPopup() }}
|
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="text-xl font-IconFamily"></span>
|
||||||
<span class="ml-2">Create your own</span>
|
<span class="ml-2">Create your own</span>
|
||||||
|
|||||||
@@ -4,14 +4,8 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
button {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
@apply cursor-pointer;
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
@@ -48,5 +42,9 @@ input {
|
|||||||
.cm-editor {
|
.cm-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100px;
|
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" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>BetterSEQTA+ Settings</title>
|
<title>BetterSEQTA+ Settings</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="h-[600px]">
|
||||||
<div id="app"></div>
|
<div id="app" style="height: 100%;"></div>
|
||||||
<script type="module" src="./index.ts"></script>
|
<script type="module" src="./index.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-19
@@ -1,25 +1,8 @@
|
|||||||
import "./index.css"
|
import "./index.css"
|
||||||
import { mount } from "svelte"
|
|
||||||
import type { ComponentType } from "svelte"
|
|
||||||
import Settings from "./pages/settings.svelte"
|
import Settings from "./pages/settings.svelte"
|
||||||
import IconFamily from '@/resources/fonts/IconFamily.woff'
|
import IconFamily from '@/resources/fonts/IconFamily.woff'
|
||||||
import browser from "webextension-polyfill"
|
import browser from "webextension-polyfill"
|
||||||
|
import renderSvelte from "./main"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
function InjectCustomIcons() {
|
function InjectCustomIcons() {
|
||||||
console.info('[BetterSEQTA+] Injecting Icons')
|
console.info('[BetterSEQTA+] Injecting Icons')
|
||||||
@@ -43,4 +26,4 @@ if (!mountPoint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InjectCustomIcons()
|
InjectCustomIcons()
|
||||||
renderSvelte(Settings, mountPoint)
|
renderSvelte(Settings, mountPoint, { standalone: true })
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import styles from "./index.css?inline"
|
|
||||||
import { mount } from "svelte"
|
import { mount } from "svelte"
|
||||||
import type { ComponentType } from "svelte"
|
import type { ComponentType } from "svelte"
|
||||||
|
import style from './index.css?inline'
|
||||||
|
|
||||||
export default function renderSvelte(
|
export default function renderSvelte(
|
||||||
Component: ComponentType | any,
|
Component: ComponentType | any,
|
||||||
@@ -15,10 +15,9 @@ export default function renderSvelte(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const style = document.createElement("style")
|
const styleElement = document.createElement('style')
|
||||||
style.setAttribute("type", "text/css")
|
styleElement.textContent = style
|
||||||
style.innerHTML = styles
|
mountPoint.appendChild(styleElement)
|
||||||
mountPoint.appendChild(style)
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState'
|
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 ColourPicker from '../components/ColourPicker.svelte'
|
||||||
import { settingsPopup } from '../hooks/SettingsPopup'
|
import { settingsPopup } from '../hooks/SettingsPopup'
|
||||||
|
|
||||||
@@ -56,13 +59,14 @@
|
|||||||
|
|
||||||
if (!standalone) return;
|
if (!standalone) return;
|
||||||
initializeSettingsState();
|
initializeSettingsState();
|
||||||
|
console.log('settingsState', $settingsState);
|
||||||
StandaloneStore.setStandalone(true);
|
StandaloneStore.setStandalone(true);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode ? 'dark' : ''} { standalone ? 'h-[600px]' : 'h-full rounded-xl' } overflow-clip">
|
<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="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white">
|
||||||
<div class="grid border-b border-b-zinc-200/40 place-items-center">
|
<div class="grid place-items-center border-b border-b-zinc-200/40">
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- 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} />
|
<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} />
|
<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}
|
{#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={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 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={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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,72 @@
|
|||||||
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
||||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
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 }>();
|
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
{#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">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">{title}</h2>
|
<h2 class="text-sm font-bold">{title}</h2>
|
||||||
<p class="text-xs">{description}</p>
|
<p class="text-xs">{description}</p>
|
||||||
@@ -38,26 +99,6 @@
|
|||||||
onChange: (isOn: boolean) => settingsState.transparencyEffects = isOn
|
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",
|
title: "Custom Theme Colour",
|
||||||
description: "Customise the overall theme colour of SEQTA Learn.",
|
description: "Customise the overall theme colour of SEQTA Learn.",
|
||||||
@@ -87,36 +128,6 @@
|
|||||||
onChange: (isOn: boolean) => settingsState.animations = isOn
|
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",
|
title: "12 Hour Time",
|
||||||
description: "Prefer 12 hour time format for SEQTA",
|
description: "Prefer 12 hour time format for SEQTA",
|
||||||
@@ -147,19 +158,109 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "BetterSEQTA+",
|
title: "News Feed Source",
|
||||||
description: "Enables BetterSEQTA+ features",
|
description: "Choose sources of your news feed.",
|
||||||
id: 11,
|
id: 11,
|
||||||
Component: Switch,
|
Component: Select,
|
||||||
props: {
|
props: {
|
||||||
state: $settingsState.onoff,
|
state: $settingsState.newsSource,
|
||||||
onChange: (isOn: boolean) => settingsState.onoff = isOn
|
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}
|
] as option}
|
||||||
{@render Setting(option)}
|
{@render Setting(option)}
|
||||||
{/each}
|
{/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}
|
{#if $settingsState.devMode}
|
||||||
<div class="flex items-center justify-between px-4 py-3 mt-4 pt-[1.75rem]">
|
<div class="flex items-center justify-between px-4 py-3 mt-4 pt-[1.75rem]">
|
||||||
<div class="pr-4">
|
<div class="pr-4">
|
||||||
@@ -170,7 +271,7 @@
|
|||||||
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
|
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="pr-4">
|
||||||
<h2 class="text-sm font-bold">Sensitive Hider</h2>
|
<h2 class="text-sm font-bold">Sensitive Hider</h2>
|
||||||
<p class="text-xs">Replace sensitive content with mock data</p>
|
<p class="text-xs">Replace sensitive content with mock data</p>
|
||||||
|
|||||||
@@ -9,16 +9,15 @@
|
|||||||
import type { Theme } from '../types/Theme'
|
import type { Theme } from '../types/Theme'
|
||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
import ThemeModal from '../components/store/ThemeModal.svelte'
|
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 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 { themeUpdates } from '../hooks/ThemeUpdates'
|
||||||
|
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||||
|
|
||||||
import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
|
import { loadBackground } from '@/seqta/ui/ImageBackgrounds'
|
||||||
import Backgrounds from '../components/store/Backgrounds.svelte'
|
import Backgrounds from '../components/store/Backgrounds.svelte'
|
||||||
|
|
||||||
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
let searchTerm = $state('');
|
let searchTerm = $state('');
|
||||||
let themes = $state<Theme[]>([]);
|
let themes = $state<Theme[]>([]);
|
||||||
@@ -33,8 +32,8 @@
|
|||||||
let selectedBackground = $state<string | null>(null);
|
let selectedBackground = $state<string | null>(null);
|
||||||
|
|
||||||
const fetchCurrentThemes = async () => {
|
const fetchCurrentThemes = async () => {
|
||||||
const themes = await getAvailableThemes();
|
const themes = await themeManager.getAvailableThemes();
|
||||||
currentThemes = themes.themes.filter(theme => theme !== null).map(theme => theme.id);
|
currentThemes = themes.filter(theme => theme !== null).map(theme => theme.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setDisplayTheme = (theme: Theme | null) => {
|
const setDisplayTheme = (theme: Theme | null) => {
|
||||||
@@ -123,8 +122,8 @@
|
|||||||
{setDisplayTheme}
|
{setDisplayTheme}
|
||||||
onInstall={async () => {
|
onInstall={async () => {
|
||||||
if (displayTheme) {
|
if (displayTheme) {
|
||||||
await StoreDownloadTheme({themeContent: displayTheme})
|
await themeManager.downloadTheme(displayTheme);
|
||||||
setTheme(displayTheme.id);
|
await themeManager.setTheme(displayTheme.id);
|
||||||
themeUpdates.triggerUpdate();
|
themeUpdates.triggerUpdate();
|
||||||
await fetchCurrentThemes();
|
await fetchCurrentThemes();
|
||||||
}
|
}
|
||||||
@@ -132,7 +131,7 @@
|
|||||||
onRemove={async () => {
|
onRemove={async () => {
|
||||||
if (displayTheme?.id) {
|
if (displayTheme?.id) {
|
||||||
console.debug('deleting theme', displayTheme.id);
|
console.debug('deleting theme', displayTheme.id);
|
||||||
deleteTheme(displayTheme.id)
|
await themeManager.deleteTheme(displayTheme.id);
|
||||||
themeUpdates.triggerUpdate();
|
themeUpdates.triggerUpdate();
|
||||||
await fetchCurrentThemes();
|
await fetchCurrentThemes();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
import { type LoadedCustomTheme } from '@/types/CustomThemes'
|
import { type LoadedCustomTheme } from '@/types/CustomThemes'
|
||||||
|
|
||||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||||
import { getTheme } from '@/seqta/ui/themes/getTheme'
|
|
||||||
|
|
||||||
import Divider from '@/interface/components/themeCreator/divider.svelte'
|
import Divider from '@/interface/components/themeCreator/divider.svelte'
|
||||||
import Switch from '@/interface/components/Switch.svelte'
|
import Switch from '@/interface/components/Switch.svelte'
|
||||||
@@ -22,14 +21,13 @@
|
|||||||
handleImageVariableChange,
|
handleImageVariableChange,
|
||||||
handleCoverImageUpload
|
handleCoverImageUpload
|
||||||
} from '../utils/themeImageHandlers';
|
} from '../utils/themeImageHandlers';
|
||||||
import { ClearThemePreview, UpdateThemePreview } from '@/seqta/ui/themes/UpdateThemePreview'
|
import { CloseThemeCreator } from '@/plugins/built-in/themes/ThemeCreator'
|
||||||
import { saveTheme } from '@/seqta/ui/themes/saveTheme'
|
|
||||||
import { CloseThemeCreator } from '@/seqta/ui/ThemeCreator'
|
|
||||||
import { themeUpdates } from '../hooks/ThemeUpdates'
|
import { themeUpdates } from '../hooks/ThemeUpdates'
|
||||||
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
|
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager'
|
||||||
import { setTheme } from '@/seqta/ui/themes/setTheme'
|
|
||||||
|
|
||||||
const { themeID } = $props<{ themeID: string }>()
|
const { themeID } = $props<{ themeID: string }>()
|
||||||
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
|
||||||
let theme = $state<LoadedCustomTheme>({
|
let theme = $state<LoadedCustomTheme>({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
name: '',
|
name: '',
|
||||||
@@ -46,8 +44,19 @@
|
|||||||
})
|
})
|
||||||
let closedAccordions = $state<string[]>([])
|
let closedAccordions = $state<string[]>([])
|
||||||
let themeLoaded = $state(false);
|
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)) {
|
if (closedAccordions.includes(title)) {
|
||||||
closedAccordions = closedAccordions.filter(t => t !== title);
|
closedAccordions = closedAccordions.filter(t => t !== title);
|
||||||
} else {
|
} else {
|
||||||
@@ -56,10 +65,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await disableTheme();
|
await themeManager.disableTheme();
|
||||||
|
|
||||||
if (themeID) {
|
if (themeID) {
|
||||||
const tempTheme = await getTheme(themeID)
|
const tempTheme = await themeManager.getTheme(themeID)
|
||||||
|
|
||||||
if (!tempTheme) return
|
if (!tempTheme) return
|
||||||
|
|
||||||
@@ -67,16 +76,12 @@
|
|||||||
const loadedTheme = {
|
const loadedTheme = {
|
||||||
...tempTheme,
|
...tempTheme,
|
||||||
CustomImages: tempTheme.CustomImages.map(image => ({
|
CustomImages: tempTheme.CustomImages.map(image => ({
|
||||||
...image,
|
...image
|
||||||
url: image.blob ? URL.createObjectURL(image.blob) : null
|
}))
|
||||||
})),
|
|
||||||
coverImageUrl: tempTheme.coverImage ? URL.createObjectURL(tempTheme.coverImage) : undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tempTheme) {
|
|
||||||
theme = loadedTheme
|
theme = loadedTheme
|
||||||
themeLoaded = true
|
themeLoaded = true
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
themeLoaded = true
|
themeLoaded = true
|
||||||
}
|
}
|
||||||
@@ -100,7 +105,7 @@
|
|||||||
theme = await handleCoverImageUpload(event, theme);
|
theme = await handleCoverImageUpload(event, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitTheme() {
|
async function submitTheme() {
|
||||||
const themeClone = JSON.parse(JSON.stringify(theme));
|
const themeClone = JSON.parse(JSON.stringify(theme));
|
||||||
|
|
||||||
// re-insert blobs into themeClone
|
// re-insert blobs into themeClone
|
||||||
@@ -110,15 +115,17 @@
|
|||||||
}))
|
}))
|
||||||
themeClone.coverImage = theme.coverImage
|
themeClone.coverImage = theme.coverImage
|
||||||
|
|
||||||
ClearThemePreview();
|
themeManager.clearPreview();
|
||||||
saveTheme(themeClone);
|
await themeManager.saveTheme(themeClone);
|
||||||
setTheme(themeClone.id);
|
await themeManager.setTheme(themeClone.id);
|
||||||
themeUpdates.triggerUpdate();
|
themeUpdates.triggerUpdate();
|
||||||
CloseThemeCreator();
|
CloseThemeCreator();
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
UpdateThemePreview(theme);
|
if (themeLoaded) {
|
||||||
|
void themeManager.updatePreviewDebounced(theme);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type SettingType = 'switch' | 'button' | 'slider' | 'colourPicker' | 'select' | 'codeEditor' | 'imageUpload' | 'conditional' | 'lightDarkToggle';
|
type SettingType = 'switch' | 'button' | 'slider' | 'colourPicker' | 'select' | 'codeEditor' | 'imageUpload' | 'conditional' | 'lightDarkToggle';
|
||||||
@@ -158,8 +165,8 @@
|
|||||||
<div class="flex justify-between {item.direction === 'vertical' ? 'flex-col items-start' : 'items-center'} py-3">
|
<div class="flex justify-between {item.direction === 'vertical' ? 'flex-col items-start' : 'items-center'} py-3">
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
onclick={() => { 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) }}
|
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' : ''}">
|
class="flex justify-between pr-4 {item.direction === 'vertical' ? 'cursor-pointer w-full select-none' : ''}">
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -169,6 +176,13 @@
|
|||||||
|
|
||||||
{#if item.direction === 'vertical'}
|
{#if item.direction === 'vertical'}
|
||||||
<div class="flex justify-center items-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>
|
<span class='font-IconFamily transition-transform duration-300 {closedAccordions.includes(item.title) ? 'rotate-180' : ''}'>{'\ue9e6'}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -187,21 +201,24 @@
|
|||||||
<ColourPicker savePresets={false} standalone={true} {...(item.props)} />
|
<ColourPicker savePresets={false} standalone={true} {...(item.props)} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if item.type === 'codeEditor'}
|
{:else if item.type === 'codeEditor'}
|
||||||
|
{#if !codeEditorFullscreen}
|
||||||
{#key themeLoaded}
|
{#key themeLoaded}
|
||||||
<CodeEditor {...(item.props as CodeEditorProps)} />
|
<!-- Only render inline if not fullscreen -->
|
||||||
|
<CodeEditor className="h-[400px]" {...(item.props as CodeEditorProps)} />
|
||||||
{/key}
|
{/key}
|
||||||
|
{/if}
|
||||||
{:else if item.type === 'imageUpload'}
|
{:else if item.type === 'imageUpload'}
|
||||||
{#each theme.CustomImages as image (image.id)}
|
{#each theme.CustomImages as image (image.id)}
|
||||||
<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="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">
|
<div class="h-full">
|
||||||
<img src={image.url} alt={image.variableName} class="object-contain h-full rounded" />
|
<img src={URL.createObjectURL(image.blob)} alt={image.variableName} class="object-contain h-full rounded" />
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={image.variableName}
|
bind:value={image.variableName}
|
||||||
oninput={(e) => onImageVariableChange(image.id, e.currentTarget.value)}
|
oninput={(e) => onImageVariableChange(image.id, e.currentTarget.value)}
|
||||||
placeholder="CSS Variable Name"
|
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">
|
<button onclick={() => onRemoveImage(image.id)} class="p-2 transition dark:text-white">
|
||||||
<span class='text-xl font-IconFamily'>{'\ued8c'}</span>
|
<span class='text-xl font-IconFamily'>{'\ued8c'}</span>
|
||||||
@@ -238,7 +255,20 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class='h-screen overflow-y-scroll {$settingsState.DarkMode && "dark"} no-scrollbar'>
|
<div class='h-screen overflow-y-scroll {$settingsState.DarkMode && "dark"} no-scrollbar'>
|
||||||
<div class='flex flex-col p-2 w-full min-h-screen 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>
|
<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'>
|
<a href='https://betterseqta.gitbook.io/betterseqta-docs' target='_blank' class='text-sm font-light text-zinc-500 dark:text-zinc-400'>
|
||||||
<span class='pr-0.5 no-underline font-IconFamily'>{'\ueb44'}</span>
|
<span class='pr-0.5 no-underline font-IconFamily'>{'\ueb44'}</span>
|
||||||
@@ -281,7 +311,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if theme.coverImage}
|
{#if theme.coverImage}
|
||||||
<div class="absolute z-20 w-full h-full opacity-0 transition-opacity pointer-events-none group-hover:opacity-100 bg-black/20"></div>
|
<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={theme.coverImageUrl} alt='Cover' class="object-cover absolute z-0 w-full h-full rounded" />
|
<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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promi
|
|||||||
const variableName = `custom-image-${theme.CustomImages.length}`;
|
const variableName = `custom-image-${theme.CustomImages.length}`;
|
||||||
resolve({
|
resolve({
|
||||||
...theme,
|
...theme,
|
||||||
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: URL.createObjectURL(imageBlob) }],
|
CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: null }],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
@@ -51,7 +51,7 @@ export function handleCoverImageUpload(event: Event, theme: LoadedCustomTheme):
|
|||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = async () => {
|
reader.onload = async () => {
|
||||||
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
|
const imageBlob = await fetch(reader.result as string).then(res => res.blob());
|
||||||
resolve({ ...theme, coverImage: imageBlob, coverImageUrl: URL.createObjectURL(imageBlob) });
|
resolve({ ...theme, coverImage: imageBlob });
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,11 +32,11 @@
|
|||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["*://*/*"],
|
"resources": ["*/*"],
|
||||||
"matches": ["*://*/*"]
|
"matches": ["*://*/*"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resources": ["resources/icons/*"],
|
"resources": ["resources/*"],
|
||||||
"matches": ["*://*/*"]
|
"matches": ["*://*/*"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 renderSvelte from "@/interface/main"
|
||||||
import themeCreator from "@/interface/pages/themeCreator.svelte"
|
import themeCreator from "@/interface/pages/themeCreator.svelte"
|
||||||
import { unmount } from "svelte"
|
import { unmount } from "svelte"
|
||||||
import { ClearThemePreview } from "./themes/UpdateThemePreview"
|
import { ThemeManager } from "@/plugins/built-in/themes/theme-manager"
|
||||||
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||||
|
|
||||||
let themeCreatorSvelteApp: any = null
|
let themeCreatorSvelteApp: any = null
|
||||||
|
const themeManager = ThemeManager.getInstance();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the Theme Creator sidebar, it is an embedded page loaded similar to the extension popup
|
* 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 = "") {
|
export function OpenThemeCreator(themeID: string = "") {
|
||||||
CloseThemeCreator()
|
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 width = "310px"
|
||||||
|
|
||||||
const themeCreatorDiv: HTMLDivElement = document.createElement("div")
|
const themeCreatorDiv: HTMLDivElement = document.createElement("div")
|
||||||
@@ -33,7 +41,7 @@ export function OpenThemeCreator(themeID: string = "") {
|
|||||||
closeButton.textContent = "×"
|
closeButton.textContent = "×"
|
||||||
closeButton.addEventListener("click", () => {
|
closeButton.addEventListener("click", () => {
|
||||||
CloseThemeCreator()
|
CloseThemeCreator()
|
||||||
ClearThemePreview()
|
themeManager.clearPreview()
|
||||||
})
|
})
|
||||||
|
|
||||||
document.body.appendChild(closeButton)
|
document.body.appendChild(closeButton)
|
||||||
@@ -55,7 +63,7 @@ export function OpenThemeCreator(themeID: string = "") {
|
|||||||
const mouseMoveHandler = (e: MouseEvent) => {
|
const mouseMoveHandler = (e: MouseEvent) => {
|
||||||
if (!isDragging) return
|
if (!isDragging) return
|
||||||
const windowWidth = window.innerWidth
|
const windowWidth = window.innerWidth
|
||||||
const newWidth = Math.min(Math.max(310, windowWidth - e.clientX), 600)
|
const newWidth = Math.max(310, windowWidth - e.clientX)
|
||||||
themeCreatorDiv.style.width = `${newWidth}px`
|
themeCreatorDiv.style.width = `${newWidth}px`
|
||||||
mainContent.style.width = `calc(100% - ${newWidth}px)`
|
mainContent.style.width = `calc(100% - ${newWidth}px)`
|
||||||
resizeBar.style.right = `${newWidth - 2.5}px`
|
resizeBar.style.right = `${newWidth - 2.5}px`
|
||||||
@@ -82,6 +90,9 @@ export function OpenThemeCreator(themeID: string = "") {
|
|||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
export function CloseThemeCreator() {
|
export function CloseThemeCreator() {
|
||||||
|
// Remove the stored flag
|
||||||
|
localStorage.removeItem('themeCreatorOpen');
|
||||||
|
|
||||||
const themeCreator = document.getElementById("themeCreator")
|
const themeCreator = document.getElementById("themeCreator")
|
||||||
const closeButton = document.querySelector(
|
const closeButton = document.querySelector(
|
||||||
".themeCloseButton",
|
".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: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
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,5 +1,10 @@
|
|||||||
import { addExtensionSettings, enableAnimatedBackground, GetThresholdOfColor, loadHomePage, SendNewsPage, setupSettingsButton } from "@/SEQTA";
|
import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings";
|
||||||
import { updateBgDurations } from "./Animation";
|
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 { appendBackgroundToUI } from "./ImageBackgrounds";
|
||||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
@@ -35,7 +40,6 @@ async function getUserInfo() {
|
|||||||
|
|
||||||
export async function AddBetterSEQTAElements() {
|
export async function AddBetterSEQTAElements() {
|
||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
initializeSettings();
|
|
||||||
if (settingsState.DarkMode) {
|
if (settingsState.DarkMode) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
}
|
}
|
||||||
@@ -69,11 +73,6 @@ export async function AddBetterSEQTAElements() {
|
|||||||
setupSettingsButton();
|
setupSettingsButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeSettings() {
|
|
||||||
enableAnimatedBackground();
|
|
||||||
updateBgDurations();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) {
|
function createHomeButton(fragment: DocumentFragment, menuList: HTMLElement) {
|
||||||
const container = document.getElementById('content')!;
|
const container = document.getElementById('content')!;
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
@@ -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 browser from 'webextension-polyfill'
|
||||||
import { GetThresholdOfColor } from '@/SEQTA';
|
import { GetThresholdOfColor } from '@/seqta/ui/colors/getThresholdColour';
|
||||||
import { lightenAndPaleColor } from './lightenAndPaleColor';
|
import { lightenAndPaleColor } from './lightenAndPaleColor';
|
||||||
import ColorLuminance from './ColorLuminance';
|
import ColorLuminance from './ColorLuminance';
|
||||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
|
||||||
|
|||||||
@@ -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: {
|
messageSubject: {
|
||||||
selector: '.MessageList__subject___1NV5O',
|
selector: '[class*="MessageList__subject___"]',
|
||||||
action: (element) => { element.textContent = getRandomElement(mockData.messages.subjects); }
|
action: (element) => { element.textContent = getRandomElement(mockData.messages.subjects); }
|
||||||
},
|
},
|
||||||
|
|
||||||
messageSender: {
|
messageSender: {
|
||||||
selector: '.MessageList__value___1sN24',
|
selector: '[class*="MessageList__value___"]',
|
||||||
action: (element) => { element.textContent = getRandomElement(mockData.messages.sender); }
|
action: (element) => { element.textContent = getRandomElement(mockData.messages.sender); }
|
||||||
},
|
},
|
||||||
|
|
||||||
messageRecipients: {
|
messageRecipients: {
|
||||||
selector: '.MessageList__recipients___3hqpE .MessageList__value___1sN24',
|
selector: '[class*="MessageList__recipients___"] [class*="MessageList__value___"]',
|
||||||
action: (element) => { element.textContent = 'Recipient(s) Redacted'; }
|
action: (element) => { element.textContent = 'Recipient(s) Redacted'; }
|
||||||
},
|
},
|
||||||
|
|
||||||
messageDate: {
|
messageDate: {
|
||||||
selector: '.MessageList__date___7muMb',
|
selector: '[class*="MessageList__date___"]',
|
||||||
action: (element) => { element.textContent = getRandomDate().toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'long' }); }
|
action: (element) => { element.textContent = getRandomDate().toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'long' }); }
|
||||||
},
|
},
|
||||||
avatarImage: {
|
avatarImage: {
|
||||||
selector: '.Avatar__Avatar___gE5kx',
|
selector: '[class*="Avatar__Avatar___"]',
|
||||||
action: (element) => {
|
action: (element) => {
|
||||||
if (element instanceof HTMLElement) {
|
if (element instanceof HTMLElement) {
|
||||||
element.style.removeProperty('background-image');
|
element.style.removeProperty('background-image');
|
||||||
@@ -105,7 +105,7 @@ const contentConfig: ContentConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
notificationCount: {
|
notificationCount: {
|
||||||
selector: '.notifications__bubble___1EkSQ',
|
selector: '[class*="notifications__bubble___"]',
|
||||||
action: (element) => { element.textContent = Math.floor(Math.random() * 100).toString(); }
|
action: (element) => { element.textContent = Math.floor(Math.random() * 100).toString(); }
|
||||||
},
|
},
|
||||||
schoolName: {
|
schoolName: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import stringToHTML from "../stringToHTML"
|
||||||
|
|
||||||
|
export function CreateCustomShortcutDiv(element: any) {
|
||||||
|
// Creates the stucture and element information for each seperate shortcut
|
||||||
|
var shortcut = document.createElement("a")
|
||||||
|
shortcut.setAttribute("href", element.url)
|
||||||
|
shortcut.setAttribute("target", "_blank")
|
||||||
|
var shortcutdiv = document.createElement("div")
|
||||||
|
shortcutdiv.classList.add("shortcut")
|
||||||
|
shortcutdiv.classList.add("customshortcut")
|
||||||
|
|
||||||
|
let image = stringToHTML(
|
||||||
|
`
|
||||||
|
<svg style="width:39px;height:39px" viewBox="0 0 40 40" class="shortcuticondiv">
|
||||||
|
<text
|
||||||
|
text-anchor="middle"
|
||||||
|
x="50%"
|
||||||
|
y="50%"
|
||||||
|
dy=".35em"
|
||||||
|
fill="var(--text-primary)"
|
||||||
|
font-weight="bold"
|
||||||
|
font-size="32"
|
||||||
|
dominant-baseline="middle">
|
||||||
|
${element.icon}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
`,
|
||||||
|
).firstChild
|
||||||
|
;(image as HTMLElement).classList.add("shortcuticondiv")
|
||||||
|
var text = document.createElement("p")
|
||||||
|
text.textContent = element.name
|
||||||
|
shortcutdiv.append(image!)
|
||||||
|
shortcutdiv.append(text)
|
||||||
|
shortcut.append(shortcutdiv)
|
||||||
|
|
||||||
|
document.getElementById("shortcuts")!.append(shortcut)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export function CreateElement(
|
||||||
|
type: string,
|
||||||
|
class_?: any,
|
||||||
|
id?: any,
|
||||||
|
innerText?: string,
|
||||||
|
innerHTML?: string,
|
||||||
|
style?: string,
|
||||||
|
) {
|
||||||
|
let element = document.createElement(type)
|
||||||
|
if (class_ !== undefined) {
|
||||||
|
element.classList.add(class_)
|
||||||
|
}
|
||||||
|
if (id !== undefined) {
|
||||||
|
element.id = id
|
||||||
|
}
|
||||||
|
if (innerText !== undefined) {
|
||||||
|
element.innerText = innerText
|
||||||
|
}
|
||||||
|
if (innerHTML !== undefined) {
|
||||||
|
element.innerHTML = innerHTML
|
||||||
|
}
|
||||||
|
if (style !== undefined) {
|
||||||
|
element.style.cssText = style
|
||||||
|
}
|
||||||
|
return element
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export function RemoveShortcutDiv(elements: any) {
|
||||||
|
if (elements.length === 0) return
|
||||||
|
|
||||||
|
elements.forEach((element: any) => {
|
||||||
|
const shortcuts = document.querySelectorAll(".shortcut")
|
||||||
|
shortcuts.forEach((shortcut) => {
|
||||||
|
const anchorElement = shortcut.parentElement // the <a> element is the parent
|
||||||
|
const textElement = shortcut.querySelector("p") // <p> is a direct child of .shortcut
|
||||||
|
const title = textElement ? textElement.textContent : ""
|
||||||
|
|
||||||
|
let shouldRemove = title === element.name
|
||||||
|
|
||||||
|
// Check href only if element.url exists
|
||||||
|
if (element.url) {
|
||||||
|
shouldRemove =
|
||||||
|
shouldRemove && anchorElement!.getAttribute("href") === element.url
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemove) {
|
||||||
|
anchorElement!.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement"
|
||||||
|
|
||||||
|
export function FilterUpcomingAssessments(subjectoptions: any) {
|
||||||
|
for (var item in subjectoptions) {
|
||||||
|
let subjectdivs = document.querySelectorAll(`[data-subject="${item}"]`)
|
||||||
|
|
||||||
|
for (let i = 0; i < subjectdivs.length; i++) {
|
||||||
|
const element = subjectdivs[i]
|
||||||
|
|
||||||
|
if (!subjectoptions[item]) {
|
||||||
|
element.classList.add("hidden")
|
||||||
|
}
|
||||||
|
if (subjectoptions[item]) {
|
||||||
|
element.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
(element.parentNode! as HTMLElement).classList.remove("hidden")
|
||||||
|
|
||||||
|
let children = element.parentNode!.parentNode!.children
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
const element = children[i]
|
||||||
|
if (element.hasAttribute("data-hidden")) {
|
||||||
|
element.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
element.parentNode!.children.length ==
|
||||||
|
element.parentNode!.querySelectorAll(".hidden").length
|
||||||
|
) {
|
||||||
|
if (element.parentNode!.querySelectorAll(".hidden").length > 0) {
|
||||||
|
if (
|
||||||
|
!(element.parentNode!.parentNode! as HTMLElement).hasAttribute(
|
||||||
|
"data-day",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
(element.parentNode!.parentNode! as HTMLElement).classList.add(
|
||||||
|
"hidden",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AddPlaceHolderToParent(
|
||||||
|
element.parentNode!.parentNode,
|
||||||
|
element.parentNode!.querySelectorAll(".hidden").length,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(element.parentNode!.parentNode! as HTMLElement).classList.remove(
|
||||||
|
"hidden",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddPlaceHolderToParent(parent: any, numberofassessments: any) {
|
||||||
|
let textcontainer = CreateElement("div", "upcoming-blank")
|
||||||
|
let textblank = CreateElement("p", "upcoming-hiddenassessment")
|
||||||
|
let s = ""
|
||||||
|
if (numberofassessments > 1) {
|
||||||
|
s = "s"
|
||||||
|
}
|
||||||
|
textblank.innerText = `${numberofassessments} hidden assessment${s} due`
|
||||||
|
textcontainer.append(textblank)
|
||||||
|
textcontainer.setAttribute("data-hidden", "true")
|
||||||
|
|
||||||
|
parent.append(textcontainer)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,107 @@
|
|||||||
|
import stringToHTML from "../stringToHTML"
|
||||||
|
import browser from "webextension-polyfill"
|
||||||
|
import { settingsState } from "../listeners/SettingsState"
|
||||||
|
import { animate, stagger } from "motion"
|
||||||
|
import { DeleteWhatsNew } from "../Whatsnew"
|
||||||
|
|
||||||
|
export function OpenAboutPage() {
|
||||||
|
const background = document.createElement("div")
|
||||||
|
background.id = "whatsnewbk"
|
||||||
|
background.classList.add("whatsnewBackground")
|
||||||
|
|
||||||
|
const container = document.createElement("div")
|
||||||
|
container.classList.add("whatsnewContainer")
|
||||||
|
|
||||||
|
var header: any = stringToHTML(
|
||||||
|
/* html */
|
||||||
|
`<div class="whatsnewHeader">
|
||||||
|
<h1>About</h1>
|
||||||
|
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
|
||||||
|
</div>`,
|
||||||
|
).firstChild
|
||||||
|
|
||||||
|
let text = stringToHTML(/* html */ `
|
||||||
|
<div class="whatsnewTextContainer" style="overflow-y: scroll;">
|
||||||
|
<img src="${settingsState.DarkMode ? "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/dark.jpg" : "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/light.jpg"}" class="aboutImg" />
|
||||||
|
|
||||||
|
<p>BetterSEQTA+ is a fork of BetterSEQTA which was originally developed by Nulkem, which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features. </p>
|
||||||
|
<p>We are currently working on fixing bugs and adding good features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below).</p>
|
||||||
|
<h1>Credits</h1>
|
||||||
|
<p>Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph and SethBurkart123.</p>
|
||||||
|
</div>
|
||||||
|
`).firstChild
|
||||||
|
|
||||||
|
let footer = stringToHTML(/* html */ `
|
||||||
|
<div class="whatsnewFooter">
|
||||||
|
<div>
|
||||||
|
Report bugs and feedback:
|
||||||
|
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding:0;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="25px" height="25px" viewBox="0 0 256 250" version="1.1" preserveAspectRatio="xMidYMid">
|
||||||
|
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding:0;">
|
||||||
|
<svg style="width:25px;height:25px" viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0;">
|
||||||
|
<svg style="width: 25px; height: 25px;" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).firstChild
|
||||||
|
|
||||||
|
let exitbutton = document.createElement("div")
|
||||||
|
exitbutton.id = "whatsnewclosebutton"
|
||||||
|
|
||||||
|
container.append(header)
|
||||||
|
container.append(text as ChildNode)
|
||||||
|
container.append(footer as ChildNode)
|
||||||
|
container.append(exitbutton)
|
||||||
|
|
||||||
|
background.append(container)
|
||||||
|
|
||||||
|
document.getElementById("container")!.append(background)
|
||||||
|
|
||||||
|
let bkelement = document.getElementById("whatsnewbk")
|
||||||
|
let popup = document.getElementsByClassName("whatsnewContainer")[0]
|
||||||
|
|
||||||
|
if (settingsState.animations) {
|
||||||
|
animate(
|
||||||
|
[popup, bkelement as HTMLElement],
|
||||||
|
{ scale: [0, 1] },
|
||||||
|
{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 220,
|
||||||
|
damping: 18,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
animate(
|
||||||
|
".whatsnewTextContainer *",
|
||||||
|
{ opacity: [0, 1], y: [10, 0] },
|
||||||
|
{
|
||||||
|
delay: stagger(0.05, { startDelay: 0.1 }),
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.22, 0.03, 0.26, 1],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete settingsState.justupdated
|
||||||
|
|
||||||
|
bkelement!.addEventListener("click", function (event) {
|
||||||
|
// Check if the click event originated from the element itself and not any of its children
|
||||||
|
if (event.target === bkelement) {
|
||||||
|
DeleteWhatsNew()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var closeelement = document.getElementById("whatsnewclosebutton")
|
||||||
|
closeelement!.addEventListener("click", function () {
|
||||||
|
DeleteWhatsNew()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
|
||||||
|
import { settingsState } from "../listeners/SettingsState"
|
||||||
|
import stringToHTML from "../stringToHTML"
|
||||||
|
import Sortable from "sortablejs"
|
||||||
|
|
||||||
|
export let MenuOptionsOpen = false
|
||||||
|
|
||||||
|
|
||||||
|
export function OpenMenuOptions() {
|
||||||
|
var container = document.getElementById("container")
|
||||||
|
var menu = document.getElementById("menu")
|
||||||
|
|
||||||
|
if (settingsState.defaultmenuorder.length == 0) {
|
||||||
|
let childnodes = menu!.firstChild!.childNodes
|
||||||
|
let newdefaultmenuorder = []
|
||||||
|
for (let i = 0; i < childnodes.length; i++) {
|
||||||
|
const element = childnodes[i]
|
||||||
|
newdefaultmenuorder.push((element as HTMLElement).dataset.key)
|
||||||
|
settingsState.defaultmenuorder = newdefaultmenuorder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let childnodes = menu!.firstChild!.childNodes
|
||||||
|
if (settingsState.defaultmenuorder.length != childnodes.length) {
|
||||||
|
for (let i = 0; i < childnodes.length; i++) {
|
||||||
|
const element = childnodes[i]
|
||||||
|
if (
|
||||||
|
!settingsState.defaultmenuorder.indexOf(
|
||||||
|
(element as HTMLElement).dataset.key,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
let newdefaultmenuorder = settingsState.defaultmenuorder
|
||||||
|
newdefaultmenuorder.push((element as HTMLElement).dataset.key)
|
||||||
|
settingsState.defaultmenuorder = newdefaultmenuorder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuOptionsOpen = true
|
||||||
|
|
||||||
|
var cover = document.createElement("div")
|
||||||
|
cover.classList.add("notMenuCover")
|
||||||
|
menu!.style.zIndex = "20"
|
||||||
|
menu!.style.setProperty("--menuHidden", "flex")
|
||||||
|
container!.append(cover)
|
||||||
|
|
||||||
|
var menusettings = document.createElement("div")
|
||||||
|
menusettings.classList.add("editmenuoption-container")
|
||||||
|
|
||||||
|
var defaultbutton = document.createElement("div")
|
||||||
|
defaultbutton.classList.add("editmenuoption")
|
||||||
|
defaultbutton.innerText = "Restore Default"
|
||||||
|
defaultbutton.id = "restoredefaultoption"
|
||||||
|
|
||||||
|
var savebutton = document.createElement("div")
|
||||||
|
savebutton.classList.add("editmenuoption")
|
||||||
|
savebutton.innerText = "Save"
|
||||||
|
savebutton.id = "restoredefaultoption"
|
||||||
|
|
||||||
|
menusettings.appendChild(defaultbutton)
|
||||||
|
menusettings.appendChild(savebutton)
|
||||||
|
|
||||||
|
menu!.appendChild(menusettings)
|
||||||
|
|
||||||
|
var ListItems = menu!.firstChild!.childNodes
|
||||||
|
for (let i = 0; i < ListItems.length; i++) {
|
||||||
|
const element1 = ListItems[i]
|
||||||
|
const element = element1 as HTMLElement
|
||||||
|
|
||||||
|
;(element as HTMLElement).classList.add("draggable")
|
||||||
|
if ((element as HTMLElement).classList.contains("hasChildren")) {
|
||||||
|
(element as HTMLElement).classList.remove("active")
|
||||||
|
;(element.firstChild as HTMLElement).classList.remove("noscroll")
|
||||||
|
}
|
||||||
|
|
||||||
|
let MenuItemToggle = stringToHTML(
|
||||||
|
`<div class="onoffswitch" style="margin: auto 0;"><input class="onoffswitch-checkbox notification menuitem" type="checkbox" id="${(element as HTMLElement).dataset.key}"><label for="${(element as HTMLElement).dataset.key}" class="onoffswitch-label"></label>`,
|
||||||
|
).firstChild
|
||||||
|
;(element as HTMLElement).append(MenuItemToggle!)
|
||||||
|
|
||||||
|
if (!element.dataset.betterseqta) {
|
||||||
|
const a = document.createElement("section")
|
||||||
|
a.innerHTML = element.innerHTML
|
||||||
|
cloneAttributes(a, element)
|
||||||
|
menu!.firstChild!.insertBefore(a, element)
|
||||||
|
element.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(settingsState.menuitems).length == 0) {
|
||||||
|
menubuttons = menu!.firstChild!.childNodes
|
||||||
|
let menuItems = {} as any
|
||||||
|
for (var i = 0; i < menubuttons.length; i++) {
|
||||||
|
var id = (menubuttons[i] as HTMLElement).dataset.key
|
||||||
|
const element: any = {}
|
||||||
|
element.toggle = true
|
||||||
|
;(menuItems[id as keyof typeof menuItems] as any) = element
|
||||||
|
}
|
||||||
|
settingsState.menuitems = menuItems
|
||||||
|
}
|
||||||
|
|
||||||
|
var menubuttons: any = document.getElementsByClassName("menuitem")
|
||||||
|
|
||||||
|
let menuItems = settingsState.menuitems as any
|
||||||
|
let buttons = document.getElementsByClassName("menuitem")
|
||||||
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
|
let id = buttons[i].id as string | undefined
|
||||||
|
if (menuItems[id as keyof typeof menuItems]) {
|
||||||
|
(buttons[i] as HTMLInputElement).checked =
|
||||||
|
menuItems[id as keyof typeof menuItems].toggle
|
||||||
|
} else {
|
||||||
|
(buttons[i] as HTMLInputElement).checked = true
|
||||||
|
}
|
||||||
|
(buttons[i] as HTMLInputElement).checked = true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var el = document.querySelector("#menu > ul")
|
||||||
|
var sortable = Sortable.create(el as HTMLElement, {
|
||||||
|
draggable: ".draggable",
|
||||||
|
dataIdAttr: "data-key",
|
||||||
|
animation: 150,
|
||||||
|
easing: "cubic-bezier(.5,0,.5,1)",
|
||||||
|
onEnd: function () {
|
||||||
|
saveNewOrder(sortable)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeDisplayProperty(element: any) {
|
||||||
|
if (!element.checked) {
|
||||||
|
element.parentNode.parentNode.style.display = "var(--menuHidden)"
|
||||||
|
}
|
||||||
|
if (element.checked) {
|
||||||
|
element.parentNode.parentNode.style.setProperty(
|
||||||
|
"display",
|
||||||
|
"flex",
|
||||||
|
"important",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function StoreMenuSettings() {
|
||||||
|
let menu = document.getElementById("menu")
|
||||||
|
const menuItems: any = {}
|
||||||
|
let menubuttons = menu!.firstChild!.childNodes
|
||||||
|
const button = document.getElementsByClassName("menuitem")
|
||||||
|
for (let i = 0; i < menubuttons.length; i++) {
|
||||||
|
const id = (menubuttons[i] as HTMLElement).dataset.key
|
||||||
|
const element: any = {}
|
||||||
|
element.toggle = (button[i] as HTMLInputElement).checked
|
||||||
|
|
||||||
|
menuItems[id as keyof typeof menuItems] = element
|
||||||
|
}
|
||||||
|
settingsState.menuitems = menuItems
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < menubuttons.length; i++) {
|
||||||
|
const element = menubuttons[i]
|
||||||
|
element.addEventListener("change", () => {
|
||||||
|
element.parentElement.parentElement.getAttribute("data-key")
|
||||||
|
StoreMenuSettings()
|
||||||
|
changeDisplayProperty(element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAll() {
|
||||||
|
menusettings?.remove()
|
||||||
|
cover?.remove()
|
||||||
|
MenuOptionsOpen = false
|
||||||
|
menu!.style.setProperty("--menuHidden", "none")
|
||||||
|
|
||||||
|
for (let i = 0; i < ListItems.length; i++) {
|
||||||
|
const element1 = ListItems[i]
|
||||||
|
const element = element1 as HTMLElement
|
||||||
|
element.classList.remove("draggable")
|
||||||
|
element.setAttribute("draggable", "false")
|
||||||
|
|
||||||
|
if (!element.dataset.betterseqta) {
|
||||||
|
const a = document.createElement("li")
|
||||||
|
a.innerHTML = element.innerHTML
|
||||||
|
cloneAttributes(a, element)
|
||||||
|
menu!.firstChild!.insertBefore(a, element)
|
||||||
|
element.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let switches = menu!.querySelectorAll(".onoffswitch")
|
||||||
|
for (let i = 0; i < switches.length; i++) {
|
||||||
|
switches[i].remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cover?.addEventListener("click", closeAll)
|
||||||
|
savebutton?.addEventListener("click", closeAll)
|
||||||
|
|
||||||
|
defaultbutton?.addEventListener("click", function () {
|
||||||
|
const options = settingsState.defaultmenuorder
|
||||||
|
settingsState.menuorder = options
|
||||||
|
|
||||||
|
ChangeMenuItemPositions(options)
|
||||||
|
|
||||||
|
for (let i = 0; i < menubuttons.length; i++) {
|
||||||
|
const element = menubuttons[i]
|
||||||
|
element.checked = true
|
||||||
|
element.parentNode.parentNode.style.setProperty(
|
||||||
|
"display",
|
||||||
|
"flex",
|
||||||
|
"important",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
saveNewOrder(sortable)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveNewOrder(sortable: any) {
|
||||||
|
var order = sortable.toArray()
|
||||||
|
settingsState.menuorder = order
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneAttributes(target: any, source: any) {
|
||||||
|
[...source.attributes].forEach((attr) => {
|
||||||
|
target.setAttribute(attr.nodeName, attr.nodeValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChangeMenuItemPositions(storage: any) {
|
||||||
|
let menuorder = storage
|
||||||
|
|
||||||
|
var menuList = document.querySelector("#menu")!.firstChild!.childNodes
|
||||||
|
|
||||||
|
let listorder = []
|
||||||
|
for (let i = 0; i < menuList.length; i++) {
|
||||||
|
const menu = menuList[i] as HTMLElement
|
||||||
|
|
||||||
|
let a = menuorder.indexOf(menu.dataset.key)
|
||||||
|
|
||||||
|
listorder.push(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newArr = []
|
||||||
|
for (var i = 0; i < listorder.length; i++) {
|
||||||
|
newArr[listorder[i]] = menuList[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
let listItemsDOM = document.getElementById("menu")!.firstChild
|
||||||
|
for (let i = 0; i < newArr.length; i++) {
|
||||||
|
const element = newArr[i]
|
||||||
|
if (element) {
|
||||||
|
const elem = element as HTMLElement
|
||||||
|
elem.setAttribute("data-checked", "true")
|
||||||
|
listItemsDOM!.appendChild(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
class ReactFiber {
|
||||||
|
private selector: string;
|
||||||
|
private debug: boolean;
|
||||||
|
private messageIdCounter: number = 0; // Counter for unique message IDs
|
||||||
|
|
||||||
|
constructor(selector: string, options: {
|
||||||
|
debug ? : boolean
|
||||||
|
} = {}) {
|
||||||
|
this.selector = selector;
|
||||||
|
this.debug = options.debug || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static find(selector: string, options: {
|
||||||
|
debug ? : boolean
|
||||||
|
} = {}) {
|
||||||
|
return new ReactFiber(selector, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendMessage(action: string, payload: any = {}): Promise < any > {
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
const messageId = this.messageIdCounter++;
|
||||||
|
const message = {
|
||||||
|
type: "reactFiberRequest",
|
||||||
|
selector: this.selector,
|
||||||
|
action,
|
||||||
|
payload,
|
||||||
|
debug: this.debug,
|
||||||
|
messageId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listener = (response: any) => {
|
||||||
|
if (response.data?.type === 'reactFiberResponse' && response.data?.messageId === messageId) {
|
||||||
|
if (this.debug) {
|
||||||
|
console.log("Content Received Response:", response.data.response);
|
||||||
|
}
|
||||||
|
resolve(response.data.response);
|
||||||
|
window.removeEventListener("message", listener)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', listener);
|
||||||
|
window.postMessage(message, "*");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getState(key ? : string | string[]): Promise < any > {
|
||||||
|
return this.sendMessage("getState", {
|
||||||
|
key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setState(update: any | ((prevState: any) => any)): Promise < ReactFiber > {
|
||||||
|
const updateFnString = typeof update === 'function' ? update.toString() : null;
|
||||||
|
const updateObject = typeof update !== 'function' ? update : null;
|
||||||
|
|
||||||
|
await this.sendMessage("setState", {
|
||||||
|
updateFn: updateFnString,
|
||||||
|
updateObject
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getProps(propName ? : string): Promise < any > {
|
||||||
|
return this.sendMessage("getProp", {
|
||||||
|
propName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setProp(propName: string, value: any): Promise < ReactFiber > {
|
||||||
|
await this.sendMessage("setProp", {
|
||||||
|
propName,
|
||||||
|
value
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async forceUpdate(): Promise < ReactFiber > {
|
||||||
|
await this.sendMessage("forceUpdate");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactFiber;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user