Compare commits

..

3 Commits

Author SHA1 Message Date
SethBurkart123 51c265400c feat(app): improved input handling + better UI 2024-12-11 11:44:56 +11:00
SethBurkart123 6209b65afe feat(app): add icons to electron 2024-12-05 18:11:13 +11:00
SethBurkart123 bb388ab000 feat: electron building 2024-12-05 18:09:06 +11:00
223 changed files with 22603 additions and 40155 deletions
-397
View File
@@ -1,397 +0,0 @@
/** @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 -10
View File
@@ -12,15 +12,12 @@
}, },
"rules": { "rules": {
// allow importing ts extensions // allow importing ts extensions
"sort-imports": [ "sort-imports": ["error", {
"error", "ignoreCase": true,
{ "ignoreDeclarationSort": true,
"ignoreCase": true, "ignoreMemberSort": false,
"ignoreDeclarationSort": true, "memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
"ignoreMemberSort": false, }],
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"]
}
],
"import/extensions": [ "import/extensions": [
"error", "error",
"ignorePackages", "ignorePackages",
@@ -32,4 +29,4 @@
] ]
}, },
"plugins": ["import"] "plugins": ["import"]
} }
+27
View File
@@ -0,0 +1,27 @@
---
name: Report A bug.
about: Create a report of a present bug.
title: "[BUG]"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Please indicate how did you make this happen.
**Expected behaviuor**
Please add a clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- If using Windows, the build number. Find this by using ```winver``` and copying down the build id.
**Additional context**
Add any other context about the problem here.
-56
View File
@@ -1,56 +0,0 @@
name: Bug report
description: Report an issue with the modpack in its unmodified state. For other issues, use Discord.
labels: bug
title: "[BUG]"
body:
- type: markdown
attributes:
value: |
Before reporting an issue, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) to make sure it has not already been reported (make sure to search closed issues as well!).
- type: textarea
attributes:
label: Describe the bug
description: Describe your issue. For general issues and questions you'll get a faster answer [from our Discord.](https://discord.gg/YzmbnCDkat)
validations:
required: true
- type: input
attributes:
label: Extension version
description: What version of the extension are you using?
placeholder: Find it by opening the config menu and clicking the about icon in the top right.
validations:
required: true
- type: dropdown
attributes:
label: Browser
description: Which Browser are you using?
options:
- Chrome
- Firefox
- Brave
- Safari
- DuckDuckGO
- Microsoft Edge
- Other Chromium-Based Browser
- Other Non-Chromium-Based Browser
validations:
required: true
- type: checkboxes
attributes:
label: Confirm
options:
- label: This bug report is about an issue with the extension itself. I have not modified the extension nor added any unsupported plugins. If this is not the case, I know that I should post the issue to the extension's Discord support channel instead.
required: true
- type: textarea
attributes:
label: Additional context
description: Screenshots, video or any other information. Include photos of the console if possible
placeholder: |
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false
-4
View File
@@ -1,4 +0,0 @@
contact_links:
- name: BetterSEQTA Community Support
url: https://discord.gg/YzmbnCDkat
about: Join our discord for community updates, discussion, and more!
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FR] "
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -1,51 +0,0 @@
name: Feature request
description: Suggest a new Feature to be added or replaced in BetterSeqtaPLUS
labels: enhancement
title: "[FR]"
body:
- type: checkboxes
attributes:
label: Confirm
options:
- label: "Is this feature request related to a Bug report?"
required: false
- type: input
attributes:
label: Bug report link
description: "If this feature request is related to a bug report, please insert the link to the bug report here"
placeholder: "https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues/..."
validations:
required: false
- type: markdown
attributes:
value: |
## Feature details
Before you request a feature, [please search](https://github.com/BetterSEQTA/BetterSEQTA-Plus/issues) if it has already been requested. (Make sure to check closed issues as well!)
- type: dropdown
attributes:
label: Feature type
multiple: false
options:
- Graphical
- Functional
- Not Sure
validations:
required: true
- type: input
attributes:
label: Feature Details
description: Please write, with as much detail as possible, what you would like to see from this feature.
placeholder: it would be cool if
validations:
required: false
- type: textarea
attributes:
label: Additional details
description: Anything else that would help describe your vision (reference images, descriptions, etc)
validations:
required: false
+17
View File
@@ -0,0 +1,17 @@
---
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.
@@ -1,14 +0,0 @@
## 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
+19 -19
View File
@@ -2,9 +2,9 @@ name: NodeJS Build
on: on:
push: push:
branches: ["main"] branches: [ "main" ]
pull_request: pull_request:
branches: ["main"] branches: [ "main" ]
jobs: jobs:
build: build:
@@ -15,24 +15,24 @@ jobs:
node-version: [20.x] node-version: [20.x]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Build - name: Build
run: | run: |
npm install --legacy-peer-deps npm install --legacy-peer-deps
npm run build npm run build
- name: Zip dist folder - name: Zip dist folder
run: | run: |
zip -r dist.zip dist zip -r dist.zip dist
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: dist-zip name: dist-zip
path: dist.zip path: dist.zip
+4 -4
View File
@@ -9,15 +9,15 @@ yarn.lock
.env .env
.env.submit .env.submit
dependency-graph.svg
# Build # Build
extension.zip extension.zip
build/
dist/ dist/
betterseqtaplus-safari/ betterseqtaplus-safari/
.million/ .million/
.vscode/ .vscode/
**/.DS_Store **/.DS_Store
# Electron
electron-dist/
+5
View File
@@ -0,0 +1,5 @@
{
"plugins": {
"tailwindcss": {}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"tabWidth": 2, "tabWidth": 2,
"useTabs": false, "useTabs": false,
"semi": true "semi": false
} }
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
- Demonstrating empathy and kindness toward other people * Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences * Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback * Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, * Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
- Focusing on what is best not just for us as individuals, but for the * Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or * The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks * Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment * Public or private harassment
- Publishing others' private information, such as a physical or email * Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a * Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban ### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community **Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals. individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within **Consequence**: A permanent ban from any sort of public interaction within
-15
View File
@@ -3,21 +3,6 @@
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.
+39 -38
View File
@@ -1,4 +1,5 @@
#
#
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel"> <a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" /> <img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
@@ -43,18 +44,23 @@
- 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 official theme store - Fully customisable themes and an offical 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, Edge, Brave, Opera and other Chromium-Based browsers are supported - Chrome Supported
- Firefox Supported: [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)! - Edge Supported
- Safari (Experimental and not recommended - only available via compilation) - Brave Supported
- Opera Supported
- Vivaldi Supported
- Chromium-based browsers are supported
- Firefox (Experimental - available [here](https://addons.mozilla.org/en-US/firefox/addon/betterseqta-plus/)
- Safari (Experimental - only available via compilation)
## 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](https://discord.gg/YzmbnCDkat). 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. We're open and happy to help out! Happy creating :)
## Getting started ## Getting started
@@ -64,40 +70,18 @@ Don't worry- if you get stuck feel free to ask around in the [discord](https://d
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
``` ```
1. Install dependencies ### Running Development
You may install the dependencies like below: 1. Install dependencies
``` ```
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 # or use your perferred package manager npm run dev
```
### 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
@@ -107,7 +91,27 @@ npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also
- Click `Load unpacked` - Click `Load unpacked`
- Select the `dist` folder - Select the `dist` folder
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. Just remember, in order to update changes to the extension, you need to click the refresh button on the extension in `chrome://extensions` whenever anything's changed.
### Building for production
1. Install dependencies
```
npm install # or your preferred package manager like pnpm or yarn
```
2. Run the build script
```
npm run build
```
3. Package it up (optional)
```
npm run zip # This requires 7-Zip to be installed in order to work
```
## Folder Structure ## Folder Structure
@@ -115,8 +119,6 @@ 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.
- The `dist` folder is where the compiled code ends up, this is the folder what you need to load into chrome as an unpacked extension for development. - The `dist` folder is where the compiled code ends up, this is the folder what you need to load into chrome as an unpacked extension for development.
@@ -128,11 +130,10 @@ The folder structure is as follows:
</a> </a>
Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/CONTRIBUTING.md) Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plus/blob/main/CONTRIBUTING.md)
## Credits ## Credits
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development from lead developers [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) with help from other volunteers This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development by [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph)
## Star History ## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=BetterSEQTA/BetterSEQTA-Plus&type=Date)](https://star-history.com/#BetterSEQTA/BetterSEQTA-Plus&Date) [![Star History Chart](https://api.star-history.com/svg?repos=BetterSEQTA/BetterSEQTA-Plus&type=Date)](https://star-history.com/#sethburkart123/EvenBetterSEQTA&Date)
+5 -5
View File
@@ -4,13 +4,13 @@
Below here is the supported versions of BetterSEQTA+. Anything older than this is not supported and contains bugs. Below here is the supported versions of BetterSEQTA+. Anything older than this is not supported and contains bugs.
| Version | Supported | | Version | Supported |
| ------- | --------- | | ------- | ------------------ |
| 3.4.3 | | | 3.4.0 | :white_check_mark: |
| < 3.4.3 | :x: | | <= 3.3 | :x: |
`*` May not work on other devices. `*` May not work on other devices.
## Reporting a Vulnerability ## Reporting a Vulnerability
If you find vulnerabilities, REPORT IT IMMEDIATELY. open the [advisories tab](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories) on the left and click the green "report a vulnerability" button or use [this quick-link](https://github.com/BetterSEQTA/BetterSEQTA-Plus/security/advisories/new) to create a new report If you find vulnerabilities, REPORT IT IMMEDIATELY. Make an issue and use the template provided for vulnerabilities.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

-52
View File
@@ -1,52 +0,0 @@
# 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).
-268
View File
@@ -1,268 +0,0 @@
# 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.
-182
View File
@@ -1,182 +0,0 @@
# 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)
-297
View File
@@ -1,297 +0,0 @@
# 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,
// Optional: Mark your plugin as beta to show a "Beta" tag in settings
beta: 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. We set `beta: true` to mark the plugin as beta
5. The `run` function is where we put our plugin's code
6. We use `api.seqta.onMount` to wait for the homepage to load
7. We create and style a message element
8. 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
- Use `beta: true` for experimental features to let users know they're trying something new
## Plugin Metadata Options
Your plugin object supports several optional flags to customize how it appears and behaves:
```typescript
const myPlugin: Plugin = {
id: "my-plugin",
name: "My Plugin",
description: "What my plugin does",
version: "1.0.0",
// Optional flags:
disableToggle: true, // Show enable/disable toggle in settings
defaultEnabled: false, // Start disabled by default (requires disableToggle: true)
beta: true, // Show "Beta" tag in settings UI
// Your plugin code...
run: async (api) => { /* ... */ },
};
```
- **`disableToggle`**: When `true`, users can enable/disable your plugin in settings
- **`defaultEnabled`**: When `false`, your plugin starts disabled (only works with `disableToggle: true`)
- **`beta`**: When `true`, shows an orange "Beta" tag next to your plugin name in settings
## 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)
-366
View File
@@ -1,366 +0,0 @@
# 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,
beta: 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;
```
## Plugin Metadata
The plugin object supports several metadata fields and options:
```typescript
interface Plugin {
// Required fields
id: string; // Unique identifier (lowercase, dashes)
name: string; // Display name shown to users
description: string; // Brief description of what the plugin does
version: string; // Semantic version (e.g., "1.0.0")
settings: PluginSettings; // Plugin settings object
run: (api: PluginAPI) => void; // Main plugin function
// Optional fields
styles?: string; // CSS styles to inject
disableToggle?: boolean; // Show enable/disable toggle in settings
defaultEnabled?: boolean; // Start enabled/disabled (requires disableToggle)
beta?: boolean; // Show "Beta" tag in settings UI
}
```
### Metadata Options
- **`disableToggle`**: When `true`, users can enable/disable your plugin in the settings page
- **`defaultEnabled`**: When `false`, your plugin starts disabled by default (only works with `disableToggle: true`)
- **`beta`**: When `true`, displays an orange "Beta" tag next to your plugin name in the settings UI
- **`styles`**: CSS string that gets injected into the page when your plugin runs
## 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
+162
View File
@@ -0,0 +1,162 @@
<!DOCTYPE html>
<html>
<head>
<title>BetterSEQTA Settings</title>
<style>
:root {
--background-primary: #ffffff;
--background-secondary: #e5e7eb;
--text-primary: black;
--theme-primary: #4F46E5;
--theme-hover: #4338CA;
}
@media (prefers-color-scheme: dark) {
:root {
--background-primary: #232323;
--background-secondary: #1a1a1a;
--text-primary: white;
--theme-primary: #6366F1;
--theme-hover: #818CF8;
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
background: var(--background-primary);
color: var(--text-primary);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.container {
width: 100%;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1rem;
}
h1 {
font-size: 1.75rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.subtitle {
color: #666;
font-size: 1rem;
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 500;
color: var(--text-primary);
}
input[type="url"] {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--background-secondary);
border-radius: 8px;
font-size: 1rem;
background: var(--background-secondary);
color: var(--text-primary);
transition: all 0.2s;
}
input[type="url"]:focus {
outline: none;
border-color: var(--theme-primary);
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1);
}
button {
margin-top: 1rem;
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 8px;
background: var(--theme-primary);
color: white;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
background: var(--theme-hover);
}
#error-message {
color: #EF4444;
font-size: 0.875rem;
margin-top: 0.5rem;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>BetterSEQTA Settings</h1>
<div class="subtitle">It's time to get started! To begin type in your school's SEQTA URL below.</div>
<label for="seqtaUrl">SEQTA Website URL</label>
<input type="url" id="seqtaUrl" placeholder="Enter your school's SEQTA URL (e.g https://seqta.school.edu.au)" required>
<div id="error-message"></div>
<button onclick="saveSettings()">Save Settings</button>
</div>
<script>
const electron = window.require('electron');
const { ipcRenderer } = electron;
const Store = window.require('electron-store');
const store = new Store();
// Load saved URL on page load
window.addEventListener('DOMContentLoaded', () => {
const savedUrl = store.get('seqtaUrl') || '';
document.getElementById('seqtaUrl').value = savedUrl;
});
// Handle error messages from main process
ipcRenderer.on('seqta-url-error', (event, message) => {
const errorElement = document.getElementById('error-message');
errorElement.textContent = message;
errorElement.style.display = 'block';
});
// Save settings
function saveSettings() {
const url = document.getElementById('seqtaUrl').value;
const errorElement = document.getElementById('error-message');
errorElement.style.display = 'none';
if (url) {
ipcRenderer.send('set-seqta-url', url);
} else {
errorElement.textContent = 'Please enter a URL';
errorElement.style.display = 'block';
}
}
// Handle enter key
document.getElementById('seqtaUrl').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
saveSettings();
}
});
</script>
</body>
</html>
+333
View File
@@ -0,0 +1,333 @@
import { app, BrowserWindow, ipcMain, session, Menu } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import Store from 'electron-store';
// Fix for __dirname in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const store = new Store();
let mainWindow = null;
let settingsWindow = null;
// CSS to inject
const customCSS = `
#alertBar {
display: none !important;
}
/* Match SEQTA's styling */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif !important;
}
`;
// Create the application menu
function createAppMenu() {
const isMac = process.platform === 'darwin';
const template = [
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideOthers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
}] : []),
{
label: 'Settings',
submenu: [
{
label: 'Configure SEQTA URL',
accelerator: isMac ? 'Cmd+,' : 'Ctrl+,',
click: () => {
createSettingsWindow();
}
}
]
},
{
label: 'View',
submenu: [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
{ role: 'zoomIn' },
{ role: 'zoomOut' },
{ type: 'separator' },
{ role: 'togglefullscreen' }
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
// Validate SEQTA URL
function isValidSeqtaUrl(url) {
try {
const urlObj = new URL(url);
// Only ensure it's a valid HTTPS URL
return urlObj.protocol === 'https:';
} catch {
return false;
}
}
// Get the correct path for the extension based on whether we're in development or production
function getExtensionPath() {
if (app.isPackaged) {
// In production, the extension is in the resources directory
return path.join(process.resourcesPath, 'chrome-extension');
} else {
// In development, the extension is in the dist directory
return path.join(__dirname, '..', 'dist', 'chrome');
}
}
// Load the Chrome extension
async function loadExtension() {
try {
const extensionPath = getExtensionPath();
console.log('Loading extension from:', extensionPath);
await session.defaultSession.loadExtension(extensionPath, {
allowFileAccess: true
});
console.log('Extension loaded successfully!');
} catch (err) {
console.error('Failed to load extension:', err);
}
}
function createMainWindow() {
console.log('🚀 Creating main window...');
if (mainWindow) {
if (!mainWindow.isDestroyed()) {
console.log('✨ Existing window found, focusing it');
mainWindow.focus();
return mainWindow;
}
console.log('🔄 Old window was destroyed, creating new one');
}
console.log('📦 Initializing new BrowserWindow');
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
backgroundThrottling: false,
enableWebSQL: false,
webgl: false,
offscreen: false
},
show: false,
backgroundColor: '#ffffff'
});
const seqtaUrl = store.get('seqtaUrl');
console.log('📍 Stored SEQTA URL:', seqtaUrl);
// Register keyboard shortcut for settings
mainWindow.webContents.on('before-input-event', (event, input) => {
if ((input.meta || input.control) && input.key === ',') {
createSettingsWindow();
}
});
// Inject CSS when the page loads
mainWindow.webContents.on('did-finish-load', () => {
console.log('🎨 Page loaded, injecting CSS');
mainWindow.webContents.insertCSS(customCSS).catch(err => {
console.error('Failed to inject CSS:', err);
});
});
// Only show window when it's ready
mainWindow.once('ready-to-show', () => {
console.log('🎉 Window ready to show!');
mainWindow.show();
mainWindow.focus();
});
if (seqtaUrl) {
if (!isValidSeqtaUrl(seqtaUrl)) {
console.error('❌ Invalid SEQTA URL stored:', seqtaUrl);
createSettingsWindow();
return;
}
console.log('🌐 Loading SEQTA URL:', seqtaUrl);
mainWindow.loadURL(seqtaUrl)
.then(() => {
console.log('✅ Successfully loaded SEQTA URL');
mainWindow.show();
mainWindow.focus();
})
.catch(err => {
console.error('❌ Failed to load SEQTA URL:', err);
createSettingsWindow();
});
} else {
console.log('⚙️ No SEQTA URL found, opening settings');
createSettingsWindow();
}
return mainWindow;
}
function createSettingsWindow() {
if (settingsWindow) {
settingsWindow.focus();
return;
}
settingsWindow = new BrowserWindow({
width: 600,
height: 400,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
},
show: false,
backgroundColor: '#ffffff'
});
const settingsPath = path.join(__dirname, 'index.html');
settingsWindow.loadFile(settingsPath);
settingsWindow.once('ready-to-show', () => {
settingsWindow.show();
settingsWindow.focus();
});
// Only enable DevTools in development
if (process.env.NODE_ENV === 'development') {
settingsWindow.webContents.openDevTools();
}
settingsWindow.on('closed', () => {
settingsWindow = null;
});
}
// Performance optimization: Disable hardware acceleration if running on low-end device
if (process.platform !== 'darwin') { // Skip for macOS
app.disableHardwareAcceleration();
}
// Performance optimization: Disable smooth scrolling
app.commandLine.appendSwitch('disable-smooth-scrolling');
// Wait for app to be ready before creating windows
app.whenReady().then(async () => {
createAppMenu();
await loadExtension();
createMainWindow();
});
// Performance optimization: Quit immediately instead of gracefully
app.on('window-all-closed', () => {
app.quit();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createMainWindow();
}
});
// Format and validate SEQTA URL
function formatAndValidateUrl(url) {
// Remove any whitespace
url = url.trim();
// If no protocol specified, add https://
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
// If it's http://, upgrade to https://
if (url.startsWith('http://')) {
url = 'https://' + url.slice(7);
}
try {
const urlObj = new URL(url);
// Ensure it's https
if (urlObj.protocol !== 'https:') {
throw new Error('URL must use HTTPS');
}
return { isValid: true, url: url };
} catch (error) {
return { isValid: false, url: url, error: error.message };
}
}
// Handle setting the SEQTA URL
ipcMain.on('set-seqta-url', (event, url) => {
console.log('🔧 Received new SEQTA URL:', url);
const { isValid, url: formattedUrl, error } = formatAndValidateUrl(url);
if (!isValid) {
console.error('❌ Invalid URL format:', error);
event.reply('seqta-url-error', 'Please enter a valid URL');
return;
}
console.log('💾 Saving URL to store:', formattedUrl);
store.set('seqtaUrl', formattedUrl);
// Create main window if it doesn't exist
if (!mainWindow || mainWindow.isDestroyed()) {
console.log('🆕 Creating new main window');
createMainWindow();
} else {
console.log('🔄 Loading new URL in existing window:', formattedUrl);
mainWindow.loadURL(formattedUrl).then(() => {
console.log('✅ URL loaded successfully');
console.log('🎨 Injecting CSS and settings button');
mainWindow.webContents.insertCSS(customCSS).catch(err => {
console.error('Failed to inject CSS:', err);
});
mainWindow.webContents.executeJavaScript(`
if (!document.getElementById('bsp-settings-button')) {
document.body.insertAdjacentHTML('beforeend', ${JSON.stringify(settingsButtonHTML)});
document.getElementById('bsp-settings-button').addEventListener('click', () => {
window.postMessage('open-settings', '*');
});
}
`).catch(err => {
console.error('Failed to inject settings button:', err);
});
console.log('👀 Showing and focusing window');
mainWindow.show();
mainWindow.focus();
}).catch(err => {
console.error('❌ Failed to load SEQTA URL:', err);
event.reply('seqta-url-error', 'Failed to load SEQTA. Please check your connection and URL.');
});
}
// Close settings window if it exists
if (settingsWindow && !settingsWindow.isDestroyed()) {
console.log('🚪 Closing settings window');
settingsWindow.close();
}
});
-17
View File
@@ -1,17 +0,0 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: [
'**/__tests__/**/*.ts',
'**/?(*.)+(spec|test).ts'
],
transform: {
'^.+\\.ts$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
};
+2 -2
View File
@@ -7,10 +7,10 @@ export const base64Loader = {
const [filePath, query] = id.split("?"); const [filePath, query] = id.split("?");
if (query !== "base64") return null; if (query !== "base64") return null;
const data = fs.readFileSync(filePath, { encoding: "base64" }); const data = fs.readFileSync(filePath, { encoding: 'base64' });
const mimeType = mime.lookup(filePath); const mimeType = mime.lookup(filePath);
const dataURL = `data:${mimeType};base64,${data}`; const dataURL = `data:${mimeType};base64,${data}`;
return `export default '${dataURL}';`; return `export default '${dataURL}';`;
}, },
}; };
-25
View File
@@ -1,25 +0,0 @@
// ref: https://stackoverflow.com/a/76920975
import type { Plugin } from "vite";
export default function ClosePlugin(): Plugin {
return {
name: "ClosePlugin", // required, will show up in warnings and errors
// use this to catch errors when building
buildEnd(error) {
if (error) {
console.error("Error bundling");
console.error(error);
process.exit(1);
} else {
console.log("Build ended");
}
},
// use this to catch the end of a build without errors
closeBundle() {
console.log("Bundle closed");
process.exit(0);
},
};
}
+5 -5
View File
@@ -1,5 +1,5 @@
import type { Browser, BuildTarget, Manifest } from "./types"; import type { Browser, BuildTarget, Manifest } from './types'
import type { AnyCase } from "./utils"; import type { AnyCase } from './utils'
/** /**
* *
* *
@@ -15,7 +15,7 @@ export function createManifest(
return { return {
manifest, manifest,
browser, browser,
}; }
} }
/** /**
@@ -29,5 +29,5 @@ export function createManifest(
* @return {*} {@link Manifest} * @return {*} {@link Manifest}
*/ */
export function createManifestBase(manifest: Manifest): Manifest { export function createManifestBase(manifest: Manifest): Manifest {
return manifest; return manifest
} }
-37
View File
@@ -1,37 +0,0 @@
// vite-plugin-inline-worker-dev.ts
import { Plugin } from "vite";
import fs from "fs/promises";
import { build, transform } from "esbuild";
export default function InlineWorkerDevPlugin(): Plugin {
return {
name: "vite:inline-worker-dev",
async load(id) {
if (id.includes("?inlineWorker")) {
const [cleanPath] = id.split("?");
console.log("cleanPath", cleanPath);
const code = await fs.readFile(cleanPath, "utf-8");
const result = await build({
entryPoints: [cleanPath],
bundle: true,
write: false,
platform: "browser",
format: "iife",
target: "esnext",
});
const workerCode = result.outputFiles[0].text;
const workerBlobCode = `
const code = ${JSON.stringify(workerCode)};
export default function InlineWorker() {
const blob = new Blob([code], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob), { type: 'module' });
}
`;
return workerBlobCode;
}
return null;
},
};
}
+65
View File
@@ -0,0 +1,65 @@
/*
TEMPORARY FIX FOR CHROME 130+ builds
*/
import path from 'node:path';
import fs from 'fs';
import { PluginOption } from 'vite';
import { ManifestV3Export } from '@crxjs/vite-plugin';
const manifestPath = path.resolve('dist/chrome/manifest.json');
export function updateManifestPlugin(): PluginOption {
return {
name: 'update-manifest-plugin',
enforce: 'post',
closeBundle() {
forceDisableUseDynamicUrl();
},
configureServer(server) {
server.httpServer?.once('listening', () => {
const updated = forceDisableUseDynamicUrl();
if (updated) {
server.ws.send({ type: 'full-reload' });
console.log('** updated **');
}
fs.watchFile(manifestPath, () => {
console.log('** watchFile ** ');
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
if (manifestContents.web_accessible_resources.some((resource: any) => resource.use_dynamic_url)) {
const updated = forceDisableUseDynamicUrl();
if (updated) {
server.ws.send({ type: 'full-reload' });
console.log('** updated **');
}
}
});
});
},
writeBundle() {
console.log('### writeBundle ##');
forceDisableUseDynamicUrl();
},
};
}
function forceDisableUseDynamicUrl() {
if (!fs.existsSync(manifestPath)) {
return false;
}
const manifestContents = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Awaited<ManifestV3Export>;
if (typeof manifestContents === 'function' || !manifestContents.web_accessible_resources) return false;
if (manifestContents.web_accessible_resources.every((resource) => !resource.use_dynamic_url)) return false;
manifestContents.web_accessible_resources.forEach((resource) => {
if (resource.use_dynamic_url) resource.use_dynamic_url = false;
});
fs.writeFileSync(manifestPath, JSON.stringify(manifestContents, null, 2));
return true;
}
+42 -83
View File
@@ -1,63 +1,31 @@
const glob = require("glob"); const glob = require('glob');
const semver = require("semver"); const semver = require('semver');
const { execSync } = require("child_process"); const { execSync } = require('child_process');
const path = require("path"); const path = require('path');
function getLatestVersion(files) { function getLatestVersion(files) {
console.log("Files passed to getLatestVersion:", files); console.log('Files passed to getLatestVersion:', files);
const versions = files.map(file => {
const match = file.match(/@(\d+\.\d+\.\d+)-/);
console.log('Matching file:', file, 'Version found:', match ? match[1] : 'None');
return match ? match[1] : null;
}).filter(Boolean);
const versions = files console.log('Extracted versions:', versions);
.map((file) => { const latestVersion = semver.maxSatisfying(versions, '*');
const match = file.match(/@([\d\.]+)-/); console.log('Latest version:', latestVersion);
console.log(
"Matching file:",
file,
"Version found:",
match ? match[1] : "None",
);
if (!match) return null;
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
const semverVersion = fullVersion.split(".").slice(0, 3).join("."); // Trim to 3.4.5
return { fullVersion, semverVersion };
})
.filter(Boolean);
console.log(
"Extracted versions:",
versions.map((v) => v.semverVersion),
);
// Find latest version using the trimmed semver format
const latestSemver = semver.maxSatisfying(
versions.map((v) => v.semverVersion),
"*",
);
console.log("Latest SemVer-compatible version:", latestSemver);
// Get the full version that matches the latest SemVer version
const latestVersion =
versions.find((v) => v.semverVersion === latestSemver)?.fullVersion || null;
console.log("Final selected latest version:", latestVersion);
return latestVersion; return latestVersion;
} }
function getLatestFiles(browser) { function getLatestFiles(browser) {
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`; const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
console.log("Glob pattern:", pattern); console.log('Glob pattern:', pattern);
const files = glob.sync(pattern); const files = glob.sync(pattern);
console.log("Files found for browser", browser, ":", files); console.log('Files found for browser', browser, ':', files);
const latestVersion = getLatestVersion(files); const latestVersion = getLatestVersion(files);
// Find the exact file by matching the original full version const latestFile = files.find(file => file.includes(latestVersion));
const latestFile = files.find((file) => file.includes(`@${latestVersion}-`)); console.log('Latest file for browser', browser, ':', latestFile);
console.log("Latest file for browser", browser, ":", latestFile);
return latestFile; return latestFile;
} }
@@ -65,53 +33,44 @@ function zipSources() {
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`; const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
const excludePatterns = [ const excludePatterns = [
"node_modules", 'node_modules',
"dist", 'dist',
".env*", '.env*',
".git", '.git',
".github", '.github',
".vscode", '.vscode',
"LICENSE", 'LICENSE',
"package.json", 'package.json'
] ].map(pattern => `-x!${pattern}`).join(' ');
.map((pattern) => `-x!${pattern}`)
.join(" ");
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`; const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
console.log("Zipping project sources with command:", zipCommand); console.log('Zipping project sources with command:', zipCommand);
execSync(zipCommand, { stdio: "inherit" }); execSync(zipCommand, { stdio: 'inherit' });
return zipFileName; return zipFileName;
} }
function runPublishCommand(browsers) { function runPublishCommand(browsers) {
const chromeZip = browsers.includes("chrome") const chromeZip = browsers.includes('chrome') ? getLatestFiles('chrome') : null;
? getLatestFiles("chrome") const firefoxZip = browsers.includes('firefox') ? getLatestFiles('firefox') : null;
: null; const firefoxSourcesZip = browsers.includes('firefox') ? zipSources() : null;
const firefoxZip = browsers.includes("firefox")
? getLatestFiles("firefox")
: null;
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
console.log("Chrome zip:", chromeZip); console.log('Chrome zip:', chromeZip);
console.log("Firefox zip:", firefoxZip); console.log('Firefox zip:', firefoxZip);
console.log("Firefox sources zip:", firefoxSourcesZip); console.log('Firefox sources zip:', firefoxSourcesZip);
if (browsers.length === 0) { if (browsers.length === 0) {
console.log("No browsers specified. Exiting."); console.log('No browsers specified. Exiting.');
process.exit(0); process.exit(0);
} }
if ( if ((browsers.includes('chrome') && !chromeZip) || (browsers.includes('firefox') && (!firefoxZip || !firefoxSourcesZip))) {
(browsers.includes("chrome") && !chromeZip) || console.error('Could not find required zip files for specified browsers.');
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
) {
console.error("Could not find required zip files for specified browsers.");
process.exit(1); process.exit(1);
} }
let command = "publish-extension"; let command = 'publish-extension';
if (chromeZip) { if (chromeZip) {
command += ` --chrome-zip ${chromeZip}`; command += ` --chrome-zip ${chromeZip}`;
} }
@@ -119,13 +78,13 @@ function runPublishCommand(browsers) {
command += ` --firefox-zip ${firefoxZip} --firefox-sources-zip ${firefoxSourcesZip}`; command += ` --firefox-zip ${firefoxZip} --firefox-sources-zip ${firefoxSourcesZip}`;
} }
console.log("Running command:", command); console.log('Running command:', command);
execSync(command, { stdio: "inherit" }); execSync(command, { stdio: 'inherit' });
} }
// Parse command-line arguments // Parse command-line arguments
const args = process.argv.slice(2); const args = process.argv.slice(2);
const browserIndex = args.indexOf("--b"); const browserIndex = args.indexOf('--b');
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : []; const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
runPublishCommand(browsers); runPublishCommand(browsers);
-17
View File
@@ -1,17 +0,0 @@
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());
}
});
},
};
}
+67 -67
View File
@@ -1,104 +1,104 @@
import type { ManifestV3Export } from "@crxjs/vite-plugin"; import type { ManifestV3Export } from '@crxjs/vite-plugin'
import { type AnyCase, createEnum } from "./utils"; import { type AnyCase, createEnum } from './utils'
export const FrameworkEnum = { export const FrameworkEnum = {
React: "React", React: 'React',
Vanilla: "Vanilla", Vanilla: 'Vanilla',
Preact: "Preact", Preact: 'Preact',
Lit: "Lit", Lit: 'Lit',
Svelte: "Svelte", Svelte: 'Svelte',
Vue: "Vue", Vue: 'Vue',
} as const; } as const
export const BrowserEnum = { export const BrowserEnum = {
Chrome: "Chrome", Chrome: 'Chrome',
Brave: "Brave", Brave: 'Brave',
Opera: "Opera", Opera: 'Opera',
Edge: "Edge", Edge: 'Edge',
Firefox: "Firefox", Firefox: 'Firefox',
Safari: "Safari", Safari: 'Safari',
} as const; } as const
const LanguageEnum = { const LanguageEnum = {
TypeScript: "TypeScript", TypeScript: 'TypeScript',
JavaScript: "JavaScript", JavaScript: 'JavaScript',
} as const; } as const
export const StyleEnum = { export const StyleEnum = {
Tailwind: "Tailwind", Tailwind: 'Tailwind',
} as const; } as const
export const PackageManagerEnum = { export const PackageManagerEnum = {
Bun: "Bun", Bun: 'Bun',
PnPm: "PnPm", PnPm: 'PnPm',
Npm: "Npm", Npm: 'Npm',
Yarn: "Yarn", Yarn: 'Yarn',
} as const; } as const
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts // see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
export type BrowserSpecificSettings = { export type BrowserSpecificSettings = {
browser_specific_settings?: { browser_specific_settings?: {
gecko?: { gecko?: {
id: string; id: string
strict_min_version?: string; strict_min_version?: string
strict_max_version?: string; strict_max_version?: string
}; }
}; }
}; }
export type Manifest = ManifestV3Export; export type Manifest = ManifestV3Export
export type ManifestIcons = chrome.runtime.ManifestIcons; export type ManifestIcons = chrome.runtime.ManifestIcons
export type ManifestBackground = chrome.runtime.ManifestV3["background"]; export type ManifestBackground = chrome.runtime.ManifestV3['background']
export type ManifestContentScripts = export type ManifestContentScripts =
chrome.runtime.ManifestV3["content_scripts"]; chrome.runtime.ManifestV3['content_scripts']
export type ManifestWebAccessibleResources = export type ManifestWebAccessibleResources =
chrome.runtime.ManifestV3["web_accessible_resources"]; chrome.runtime.ManifestV3['web_accessible_resources']
export type ManifestCommands = chrome.runtime.ManifestV3["commands"]; export type ManifestCommands = chrome.runtime.ManifestV3['commands']
export type ManifestAction = chrome.runtime.ManifestV3["action"]; export type ManifestAction = chrome.runtime.ManifestV3['action']
export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"]; export type ManifestPermissions = chrome.runtime.ManifestV3['permissions']
export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"]; export type ManifestOptionsUI = chrome.runtime.ManifestV3['options_ui']
export type ManifestURLOverrides = export type ManifestURLOverrides =
chrome.runtime.ManifestV3["chrome_url_overrides"]; chrome.runtime.ManifestV3['chrome_url_overrides']
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>; export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>
export type BrowserEnumType<T extends string> = { export type BrowserEnumType<T extends string> = {
[browser in BrowserName<T>]: BrowserName<T>; [browser in BrowserName<T>]: BrowserName<T>
}; }
export type BuildMode = AnyCase<Browser>; export type BuildMode = AnyCase<Browser>
export type BuildTarget = { export type BuildTarget = {
manifest: Manifest; manifest: Manifest
browser: AnyCase<Browser>; browser: AnyCase<Browser>
}; }
export type BuildConfig = { export type BuildConfig = {
command?: "build" | "serve"; command?: 'build' | 'serve'
mode?: AnyCase<Browser> | string | undefined; mode?: AnyCase<Browser> | string | undefined
}; }
export interface Repository { export interface Repository {
type: string; type: string
url?: string; url?: string
bugs?: Bugs; bugs?: Bugs
} }
export interface Bugs { export interface Bugs {
url?: string; url?: string
email?: string; email?: string
} }
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum]; export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum]
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum); export const Browser: AnyCase<Browser> = createEnum(BrowserEnum)
export type PackageManager = export type PackageManager =
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum]; (typeof PackageManagerEnum)[keyof typeof PackageManagerEnum]
export const PackageManager: AnyCase<PackageManager> = export const PackageManager: AnyCase<PackageManager> =
createEnum(PackageManagerEnum); createEnum(PackageManagerEnum)
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum]; export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum]
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum); export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum)
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum]; export type Style = (typeof StyleEnum)[keyof typeof StyleEnum]
export const Style: AnyCase<Style> = createEnum(StyleEnum); export const Style: AnyCase<Style> = createEnum(StyleEnum)
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum]; export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum]
export const Language: AnyCase<Language> = createEnum(LanguageEnum); export const Language: AnyCase<Language> = createEnum(LanguageEnum)
+6 -6
View File
@@ -1,21 +1,21 @@
export type ObjectValues<T> = T[keyof T]; export type ObjectValues<T> = T[keyof T]
export function createEnum<T extends Record<string, string>>(enumObj: T) { export function createEnum<T extends Record<string, string>>(enumObj: T) {
return Object.values(enumObj) as unknown as ObjectValues<T>; return Object.values(enumObj) as unknown as ObjectValues<T>
} }
export type AnyCase<T extends string> = export type AnyCase<T extends string> =
| Uppercase<T> | Uppercase<T>
| Lowercase<T> | Lowercase<T>
| Capitalize<T> | Capitalize<T>
| Uncapitalize<T>; | Uncapitalize<T>
export type AnyCaseLanguage<T extends string, K extends string> = export type AnyCaseLanguage<T extends string, K extends string> =
| Uppercase<T | K> | Uppercase<T | K>
| Lowercase<T | K> | Lowercase<T | K>
| Capitalize<T | K> | Capitalize<T | K>
| Uncapitalize<T | K>; | Uncapitalize<T | K>
export type OptionalKeys<T> = { export type OptionalKeys<T> = {
[K in keyof T as undefined extends T[K] ? K : never]: T[K]; [K in keyof T as undefined extends T[K] ? K : never]: T[K]
}; }
+102 -64
View File
@@ -1,8 +1,9 @@
{ {
"name": "betterseqtaplus", "name": "betterseqtaplus",
"version": "3.4.6.1", "version": "3.4.2",
"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, while incorporating a plethora of new and improved features!",
"main": "electron/main.js",
"browserslist": "> 0.5%, last 2 versions, not dead", "browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": { "scripts": {
"dev": "cross-env MODE=chrome vite dev", "dev": "cross-env MODE=chrome vite dev",
@@ -11,15 +12,14 @@
"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",
"test": "jest", "electron-dev": "electron .",
"test:watch": "jest --watch", "electron-build": "electron-builder",
"test:coverage": "jest --coverage" "electron-pack": "npm run build:chrome && electron-builder --dir",
"electron-dist": "npm run build:chrome && electron-builder"
}, },
"targets": { "targets": {
"prod": { "prod": {
@@ -36,77 +36,115 @@
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-runtime": "^7.26.9", "@babel/plugin-transform-runtime": "^7.25.9",
"@babel/runtime": "^7.26.9", "@babel/runtime": "^7.26.0",
"@bedframe/cli": "^0.0.91", "@crxjs/vite-plugin": "2.0.0-beta.25",
"@crxjs/vite-plugin": "2.0.0-beta.32",
"@types/jest": "^29.5.14",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"@types/react": "^19.0.10", "@vitejs/plugin-react-swc": "^3.7.0",
"@types/react-dom": "^19.0.4",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dependency-cruiser": "^16.10.0", "electron": "^33.2.1",
"eslint": "9.22.0", "electron-builder": "^25.1.8",
"glob": "^11.0.1", "eslint": "^8.57.0",
"jest": "^29.7.0", "glob": "^11.0.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"prettier": "^3.5.3", "prettier": "^3.3.3",
"process": "^0.11.10", "process": "^0.11.10",
"publish-browser-extension": "^3.0.0", "sass": "^1.78.0",
"sass": "^1.85.1", "sass-loader": "^13.3.3",
"sass-loader": "^16.0.5", "semver": "^7.6.3",
"semver": "^7.7.1",
"tailwindcss": "3",
"ts-jest": "^29.3.4",
"url": "^0.11.4" "url": "^0.11.4"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.18.6", "@bedframe/cli": "^0.0.85",
"@codemirror/commands": "^6.8.0", "@codemirror/lang-css": "^6.3.0",
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-less": "^6.0.2",
"@codemirror/language": "^6.10.8", "@codemirror/theme-one-dark": "^6.1.2",
"@codemirror/search": "^6.5.10", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@codemirror/state": "^6.5.2", "@tailwindcss/forms": "^0.5.9",
"@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.308", "@types/chrome": "^0.0.270",
"@types/color": "^4.2.0", "@types/color": "^3.0.6",
"@types/lodash": "^4.17.16", "@types/dompurify": "^3.0.5",
"@types/node": "^22.13.10", "@types/lodash": "^4.17.7",
"@types/node": "^20.16.5",
"@types/react": "17",
"@types/react-dom": "17",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@types/uuid": "^10.0.0", "@types/uuid": "^9.0.8",
"@types/webextension-polyfill": "^0.12.3", "@types/webextension-polyfill": "^0.10.7",
"@uiw/codemirror-extensions-color": "^4.23.10", "@uiw/codemirror-extensions-color": "^4.23.3",
"@uiw/codemirror-theme-github": "^4.23.10", "@uiw/codemirror-theme-github": "^4.23.3",
"autoprefixer": "^10.4.21", "@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"caniuse-lite": "^1.0.30001684",
"classnames": "^2.5.1",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"color": "^5.0.0", "color": "^4.2.3",
"dompurify": "^3.2.4", "dompurify": "^3.1.6",
"embeddia": "^1.2.1", "electron-store": "^10.0.0",
"embla-carousel-autoplay": "^8.5.2", "embla-carousel-autoplay": "^8.3.1",
"embla-carousel-svelte": "^8.5.2", "embla-carousel-svelte": "^8.3.1",
"esbuild": "^0.25.3", "fuse.js": "^7.0.0",
"events": "^3.3.0", "idb": "^8.0.0",
"flexsearch": "^0.8.147", "kolorist": "^1.8.0",
"fuse.js": "^7.1.0",
"idb": "^8.0.2",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mathjs": "^14.4.0",
"million": "^3.1.11", "million": "^3.1.11",
"motion": "^12.4.12", "motion": "^11.12.0",
"postcss": "^8.5.3", "postcss": "^8.4.45",
"publish-browser-extension": "^2.2.1",
"react": "17", "react": "17",
"react-best-gradient-color-picker": "3.0.11", "react-best-gradient-color-picker": "^3.0.10",
"react-dom": "17", "react-dom": "17",
"rss-parser": "^3.13.0", "sortablejs": "^1.15.3",
"sortablejs": "^1.15.6", "svelte": "^5.1.9",
"svelte": "^5.22.6", "tailwindcss": "^3.4.11",
"typescript": "^5.8.2", "typescript": "^5.6.2",
"uuid": "^11.1.0", "uuid": "^9.0.1",
"vite": "^6.2.1", "vite": "^5.4.4",
"webextension-polyfill": "^0.12.0" "webextension-polyfill": "^0.10.0"
},
"build": {
"appId": "com.betterseqta.app",
"productName": "BetterSEQTA",
"directories": {
"output": "electron-dist",
"buildResources": "build"
},
"files": [
"dist/**/*",
"electron/**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
"!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}",
"!**/node_modules/*.d.ts",
"!**/node_modules/.bin",
"!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}",
"!.editorconfig",
"!**/._*",
"!**/{.DS_Store,.git,.hg,.svn,CVS,RCS,SCCS,.gitignore,.gitattributes}",
"!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}",
"!**/{appveyor.yml,.travis.yml,circle.yml}",
"!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}"
],
"extraResources": [
{
"from": "dist/chrome",
"to": "chrome-extension",
"filter": ["**/*"]
}
],
"mac": {
"category": "public.app-category.education",
"target": ["dmg", "zip"],
"icon": "build/icon.icns"
},
"win": {
"target": "nsis",
"icon": "build/icon.ico"
},
"linux": {
"target": "AppImage",
"icon": "build/icon.png"
}
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
module.exports = { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
}; }
-126
View File
@@ -1,126 +0,0 @@
--- 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
+2779 -48
View File
File diff suppressed because it is too large Load Diff
+203 -123
View File
@@ -1,68 +1,138 @@
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({})
function open(tabs: any) { function open (tabs: any) {
for (let tab of tabs) { for (let tab of tabs) {
if (tab.title.includes("SEQTA Learn")) { if (tab.title.includes('SEQTA Learn')) {
browser.tabs.reload(tab.id); browser.tabs.reload(tab.id);
} }
} }
} }
result.then(open, console.error); result.then(open, console.error)
} }
// @ts-ignore // Main message listener
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener((request: any, _sender: any, sendResponse: any) => {
(request: any, _: any, sendResponse: (response?: any) => void) => { switch (request.type) {
switch (request.type) { case 'reloadTabs':
case "reloadTabs": reloadSeqtaPages();
reloadSeqtaPages(); break;
break;
case 'extensionPages':
browser.tabs.query({}).then(function (tabs) {
for (let tab of tabs) {
if (tab.url?.includes('chrome-extension://')) {
browser.tabs.sendMessage(tab.id!, request);
}
}
});
break;
case 'currentTab':
browser.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
browser.tabs.sendMessage(tabs[0].id!, request).then(function (response) {
sendResponse(response);
});
});
return true;
case 'githubTab':
browser.tabs.create({ url: 'github.com/BetterSEQTA/BetterSEQTA-Plus' });
break;
case 'setDefaultStorage':
SetStorageValue(DefaultValues);
break;
case "extensionPages": case 'sendNews':
browser.tabs.query({}).then(function (tabs) { const date = new Date();
for (let tab of tabs) {
if (tab.url?.includes("chrome-extension://")) {
browser.tabs.sendMessage(tab.id!, request);
}
}
});
break;
case "currentTab": const from =
browser.tabs date.getFullYear() +
.query({ active: true, currentWindow: true }) '-' +
.then(function (tabs) { (date.getMonth() + 1) +
browser.tabs '-' +
.sendMessage(tabs[0].id!, request) (date.getDate() - 5);
.then(function (response) {
sendResponse(response);
});
});
return true;
case "githubTab": const url = `https://newsapi.org/v2/everything?domains=abc.net.au&from=${from}&apiKey=17c0da766ba347c89d094449504e3080`;
browser.tabs.create({ url: "github.com/BetterSEQTA/BetterSEQTA-Plus" });
break;
case "setDefaultStorage": GetNews(sendResponse, url);
SetStorageValue(DefaultValues); return true;
break;
default:
console.log('Unknown request type');
}
});
case "sendNews": function GetNews(sendResponse: any, url: string) {
fetchNews(request.source ?? "australia", sendResponse); fetch(url)
return true; .then((result) => result.json())
.then((response) => {
default: if (response.code == 'rateLimited') {
console.log("Unknown request type"); GetNews(sendResponse, url += '%00');
} } else {
sendResponse({ news: response });
return false; }
}, });
); }
const DefaultValues: SettingsState = { const DefaultValues: SettingsState = {
onoff: true, onoff: true,
@@ -70,6 +140,7 @@ 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 },
@@ -91,31 +162,64 @@ const DefaultValues: SettingsState = {
}, },
menuorder: [], menuorder: [],
subjectfilters: {}, subjectfilters: {},
selectedTheme: "", selectedTheme: '',
selectedColor: selectedColor: 'linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)',
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)", originalSelectedColor: '',
originalSelectedColor: "",
DarkMode: true, DarkMode: true,
animations: true, animations: true,
assessmentsAverage: true, assessmentsAverage: true,
defaultPage: "home", defaultPage: 'home',
shortcuts: [ shortcuts: [
{ {
name: "Outlook", name: 'YouTube',
enabled: false,
},
{
name: 'Outlook',
enabled: true, enabled: true,
}, },
{ {
name: "Office", name: 'Office',
enabled: true, enabled: true,
}, },
{ {
name: "Google", name: 'Spotify',
enabled: false,
},
{
name: 'Google',
enabled: true, enabled: true,
}, },
{
name: 'DuckDuckGo',
enabled: false,
},
{
name: 'Cool Math Games',
enabled: false,
},
{
name: 'SACE',
enabled: false,
},
{
name: 'Google Scholar',
enabled: false,
},
{
name: 'Gmail',
enabled: false,
},
{
name: 'Netflix',
enabled: false,
},
{
name: 'Education Perfect',
enabled: false,
},
], ],
customshortcuts: [], customshortcuts: [],
lettergrade: false,
newsSource: "australia",
}; };
function SetStorageValue(object: any) { function SetStorageValue(object: any) {
@@ -124,78 +228,54 @@ function SetStorageValue(object: any) {
} }
} }
function convertBksliderToSpeed(bksliderinput: number): number { async function UpdateCurrentValues() {
const minBase = 50; try {
const maxBase = 150; const items = await browser.storage.local.get();
const CurrentValues = items;
const scaledValue = const NewValue = Object.assign({}, DefaultValues, CurrentValues);
2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
const baseSpeed = 3;
const speed = baseSpeed / scaledValue; function CheckInnerElement(element: any) {
return speed; for (let i in element) {
} if (typeof element[i] === 'object') {
// @ts-expect-error
if (!Array.isArray(DefaultValues[i])) {
// @ts-expect-error
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
} else {
// @ts-expect-error
const length = DefaultValues[i].length;
// @ts-expect-error
NewValue[i] = Object.assign({}, DefaultValues[i], CurrentValues[i]);
let NewArray = [];
for (let j = 0; j < length; j++) {
NewArray.push(NewValue[i][j]);
}
NewValue[i] = NewArray;
}
}
}
}
async function migrateLegacySettings() { CheckInnerElement(DefaultValues);
const storage = (await browser.storage.local.get(
null,
)) as unknown as SettingsState;
// Animated Background Migration if (items['customshortcuts']) {
if ("animatedbk" in storage || "bksliderinput" in storage) { NewValue['customshortcuts'] = items['customshortcuts'];
const animatedSettings = { }
enabled: storage.animatedbk ?? true,
speed: storage.bksliderinput SetStorageValue(NewValue);
? convertBksliderToSpeed(parseFloat(storage.bksliderinput)) console.log('[BetterSEQTA+] Values updated successfully');
: 1, } catch (error) {
}; console.error('[BetterSEQTA+] Error updating values:', error);
await browser.storage.local.set({
"plugin.animated-background.settings": animatedSettings,
});
} }
// Assessments Average Migration
if ("assessmentsAverage" in storage || "lettergrade" in storage) {
const assessmentsSettings = {
enabled: storage.assessmentsAverage ?? true,
lettergrade: storage.lettergrade ?? false,
};
await browser.storage.local.set({
"plugin.assessments-average.settings": assessmentsSettings,
});
}
if ("selectedTheme" in storage) {
const themesSettings = { enabled: true };
await browser.storage.local.set({
"plugin.themes.settings": themesSettings,
});
}
if (storage.notificationCollector !== false) {
await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: true },
});
} else {
await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: false },
});
}
const keysToRemove = [
"animatedbk",
"bksliderinput",
"assessmentsAverage",
"lettergrade",
];
await browser.storage.local.remove(keysToRemove);
} }
browser.runtime.onInstalled.addListener(function (event) { browser.runtime.onInstalled.addListener(function (event) {
browser.storage.local.remove(["justupdated"]); browser.storage.local.remove(['justupdated']);
browser.storage.local.remove(["data"]); browser.storage.local.remove(['data']);
if (event.reason == "install" || event.reason == "update") { UpdateCurrentValues();
if ( event.reason == 'install', event.reason == 'update' ) {
browser.storage.local.set({ justupdated: true }); browser.storage.local.set({ justupdated: true });
migrateLegacySettings();
} }
}); });
-112
View File
@@ -1,112 +0,0 @@
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://news.ltn.com.tw/rss/all.xml",
"https://www.taipeitimes.com/xml/index.rss",
"https://international.thenewslens.com/rss",
],
hong_kong: [
"https://rthk9.rthk.hk/rthk/news/rss/e_expressnews_elocal.xml",
"https://www.scmp.com/rss/91/feed",
],
panama: [
"https://critica.com.pa/rss.xml",
"https://www.panamaamerica.com.pa/rss.xml",
"https://noticiassin.com/feed/",
"https://elcapitalfinanciero.com/feed/",
],
canada: [
"https://www.cbc.ca/cmlink/rss-topstories",
"https://calgaryherald.com/feed",
"https://ottawacitizen.com/feed",
"https://www.montrealgazette.com/feed",
],
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://www3.nhk.or.jp/nhkworld/en/news/feeds/",
"https://news.livedoor.com/topics/rss/int.xml",
],
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
};
export async function fetchNews(source: string, sendResponse: any) {
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;
}
const parser = new Parser();
let feeds: string[];
console.log("fetchNews", source);
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 } });
}
+3 -5
View File
@@ -15,7 +15,7 @@
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>. * along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
*/ */
@use "injected/popup.scss"; @use 'injected/popup.scss';
html { html {
background: #161616 !important; background: #161616 !important;
@@ -77,9 +77,7 @@ html {
transform-origin: top; transform-origin: top;
transition: transform 0.2s; transition: transform 0.2s;
} }
body:has(.outside-container:not(.hide)) body:has(.outside-container:not(.hide)) #AddedSettings.tooltip:hover > .tooltiptext {
#AddedSettings.tooltip:hover
> .tooltiptext {
transform: scale(0); transform: scale(0);
} }
.assessmenttooltip svg { .assessmenttooltip svg {
@@ -93,4 +91,4 @@ body:has(.outside-container:not(.hide))
top: 115% !important; top: 115% !important;
background: var(--text-primary) !important; background: var(--text-primary) !important;
color: var(--theme-primary) !important; color: var(--theme-primary) !important;
} }
+1 -1
View File
@@ -1 +1 @@
import "./documentload.scss"; import './documentload.scss';
+2 -11
View File
@@ -15,7 +15,7 @@
* along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>. * along with EvenBetterSEQTA. If not, see <https://www.gnu.org/licenses/>.
*/ */
body { body {
background: transparent; background: transparent;
} }
@@ -25,9 +25,7 @@ body {
span, span,
body { body {
color: white !important; color: white !important;
text-shadow: text-shadow: 1px 1px 2px #161616, 0 0 1em #161616;
1px 1px 2px #161616,
0 0 1em #161616;
} }
body { body {
@@ -113,11 +111,4 @@ body {
text-decoration: underline; text-decoration: underline;
transition: text-shadow 0.5s; transition: text-shadow 0.5s;
} }
}
.cke_panel_listItem > a {
&:hover {
background: #3d3d3e !important;
}
} }
+208 -619
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -36,7 +36,5 @@
transform-origin: 70% 0; transform-origin: 70% 0;
will-change: opacity, transform; will-change: opacity, transform;
transform: translateZ(0); // promotes GPU rendering transform: translateZ(0); // promotes GPU rendering
transition: transition: opacity 0.05s, transform 0.05s;
opacity 0.05s, }
transform 0.05s;
}
+2 -2
View File
@@ -25,7 +25,7 @@
padding-top: 2px; padding-top: 2px;
} }
.sub:has(ul > li.hasChildren.active) > .nav > .back { .sub:has(ul>li.hasChildren.active) > .nav > .back {
display: none !important; display: none !important;
} }
@@ -40,4 +40,4 @@
#menu > ul:has(li.hasChildren.active) > li.active { #menu > ul:has(li.hasChildren.active) > li.active {
background: transparent !important; background: transparent !important;
} }
+1 -1
View File
@@ -12,4 +12,4 @@ html:not(.dark) {
--theme-primary: #ffffff; --theme-primary: #ffffff;
--theme-secondary: #e5e7eb; --theme-secondary: #e5e7eb;
--text-primary: black; --text-primary: black;
} }
+11 -11
View File
@@ -8,9 +8,10 @@ html.transparencyEffects:not(.dark) {
--background-secondary: rgba(229, 231, 235, 0.6); --background-secondary: rgba(229, 231, 235, 0.6);
} }
html.transparencyEffects { html.transparencyEffects {
/* Background Fixes */ /* Background Fixes */
[class*="notifications__item___"], .notifications__item___2ErJN,
#shortcuts { #shortcuts {
backdrop-filter: unset !important; backdrop-filter: unset !important;
} }
@@ -23,31 +24,30 @@ html.transparencyEffects {
/* Blurs */ /* Blurs */
.draggable, .draggable,
.notice, .notice,
[class*="BasicPanel__BasicPanel___"], .BasicPanel__BasicPanel___1GP6s,
.message.addMessage, .message.addMessage,
.singleSelect, .singleSelect,
.uiFileHandlerPanel, .uiFileHandlerPanel,
[class*="Module__wrapper___"], .Module__wrapper___2sbOo,
[class*="notifications__list___"], .notifications__list___rp2L2,
.thread, .thread,
.calendar, .calendar,
.navigator, .navigator,
#title, #title,
[class*="LabelList__selected___"], .LabelList__selected___3Egk7,
.buttonChecklist, .buttonChecklist,
.pane, .pane,
.legacy-root button, .legacy-root button, .legacy-root a,
.legacy-root a, .MessageList__MessageList___3DxoC {
[class*="MessageList__MessageList___"] {
backdrop-filter: blur(80px); backdrop-filter: blur(80px);
} }
.report { .report {
backdrop-filter: blur(10px) !important; backdrop-filter: blur(10px) !important;
} }
.whatsnewContainer, .whatsnewContainer,
[class*="Message__Message___"] { .Message__Message___3oJaU {
backdrop-filter: blur(50px); backdrop-filter: blur(50px);
} }
@@ -71,4 +71,4 @@ html.transparencyEffects {
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
} }
} }
} }
+6 -12
View File
@@ -1,14 +1,8 @@
declare module "*.mp4"; declare module '*.mp4';
declare module "*.woff"; declare module '*.woff';
declare module "*.scss"; declare module '*.scss';
declare module "*.png"; declare module '*.png';
declare module "*.html"; declare module '*.html';
declare module "*.svelte";
declare module "*?inlineWorker" {
const value: () => Worker;
export default value;
}
declare module "*.png?base64" { declare module "*.png?base64" {
const value: string; const value: string;
@@ -33,4 +27,4 @@ declare module "*.gif?base64" {
declare module "*.svg?base64" { declare module "*.svg?base64" {
const value: string; const value: string;
export default value; export default value;
} }
+1 -1
View File
@@ -2,6 +2,6 @@
let { onClick, text } = $props<{ onClick: () => void, text: string, [key: string]: any }>(); let { onClick, text } = $props<{ onClick: () => void, text: string, [key: string]: any }>();
</script> </script>
<button onclick={onClick} class='px-5 py-1.5 text-[0.75rem] shadow-2xl border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 dark:text-white rounded-lg'> <button onclick={onClick} class='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>
{text} {text}
</button> </button>
+2 -2
View File
@@ -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, className } = $props<{value: string, onChange: (value: string) => void, className?: string}>() let { value, onChange } = $props<{value: string, onChange: (value: string) => void}>()
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 ${className}`} bind:this={editor}></div> <div class="rounded-lg text-[13px] overflow-clip w-full bg-white dark:bg-zinc-900" bind:this={editor}></div>
@@ -8,13 +8,6 @@ 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,
+3 -3
View File
@@ -81,20 +81,20 @@
</script> </script>
{#if standalone} {#if standalone}
<div class="h-auto overflow-clip rounded-xl"> <div class="h-auto rounded-xl overflow-clip">
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} /> <ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
</div> </div>
{:else} {:else}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
bind:this={background} bind:this={background}
class="flex absolute top-0 left-0 z-50 justify-center items-center w-full h-full shadow-2xl cursor-pointer bg-black/20 border border-[#DDDDDD]/30 dark:border-[#38373D]/30" class="absolute top-0 left-0 z-50 flex items-center justify-center w-full h-full cursor-pointer bg-black/20"
onclick={handleBackgroundClick} onclick={handleBackgroundClick}
onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }} onkeydown={(e) => { e.key === 'Enter' && handleBackgroundClick }}
> >
<div <div
bind:this={content} bind:this={content}
class="p-4 h-auto bg-white rounded-xl border shadow-lg cursor-auto dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700" class="h-auto p-4 bg-white border shadow-lg cursor-auto rounded-xl dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
> >
<ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} /> <ReactAdapter customOnChange={customOnChange} customState={customState} savePresets={savePresets} el={ColourPicker} />
</div> </div>
+31 -46
View File
@@ -1,6 +1,6 @@
import ColorPicker from "react-best-gradient-color-picker"; import ColorPicker from "react-best-gradient-color-picker"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react"
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"; import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
const defaultPresets = [ const defaultPresets = [
"linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)", "linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)",
@@ -22,12 +22,12 @@ const defaultPresets = [
"rgba(30, 64, 175, 0.89)", "rgba(30, 64, 175, 0.89)",
"rgba(134, 25, 143, 1)", "rgba(134, 25, 143, 1)",
"rgba(14, 165, 233, 0.9)", "rgba(14, 165, 233, 0.9)",
]; ]
interface PickerProps { interface PickerProps {
customOnChange?: (color: string) => void; customOnChange?: (color: string) => void
customState?: string; customState?: string
savePresets?: boolean; savePresets?: boolean
} }
export default function Picker({ export default function Picker({
@@ -35,49 +35,37 @@ export default function Picker({
customState, customState,
savePresets = true, savePresets = true,
}: PickerProps) { }: PickerProps) {
const [customThemeColor, setCustomThemeColor] = useState<string | null>(); const [customThemeColor, setCustomThemeColor] = useState<string | null>()
const [presets, setPresets] = useState<string[]>(); const [presets, setPresets] = useState<string[]>()
const latestValuesRef = useRef({ const latestValuesRef = useRef({ customThemeColor, customOnChange, savePresets, presets });
customThemeColor,
customOnChange,
savePresets,
presets,
});
useEffect(() => { useEffect(() => {
if (customState !== undefined && customState !== null) { if (customState !== undefined && customState !== null) {
setCustomThemeColor(customState); setCustomThemeColor(customState)
} else { } else {
setCustomThemeColor(settingsState.selectedColor ?? null); setCustomThemeColor(settingsState.selectedColor ?? null)
} }
if (presets === undefined) { if (presets === undefined) {
const savedPresets = localStorage.getItem("colorPickerPresets"); const savedPresets = localStorage.getItem("colorPickerPresets")
setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets); setPresets(savedPresets ? JSON.parse(savedPresets) : defaultPresets)
} }
}, []); }, [])
useEffect(() => { useEffect(() => {
latestValuesRef.current = { latestValuesRef.current = { customThemeColor, customOnChange, savePresets, presets };
customThemeColor,
customOnChange,
savePresets,
presets,
};
}, [customThemeColor, customOnChange, savePresets, presets]); }, [customThemeColor, customOnChange, savePresets, presets]);
useEffect(() => { useEffect(() => {
return () => { return () => {
const { customThemeColor, customOnChange, savePresets, presets } = const { customThemeColor, customOnChange, savePresets, presets } = latestValuesRef.current;
latestValuesRef.current; if (!(customThemeColor && !customOnChange && savePresets && presets)) return;
if (!(customThemeColor && !customOnChange && savePresets && presets))
return;
// Only proceed if presets are different (avoid unnecessary updates) // Only proceed if presets are different (avoid unnecessary updates)
const existingIndex = presets.indexOf(customThemeColor); const existingIndex = presets.indexOf(customThemeColor);
let updatedPresets; let updatedPresets;
if (existingIndex === 0) { if (existingIndex === 0) {
// No need to update if the selected color is already the first element // No need to update if the selected color is already the first element
return; return;
@@ -90,34 +78,31 @@ export default function Picker({
} else { } else {
updatedPresets = [customThemeColor, ...presets].slice(0, 18); updatedPresets = [customThemeColor, ...presets].slice(0, 18);
} }
localStorage.setItem( localStorage.setItem("colorPickerPresets", JSON.stringify(updatedPresets));
"colorPickerPresets", }
JSON.stringify(updatedPresets), }, [])
);
};
}, []);
useEffect(() => { useEffect(() => {
if (customThemeColor && !customOnChange) { if (customThemeColor && !customOnChange) {
settingsState.selectedColor = customThemeColor; settingsState.selectedColor = customThemeColor
} }
}, [customThemeColor, customOnChange]); }, [customThemeColor, customOnChange])
return ( return (
<ColorPicker <ColorPicker
disableDarkMode={true} disableDarkMode={true}
presets={presets} presets={presets}
hideInputs={customOnChange ? false : true} hideInputs={true}
value={customThemeColor ?? ""} value={customThemeColor ?? ""}
onChange={(color: string) => { onChange={(color: string) => {
if (customOnChange) { if (customOnChange) {
customOnChange(color); customOnChange(color)
setCustomThemeColor(color); setCustomThemeColor(color)
} else { } else {
setCustomThemeColor(color); setCustomThemeColor(color)
} }
}} }}
/> />
); )
} }
-227
View File
@@ -1,227 +0,0 @@
<script lang="ts">
import { isValidHotkey, parseHotkey } from '@/plugins/built-in/globalSearch/src/utils/hotkeyUtils';
let { value, onChange } = $props<{
value: string,
onChange: (newValue: string) => void
}>();
let isRecording = $state(false);
let recordedKeys = $state<Set<string>>(new Set());
let inputElement = $state<HTMLInputElement>();
const formatKeyForHotkey = (key: string): string => {
// Map special keys to their hotkey format
const keyMap: Record<string, string> = {
'Control': 'ctrl',
'Meta': 'cmd',
'Alt': 'alt',
'Shift': 'shift',
' ': 'space',
'ArrowUp': 'up',
'ArrowDown': 'down',
'ArrowLeft': 'left',
'ArrowRight': 'right',
'Escape': 'esc',
'Enter': 'enter',
'Tab': 'tab',
'Backspace': 'backspace',
'Delete': 'delete',
};
return keyMap[key] || key.toLowerCase();
};
const formatKeyForDisplay = (key: string): string => {
// Map keys to their display format
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const keyMap: Record<string, string> = {
'ctrl': isMac ? '⌃' : 'Ctrl',
'cmd': '⌘',
'meta': '⌘',
'alt': isMac ? '⌥' : 'Alt',
'shift': isMac ? '⇧' : 'Shift',
'space': 'Space',
'up': '↑',
'down': '↓',
'left': '←',
'right': '→',
'esc': 'Esc',
'enter': 'Enter',
'tab': 'Tab',
'backspace': 'Backspace',
'delete': 'Delete',
};
return keyMap[key.toLowerCase()] || key.toUpperCase();
};
const getHotkeyParts = (hotkeyString: string): string[] => {
if (!hotkeyString || !isValidHotkey(hotkeyString)) {
return [];
}
const parsed = parseHotkey(hotkeyString);
const parts: string[] = [];
// Add modifiers in a consistent order
if (parsed.ctrl) parts.push('ctrl');
if (parsed.meta) parts.push('cmd');
if (parsed.alt) parts.push('alt');
if (parsed.shift) parts.push('shift');
// Add the main key
if (parsed.key) parts.push(parsed.key);
return parts;
};
const startRecording = () => {
isRecording = true;
recordedKeys.clear();
inputElement?.focus();
};
const stopRecording = () => {
if (recordedKeys.size > 0) {
if (recordedKeys.has('esc')) {
onChange('');
isRecording = false;
recordedKeys.clear();
inputElement?.blur();
return;
}
// Build the hotkey string
const modifiers: string[] = [];
let mainKey = '';
for (const key of recordedKeys) {
if (['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
modifiers.push(key);
} else {
mainKey = key;
}
}
if (mainKey) {
const hotkeyString = [...modifiers, mainKey].join('+');
if (isValidHotkey(hotkeyString)) {
onChange(hotkeyString);
}
}
}
isRecording = false;
recordedKeys.clear();
inputElement?.blur();
};
const handleKeyDown = (e: KeyboardEvent) => {
if (!isRecording) return;
e.preventDefault();
e.stopPropagation();
const key = formatKeyForHotkey(e.key);
// Add modifiers
if (e.ctrlKey) recordedKeys.add('ctrl');
if (e.metaKey) recordedKeys.add('cmd');
if (e.altKey) recordedKeys.add('alt');
if (e.shiftKey) recordedKeys.add('shift');
// Add the main key (ignore modifier keys themselves)
if (!['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
recordedKeys.add(key);
}
// Auto-stop recording if we have a main key
if (!['ctrl', 'cmd', 'alt', 'shift'].includes(key)) {
setTimeout(stopRecording, 100);
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (!isRecording) return;
e.preventDefault();
e.stopPropagation();
};
const handleBlur = () => {
if (isRecording) {
stopRecording();
}
};
$effect(() => {
if (isRecording && inputElement) {
inputElement.focus();
}
});
// Get the parts to display
const hotkeyParts = $derived(isRecording
? Array.from(recordedKeys).map(formatKeyForDisplay)
: getHotkeyParts(value).map(formatKeyForDisplay));
</script>
<div class="flex gap-2 items-center">
<div class="relative">
{#if isRecording}
<!-- Recording state -->
<div
class="flex items-center justify-center px-3 py-1.5 text-sm rounded-md dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 dark:text-white border cursor-pointer text-nowrap"
onclick={startRecording}
onkeydown={startRecording}
role="button"
tabindex="0"
>
Press keys...
</div>
{:else if hotkeyParts.length > 0}
<!-- Display current hotkey -->
<div
class="flex gap-1 items-center text-sm rounded-md border-none cursor-pointer dark:text-white"
onclick={startRecording}
onkeydown={startRecording}
role="button"
tabindex="0"
>
{#each hotkeyParts as part}
<div class="size-8 text-sm flex items-center justify-center rounded-md border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30">
{part}
</div>
{/each}
</div>
{:else}
<!-- Empty state -->
<div
class="flex items-center justify-center px-3 py-2 text-sm rounded-md dark:bg-[#38373D]/50 bg-[#DDDDDD] dark:text-white border-none cursor-pointer text-nowrap"
onclick={startRecording}
onkeydown={startRecording}
role="button"
tabindex="0"
>
<span class="text-gray-500 dark:text-gray-400">Click to set</span>
</div>
{/if}
<!-- Hidden input for focus management -->
<input
bind:this={inputElement}
type="text"
readonly
class="absolute inset-0 opacity-0 pointer-events-none"
onkeydown={handleKeyDown}
onkeyup={handleKeyUp}
onblur={handleBlur}
/>
</div>
</div>
<style>
input:focus {
outline: none;
}
</style>
+1 -2
View File
@@ -5,8 +5,7 @@
</script> </script>
<button <button
aria-label="Color Picker Swatch"
onclick={onClick} onclick={onClick}
style="background: {$settingsState.selectedColor}" style="background: {$settingsState.selectedColor}"
class="w-16 h-8 rounded-md shadow-2xl ring-[1px] ring-[#DDDDDD]/30 dark:ring-[#38373D]/30" class="w-16 h-8 rounded-md"
></button> ></button>
+12 -14
View File
@@ -8,17 +8,15 @@
let select: HTMLSelectElement; let select: HTMLSelectElement;
</script> </script>
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-lg w-full overflow-clip"> <select
<select bind:this={select}
bind:this={select} value={state}
value={state} onchange={() => onChange(select.value)}
onchange={() => onChange(select.value)} class="px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md w-full"
class="px-4 py-1 text-[0.75rem] dark:text-white w-full border-none bg-transparent focus:ring-0 focus:bg-white/20 dark:focus:bg-black/10" >
> {#each options as option}
{#each options as option} <option value={option.value}>
<option value={option.value}> {option.label}
{option.label} </option>
</option> {/each}
{/each} </select>
</select>
</div>
+7 -15
View File
@@ -1,24 +1,17 @@
<script lang="ts"> <script lang="ts">
let { state, onChange, min = 0, max = 100, step = 1 } = $props<{ let { state, onChange } = $props<{ state: number, onChange: (value: number) => void }>();
state: number, let percentage = $derived((state / 100) * 100);
onChange: (value: number) => void,
min?: number,
max?: number,
step?: number
}>();
let percentage = $derived(((state - min) / (max - min)) * 100);
</script> </script>
<div class="relative mx-auto w-full max-w-lg"> <div class="relative w-full max-w-lg mx-auto">
<input <input
type="range" type="range"
min={min} min="0"
max={max} max="100"
step={step}
bind:value={state} bind:value={state}
style={`background: linear-gradient(to right, #30d259ad 0%, #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))}
class="w-full h-1 rounded-full appearance-none cursor-pointer slider" class="w-full h-1 rounded-full appearance-none cursor-pointer dark:bg-[#38373D] bg-[#DDDDDD] slider"
/> />
</div> </div>
@@ -38,7 +31,6 @@
height: 24px; height: 24px;
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3); box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
background: white; background: white;
color: #30d259ad;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
} }
+2 -2
View File
@@ -1,4 +1,4 @@
.dark .switch[data-ison="true"], .dark .switch[data-ison="true"],
.switch[data-ison="true"] { .switch[data-ison="true"] {
background-color: #30d259; background-color: #30D259;
} }
+9 -2
View File
@@ -30,7 +30,8 @@
</script> </script>
<div <div
class="flex w-14 p-1 cursor-pointer transition-all duration-150 rounded-full bg-gradient-to-tr select-none shadow-2xl ring-[1px] ring-[#DDDDDD]/30 dark:ring-[#38373D]/30 {state ? 'to-[#30D259]/80 from-[#30D259] dark:from-[#30D259]/40 dark:to-[#30D259]' : 'dark:from-[#38373D]/50 dark:to-[#38373D] to-[#DDDDDD]/50 from-[#DDDDDD]'}" class="flex w-14 p-1 cursor-pointer transition-all duration-150 rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch select-none"
data-ison={state}
onclick={() => onChange(!state)} onclick={() => onChange(!state)}
onkeydown={(e) => e.key === "Enter" && onChange(!state)} onkeydown={(e) => e.key === "Enter" && onChange(!state)}
role="switch" role="switch"
@@ -41,4 +42,10 @@
bind:this={handle} bind:this={handle}
class="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md" class="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
></div> ></div>
</div> </div>
<style>
.switch[data-ison="true"] {
background-color: #30D259;
}
</style>
+1 -1
View File
@@ -1,3 +1,3 @@
.tab-width { .tab-width {
width: var(--tab-width); width: var(--tab-width);
} }
@@ -5,6 +5,7 @@
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);
@@ -23,6 +24,10 @@
return 0; return 0;
}; };
$effect(() => {
calcXPos(hoveredTab);
});
onMount(() => { onMount(() => {
updateTabWidth(); updateTabWidth();
@@ -40,24 +45,26 @@
</script> </script>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<div class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 px-2 tab-width-container"> <div bind:this={containerRef} class="top-0 z-10 text-[0.875rem] pb-0.5 mx-4 tab-width-container">
<div bind:this={containerRef} class="flex relative"> <div class="relative flex">
<MotionDiv <MotionDiv
class="absolute top-0 left-0 z-0 h-full bg-gradient-to-tr dark:from-[#38373D]/80 dark:to-[#38373D] from-[#DDDDDD]/80 to-[#DDDDDD] 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(activeTab) }} animate={{ x: calcXPos(hoveredTab) }}
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="overflow-hidden px-4 h-full"> <div class="h-full px-4 overflow-hidden">
<MotionDiv <MotionDiv
class="h-full" class="h-full"
animate={{ x: `${-activeTab * 100}%` }} animate={{ x: `${-activeTab * 100}%` }}
@@ -65,13 +72,12 @@
> >
<div class="flex"> <div class="flex">
{#each tabs as { Content, props }, index} {#each tabs as { Content, props }, index}
<div class="absolute focus:outline-none w-full pt-2 transition-opacity duration-300 overflow-y-scroll no-scrollbar pb-2 h-full tab {activeTab === index ? 'opacity-100 active' : 'opacity-0'}" <div class="absolute focus:outline-none w-full transition-opacity duration-300 overflow-y-scroll no-scrollbar h-full tab {activeTab === index ? 'opacity-100 active' : 'opacity-0'}"
style="left: {index * 100}%;"> style="left: {index * 100}%;">
<div style="left: {index * 100}%;" class="fixed top-0 w-full h-8 bg-gradient-to-b to-transparent pointer-events-none z-[100] from-white dark:from-zinc-800 dark:to-transparent"></div>
<Content {...props} /> <Content {...props} />
</div> </div>
{/each} {/each}
</div> </div>
</MotionDiv> </MotionDiv>
</div> </div>
</div> </div>
@@ -1,12 +1,10 @@
<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 { Index } from 'flexsearch'; 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 }>();
@@ -20,12 +18,19 @@
let savedBackgrounds = $state<string[]>([]); let savedBackgrounds = $state<string[]>([]);
let installingBackgrounds = $state<Set<string>>(new Set()); let installingBackgrounds = $state<Set<string>>(new Set());
let debugInfo = $state<string>(''); let debugInfo = $state<string>('');
let searchIndex = $state<Index | null>(null);
// New state variables // New state variables
let activeTab = $state<'all' | 'installed' | 'photos' | 'videos'>('all'); let activeTab = $state<'all' | 'installed' | 'photos' | 'videos'>('all');
let sortBy = $state<'newest' | 'popular' | 'name'>('newest'); let sortBy = $state<'newest' | 'popular' | 'name'>('newest');
// Add Fuse.js options
const fuseOptions = {
keys: ['name', 'description'],
threshold: 0.4,
ignoreLocation: true
};
let fuse: Fuse<Background>;
// Existing functions // Existing functions
const loadStore = async () => { const loadStore = async () => {
try { try {
@@ -36,19 +41,7 @@
} }
const data = await response.json(); const data = await response.json();
backgrounds = data.backgrounds; backgrounds = data.backgrounds;
fuse = new Fuse(backgrounds, fuseOptions);
// Initialize FlexSearch index
const index = new Index({
tokenize: "forward",
preset: "score"
});
// Add backgrounds to the index
backgrounds.forEach((bg, i) => {
index.add(i, bg.name + " " + bg.description);
});
searchIndex = index;
debugInfo = `Loaded ${backgrounds.length} backgrounds`; debugInfo = `Loaded ${backgrounds.length} backgrounds`;
await loadSavedBackgrounds(); await loadSavedBackgrounds();
} catch (e) { } catch (e) {
@@ -79,10 +72,14 @@
let filteredBackgrounds = $derived((() => { let filteredBackgrounds = $derived((() => {
let filtered = backgrounds; let filtered = backgrounds;
// Use FlexSearch if there's a search term // Use Fuse.js search if there's a search term
if (searchTerm.trim() && searchIndex) { if (searchTerm.trim()) {
const results = searchIndex.search(searchTerm) as number[]; // @ts-ignore
filtered = results.map(i => backgrounds[i]); if (fuse) {
filtered = fuse.search(searchTerm).map((result: any) => result.item) ?? [];
} else {
filtered = backgrounds.filter(bg => bg.name.toLowerCase().includes(searchTerm.toLowerCase()));
}
} }
// Apply category filtering // Apply category filtering
@@ -173,13 +170,13 @@
function selectNoBackground() { function selectNoBackground() {
selectedBackground = null; selectedBackground = null;
themeManager.setTheme(''); setTheme('');
} }
</script> </script>
<div class="flex h-full"> <div class="flex h-full">
<!-- Sidebar --> <!-- Sidebar -->
<div class="p-4 w-64 h-full border-r border-zinc-200 dark:border-zinc-700"> <div class="w-64 h-full p-4 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">
@@ -211,15 +208,15 @@
</div> </div>
<!-- Main Content --> <!-- Main Content -->
<div class="overflow-auto flex-1"> <div class="flex-1 overflow-auto">
<!-- 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 justify-between items-center mb-4"> <div class="flex items-center justify-between 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 gap-4 items-center"> <div class="flex items-center gap-4">
<select <select
bind:value={sortBy} bind:value={sortBy}
class="p-2 rounded-lg border border-zinc-200 dark:border-zinc-700 dark:bg-zinc-800" class="p-2 border rounded-lg 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>
@@ -233,7 +230,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-zinc-700 hover:bg-zinc-200 dark:hover:bg-zinc-700/20'}`} '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'}`}
onclick={() => activeTab = tab.toLowerCase() as typeof activeTab} onclick={() => activeTab = tab.toLowerCase() as typeof activeTab}
> >
{tab} {tab}
@@ -247,15 +244,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="overflow-hidden relative rounded-lg animate-pulse"> <div class="relative overflow-hidden 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 right-0 bottom-0 left-0 h-16 to-transparent bg-linear-to-t from-zinc-300 dark:from-zinc-700"> <div class="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-zinc-300 dark:from-zinc-700 to-transparent">
<!-- Title placeholder --> <!-- Title placeholder -->
<div class="absolute right-2 bottom-2 left-2"> <div class="absolute bottom-2 left-2 right-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="mt-2 w-1/2 h-3 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> </div>
</div> </div>
</div> </div>
@@ -274,7 +271,7 @@
return true; return true;
}) as background (background.id)} }) as background (background.id)}
<div <div
class="overflow-hidden relative rounded-lg shadow-lg cursor-pointer group" class="relative overflow-hidden 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 === ' ') {
@@ -289,7 +286,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={`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' : ''}`}> <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">
{#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 overflow-clip rounded-xl transition-opacity" transition:fade> <div class="relative w-full transition-opacity rounded-xl overflow-clip" 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 to-transparent bg-linear-to-t from-black/80'></div> <div class='absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-black/80 to-transparent'></div>
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
<!-- Navigation buttons --> <!-- Navigation buttons -->
<div class='flex absolute right-2 bottom-2 z-10 gap-2'> <div class='absolute z-10 flex gap-2 bottom-2 right-2'>
<button aria-label="Previous" onclick={slidePrev} class='flex justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'> <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'>
<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 justify-center items-center w-8 h-8 text-white rounded-full bg-black/50 dark:bg-zinc-800'> <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'>
<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,5 +1,9 @@
<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({
@@ -9,9 +13,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)) {
@@ -38,19 +42,21 @@
<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')} onchange={() => toggleFilter('type', 'image')}> <input type="checkbox" checked={filters.type.includes('image')} on:change={() => 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')} onchange={() => toggleFilter('type', 'video')}> <input type="checkbox" checked={filters.type.includes('video')} on:change={() => 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"
onclick={clearFilters} on:click={clearFilters}
> >
Clear Filters Clear Filters
</button> </button>
+4 -4
View File
@@ -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 justify-between items-center px-4 py-1"> <div class="flex items-center justify-between px-4 py-1">
<div class="flex gap-4 place-items-center cursor-pointer" onkeydown={(e) => { if (e.key === 'Enter') clearSearch() }} onclick={clearSearch} role="button" tabindex="0"> <div class="flex gap-4 cursor-pointer place-items-center" 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="flex relative gap-2"> <div class="relative flex 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 left-3 top-1/2 w-5 h-5 text-gray-400 transform -translate-y-1/2 dark:text-gray-200" class="absolute w-5 h-5 text-gray-400 transform -translate-y-1/2 left-3 top-1/2 dark:text-gray-200"
fill="none" fill="none"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
@@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { Theme } from '@/interface/types/Theme'
let { theme, onClick } = $props<{ theme: Theme; onClick: () => void }>(); let { theme, onClick } = $props<{ theme: Theme; onClick: () => void }>();
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@@ -8,12 +6,12 @@
<div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={onClick}> <div class="w-full cursor-pointer" role="button" tabindex="-1" onkeydown={onClick} onclick={onClick}>
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border" transition:fade> <div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border" transition:fade>
<div class="absolute bottom-1 left-3 z-10 mb-1 text-xl font-bold text-white"> <div class="absolute z-10 mb-1 text-xl font-bold text-white bottom-1 left-3">
{theme.name} {theme.name}
</div> </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='absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent'></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.coverImage} alt="Theme Preview" class="object-cover w-full h-48 rounded-md" />
</div> </div>
</div> </div>
</div> </div>
@@ -54,7 +54,7 @@
</script> </script>
<div <div
class="flex fixed inset-0 z-50 justify-center items-end bg-black/70" class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-70"
onclick={(e) => { onclick={(e) => {
if (e.target === e.currentTarget) hideModal(); if (e.target === e.currentTarget) hideModal();
}} }}
@@ -79,12 +79,12 @@
<h2 class="mb-4 text-2xl font-bold"> <h2 class="mb-4 text-2xl font-bold">
{theme.name} {theme.name}
</h2> </h2>
<img src={theme.marqueeImage} alt="Theme Cover" class="object-cover mb-4 w-full rounded-md" /> <img src={theme.marqueeImage} alt="Theme Cover" class="object-cover w-full mb-4 rounded-md" />
<p class="mb-4 text-gray-700 dark:text-gray-300"> <p class="mb-4 text-gray-700 dark:text-gray-300">
{theme.description} {theme.description}
</p> </p>
{#if currentThemes.includes(theme.id)} {#if currentThemes.includes(theme.id)}
<button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200"> <button onclick={async () => {installing = true; await onRemove(theme.id); installing = false}} class="relative flex items-center justify-center w-32 px-4 py-2 mt-4 ml-auto text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
{#if installing} {#if installing}
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/> <path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
@@ -93,7 +93,7 @@
<span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span> <span class="{ installing ? 'opacity-0' : 'opacity-100' }">Remove</span>
</button> </button>
{:else} {:else}
<button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="flex relative justify-center items-center px-4 py-2 mt-4 ml-auto w-32 text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200"> <button onclick={async () => {installing = true; await onInstall(theme.id); installing = false}} class="relative flex items-center justify-center w-32 px-4 py-2 mt-4 ml-auto text-black rounded-full dark:text-white bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600/50 hover:bg-zinc-200">
{#if installing} {#if installing}
<svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="absolute w-4 h-4 { installing ? 'opacity-100' : 'opacity-0' }" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/> <path stroke="currentColor" fill="currentColor" class="origin-center animate-spin-fast" d="M2,12A11.2,11.2,0,0,1,13,1.05C12.67,1,12.34,1,12,1a11,11,0,0,0,0,22c.34,0,.67,0,1-.05C6,23,2,17.74,2,12Z"/>
@@ -112,11 +112,11 @@
{#each getRelatedThemes() as relatedTheme (relatedTheme.id)} {#each getRelatedThemes() as relatedTheme (relatedTheme.id)}
<button onclick={() => { hideModal(relatedTheme) }} class="w-full cursor-pointer"> <button onclick={() => { hideModal(relatedTheme) }} class="w-full cursor-pointer">
<div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border"> <div class="bg-gray-50 w-full transition-all hover:scale-105 duration-500 relative group group/card flex flex-col hover:shadow-2xl dark:hover:shadow-white/[0.1] hover:shadow-white/[0.8] dark:bg-zinc-800 dark:border-white/[0.1] h-auto rounded-xl overflow-clip border">
<div class="absolute 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 z-10 mb-1 text-xl font-bold text-white transition-all duration-500 group-hover:-translate-y-0.5 bottom-1 left-3">
{relatedTheme.name} {relatedTheme.name}
</div> </div>
<div class="absolute bottom-0 z-0 w-full h-3/4 to-transparent from-black/80 bg-linear-to-t"></div> <div class="absolute bottom-0 z-0 w-full h-3/4 bg-gradient-to-t from-black/80 to-transparent"></div>
<img src={relatedTheme.marqueeImage} alt="Theme Preview" class="object-cover w-full h-48" /> <img src={relatedTheme.coverImage} alt="Theme Preview" class="object-cover w-full h-48" />
</div> </div>
</button> </button>
{/each} {/each}
@@ -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-3 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 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,28 +1,29 @@
<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 '@/plugins/built-in/themes/ThemeCreator' import { OpenThemeCreator } from '@/seqta/ui/ThemeCreator'
import shareTheme from '@/seqta/ui/themes/shareTheme'
import { InstallTheme } from '@/seqta/ui/themes/downloadTheme'
import { disableTheme } from '@/seqta/ui/themes/disableTheme'
import { setTheme } from '@/seqta/ui/themes/setTheme'
import { deleteTheme } from '@/seqta/ui/themes/deleteTheme'
import { 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/utils/Closers/closeExtensionPopup' import { closeExtensionPopup } from '@/SEQTA'
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 }>();
let isDragging = $state(false); let isDragging = $state(false);
let tempTheme = $state(null); let tempTheme = $state(null);
const handleThemeClick = async (theme: CustomTheme, e: MouseEvent) => { const handleThemeClick = async (theme: CustomTheme) => {
if (isEditMode) return; if (isEditMode) return;
if (theme.id === themes?.selectedTheme) { if (theme.id === themes?.selectedTheme) {
themeManager.setTransitionPoint(e.clientX, e.clientY); await disableTheme();
await themeManager.disableTheme();
themes.selectedTheme = ''; themes.selectedTheme = '';
} else { } else {
themeManager.setTransitionPoint(e.clientX, e.clientY); await setTheme(theme.id);
await themeManager.setTheme(theme.id);
if (!themes) return; if (!themes) return;
themes.selectedTheme = theme.id; themes.selectedTheme = theme.id;
} }
@@ -30,13 +31,13 @@
const handleThemeDelete = async (themeId: string) => { const handleThemeDelete = async (themeId: string) => {
try { try {
await themeManager.deleteTheme(themeId); await 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 themeManager.disableTheme(); await disableTheme();
} }
} catch (error) { } catch (error) {
console.error('Error deleting theme:', error); console.error('Error deleting theme:', error);
@@ -45,7 +46,7 @@
const handleShareTheme = async (theme: CustomTheme) => { const handleShareTheme = async (theme: CustomTheme) => {
try { try {
await themeManager.shareTheme(theme.id); await shareTheme(theme.id);
} catch (error) { } catch (error) {
console.error('Error sharing theme:', error); console.error('Error sharing theme:', error);
} }
@@ -71,10 +72,9 @@
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 themeManager.installTheme(result); await 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,10 +83,7 @@
} }
const fetchThemes = async () => { const fetchThemes = async () => {
themes = { themes = await getAvailableThemes();
themes: await themeManager.getAvailableThemes(),
selectedTheme: themeManager.getSelectedThemeId() || '',
}
} }
onMount(async () => { onMount(async () => {
@@ -101,7 +98,7 @@
</script> </script>
<div <div
class="pt-5 mb-1 w-full" class="w-full pt-5 mb-1"
role="list" role="list"
tabindex="-1" tabindex="-1"
ondragover={handleDragOver} ondragover={handleDragOver}
@@ -109,9 +106,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 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="sticky w-full h-64 bg-white shadow-xl dark:bg-zinc-900 top-5 dark:text-white rounded-xl outline-dashed outline-4 outline-zinc-200 dark:outline-zinc-700">
<div class="flex justify-center items-center h-full"> <div class="flex items-center justify-center h-full">
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col items-center justify-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"/>
@@ -129,11 +126,11 @@
{#each themes.themes as theme (theme.id)} {#each themes.themes as theme (theme.id)}
<button <button
class="relative group w-full aspect-theme flex justify-center items-center rounded-xl transition ring dark:ring-white ring-zinc-300 {theme.id === themes.selectedTheme ? 'dark:ring-2 ring-4' : 'ring-0'}" class="relative group w-full aspect-theme flex justify-center items-center rounded-xl transition ring dark:ring-white ring-zinc-300 {theme.id === themes.selectedTheme ? 'dark:ring-2 ring-4' : 'ring-0'}"
onclick={(e) => handleThemeClick(theme, e)} onclick={() => handleThemeClick(theme)}
> >
{#if isEditMode} {#if isEditMode}
<div <div
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" 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"
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"
@@ -155,7 +152,7 @@
</div> </div>
<div <div
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" 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"
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"
@@ -170,7 +167,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="object-cover absolute inset-0 z-0 w-full h-full pointer-events-none" class="absolute inset-0 z-0 object-cover w-full h-full pointer-events-none"
/> />
{/if} {/if}
{#if !theme.hideThemeName} {#if !theme.hideThemeName}
@@ -182,7 +179,7 @@
{/if} {/if}
{#if tempTheme} {#if tempTheme}
<div class="flex justify-center place-items-center w-full bg-gray-200 rounded-xl animate-pulse dark:bg-zinc-700/50 aspect-theme"> <div class="flex justify-center w-full bg-gray-200 rounded-xl dark:bg-zinc-700/50 place-items-center aspect-theme animate-pulse">
<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>
@@ -196,7 +193,7 @@
<button <button
onclick={() => OpenStorePage()} onclick={() => OpenStorePage()}
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white" class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
> >
<span class="text-xl font-IconFamily">&#xecc5;</span> <span class="text-xl font-IconFamily">&#xecc5;</span>
<span class="ml-2">Theme Store</span> <span class="ml-2">Theme Store</span>
@@ -204,7 +201,7 @@
<button <button
onclick={() => { OpenThemeCreator(); closeExtensionPopup() }} onclick={() => { OpenThemeCreator(); closeExtensionPopup() }}
class="flex justify-center items-center w-full rounded-xl transition aspect-theme bg-zinc-100 dark:bg-zinc-900 dark:text-white" class="flex items-center justify-center w-full transition aspect-theme rounded-xl bg-zinc-100 dark:bg-zinc-900 dark:text-white"
> >
<span class="text-xl font-IconFamily">&#xec60;</span> <span class="text-xl font-IconFamily">&#xec60;</span>
<span class="ml-2">Create your own</span> <span class="ml-2">Create your own</span>
+16 -26
View File
@@ -1,4 +1,4 @@
import { type DBSchema, type IDBPDatabase, openDB } from "idb"; import { type DBSchema, type IDBPDatabase, openDB } from 'idb';
interface BackgroundDB extends DBSchema { interface BackgroundDB extends DBSchema {
backgrounds: { backgrounds: {
@@ -16,46 +16,38 @@ let db: IDBPDatabase<BackgroundDB> | null = null;
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> { export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
if (db) return db; if (db) return db;
db = await openDB<BackgroundDB>("BackgroundDB", 1, { db = await openDB<BackgroundDB>('BackgroundDB', 1, {
upgrade(db: IDBPDatabase<BackgroundDB>) { upgrade(db: IDBPDatabase<BackgroundDB>) {
db.createObjectStore("backgrounds", { keyPath: "id" }); db.createObjectStore('backgrounds', { keyPath: 'id' });
}, },
}); });
return db; return db;
} }
export async function readAllData(): Promise< export async function readAllData(): Promise<Array<{ id: string; type: string; blob: Blob }>> {
Array<{ id: string; type: string; blob: Blob }>
> {
const db = await openDatabase(); const db = await openDatabase();
return db.getAll("backgrounds"); return db.getAll('backgrounds');
} }
export async function writeData( export async function writeData(id: string, type: string, blob: Blob): Promise<void> {
id: string,
type: string,
blob: Blob,
): Promise<void> {
const db = await openDatabase(); const db = await openDatabase();
await db.put("backgrounds", { id, type, blob }); await db.put('backgrounds', { id, type, blob });
} }
export async function deleteData(id: string): Promise<void> { export async function deleteData(id: string): Promise<void> {
const db = await openDatabase(); const db = await openDatabase();
await db.delete("backgrounds", id); await db.delete('backgrounds', id);
} }
export async function clearAllData(): Promise<void> { export async function clearAllData(): Promise<void> {
const db = await openDatabase(); const db = await openDatabase();
await db.clear("backgrounds"); await db.clear('backgrounds');
} }
export async function getDataById( export async function getDataById(id: string): Promise<{ id: string; type: string; blob: Blob } | undefined> {
id: string,
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
const db = await openDatabase(); const db = await openDatabase();
return db.get("backgrounds", id); return db.get('backgrounds', id);
} }
export function closeDatabase(): void { export function closeDatabase(): void {
@@ -67,19 +59,17 @@ export function closeDatabase(): void {
// Helper function to check if IndexedDB is supported // Helper function to check if IndexedDB is supported
export function isIndexedDBSupported(): boolean { export function isIndexedDBSupported(): boolean {
return "indexedDB" in window; return 'indexedDB' in window;
} }
// Helper function to check if there's enough storage space // Helper function to check if there's enough storage space
export async function hasEnoughStorageSpace( export async function hasEnoughStorageSpace(requiredSpace: number): Promise<boolean> {
requiredSpace: number, if ('storage' in navigator && 'estimate' in navigator.storage) {
): Promise<boolean> {
if ("storage" in navigator && "estimate" in navigator.storage) {
const { quota, usage } = await navigator.storage.estimate(); const { quota, usage } = await navigator.storage.estimate();
if (quota !== undefined && usage !== undefined) { if (quota !== undefined && usage !== undefined) {
return quota - usage > requiredSpace; return (quota - usage) > requiredSpace;
} }
} }
// If we can't determine, assume there's enough space // If we can't determine, assume there's enough space
return true; return true;
} }
+1 -1
View File
@@ -22,7 +22,7 @@ class BackgroundUpdates {
} }
public triggerUpdate(): void { public triggerUpdate(): void {
this.listeners.forEach((callback) => callback()); this.listeners.forEach(callback => callback());
} }
} }
+3 -3
View File
@@ -1,13 +1,13 @@
type SettingsPopupCallback = () => void; type SettingsPopupCallback = () => void;
/** /**
* This is a singleton that triggers an update when the settings popup is closed. * This is a singleton that triggers an update when the settings popup is closed.
* This is used to close the colour picker. * This is used to close the colour picker.
* Usage: * Usage:
* settingsPopup.addListener(() => { * settingsPopup.addListener(() => {
* console.log('Settings popup closed'); * console.log('Settings popup closed');
* }); * });
*/ */
class SettingsPopup { class SettingsPopup {
private static instance: SettingsPopup; private static instance: SettingsPopup;
private listeners: Set<SettingsPopupCallback> = new Set(); private listeners: Set<SettingsPopupCallback> = new Set();
@@ -30,7 +30,7 @@ class SettingsPopup {
} }
public triggerClose(): void { public triggerClose(): void {
this.listeners.forEach((callback) => callback()); this.listeners.forEach(callback => callback());
} }
} }
+1 -1
View File
@@ -22,7 +22,7 @@ class ThemeUpdates {
} }
public triggerUpdate(): void { public triggerUpdate(): void {
this.listeners.forEach((callback) => callback()); this.listeners.forEach(callback => callback());
} }
} }
@@ -1 +1 @@
export let selectedBackground = $state<string | null>(null); export let selectedBackground = $state<string | null>(null);
+11 -9
View File
@@ -1,11 +1,17 @@
@import "./components/ColourPicker.css"; @import './components/ColourPicker.css';
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
button { :root {
@apply cursor-pointer; font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
@@ -42,9 +48,5 @@ input {
.cm-editor { .cm-editor {
width: 100%; width: 100%;
min-height: 100px; min-height: 100px;
height: inherit; max-height: 400px;
} }
.editorHeight {
height: calc(100vh - 58px);
}
+3 -3
View File
@@ -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 class="h-[600px]"> <body>
<div id="app" style="height: 100%"></div> <div id="app"></div>
<script type="module" src="./index.ts"></script> <script type="module" src="./index.ts"></script>
</body> </body>
</html> </html>
+32 -15
View File
@@ -1,29 +1,46 @@
import "./index.css"; import "./index.css"
import Settings from "./pages/settings.svelte"; import { mount } from "svelte"
import IconFamily from "@/resources/fonts/IconFamily.woff"; import type { ComponentType } from "svelte"
import browser from "webextension-polyfill"; import Settings from "./pages/settings.svelte"
import renderSvelte from "./main"; import IconFamily from '@/resources/fonts/IconFamily.woff'
import browser from "webextension-polyfill"
export default function renderSvelte(
Component: ComponentType | any,
mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {},
) {
const app = mount(Component, {
target: mountPoint,
props: {
standalone: true,
...props,
},
})
return app
}
function InjectCustomIcons() { function InjectCustomIcons() {
console.info("[BetterSEQTA+] Injecting Icons"); console.info('[BetterSEQTA+] Injecting Icons')
const style = document.createElement("style"); const style = document.createElement('style')
style.setAttribute("type", "text/css"); style.setAttribute('type', 'text/css')
style.innerHTML = ` style.innerHTML = `
@font-face { @font-face {
font-family: 'IconFamily'; font-family: 'IconFamily';
src: url('${browser.runtime.getURL(IconFamily)}') format('woff'); src: url('${browser.runtime.getURL(IconFamily)}') format('woff');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
}`; }`
document.head.appendChild(style); document.head.appendChild(style)
} }
const mountPoint = document.getElementById("app"); const mountPoint = document.getElementById('app')
if (!mountPoint) { if (!mountPoint) {
console.error("Mount point #app not found"); console.error('Mount point #app not found')
throw new Error("Mount point #app not found"); throw new Error('Mount point #app not found')
} }
InjectCustomIcons(); InjectCustomIcons()
renderSvelte(Settings, mountPoint, { standalone: true }); renderSvelte(Settings, mountPoint)
+2 -2
View File
@@ -1,6 +1,6 @@
import "./index.css"; import './index.css';
declare module "*.png"; declare module "*.png";
declare module "*.svg"; declare module "*.svg";
declare module "*.jpeg"; declare module "*.jpeg";
declare module "*.jpg"; declare module "*.jpg";
+10 -9
View File
@@ -1,9 +1,9 @@
import { mount } from "svelte"; import styles from "./index.css?inline"
import type { SvelteComponent } from "svelte"; import { mount } from "svelte"
import style from "./index.css?inline"; import type { ComponentType } from "svelte"
export default function renderSvelte( export default function renderSvelte(
Component: SvelteComponent | any, Component: ComponentType | any,
mountPoint: ShadowRoot | HTMLElement, mountPoint: ShadowRoot | HTMLElement,
props: Record<string, any> = {}, props: Record<string, any> = {},
) { ) {
@@ -13,11 +13,12 @@ export default function renderSvelte(
standalone: false, standalone: false,
...props, ...props,
}, },
}); })
const styleElement = document.createElement("style"); const style = document.createElement("style")
styleElement.textContent = style; style.setAttribute("type", "text/css")
mountPoint.appendChild(styleElement); style.innerHTML = styles
mountPoint.appendChild(style)
return app; return app
} }
+5 -8
View File
@@ -9,10 +9,7 @@
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 } from "@/seqta/utils/Closers/closeExtensionPopup" import { closeExtensionPopup, OpenAboutPage, OpenWhatsNewPopup } from "@/SEQTA"
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'
@@ -64,8 +61,8 @@
</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="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white"> <div class="relative flex flex-col h-full gap-2 bg-white overflow-clip dark:bg-zinc-800 dark:text-white">
<div class="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40"> <div class="grid border-b border-b-zinc-200/40 place-items-center">
<!-- 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} />
@@ -74,8 +71,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 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={openChangelog} class="absolute w-8 h-8 text-lg rounded-xl font-IconFamily top-1 right-1 bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
<button onclick={openAbout} class="absolute top-1 right-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</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>
{/if} {/if}
</div> </div>
+76 -209
View File
@@ -3,90 +3,19 @@
import Button from "../../components/Button.svelte" import Button from "../../components/Button.svelte"
import Slider from "../../components/Slider.svelte" import Slider from "../../components/Slider.svelte"
import Select from "@/interface/components/Select.svelte" import Select from "@/interface/components/Select.svelte"
import HotkeyInput from "@/interface/components/HotkeyInput.svelte"
import browser from "webextension-polyfill" import browser from "webextension-polyfill"
import type { SettingsList } from "@/interface/types/SettingsProps" import type { SettingsList } from "@/interface/types/SettingsProps"
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
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, ButtonSetting, HotkeySetting } 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[]
}) |
(Omit<ButtonSetting, 'type'> & {
type: 'button',
id: string
}) |
(Omit<HotkeySetting, 'type'> & {
type: 'hotkey',
id: string
});
interface Plugin {
pluginId: string;
name: string;
description: string;
beta?: boolean;
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 && setting.type !== 'button') {
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 justify-between items-center px-4 py-3"> <div class="flex items-center justify-between 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>
@@ -109,6 +38,26 @@
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.",
@@ -138,6 +87,36 @@
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",
@@ -168,152 +147,40 @@
} }
}, },
{ {
title: "News Feed Source", title: "BetterSEQTA+",
description: "Choose sources of your news feed.", description: "Enables BetterSEQTA+ features",
id: 11, id: 11,
Component: Select, Component: Switch,
props: { props: {
state: $settingsState.newsSource, state: $settingsState.onoff,
onChange: (value: string) => settingsState.newsSource = value, onChange: (isOn: boolean) => settingsState.onoff = isOn
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 class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}">
<!-- 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="flex gap-2 items-center text-sm font-bold">
Enable {plugin.name}
{#if plugin.beta}
<span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full border border-orange-300/30 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-900/30">
Beta
</span>
{/if}
</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}
{#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]/50 bg-[#DDDDDD] dark:text-white border-none"
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)
}))}
/>
{:else if setting.type === 'button'}
<Button
onClick={() => setting.trigger?.()}
text={setting.title}
/>
{:else if setting.type === 'hotkey'}
<HotkeyInput
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
/>
{/if}
</div>
</div>
{/if}
{/each}
{/if}
</div>
</div>
{/each}
<div class="p-1 border-none"></div>
{@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-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="flex items-center justify-between px-4 py-3 mt-4 pt-[1.75rem]">
<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">Developer Mode</h2>
<h2 class="text-sm font-bold">Developer Mode</h2> <p class="text-xs">Enables developer mode, allowing you to test new features and changes.</p>
<p class="text-xs">Enables developer mode, allowing you to test new features and changes.</p>
</div>
<div>
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
</div>
</div> </div>
<div class="flex justify-between items-center px-4 py-3"> <div>
<div class="pr-4"> <Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
<h2 class="text-sm font-bold">Sensitive Hider</h2> </div>
<p class="text-xs">Replace sensitive content with mock data</p> </div>
</div> <div class="flex items-center justify-between px-4 py-3">
<div> <div class="pr-4">
<Button <h2 class="text-sm font-bold">Sensitive Hider</h2>
onClick={() => hideSensitiveContent()} <p class="text-xs">Replace sensitive content with mock data</p>
text="Hide" </div>
/> <div>
</div> <Button
onClick={() => hideSensitiveContent()}
text="Hide"
/>
</div> </div>
</div> </div>
{/if} {/if}
</div> </div>
+20 -22
View File
@@ -3,7 +3,6 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts" import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
import Switch from "@/interface/components/Switch.svelte" import Switch from "@/interface/components/Switch.svelte"
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Shortcuts from "@/seqta/content/links.json"
let isLoaded = $state(false); let isLoaded = $state(false);
@@ -22,14 +21,10 @@
}); });
}); });
const switchChange = (shortcut: any) => { const switchChange = (index: number) => {
const value = $settingsState.shortcuts.find(s => s.name === shortcut); const updatedShortcuts = [...settingsState.shortcuts];
if (value) { updatedShortcuts[index].enabled = !updatedShortcuts[index].enabled;
value.enabled = !value.enabled; settingsState.shortcuts = updatedShortcuts;
settingsState.shortcuts = settingsState.shortcuts;
} else {
settingsState.shortcuts = [...settingsState.shortcuts, { name: shortcut, enabled: true }];
}
} }
let isFormVisible = $state(false); let isFormVisible = $state(false);
@@ -70,6 +65,15 @@
}; };
</script> </script>
{#snippet Shortcuts([index, Shortcut]: [string, { name: string, enabled: boolean }]) }
<div class="flex items-center justify-between px-4 py-3">
<div class="pr-4">
<h2 class="text-sm">{Shortcut.name}</h2>
</div>
<Switch state={Shortcut.enabled} onChange={() => switchChange(parseInt(index))} />
</div>
{/snippet}
<div class="flex flex-col pt-4 divide-y divide-zinc-100 dark:divide-zinc-700"> <div class="flex flex-col pt-4 divide-y divide-zinc-100 dark:divide-zinc-700">
{#if isLoaded} {#if isLoaded}
<div> <div>
@@ -91,7 +95,7 @@
class="w-full" class="w-full"
> >
<input <input
class="p-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600" class="w-full p-2 transition border-0 rounded-lg placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
type="text" type="text"
placeholder="Shortcut Name" placeholder="Shortcut Name"
bind:value={newTitle} bind:value={newTitle}
@@ -104,7 +108,7 @@
class="w-full" class="w-full"
> >
<input <input
class="p-2 my-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600" class="w-full p-2 my-2 transition border-0 rounded-lg placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
type="text" type="text"
placeholder="URL eg. https://google.com" placeholder="URL eg. https://google.com"
bind:value={newURL} bind:value={newURL}
@@ -132,21 +136,15 @@
</MotionDiv> </MotionDiv>
</div> </div>
{#each Object.entries(Shortcuts) as shortcut} {#each Object.entries($settingsState.shortcuts) as shortcut}
<div class="flex justify-between items-center px-4 py-3"> {@render Shortcuts(shortcut)}
<div class="pr-4">
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
<h2 class="text-sm">{shortcut[1].DisplayName || shortcut[0]}</h2>
</div>
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
</div>
{/each} {/each}
<!-- Custom Shortcuts Section --> <!-- Custom Shortcuts Section -->
{#each $settingsState.customshortcuts as shortcut, index} {#each $settingsState.customshortcuts as shortcut, index}
<div class="flex justify-between items-center px-4 py-3"> <div class="flex items-center justify-between px-4 py-3">
{shortcut.name} {shortcut.name}
<button aria-label="Delete Shortcut" onclick={() => deleteCustomShortcut(index)}> <button onclick={() => deleteCustomShortcut(index)}>
<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="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
@@ -158,4 +156,4 @@
Loading shortcuts... Loading shortcuts...
</div> </div>
{/if} {/if}
</div> </div>
+9 -8
View File
@@ -9,15 +9,16 @@
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[]>([]);
@@ -32,8 +33,8 @@
let selectedBackground = $state<string | null>(null); let selectedBackground = $state<string | null>(null);
const fetchCurrentThemes = async () => { const fetchCurrentThemes = async () => {
const themes = await themeManager.getAvailableThemes(); const themes = await getAvailableThemes();
currentThemes = themes.filter(theme => theme !== null).map(theme => theme.id); currentThemes = themes.themes.filter(theme => theme !== null).map(theme => theme.id);
}; };
const setDisplayTheme = (theme: Theme | null) => { const setDisplayTheme = (theme: Theme | null) => {
@@ -122,8 +123,8 @@
{setDisplayTheme} {setDisplayTheme}
onInstall={async () => { onInstall={async () => {
if (displayTheme) { if (displayTheme) {
await themeManager.downloadTheme(displayTheme); await StoreDownloadTheme({themeContent: displayTheme})
await themeManager.setTheme(displayTheme.id); setTheme(displayTheme.id);
themeUpdates.triggerUpdate(); themeUpdates.triggerUpdate();
await fetchCurrentThemes(); await fetchCurrentThemes();
} }
@@ -131,7 +132,7 @@
onRemove={async () => { onRemove={async () => {
if (displayTheme?.id) { if (displayTheme?.id) {
console.debug('deleting theme', displayTheme.id); console.debug('deleting theme', displayTheme.id);
await themeManager.deleteTheme(displayTheme.id); deleteTheme(displayTheme.id)
themeUpdates.triggerUpdate(); themeUpdates.triggerUpdate();
await fetchCurrentThemes(); await fetchCurrentThemes();
} }
+40 -72
View File
@@ -7,6 +7,7 @@
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'
@@ -21,13 +22,13 @@
handleImageVariableChange, handleImageVariableChange,
handleCoverImageUpload handleCoverImageUpload
} from '../utils/themeImageHandlers'; } from '../utils/themeImageHandlers';
import { CloseThemeCreator } from '@/plugins/built-in/themes/ThemeCreator' import { ClearThemePreview, UpdateThemePreview } from '@/seqta/ui/themes/UpdateThemePreview'
import { saveTheme } from '@/seqta/ui/themes/saveTheme'
import { CloseThemeCreator } from '@/seqta/ui/ThemeCreator'
import { themeUpdates } from '../hooks/ThemeUpdates' import { themeUpdates } from '../hooks/ThemeUpdates'
import { ThemeManager } from '@/plugins/built-in/themes/theme-manager' import { disableTheme } from '@/seqta/ui/themes/disableTheme'
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: '',
@@ -44,19 +45,8 @@
}) })
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 {
@@ -65,10 +55,10 @@
} }
onMount(async () => { onMount(async () => {
await themeManager.disableTheme(); disableTheme();
if (themeID) { if (themeID) {
const tempTheme = await themeManager.getTheme(themeID) const tempTheme = await getTheme(themeID)
if (!tempTheme) return if (!tempTheme) return
@@ -76,12 +66,16 @@
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
} }
theme = loadedTheme if (tempTheme) {
themeLoaded = true theme = loadedTheme
themeLoaded = true
}
} else { } else {
themeLoaded = true themeLoaded = true
} }
@@ -105,7 +99,7 @@
theme = await handleCoverImageUpload(event, theme); theme = await handleCoverImageUpload(event, theme);
} }
async function submitTheme() { 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
@@ -115,17 +109,14 @@
})) }))
themeClone.coverImage = theme.coverImage themeClone.coverImage = theme.coverImage
themeManager.clearPreview(); ClearThemePreview();
await themeManager.saveTheme(themeClone); saveTheme(themeClone);
await themeManager.setTheme(themeClone.id);
themeUpdates.triggerUpdate(); themeUpdates.triggerUpdate();
CloseThemeCreator(); CloseThemeCreator();
} }
$effect(() => { $effect(() => {
if (themeLoaded) { UpdateThemePreview(theme);
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';
@@ -165,8 +156,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={(e) => { item.direction === 'vertical' && toggleAccordion(item.title, e) }} onclick={() => { item.direction === 'vertical' && toggleAccordion(item.title) }}
onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title, e) }} onkeydown={(e) => { e.key === 'Enter' && item.direction === 'vertical' && toggleAccordion(item.title) }}
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>
@@ -175,14 +166,7 @@
</div> </div>
{#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 items-center justify-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}
@@ -201,24 +185,21 @@
<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 --> {/key}
<CodeEditor className="h-[400px]" {...(item.props as CodeEditorProps)} />
{/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 items-center h-16 gap-2 px-2 py-2 mb-4 bg-white rounded-lg shadow-lg dark:bg-zinc-700">
<div class="h-full"> <div class="h-full ">
<img src={URL.createObjectURL(image.blob)} alt={image.variableName} class="object-contain h-full rounded" /> <img src={image.url} 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="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" 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"
/> />
<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>
@@ -226,14 +207,14 @@
</div> </div>
{/each} {/each}
<div class="flex overflow-hidden relative gap-1 justify-center place-items-center w-full h-8 rounded-lg transition bg-zinc-200 dark:bg-zinc-700"> <div class="relative flex justify-center w-full h-8 gap-1 overflow-hidden transition rounded-lg place-items-center bg-zinc-200 dark:bg-zinc-700">
<span class='font-IconFamily'>{'\uec60'}</span> <span class='font-IconFamily'>{'\uec60'}</span>
<span class='dark:text-white'>Add image</span> <span class='dark:text-white'>Add image</span>
<input type="file" accept='image/*' onchange={onImageUpload} class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" /> <input type="file" accept='image/*' onchange={onImageUpload} class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
</div> </div>
{:else if item.type === 'lightDarkToggle'} {:else if item.type === 'lightDarkToggle'}
<button <button
class="overflow-hidden relative px-4 py-1 text-xl font-medium rounded-lg transition bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 font-IconFamily" class="relative px-4 py-1 overflow-hidden text-xl font-medium transition rounded-lg bg-zinc-200 dark:bg-zinc-700 hover:bg-zinc-300 dark:hover:bg-zinc-600 font-IconFamily"
onclick={() => (item.props as LightDarkToggleProps).onChange(!(item.props as LightDarkToggleProps).state)} onclick={() => (item.props as LightDarkToggleProps).onChange(!(item.props as LightDarkToggleProps).state)}
> >
{#key (item.props as LightDarkToggleProps).state} {#key (item.props as LightDarkToggleProps).state}
@@ -255,23 +236,10 @@
{/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'>
{#if codeEditorFullscreen} <div class='flex flex-col w-full min-h-screen p-2 bg-zinc-100 dark:bg-zinc-800 dark:text-white'>
<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='no-underline font-IconFamily pr-0.5'>{'\ueb44'}</span>
<span class='underline'> <span class='underline'>
Need help? Check out the docs! Need help? Check out the docs!
</span> </span>
@@ -286,7 +254,7 @@
type='text' type='text'
placeholder='What is your theme called?' placeholder='What is your theme called?'
bind:value={theme.name} bind:value={theme.name}
class='p-2 mb-4 w-full rounded-lg border-0 transition dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600' /> class='w-full p-2 mb-4 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600' />
</div> </div>
<div> <div>
@@ -295,23 +263,23 @@
id='themeDescription' id='themeDescription'
placeholder="Don't worry, this one's optional!" placeholder="Don't worry, this one's optional!"
bind:value={theme.description} bind:value={theme.description}
class='p-2 w-full rounded-lg border-0 transition dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:outline-none focus:ring-1 focus:ring-zinc-100 dark:focus:ring-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600'></textarea> class='w-full p-2 transition border-0 rounded-lg dark:placeholder-zinc-300 bg-zinc-200 dark:bg-zinc-700 focus:outline-none focus:ring-1 focus:ring-zinc-100 dark:focus:ring-zinc-700 focus:bg-zinc-300/50 dark:focus:bg-zinc-600'></textarea>
</div> </div>
<Divider /> <Divider />
<div class="flex overflow-hidden relative gap-1 justify-center place-items-center w-full rounded-lg transition aspect-theme group bg-zinc-200 dark:bg-zinc-700"> <div class="relative flex justify-center w-full gap-1 overflow-hidden transition rounded-lg aspect-theme group place-items-center bg-zinc-200 dark:bg-zinc-700">
<div class={`transition pointer-events-none z-30 font-IconFamily ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}> <div class={`transition pointer-events-none z-30 font-IconFamily ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>
{'\uec60'} {'\uec60'}
</div> </div>
<span class={`dark:text-white pointer-events-none z-30 transition ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>{theme.coverImage ? 'Change' : 'Add'} cover image</span> <span class={`dark:text-white pointer-events-none z-30 transition ${ theme.coverImage ? 'opacity-0 group-hover:opacity-100' : ''}`}>{theme.coverImage ? 'Change' : 'Add'} cover image</span>
<input type="file" accept='image/*' onchange={onCoverImageUpload} class="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer" /> <input type="file" accept='image/*' onchange={onCoverImageUpload} class="absolute inset-0 z-10 w-full h-full opacity-0 cursor-pointer" />
{#if !theme.hideThemeName && theme.coverImage} {#if !theme.hideThemeName && theme.coverImage}
<div class="absolute z-30 opacity-100 transition-opacity pointer-events-none group-hover:opacity-0">{theme.name}</div> <div class="absolute z-30 transition-opacity opacity-100 pointer-events-none group-hover:opacity-0">{theme.name}</div>
{/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 transition-opacity opacity-0 pointer-events-none group-hover:opacity-100 bg-black/20"></div>
<img src="{typeof theme.coverImage === 'string' ? theme.coverImage : URL.createObjectURL(theme.coverImage)}" alt='Cover' class="object-cover absolute z-0 w-full h-full rounded" /> <img src={theme.coverImageUrl} alt='Cover' class="absolute z-0 object-cover w-full h-full rounded" />
{/if} {/if}
</div> </div>
+2 -2
View File
@@ -2,6 +2,6 @@ export interface SettingsList {
title: string; title: string;
id: number; id: number;
description: string; description: string;
Component: any /* TODO: Give this a type */; Component: any; /* TODO: Give this a type */
props?: any; props?: any;
} }
+1 -1
View File
@@ -4,4 +4,4 @@ export type Theme = {
coverImage: string; coverImage: string;
marqueeImage: string; marqueeImage: string;
id: string; id: string;
}; };
+25 -25
View File
@@ -1,36 +1,36 @@
import type { Subscriber, Unsubscriber } from "svelte/store"; import type { Subscriber, Unsubscriber } from "svelte/store";
export class Standalone { export class Standalone {
private static instance: Standalone; private static instance: Standalone;
private _standalone = $state(false); private _standalone = $state(false);
private subscribers = new Set<Subscriber<boolean>>(); private subscribers = new Set<Subscriber<boolean>>();
private constructor() {} private constructor() {}
public static getInstance(): Standalone { public static getInstance(): Standalone {
if (!Standalone.instance) { if (!Standalone.instance) {
Standalone.instance = new Standalone(); Standalone.instance = new Standalone();
} }
return Standalone.instance; return Standalone.instance;
} }
public setStandalone(value: boolean) { public setStandalone(value: boolean) {
this._standalone = value; this._standalone = value;
this.subscribers.forEach((subscriber) => subscriber(value)); this.subscribers.forEach(subscriber => subscriber(value));
} }
public get standalone() { public get standalone() {
return this._standalone; return this._standalone;
} }
public subscribe(run: Subscriber<boolean>): Unsubscriber { public subscribe(run: Subscriber<boolean>): Unsubscriber {
this.subscribers.add(run); this.subscribers.add(run);
run(this._standalone); run(this._standalone);
return () => { return () => {
this.subscribers.delete(run); this.subscribers.delete(run);
}; };
} }
} }
export const standalone = Standalone.getInstance(); export const standalone = Standalone.getInstance();
+13 -33
View File
@@ -1,31 +1,23 @@
import type { LoadedCustomTheme } from "@/types/CustomThemes"; import type { LoadedCustomTheme } from '@/types/CustomThemes';
export function generateImageId(): string { export function generateImageId(): string {
return Math.random().toString(36).substr(2, 9); return Math.random().toString(36).substr(2, 9);
} }
export function handleImageUpload( export function handleImageUpload(event: Event, theme: LoadedCustomTheme): Promise<LoadedCustomTheme> | LoadedCustomTheme {
event: Event,
theme: LoadedCustomTheme,
): Promise<LoadedCustomTheme> | LoadedCustomTheme {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ""; input.value = '';
if (file) { if (file) {
return new Promise((resolve) => { return new Promise((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const imageBlob = await fetch(reader.result as string).then((res) => const imageBlob = await fetch(reader.result as string).then(res => res.blob());
res.blob(),
);
const imageId = generateImageId(); const imageId = generateImageId();
const variableName = `custom-image-${theme.CustomImages.length}`; const variableName = `custom-image-${theme.CustomImages.length}`;
resolve({ resolve({
...theme, ...theme,
CustomImages: [ CustomImages: [...theme.CustomImages, { id: imageId, blob: imageBlob, variableName, url: URL.createObjectURL(imageBlob) }],
...theme.CustomImages,
{ id: imageId, blob: imageBlob, variableName, url: null },
],
}); });
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
@@ -34,47 +26,35 @@ export function handleImageUpload(
return theme; return theme;
} }
export function handleRemoveImage( export function handleRemoveImage(imageId: string, theme: LoadedCustomTheme): LoadedCustomTheme {
imageId: string,
theme: LoadedCustomTheme,
): LoadedCustomTheme {
return { return {
...theme, ...theme,
CustomImages: theme.CustomImages.filter((image) => image.id !== imageId), CustomImages: theme.CustomImages.filter((image) => image.id !== imageId),
} as LoadedCustomTheme; } as LoadedCustomTheme;
} }
export function handleImageVariableChange( export function handleImageVariableChange(imageId: string, variableName: string, theme: LoadedCustomTheme): LoadedCustomTheme {
imageId: string,
variableName: string,
theme: LoadedCustomTheme,
): LoadedCustomTheme {
return { return {
...theme, ...theme,
CustomImages: theme.CustomImages.map((image) => CustomImages: theme.CustomImages.map((image) =>
image.id === imageId ? { ...image, variableName } : image, image.id === imageId ? { ...image, variableName } : image
), ),
} as LoadedCustomTheme; } as LoadedCustomTheme;
} }
export function handleCoverImageUpload( export function handleCoverImageUpload(event: Event, theme: LoadedCustomTheme): Promise<LoadedCustomTheme> {
event: Event,
theme: LoadedCustomTheme,
): Promise<LoadedCustomTheme> {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ""; input.value = '';
if (file) { if (file) {
return new Promise((resolve) => { return new Promise((resolve) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async () => { reader.onload = async () => {
const imageBlob = await fetch(reader.result as string).then((res) => const imageBlob = await fetch(reader.result as string).then(res => res.blob());
res.blob(), resolve({ ...theme, coverImage: imageBlob, coverImageUrl: URL.createObjectURL(imageBlob) });
);
resolve({ ...theme, coverImage: imageBlob });
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
}); });
} }
return Promise.resolve(theme); return Promise.resolve(theme);
} }
+3 -11
View File
@@ -1,12 +1,4 @@
import { createManifest } from "../../lib/createManifest"; import { createManifest } from '../../lib/createManifest'
import baseManifest from "./manifest.json"; import baseManifest from './manifest.json'
import pkg from "../../package.json";
export const brave = createManifest( export const brave = createManifest(baseManifest, 'brave')
{
...baseManifest,
version: pkg.version,
description: pkg.description,
},
"brave",
);
+3 -11
View File
@@ -1,12 +1,4 @@
import { createManifest } from "../../lib/createManifest"; import { createManifest } from '../../lib/createManifest'
import baseManifest from "./manifest.json"; import baseManifest from './manifest.json'
import pkg from "../../package.json";
export const chrome = createManifest( export const chrome = createManifest(baseManifest, 'chrome')
{
...baseManifest,
version: pkg.version,
description: pkg.description,
},
"chrome",
);
+3 -11
View File
@@ -1,12 +1,4 @@
import { createManifest } from "../../lib/createManifest"; import { createManifest } from '../../lib/createManifest'
import baseManifest from "./manifest.json"; import baseManifest from './manifest.json'
import pkg from "../../package.json";
export const edge = createManifest( export const edge = createManifest(baseManifest, 'edge')
{
...baseManifest,
version: pkg.version,
description: pkg.description,
},
"edge",
);
+7 -9
View File
@@ -1,22 +1,20 @@
import { createManifest } from "../../lib/createManifest"; import { createManifest } from '../../lib/createManifest'
import baseManifest from "./manifest.json"; import baseManifest from './manifest.json'
import pkg from "../../package.json"; import pkg from '../../package.json'
const updatedFirefoxManifest = { const updatedFirefoxManifest = {
...baseManifest, ...baseManifest,
version: pkg.version,
description: pkg.description,
background: { background: {
scripts: [baseManifest.background.service_worker], scripts: [baseManifest.background.service_worker],
}, },
action: { action: {
default_popup: "interface/index.html#settings", "default_popup": "interface/index.html#settings",
}, },
browser_specific_settings: { browser_specific_settings: {
gecko: { gecko: {
id: pkg.author.email, id: pkg.author.email,
}, },
}, }
}; }
export const firefox = createManifest(updatedFirefoxManifest, "firefox"); export const firefox = createManifest(updatedFirefoxManifest, 'firefox')
+10
View File
@@ -1,6 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "BetterSEQTA+", "name": "BetterSEQTA+",
"version": "3.4.2",
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
"icons": { "icons": {
"32": "resources/icons/icon-32.png", "32": "resources/icons/icon-32.png",
"48": "resources/icons/icon-48.png", "48": "resources/icons/icon-48.png",
@@ -31,9 +33,17 @@
} }
], ],
"web_accessible_resources": [ "web_accessible_resources": [
{
"resources": ["*://*/*"],
"matches": ["*://*/*"]
},
{ {
"resources": ["resources/icons/*"], "resources": ["resources/icons/*"],
"matches": ["*://*/*"] "matches": ["*://*/*"]
},
{
"resources": ["seqta/utils/migration/migrate.html"],
"matches": ["*://*/*"]
} }
] ]
} }
+3 -11
View File
@@ -1,12 +1,4 @@
import { createManifest } from "../../lib/createManifest"; import { createManifest } from '../../lib/createManifest'
import baseManifest from "./manifest.json"; import baseManifest from './manifest.json'
import pkg from "../../package.json";
export const opera = createManifest( export const opera = createManifest(baseManifest, 'opera')
{
...baseManifest,
version: pkg.version,
description: pkg.description,
},
"opera",
);
+6 -9
View File
@@ -1,19 +1,16 @@
import { createManifest } from "../../lib/createManifest"; import { createManifest } from '../../lib/createManifest'
import baseManifest from "./manifest.json"; import baseManifest from './manifest.json'
import pkg from "../../package.json";
const updatedSafariManifest = { const updatedSafariManifest = {
...baseManifest, ...baseManifest,
version: pkg.version,
description: pkg.description,
browser_specific_settings: { browser_specific_settings: {
safari: { safari: {
strict_min_version: "15.4", strict_min_version: '15.4',
strict_max_version: "*", strict_max_version: '*',
}, },
// ^^^ https://developer.apple.com/documentation/safariservices/safari_web_extensions/optimizing_your_web_extension_for_safari#3743239 // ^^^ https://developer.apple.com/documentation/safariservices/safari_web_extensions/optimizing_your_web_extension_for_safari#3743239
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings#safari_properties // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings#safari_properties
}, },
}; }
export const safari = createManifest(updatedSafariManifest, "safari"); export const safari = createManifest(updatedSafariManifest, 'safari')
-226
View File
@@ -1,226 +0,0 @@
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,
},
"*",
);
} else if (event.data.type === "triggerKeyboardEvent") {
// Handle keyboard event triggering from content script
const { key, code, altKey, ctrlKey, metaKey, shiftKey, keyCode } = event.data;
const keyboardEvent = new KeyboardEvent('keydown', {
key,
code,
keyCode: keyCode || 0,
which: keyCode || 0,
altKey: altKey || false,
ctrlKey: ctrlKey || false,
metaKey: metaKey || false,
shiftKey: shiftKey || false,
bubbles: true,
cancelable: true
});
document.dispatchEvent(keyboardEvent);
}
});

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