mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40d7ece12b | |||
| 098ab27a01 | |||
| 170b1cf5c3 | |||
| 004c3cc61d | |||
| 1b938e2748 | |||
| 652e84783b | |||
| 17c2685cae | |||
| 5e89507276 | |||
| 0204b97de8 | |||
| d849951a66 | |||
| 1531afd046 | |||
| add8a90c77 | |||
| b9c3c2b5c5 | |||
| 1cc34c38a8 | |||
| e5859f419a | |||
| aadc295bdb | |||
| af2ef23078 | |||
| 7150f03d77 | |||
| a381de7c9b | |||
| ce6a5cfdc4 | |||
| de75468f2b | |||
| 011c1eddb4 | |||
| c9443bad27 | |||
| 445aa9d071 | |||
| 14a2e93b3a | |||
| 3746b05af2 | |||
| 1d215d8c75 | |||
| b4b1fed576 | |||
| e0009ad8dc | |||
| 401947031b | |||
| 9da8e104a8 | |||
| 3aef2312d0 | |||
| b402221477 | |||
| 8b6bda6dff | |||
| eed8bac45a | |||
| 3b7bbc9bc6 | |||
| 9820595a70 | |||
| 0a33ca7f6e | |||
| bba96d5159 | |||
| d9fe70f442 | |||
| 32c5c8392b | |||
| cc3f06b383 | |||
| 9ad90e9416 | |||
| 87fdda459a | |||
| 6c11bb8143 | |||
| 391fcfb9dd | |||
| 355c5f2d46 | |||
| afdb4336f8 | |||
| 7a04b22b22 | |||
| f594ed4902 | |||
| f1afa74ee6 | |||
| db3f0e0d81 | |||
| aceefa16c0 | |||
| 89589fe3dc | |||
| 2afe2364e4 | |||
| 2c0f48877f | |||
| 8791038bcf | |||
| aba2ba5bfa | |||
| 2b01834765 | |||
| 79c4fb511b | |||
| 14823dcc91 | |||
| 9d3494eb56 | |||
| 6af7c32c88 | |||
| c205a52f03 | |||
| a6d95f27ed | |||
| f05cd66e88 | |||
| a151e7a07e | |||
| 1f49fa4bae | |||
| 86d9cfe50c | |||
| ae59640162 | |||
| 5f935cd819 | |||
| 90e3a946bf | |||
| 51c940cdd9 | |||
| 07ff6d25ca | |||
| 89fd9bbd89 | |||
| c7033e61fb | |||
| 39f8cb1634 | |||
| 705c106da8 | |||
| 9b52bae404 | |||
| 9a002d18b0 | |||
| 3847ef4269 | |||
| f0d0068a2e | |||
| b89a6c634c | |||
| 29cfb4c792 | |||
| 5b590512ee | |||
| 3ff8ef144a | |||
| d9abed1c5d | |||
| 82a789bbec | |||
| ce6538f850 | |||
| 979ae7149f | |||
| 6e71437fe8 | |||
| 940ecf8714 | |||
| e0cc2e0fdf | |||
| 5a19ef92e8 | |||
| 0a3781e9c2 | |||
| a2e39c9d84 | |||
| 520abbb5c3 | |||
| d0a11da15f | |||
| fd5802f9a3 | |||
| 380d829d19 | |||
| 702528fb0c | |||
| 2c077bc755 | |||
| fd86e57442 | |||
| 60ce18280e | |||
| 668dbfd78b | |||
| 810aa17f15 | |||
| b64558e50a | |||
| 9b969bd708 | |||
| 1945f7c592 | |||
| 3e26d9af3c | |||
| 3c8d7e246b | |||
| 2e56518330 | |||
| e67f3110e0 | |||
| a67f4d2e25 | |||
| d6025140fd | |||
| 88e9ddf29c | |||
| 11adc4f933 | |||
| 15691e8d94 | |||
| 754b8d0589 | |||
| 1d634d0da1 | |||
| 7136de90be | |||
| 466628479e | |||
| 9c08d0bac2 | |||
| 6c5320007f | |||
| 4734a443b4 | |||
| 7c38e1dc29 | |||
| f3f90ef2a8 | |||
| 9bcc94aa8a | |||
| ff2431f269 | |||
| b442194bc5 | |||
| b59c0eae25 | |||
| e895ce9f6b | |||
| 7192f41535 | |||
| f1b707ab25 | |||
| 7f47cb8183 | |||
| 7f5d138bc9 | |||
| cef0f29640 | |||
| 157343dda9 | |||
| 7705c0a3cd | |||
| 7def7b190c | |||
| c294fb7369 | |||
| 0dbbef0eb1 | |||
| c3c747d996 | |||
| cdc8062275 | |||
| 1857b5ff01 | |||
| 700e3ebb48 | |||
| 16b9610301 | |||
| 7d11e203a6 | |||
| 530f07e640 | |||
| 08586781ce | |||
| 3ca5a49769 | |||
| 886c79b3ee | |||
| 30aa39142d | |||
| 4188ef0d67 | |||
| ad9a013b00 | |||
| cd1f954cc7 | |||
| 6ef6c986dc | |||
| f2e28175a0 | |||
| 3ddcb204ef | |||
| 766f0e6d3f | |||
| f1fcba58ef | |||
| dba2d13bb3 | |||
| 30bf345b86 | |||
| 0e98f52058 | |||
| f89508deb2 | |||
| c7b69ad97b | |||
| 2ef8bb215a | |||
| 16273cf012 | |||
| 13d3ccd8e4 | |||
| 7ebc4db9db | |||
| ed9d662ba4 | |||
| 8647e0b272 | |||
| d93abec615 | |||
| 339b409937 | |||
| 0fb05c7f26 | |||
| b866dde6e2 | |||
| a42d781955 | |||
| b03e99faa2 | |||
| c87cbce218 | |||
| 0d6aa1e5fd | |||
| a396aa8a9d | |||
| f3048d0cae | |||
| adb3beb2b1 | |||
| 860916a5b8 | |||
| 21e0b0a05e | |||
| f7ca1c7ddd | |||
| 3fb70f280a | |||
| 58b1a70cc9 | |||
| ce2b376469 | |||
| 2ded9b3f83 | |||
| a0e8fc2233 | |||
| 3527817ed1 | |||
| 5cf0a928c9 | |||
| ae84a22128 | |||
| b16a48c26c | |||
| ceb9424ab9 | |||
| 52192002e7 | |||
| 4160f6ee10 | |||
| 028c011a98 | |||
| bb6bf7bfb2 | |||
| c5cef0c9a7 | |||
| e6d418d569 | |||
| c4ff994e38 | |||
| da9a1e8c0b | |||
| 6eebb6911a | |||
| c0271968e2 | |||
| 871b893532 | |||
| 0cad870c28 | |||
| 4f38a28d9c | |||
| f3029d6d9a | |||
| 10f67c8d60 | |||
| 9030f20540 | |||
| e12a724ab8 | |||
| b5f418938a | |||
| 743deb9fe0 | |||
| a696f5b333 | |||
| 397e440b6f | |||
| a6f0e5bc55 | |||
| cadb8f6269 | |||
| 0f3f5fca83 | |||
| e12fe43ed8 | |||
| 5b94c2c9b5 | |||
| f2d748baf9 | |||
| 5dfd738848 | |||
| e88b2e0404 | |||
| 10f3c1e942 | |||
| 9911966fe7 | |||
| d49f4c539c | |||
| 77074f085a | |||
| ae8b890282 | |||
| 3d5aa7ebd9 | |||
| 7251e4eee5 | |||
| ad0a329331 | |||
| 43a780de8e | |||
| de9c6bc481 | |||
| bf1fe51e94 | |||
| 1f3dea55bb | |||
| 980432c501 | |||
| b5c3a0fce8 | |||
| 64bc9e6cad | |||
| 839366432e | |||
| e5a410ff58 | |||
| 26613beb02 | |||
| db92af7405 | |||
| 78909bc242 | |||
| b503363d64 | |||
| b69d5f47fc | |||
| 404d3c02f3 | |||
| e28a3e1bc6 | |||
| f1b7c3475e | |||
| 964a026e7a | |||
| c7d9e1d955 | |||
| e305b70035 |
@@ -0,0 +1,114 @@
|
||||
name: 🙋 New Contributor - Need Help Getting Started
|
||||
description: Perfect for first-time contributors who need guidance
|
||||
labels: ["help wanted", "documentation"]
|
||||
title: "[NEW CONTRIBUTOR] "
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Hi there! 👋
|
||||
Welcome to BetterSEQTA+! We're excited to have you join our community.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Tell us about yourself (check all that apply)
|
||||
options:
|
||||
- label: "This is my first time contributing to open source"
|
||||
required: false
|
||||
- label: "I'm new to browser extensions"
|
||||
required: false
|
||||
- label: "I'm new to TypeScript/JavaScript"
|
||||
required: false
|
||||
- label: "I have some coding experience but new to this project"
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: What would you like to work on? (check all that apply)
|
||||
options:
|
||||
- label: "Fix a bug 🐛"
|
||||
required: false
|
||||
- label: "Add a new feature ✨"
|
||||
required: false
|
||||
- label: "Improve documentation 📚"
|
||||
required: false
|
||||
- label: "Create a plugin 🧩"
|
||||
required: false
|
||||
- label: "Improve the UI/design 🎨"
|
||||
required: false
|
||||
- label: "Write tests 🧪"
|
||||
required: false
|
||||
- label: "Not sure - I want to help but need guidance!"
|
||||
required: false
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Have you read our guides?
|
||||
options:
|
||||
- label: "Getting Started Guide (see docs/GETTING_STARTED_CONTRIBUTING.md)"
|
||||
required: true
|
||||
- label: "Architecture Guide (see docs/ARCHITECTURE.md)"
|
||||
required: true
|
||||
- label: "Plugin Development Guide (see docs/plugins/README.md)"
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Have you set up the development environment yet?
|
||||
options:
|
||||
- label: Yes, everything works! 🎉
|
||||
required: false
|
||||
- label: Partially - I can run `npm run dev` but having some issues
|
||||
required: false
|
||||
- label: No, I need help with setup
|
||||
required: false
|
||||
- label: I tried but ran into errors (please describe below)
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Errors
|
||||
description: "Please list any encountered errors here:"
|
||||
placeholder: "I am encountering issues with..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Questions or Issues
|
||||
description: "Tell us:
|
||||
1. What specifically would you like help with?
|
||||
2. Are you stuck on anything?
|
||||
3. Do you have any questions about the codebase?
|
||||
4. Is there anything in our documentation that's unclear?"
|
||||
placeholder: "I want help with..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Ideas or Suggestions
|
||||
description: "If you have any ideas for features, improvements, or just want to share your thoughts:"
|
||||
placeholder: "It would be cool if I could help add..."
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## What happens next?
|
||||
|
||||
A maintainer will respond within 24-48 hours to:
|
||||
- Answer your questions
|
||||
- Suggest some good issues to work on
|
||||
- Help you with setup if needed
|
||||
- Point you to relevant documentation
|
||||
|
||||
Don't worry if you're new to this - we're here to help! Every expert was once a beginner. 🚀
|
||||
|
||||
**Join our [Discord server](https://discord.gg/YzmbnCDkat) for real-time help and community chat!**
|
||||
|
||||
|
||||
|
||||
|
||||
+23
-5
@@ -1,13 +1,31 @@
|
||||
# Contributing
|
||||
# Contributing to BetterSEQTA+
|
||||
|
||||
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.
|
||||
Hey there! 👋 Thanks for your interest in contributing to BetterSEQTA+! We're excited to have you join our community of contributors.
|
||||
|
||||
## 🚀 New Contributors Start Here!
|
||||
|
||||
**Never contributed to an open source project before?** No worries! We've made it super easy to get started:
|
||||
|
||||
- **📖 Read our [Getting Started Guide](./docs/GETTING_STARTED_CONTRIBUTING.md)** - This walks you through everything step-by-step, from setting up your development environment to making your first pull request.
|
||||
- **🏗️ Understand the codebase** with our [Architecture Guide](./docs/ARCHITECTURE.md)
|
||||
- **🔧 Having issues?** Check our [Troubleshooting Guide](./docs/TROUBLESHOOTING.md)
|
||||
|
||||
We have lots of [`good first issue`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/good%20first%20issue) labels that are perfect for beginners!
|
||||
|
||||
## Discussion Before Contributing
|
||||
|
||||
For significant changes, please first discuss what you'd like to change via:
|
||||
- Opening an issue
|
||||
- Joining our Discord server
|
||||
- Emailing the maintainers
|
||||
|
||||
This helps ensure your contribution aligns with the project's goals and saves you time!
|
||||
|
||||
## 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)
|
||||
- **Discord Server**: [Join our Discord](https://discord.gg/YzmbnCDkat)
|
||||
- **GitHub Discussions**: For longer-form conversations
|
||||
- **GitHub Issues**: For bug reports and feature requests
|
||||
|
||||
@@ -21,7 +39,7 @@ If you're interested in creating plugins for BetterSEQTA+, check out our plugin
|
||||
## 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.
|
||||
2. Fork the repo and create your branch from `master`.
|
||||
2. Fork the repo and create your branch from `main`.
|
||||
3. When writing your pull request, make sure to use the pull request template.
|
||||
|
||||
### Pull Request Template
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#
|
||||
|
||||
<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" />
|
||||
</a>
|
||||
@@ -10,7 +8,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://chrome.google.com/webstore/detail/betterseqta%20/afdgaoaclhkhemfkkkonemoapeinchel"><img src="https://user-images.githubusercontent.com/95666457/149519713-159d7ef7-2c21-4034-a616-f037ff46d9a4.png" alt="ChromeDownload" width="250"></a>
|
||||
<a target="_blank" href="https://discord.gg/YzmbnCDkat"><img src="https://github.com/SethBurkart123/EvenBetterSEQTA/assets/108050083/23055730-b16e-44c0-9bef-221d8545af92" width="240" style="border-radius:10%;" /></a>
|
||||
<a target="_blank" href="https://discord.gg/YzmbnCDkat"><img src="https://github.com/BetterSEQTA/BetterSEQTA-Plus/assets/108050083/23055730-b16e-44c0-9bef-221d8545af92" width="240" style="border-radius:10%;" /></a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@@ -56,58 +54,48 @@ If you are looking to create custom themes, I would recommend you start at the o
|
||||
|
||||
Don't worry- if you get stuck feel free to ask around in the [discord](https://discord.gg/YzmbnCDkat). We're open and happy to help out! Happy creating :)
|
||||
|
||||
## Getting started
|
||||
## 🚀 Want to Contribute?
|
||||
|
||||
**1. Clone the repository**
|
||||
**New contributors welcome!** 🎉 We've made it easy to get started:
|
||||
|
||||
```
|
||||
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
|
||||
- **👋 New to the project?** Start with our [Getting Started Guide](./docs/GETTING_STARTED_CONTRIBUTING.md)
|
||||
- **🏗️ Want to understand the code?** Check out our [Architecture Guide](./docs/ARCHITECTURE.md)
|
||||
- **🧩 Interested in plugins?** Read our [Plugin Development Guide](./docs/plugins/README.md)
|
||||
- **🐛 Found a bug?** Open an [issue](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues) or fix it yourself!
|
||||
- **💬 Need help?** Join our [Discord community](https://discord.gg/YzmbnCDkat)
|
||||
|
||||
We have lots of https://github.com/BetterSEQTA/BetterSEQTA-Plus/labels/good%20first%20issue labels perfect for beginners!
|
||||
|
||||
## Quick Development Setup
|
||||
|
||||
**1. Fork & Clone**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/BetterSEQTA-Plus
|
||||
cd BetterSEQTA-Plus
|
||||
```
|
||||
|
||||
**2. Install dependencies**
|
||||
|
||||
You may install the dependencies like below:
|
||||
|
||||
```
|
||||
npm install # or your preferred package manager like pnpm or yarn
|
||||
**2. Install & Run**
|
||||
```bash
|
||||
npm install --legacy-peer-deps
|
||||
npm run dev
|
||||
```
|
||||
|
||||
But it is recommended to do it like this:
|
||||
**3. Load in Browser**
|
||||
1. Go to `chrome://extensions`
|
||||
2. Enable "Developer mode"
|
||||
3. Click "Load unpacked" → Select `dist` folder
|
||||
4. Visit a SEQTA page to see it work! 🎉
|
||||
> [!WARNING]
|
||||
> Whenever you update the extension while not in dev mode, you will need to use the reload button on the extension page.
|
||||
|
||||
📚 **Need more details?** Check our [detailed setup guide](./docs/GETTING_STARTED_CONTRIBUTING.md#your-first-30-minutes)
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
npm run build # Build for all browsers
|
||||
npm run zip # Package for distribution (requires 7-Zip)
|
||||
```
|
||||
npm install --legacy-peer-deps # Only NPM supported
|
||||
```
|
||||
|
||||
### Running Development
|
||||
|
||||
**3. Run the dev script (it updates as you save files)**
|
||||
|
||||
```
|
||||
npm run dev # or use your preferred package manager
|
||||
```
|
||||
|
||||
### Building for production
|
||||
|
||||
**4. Run the build script**
|
||||
|
||||
```
|
||||
npm run build # or use your preferred package manager
|
||||
```
|
||||
|
||||
**4.1. Package it up (optional)**
|
||||
|
||||
```
|
||||
npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your preferred package manager
|
||||
```
|
||||
|
||||
**5. Load the extension into chrome**
|
||||
|
||||
- Go to `chrome://extensions`
|
||||
- Enable developer mode
|
||||
- Click `Load unpacked`
|
||||
- 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.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
@@ -131,7 +119,7 @@ Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plu
|
||||
|
||||
## 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 from lead developers [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) with help from other volunteers.
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
# BetterSEQTA+ Architecture
|
||||
|
||||
Hey there! 👋 New to the codebase and feeling a bit lost? Don't worry - this guide will help you understand how everything fits together!
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [High-Level Architecture](#high-level-architecture)
|
||||
- [Core Components](#core-components)
|
||||
- [Plugin System](#plugin-system)
|
||||
- [File Structure Explained](#file-structure-explained)
|
||||
- [Data Flow](#data-flow)
|
||||
- [Browser Extension Basics](#browser-extension-basics)
|
||||
|
||||
## Overview
|
||||
|
||||
BetterSEQTA+ is a browser extension that enhances SEQTA Learn by:
|
||||
- Adding new features through a plugin system
|
||||
- Providing customizable themes and UI improvements
|
||||
- Offering better navigation and user experience
|
||||
|
||||
Think of it like this: **SEQTA Learn + BetterSEQTA+ = Enhanced SEQTA Experience**
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ BROWSER EXTENSION │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Background │ │ Content Script │ │
|
||||
│ │ Script │ │ (SEQTA.ts) │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ - Settings │◄───┤ - Page Detection│ │
|
||||
│ │ - Storage │ │ - Plugin Loading│ │
|
||||
│ │ - Updates │ │ - UI Injection │ │
|
||||
│ └─────────────────┘ └──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────▼─────────┐ │
|
||||
│ │ Plugin System │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ │ │
|
||||
│ │ │ Built-in │ │ │
|
||||
│ │ │ Plugins │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ - Themes │ │ │
|
||||
│ │ │ - Search │ │ │
|
||||
│ │ │ - Timetable │ │ │
|
||||
│ │ │ - etc... │ │ │
|
||||
│ │ └─────────────┘ │ │
|
||||
│ └───────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────▼─────────┐ │
|
||||
│ │ Settings UI │ │
|
||||
│ │ (Svelte App) │ │
|
||||
│ │ │ │
|
||||
│ │ - Plugin Config │ │
|
||||
│ │ - Theme Creator │ │
|
||||
│ │ - General Settings│ │
|
||||
│ └───────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────▼─────────┐
|
||||
│ SEQTA Learn │
|
||||
│ Website │
|
||||
└───────────────────┘
|
||||
```
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Entry Point (`src/SEQTA.ts`)
|
||||
This is where it all begins! When you visit a SEQTA page:
|
||||
1. Detects if you're on a SEQTA Learn page
|
||||
2. Injects our CSS styles
|
||||
3. Changes the favicon to BetterSEQTA+ icon
|
||||
4. Loads settings from storage
|
||||
5. Initializes the plugin system
|
||||
|
||||
### 2. Plugin System (`src/plugins/`)
|
||||
The heart of BetterSEQTA+! This is what makes it extensible:
|
||||
- **Plugin Manager**: Registers and manages all plugins
|
||||
- **Built-in Plugins**: Pre-made plugins (themes, search, etc.)
|
||||
- **Plugin API**: Provides plugins with tools to interact with SEQTA
|
||||
|
||||
### 3. Settings UI (`src/interface/`)
|
||||
A Svelte application that lets users:
|
||||
- Enable/disable plugins
|
||||
- Configure plugin settings
|
||||
- Create custom themes
|
||||
- Browse the theme store
|
||||
|
||||
### 4. Background Script (`src/background.ts`)
|
||||
Runs in the background and handles:
|
||||
- Extension-wide settings storage
|
||||
- Communication between different parts
|
||||
- Update notifications
|
||||
|
||||
## Plugin System
|
||||
|
||||
Our plugin system is what makes BetterSEQTA+ so powerful. Here's how it works:
|
||||
|
||||
### Plugin Lifecycle
|
||||
```
|
||||
Plugin Registration → Settings Loading → Plugin Initialization → Running → Cleanup
|
||||
```
|
||||
|
||||
### Built-in Plugins Overview
|
||||
|
||||
| Plugin | What it does | Files |
|
||||
|--------|-------------|-------|
|
||||
| **Themes** | Custom CSS themes and backgrounds | `src/plugins/built-in/themes/` |
|
||||
| **Global Search** | Search across all SEQTA content | `src/plugins/built-in/globalSearch/` |
|
||||
| **Timetable** | Enhanced timetable features | `src/plugins/built-in/timetable/` |
|
||||
| **Profile Picture** | Custom profile pictures | `src/plugins/built-in/profilePicture/` |
|
||||
| **Animated Background** | Moving background animations | `src/plugins/built-in/animatedBackground/` |
|
||||
|
||||
### Creating a Plugin
|
||||
Every plugin follows this structure:
|
||||
```typescript
|
||||
const myPlugin: Plugin = {
|
||||
id: "unique-plugin-id",
|
||||
name: "Human Readable Name",
|
||||
description: "What does this plugin do?",
|
||||
version: "1.0.0",
|
||||
settings: { /* user configurable options */ },
|
||||
run: async (api) => {
|
||||
// Your plugin code goes here!
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## File Structure Explained
|
||||
|
||||
```
|
||||
src/
|
||||
├── SEQTA.ts # 🚀 Main entry point - start reading here!
|
||||
├── background.ts # 🔧 Background script for extension
|
||||
├── manifests/ # 📦 Browser extension manifests
|
||||
├── plugins/ # 🧩 Plugin system (the magic happens here!)
|
||||
│ ├── core/ # 🏗️ Plugin infrastructure
|
||||
│ ├── built-in/ # 🎁 Pre-made plugins
|
||||
│ └── index.ts # 📋 Plugin registration
|
||||
├── interface/ # 🎨 Settings UI (Svelte app)
|
||||
│ ├── pages/ # 📄 Settings pages
|
||||
│ ├── components/ # 🧱 Reusable UI components
|
||||
│ └── main.ts # 🏠 Settings app entry point
|
||||
├── seqta/ # 🔗 SEQTA-specific utilities
|
||||
│ ├── main.ts # 🎯 Core SEQTA modifications
|
||||
│ ├── ui/ # 🎨 UI manipulation helpers
|
||||
│ └── utils/ # 🛠️ Helper functions
|
||||
└── css/ # 💄 Styles and themes
|
||||
```
|
||||
|
||||
### Where to Start Reading?
|
||||
1. **New to the project?** Start with `src/SEQTA.ts`
|
||||
2. **Want to understand plugins?** Look at `src/plugins/core/types.ts`
|
||||
3. **Want to see a simple plugin?** Check out `src/plugins/built-in/profilePicture/`
|
||||
4. **Interested in the UI?** Explore `src/interface/main.ts`
|
||||
|
||||
## Data Flow
|
||||
|
||||
Here's how data flows through the system:
|
||||
|
||||
```
|
||||
User visits SEQTA → SEQTA.ts detects page → Loads settings from storage
|
||||
│
|
||||
▼
|
||||
Plugin Manager initializes → Each plugin gets API access → Plugins modify SEQTA
|
||||
│
|
||||
▼
|
||||
User opens settings → Svelte UI loads → Settings changed → Storage updated
|
||||
│
|
||||
▼
|
||||
Storage change detected → Plugins notified → UI updates automatically
|
||||
```
|
||||
|
||||
## Browser Extension Basics
|
||||
|
||||
Never worked on a browser extension before? Here's what you need to know:
|
||||
|
||||
### Content Scripts vs Background Scripts
|
||||
- **Content Script** (`SEQTA.ts`): Runs on SEQTA pages, can access and modify the page
|
||||
- **Background Script** (`background.ts`): Runs in the background, handles storage and messaging
|
||||
|
||||
### Manifest Files
|
||||
Each browser needs a slightly different manifest file:
|
||||
- `manifests/chrome.ts` - Chrome, Edge, Brave
|
||||
- `manifests/firefox.ts` - Firefox
|
||||
- `manifests/safari.ts` - Safari (experimental)
|
||||
|
||||
### Communication
|
||||
Different parts of the extension communicate using:
|
||||
- `browser.runtime.sendMessage()` - Send messages
|
||||
- `browser.storage` - Shared storage, but we have created a custom storage system that is easier to use:
|
||||
```ts
|
||||
settingsState.[the setting name] = [whatever you want to set it to]
|
||||
console.log(settingsState.[the setting name])
|
||||
```
|
||||
- Custom events for plugin communication
|
||||
|
||||
## Development Tips
|
||||
|
||||
### Debugging
|
||||
1. **Chrome DevTools**: Right-click → Inspect → Console tab
|
||||
2. **Extension Console**: `chrome://extensions` → BetterSEQTA+ → "Inspect views: background page"
|
||||
3. **Look for logs**: We log everything with `[BetterSEQTA+]` prefix
|
||||
|
||||
### Making Changes
|
||||
1. Edit code → Save → Browser auto-reloads extension → Refresh SEQTA page
|
||||
2. For UI changes: The dev server hot-reloads automatically
|
||||
3. For plugin changes: May need to disable/enable the plugin in settings
|
||||
|
||||
### Common Gotchas
|
||||
- Settings take a moment to load (use `api.settings.loaded` promise)
|
||||
- Some SEQTA elements load dynamically (use `api.seqta.onMount()`)
|
||||
- Plugin cleanup is important (always return a cleanup function)
|
||||
|
||||
## Next Steps
|
||||
|
||||
Ready to contribute? Here's what to do next:
|
||||
|
||||
1. **Read the code**: Start with `src/SEQTA.ts` and follow the flow
|
||||
2. **Try creating a simple plugin**: Follow our [plugin guide](./plugins/README.md)
|
||||
3. **Look at existing issues**: Check our [GitHub issues](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues) for "good first issue" labels
|
||||
4. **Join our Discord**: Get help from the community!
|
||||
|
||||
## Questions?
|
||||
|
||||
Still confused about something? That's totally normal! Here are your options:
|
||||
- 💬 Ask in our [Discord server](https://discord.gg/YzmbnCDkat)
|
||||
- 🐛 Open an issue on GitHub
|
||||
- 📧 Email us at betterseqta.plus@gmail.com
|
||||
|
||||
Remember: **Every expert was once a beginner!** We're here to help you learn and contribute. 🚀
|
||||
@@ -0,0 +1,285 @@
|
||||
# Getting Started as a Contributor
|
||||
|
||||
Welcome to BetterSEQTA+! 🎉 This guide will walk you through making your first contribution, even if you're completely new to the project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Before You Start](#before-you-start)
|
||||
- [Your First 30 Minutes](#your-first-30-minutes)
|
||||
- [Making Your First Contribution](#making-your-first-contribution)
|
||||
- [Types of Contributions](#types-of-contributions)
|
||||
- [Finding Something to Work On](#finding-something-to-work-on)
|
||||
- [Development Workflow](#development-workflow)
|
||||
- [Getting Help](#getting-help)
|
||||
|
||||
## Before You Start
|
||||
|
||||
### What You'll Need
|
||||
- **Node.js** (v16 or higher) - [Download here](https://nodejs.org/)
|
||||
- **Git** - [Download here](https://git-scm.com/)
|
||||
- **A code editor** - We recommend [VS Code](https://code.visualstudio.com/)
|
||||
- **A Chromium browser** (Chrome, Edge, Brave) for testing (recommended, however you can use firefox although it requires being built every time you make a change)
|
||||
|
||||
### Helpful Background (but not required!)
|
||||
- Basic JavaScript/TypeScript knowledge
|
||||
- Some familiarity with HTML/CSS
|
||||
- Understanding of browser extensions (we'll teach you!)
|
||||
|
||||
**Don't worry if you're missing some of these!** We're happy to help you learn. 🤗
|
||||
|
||||
## Your First 30 Minutes
|
||||
|
||||
Let's get you up and running quickly:
|
||||
|
||||
### 1. Get the Code (3 minutes)
|
||||
```bash
|
||||
# Fork the repository on GitHub first, then:
|
||||
git clone https://github.com/YOUR_USERNAME/BetterSEQTA-plus.git
|
||||
cd BetterSEQTA-plus
|
||||
```
|
||||
|
||||
### 2. Install Dependencies (3 minutes)
|
||||
```bash
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### 3. Start Development Server (2 minutes)
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 4. Load Extension in Browser (4 minutes)
|
||||
1. Open Chrome and go to `chrome://extensions`
|
||||
2. Enable "Developer mode" (toggle in top right)
|
||||
3. Click "Load unpacked"
|
||||
4. Select the `dist` folder in your project
|
||||
5. Visit a SEQTA Learn page to see BetterSEQTA+ in action!
|
||||
|
||||
### 5. Make a Tiny Change (5 minutes)
|
||||
Let's prove everything works:
|
||||
1. Open `src/SEQTA.ts`
|
||||
2. Find the line that says `"[BetterSEQTA+] Successfully initialised"`
|
||||
3. Change it to `"[BetterSEQTA+] Successfully initialised - Hello [YOUR_NAME]!"`
|
||||
4. Save the file
|
||||
5. Go to `chrome://extensions`, click the refresh icon on BetterSEQTA+
|
||||
6. Refresh a SEQTA page and check the browser console (F12) - you should see your message!
|
||||
|
||||
### 6. Reset Your Change (3 minutes)
|
||||
```bash
|
||||
git checkout -- src/SEQTA.ts
|
||||
```
|
||||
|
||||
**Congratulations! 🎉 You've successfully set up BetterSEQTA+ for development!**
|
||||
|
||||
## Making Your First Contribution
|
||||
|
||||
### Easy First Contributions
|
||||
|
||||
Here are some great starter contributions:
|
||||
|
||||
1. **Fix a typo in documentation** - Super easy and always appreciated!
|
||||
2. **Improve error messages** - Make them more helpful
|
||||
3. **Add comments to code** - Help other contributors understand
|
||||
4. **Create a simple plugin** - Follow our plugin guide
|
||||
5. **Fix a bug you found** - If you found a bug, fix it!
|
||||
|
||||
### Step-by-Step: Your First Pull Request
|
||||
|
||||
#### Step 1: Pick an Issue
|
||||
- Go to our [Issues page](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues)
|
||||
- Look for labels like:
|
||||
- `good first issue` - Perfect for beginners
|
||||
- `help wanted` - We'd love help with these
|
||||
- `documentation` - Improve our docs
|
||||
- `bug` - Fix something broken
|
||||
|
||||
#### Step 2: Claim the Issue
|
||||
Comment on the issue saying "I'd like to work on this!" We'll assign it to you.
|
||||
|
||||
#### Step 3: Create a Branch
|
||||
```bash
|
||||
git checkout -b fix-issue-123 # Replace 123 with the issue number
|
||||
```
|
||||
|
||||
#### Step 4: Make Your Changes
|
||||
- Follow the patterns you see in existing code
|
||||
- Test your changes thoroughly
|
||||
- Keep changes focused and small
|
||||
|
||||
#### Step 5: Test Everything
|
||||
```bash
|
||||
# Test the extension still loads
|
||||
npm run dev
|
||||
|
||||
# Test in browser
|
||||
# 1. Reload extension at chrome://extensions
|
||||
# 2. Visit SEQTA page
|
||||
# 3. Verify everything still works
|
||||
```
|
||||
|
||||
#### Step 6: Commit Your Changes
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Fix issue #123: Brief description of what you fixed"
|
||||
```
|
||||
|
||||
#### Step 7: Push and Create Pull Request
|
||||
```bash
|
||||
git push origin fix-issue-123
|
||||
```
|
||||
|
||||
Then go to GitHub and create a pull request with:
|
||||
- **Clear title**: "Fix issue #123: Brief description"
|
||||
- **Description**: Explain what you changed and why
|
||||
- **Testing**: Describe how you tested it
|
||||
|
||||
## Types of Contributions
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
- Fix broken features
|
||||
- Improve error handling
|
||||
- Resolve compatibility issues
|
||||
|
||||
**Example**: "The theme selector doesn't work on Firefox"
|
||||
|
||||
### ✨ New Features
|
||||
- Add new plugins
|
||||
- Enhance existing functionality
|
||||
- Improve user experience
|
||||
|
||||
**Example**: "Add keyboard shortcuts for common actions"
|
||||
|
||||
### 📚 Documentation
|
||||
- Fix typos and unclear explanations
|
||||
- Add examples and tutorials
|
||||
- Improve code comments
|
||||
|
||||
**Example**: "Add more examples to the plugin guide"
|
||||
|
||||
### 🎨 Design & UI
|
||||
- Improve the settings interface
|
||||
- Make things more user-friendly
|
||||
- Add animations and polish
|
||||
|
||||
**Example**: "Make the theme creator more intuitive"
|
||||
|
||||
### 🔧 Technical Improvements
|
||||
- Refactor code for clarity
|
||||
- Add tests
|
||||
- Improve performance
|
||||
|
||||
**Example**: "Simplify the plugin loading logic"
|
||||
|
||||
## Finding Something to Work On
|
||||
|
||||
### Browse Issues by Label
|
||||
- [`good first issue`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/good%20first%20issue) - Perfect for beginners
|
||||
- [`help wanted`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/help%20wanted) - We need help with these
|
||||
- [`documentation`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/documentation) - Improve our docs
|
||||
- [`bug`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/bug) - Fix something broken
|
||||
- [`enhancement`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/enhancement) - Add new features
|
||||
|
||||
### Create Your Own Issue
|
||||
Found a bug or have an idea? Create an issue first to discuss it!
|
||||
|
||||
### Plugin Ideas
|
||||
Want to create a plugin? Here are some ideas:
|
||||
- **Study Timer**: Track study time across SEQTA pages
|
||||
- **Grade Tracker**: Better visualization of grades over time
|
||||
- **Quick Notes**: Add notes to any SEQTA page
|
||||
- **Homework Reminder**: Smart notifications for upcoming due dates
|
||||
- **Custom Shortcuts**: User-defined keyboard shortcuts
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Daily Development
|
||||
```bash
|
||||
# Start working
|
||||
git pull origin main
|
||||
npm run dev
|
||||
|
||||
# Make changes, test, commit
|
||||
git add .
|
||||
git commit -m "Descriptive commit message"
|
||||
|
||||
# Push when ready
|
||||
git push origin your-branch-name
|
||||
```
|
||||
|
||||
### Before Submitting PR
|
||||
1. **Test thoroughly** - Make sure nothing breaks
|
||||
2. **Check console** - No new errors
|
||||
3. **Test in different browsers** - Chrome and Firefox
|
||||
4. **Update documentation** - If you changed how something works
|
||||
|
||||
### Code Style
|
||||
- Use TypeScript where possible
|
||||
- Follow existing naming conventions
|
||||
- Add comments for complex logic
|
||||
- Keep functions small and focused
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Stuck? Here's How to Get Unstuck
|
||||
|
||||
1. **Check the docs** - [Architecture guide](./ARCHITECTURE.md) explains everything
|
||||
2. **Search existing issues** - Someone might have had the same problem
|
||||
3. **Ask in Discord** - Our community is super helpful
|
||||
4. **Create an issue** - If you found a bug or need help
|
||||
|
||||
### Discord Community
|
||||
Join our [Discord server](https://discord.gg/YzmbnCDkat) for:
|
||||
- Real-time help and discussion
|
||||
- Collaboration on features
|
||||
- Sharing ideas and feedback
|
||||
- Getting to know the community
|
||||
|
||||
### Code Review Process
|
||||
- All contributions need code review
|
||||
- We'll provide helpful feedback
|
||||
- Don't worry about making mistakes - we're here to help!
|
||||
- Reviews usually happen within 24-48 hours
|
||||
|
||||
## Common Questions
|
||||
|
||||
**Q: I'm new to browser extensions. Is this too advanced for me?**
|
||||
A: Not at all! We have lots of beginner-friendly issues, and our plugin system makes it easy to add features without understanding all the browser extension complexities.
|
||||
|
||||
**Q: How long does it take to get my first PR merged?**
|
||||
A: For simple fixes, usually 1-3 days. For larger features, it might take a week or two as we discuss the best approach.
|
||||
|
||||
**Q: I made a mistake in my PR. What do I do?**
|
||||
A: No worries! Just push more commits to the same branch and they'll be added to your PR automatically.
|
||||
|
||||
**Q: Can I work on multiple issues at once?**
|
||||
A: It's better to focus on one issue at a time, especially when starting out. This makes code review easier and reduces conflicts.
|
||||
|
||||
**Q: What if I start working on something and get stuck?**
|
||||
A: Ask for help! Create a draft PR with what you have so far, and we'll help you figure out the next steps.
|
||||
|
||||
## Recognition
|
||||
|
||||
All contributors get:
|
||||
- Recognition in our README
|
||||
- Contributor badge in Discord
|
||||
- Our eternal gratitude! 🙏
|
||||
|
||||
Significant contributors may also get:
|
||||
- Special Discord roles
|
||||
- Input on project direction
|
||||
- Maintainer status
|
||||
|
||||
## Next Steps
|
||||
|
||||
Ready to contribute? Here's what to do:
|
||||
|
||||
1. ✅ **Set up your development environment** (follow the 30-minute guide above)
|
||||
2. 🔍 **Find an issue to work on** (check the "good first issue" label)
|
||||
3. 💬 **Join our Discord** and introduce yourself
|
||||
4. 🚀 **Make your first contribution** and submit a PR
|
||||
|
||||
Remember: **Every expert was once a beginner!** We're excited to help you learn and grow as a contributor. Welcome to the team! 🎉
|
||||
|
||||
---
|
||||
|
||||
*Questions? Suggestions for improving this guide? Open an issue or message us on Discord!*
|
||||
+4
-1
@@ -10,7 +10,10 @@ Welcome to the BetterSEQTA+ documentation! This documentation will help you unde
|
||||
|
||||
- [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+
|
||||
- [Getting Started Contributing](./GETTING_STARTED_CONTRIBUTING.md) - **Start here!** Complete beginner-friendly guide
|
||||
- [Architecture Guide](./ARCHITECTURE.md) - How BetterSEQTA+ works under the hood
|
||||
- [Contributing Guide](../CONTRIBUTING.md) - Official contribution guidelines
|
||||
- [Troubleshooting](./TROUBLESHOOTING.md) - Common issues and solutions
|
||||
|
||||
### Plugin System
|
||||
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
# Theme Creation Guide
|
||||
|
||||
This guide covers everything you need to know about creating custom themes for BetterSEQTA+.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Theme Structure](#theme-structure)
|
||||
3. [CSS Variables](#css-variables)
|
||||
4. [CSS Selectors & Classes](#css-selectors--classes)
|
||||
5. [Custom Images](#custom-images)
|
||||
6. [Theme Settings](#theme-settings)
|
||||
7. [Best Practices](#best-practices)
|
||||
8. [Examples](#examples)
|
||||
|
||||
## Overview
|
||||
|
||||
Themes in BetterSEQTA+ allow you to completely customize the appearance of SEQTA Learn. A theme consists of:
|
||||
|
||||
- **Custom CSS**: CSS rules that override default styles
|
||||
- **Custom Images**: Images that can be referenced via CSS variables
|
||||
- **Theme Metadata**: Name, description, default color, etc.
|
||||
- **Theme Settings**: Options like forcing dark/light mode
|
||||
|
||||
Themes are applied by injecting CSS into the SEQTA page and setting CSS custom properties (variables) on the document root.
|
||||
|
||||
## CSS Variables
|
||||
|
||||
BetterSEQTA+ provides a comprehensive set of CSS variables that you can use in your themes. These variables automatically adapt to light/dark mode and user preferences.
|
||||
|
||||
### Core Background Variables
|
||||
|
||||
| Variable | Light Mode | Dark Mode | Description |
|
||||
|----------|------------|-----------|-------------|
|
||||
| `--background-primary` | `#ffffff` | `#232323` | Main background color |
|
||||
| `--background-secondary` | `#e5e7eb` | `#1a1a1a` | Secondary background color |
|
||||
| `--theme-primary` | `#ffffff` | `#232323` | Primary theme color (same as background-primary) |
|
||||
| `--theme-secondary` | `#e5e7eb` | `#1a1a1a` | Secondary theme color (same as background-secondary) |
|
||||
| `--text-primary` | `black` | `white` | Primary text color |
|
||||
| `--text-color` | `black` | `white` | Text color (alias for text-primary) |
|
||||
|
||||
### BetterSEQTA+ Specific Variables
|
||||
|
||||
| Variable | Description | Notes |
|
||||
|----------|-------------|-------|
|
||||
| `--better-main` | User's selected accent color | Dynamically set based on color picker |
|
||||
| `--better-sub` | Dark navy color | Always `#161616` |
|
||||
| `--better-pale` | Lightened version of accent color | Only available in light mode |
|
||||
| `--better-light` | Lighter version of accent color | Calculated based on brightness |
|
||||
| `--better-alert-highlight` | Alert/highlight color | `#c61851` |
|
||||
| `--betterseqta-logo` | Logo URL | Changes based on dark/light mode |
|
||||
| `--auto-background` | Auto background color | Falls back to `--better-pale` or `--background-secondary` |
|
||||
| `--navy` | Navy color | `#1a1a1a` |
|
||||
| `--theme-fg-parts` | Theme foreground parts | `white` |
|
||||
|
||||
### Subject/Item Color Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `--item-colour` | Subject/item color | Set dynamically per subject/item |
|
||||
| `--colour` | Generic color variable | Used in various contexts |
|
||||
| `--person-colour` | Person/avatar color | `var(--better-light)` for staff |
|
||||
|
||||
### Transparency Effects
|
||||
|
||||
When transparency effects are enabled, background variables become semi-transparent:
|
||||
|
||||
| Variable | Light Mode (Transparent) | Dark Mode (Transparent) |
|
||||
|----------|--------------------------|-------------------------|
|
||||
| `--background-primary` | `rgba(255, 255, 255, 0.6)` | `rgba(35, 35, 35, 0.6)` |
|
||||
| `--background-secondary` | `rgba(229, 231, 235, 0.6)` | `rgba(26, 26, 26, 0.6)` |
|
||||
|
||||
### Using CSS Variables
|
||||
|
||||
You can use these variables in your custom CSS:
|
||||
|
||||
```css
|
||||
/* Example: Style a custom element */
|
||||
.my-custom-element {
|
||||
background: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--better-main);
|
||||
}
|
||||
|
||||
/* Example: Create a gradient */
|
||||
.gradient-box {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--better-main),
|
||||
var(--background-secondary)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## CSS Selectors & Classes
|
||||
|
||||
BetterSEQTA+ uses specific CSS selectors and classes that you can target in your themes. Here are the most important ones:
|
||||
|
||||
### Main Layout Elements
|
||||
|
||||
| Selector | Description |
|
||||
|----------|-------------|
|
||||
| `#container` | Main container element |
|
||||
| `#content` | Content area |
|
||||
| `#main` | Main content wrapper |
|
||||
| `#title` | Top title bar |
|
||||
| `#menu` | Sidebar menu |
|
||||
|
||||
### Dark Mode
|
||||
|
||||
The `dark` class is added to `html` when dark mode is active:
|
||||
|
||||
```css
|
||||
/* Target dark mode specifically */
|
||||
html.dark #main {
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
/* Target light mode */
|
||||
html:not(.dark) #main {
|
||||
background: var(--background-primary);
|
||||
}
|
||||
```
|
||||
|
||||
### Transparency Effects
|
||||
|
||||
When transparency effects are enabled, the `transparencyEffects` class is added to `html`:
|
||||
|
||||
```css
|
||||
html.transparencyEffects .notice {
|
||||
backdrop-filter: blur(80px);
|
||||
}
|
||||
```
|
||||
|
||||
### Common SEQTA Classes
|
||||
|
||||
| Class/Selector | Description |
|
||||
|----------------|-------------|
|
||||
| `.notice` | Notice cards |
|
||||
| `.day` | Day containers in timetable |
|
||||
| `.dashboard` | Dashboard sections |
|
||||
| `.dashlet` | Dashboard widgets |
|
||||
| `.document` | Document elements |
|
||||
| `.quickbar` | Quick action bar |
|
||||
| `.calendar` | Calendar elements |
|
||||
| `.message` | Message elements |
|
||||
| `.thread` | Forum threads |
|
||||
| `.shortcut` | Shortcut buttons |
|
||||
| `.upcoming-assessment` | Upcoming assessments |
|
||||
| `.entry.class` | Timetable entries |
|
||||
|
||||
### BetterSEQTA+ Specific Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `.addedButton` | BetterSEQTA+ added buttons |
|
||||
| `.tooltip` | Tooltip elements |
|
||||
| `.notice-unified-content` | Unified notice content |
|
||||
| `.home-container` | Home page container |
|
||||
| `.timetable-container` | Timetable container |
|
||||
| `.notices-container` | Notices container |
|
||||
|
||||
### Attribute Selectors
|
||||
|
||||
SEQTA uses data attributes that you can target:
|
||||
|
||||
```css
|
||||
/* Target specific data types */
|
||||
[data-type="student"] .header {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Target specific labels */
|
||||
[data-label="inbox"] {
|
||||
/* Styles */
|
||||
}
|
||||
```
|
||||
|
||||
### CSS Modules
|
||||
|
||||
SEQTA uses CSS modules with hashed class names. You can target them using attribute selectors:
|
||||
|
||||
```css
|
||||
/* Target CSS module classes */
|
||||
[class*="MessageList__MessageList___"] {
|
||||
background: var(--background-primary);
|
||||
}
|
||||
|
||||
[class*="BasicPanel__BasicPanel___"] {
|
||||
border-radius: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Images
|
||||
|
||||
Themes can include custom images that are made available as CSS variables.
|
||||
|
||||
### Adding Images
|
||||
|
||||
1. Upload an image in the theme creator
|
||||
2. Set a CSS variable name (e.g., `custom-background`)
|
||||
3. The image will be available as `var(--custom-background)`
|
||||
|
||||
### Using Image Variables
|
||||
|
||||
```css
|
||||
/* Use as background */
|
||||
.my-element {
|
||||
background-image: var(--custom-background);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Use in content */
|
||||
.my-icon::before {
|
||||
content: '';
|
||||
background-image: var(--custom-icon);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
```
|
||||
|
||||
### Image Variable Format
|
||||
|
||||
Images are stored as `url()` values:
|
||||
|
||||
```css
|
||||
/* The variable contains: url(blob:...) */
|
||||
--custom-background: url(blob:chrome-extension://...);
|
||||
```
|
||||
|
||||
## Theme Settings
|
||||
|
||||
### Force Dark/Light Mode
|
||||
|
||||
You can force a theme to always use dark or light mode:
|
||||
|
||||
```typescript
|
||||
forceDark: true // Force dark mode
|
||||
forceDark: false // Force light mode
|
||||
forceDark: undefined // Use user's preference (default)
|
||||
```
|
||||
|
||||
When `forceDark` is set, users cannot toggle dark/light mode while the theme is active.
|
||||
|
||||
### Default Color
|
||||
|
||||
Set a default accent color for your theme:
|
||||
|
||||
```typescript
|
||||
defaultColour: "rgba(0, 123, 255, 1)" // Blue
|
||||
defaultColour: "#ff6b6b" // Red (hex format)
|
||||
```
|
||||
|
||||
### Allow Color Changes
|
||||
|
||||
Control whether users can change the accent color:
|
||||
|
||||
```typescript
|
||||
CanChangeColour: true // Users can change color
|
||||
CanChangeColour: false // Color is locked
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use CSS Variables
|
||||
|
||||
Always use CSS variables instead of hardcoded colors:
|
||||
|
||||
```css
|
||||
/* Good */
|
||||
.my-element {
|
||||
background: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Bad */
|
||||
.my-element {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Support Both Light and Dark Modes
|
||||
|
||||
Unless your theme forces a specific mode, ensure it works in both:
|
||||
|
||||
```css
|
||||
/* Use variables that adapt automatically */
|
||||
.my-element {
|
||||
background: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Or explicitly handle both modes */
|
||||
html.dark .my-element {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
html:not(.dark) .my-element {
|
||||
background: #ffffff;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Use !important Sparingly
|
||||
|
||||
Only use `!important` when necessary to override SEQTA's default styles:
|
||||
|
||||
```css
|
||||
/* Good - necessary override */
|
||||
#title {
|
||||
background: var(--background-primary) !important;
|
||||
}
|
||||
|
||||
/* Bad - unnecessary */
|
||||
.my-element {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Test Responsive Design
|
||||
|
||||
SEQTA is responsive. Test your theme at different screen sizes:
|
||||
|
||||
```css
|
||||
/* Example: Mobile-specific styles */
|
||||
@media (max-width: 900px) {
|
||||
#menu {
|
||||
transform: translate(-270px);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Use Semantic Selectors
|
||||
|
||||
Prefer semantic selectors over fragile ones:
|
||||
|
||||
```css
|
||||
/* Good - stable selector */
|
||||
#main > .dashboard > section {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
/* Caution - CSS module classes may change */
|
||||
[class*="Dashboard__Dashboard___"] {
|
||||
border-radius: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Optimize Images
|
||||
|
||||
Keep image file sizes reasonable:
|
||||
|
||||
- Use appropriate formats (PNG for transparency, JPG for photos)
|
||||
- Compress images before uploading
|
||||
- Consider using CSS for simple graphics instead of images
|
||||
|
||||
### 7. Document Your Theme
|
||||
|
||||
Include comments in your CSS explaining complex styles:
|
||||
|
||||
```css
|
||||
/*
|
||||
* Custom gradient background for dashboard
|
||||
* Uses the user's accent color for a cohesive look
|
||||
*/
|
||||
#main > .dashboard {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--better-main),
|
||||
var(--background-secondary)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Simple Color Theme
|
||||
|
||||
```css
|
||||
/* Change accent color throughout */
|
||||
:root {
|
||||
--better-main: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Style the menu */
|
||||
#menu {
|
||||
background: var(--background-primary);
|
||||
border-right: 3px solid var(--better-main);
|
||||
}
|
||||
|
||||
/* Style buttons */
|
||||
.uiButton {
|
||||
background: var(--better-main);
|
||||
color: var(--text-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Custom Background Image
|
||||
|
||||
```css
|
||||
/* Use a custom background image */
|
||||
body {
|
||||
background-image: var(--custom-background);
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Add overlay for readability */
|
||||
#main::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: -1;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Rounded Corners Theme
|
||||
|
||||
```css
|
||||
/* Make everything more rounded */
|
||||
#main > .dashboard > section,
|
||||
.dashlet,
|
||||
.notice,
|
||||
.document {
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
/* Round buttons */
|
||||
.uiButton {
|
||||
border-radius: 25px !important;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: Minimal Theme
|
||||
|
||||
```css
|
||||
/* Remove shadows and borders */
|
||||
#main > .dashboard > section,
|
||||
.dashlet,
|
||||
.notice {
|
||||
box-shadow: none !important;
|
||||
border: 1px solid var(--background-secondary) !important;
|
||||
}
|
||||
|
||||
/* Simplify colors */
|
||||
#menu {
|
||||
background: var(--background-primary) !important;
|
||||
}
|
||||
|
||||
/* Remove gradients */
|
||||
.day {
|
||||
background: var(--background-primary) !important;
|
||||
}
|
||||
```
|
||||
|
||||
### Example 5: High Contrast Theme
|
||||
|
||||
```css
|
||||
/* Increase contrast */
|
||||
:root {
|
||||
--background-primary: #000000;
|
||||
--background-secondary: #1a1a1a;
|
||||
--text-primary: #ffffff;
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
--background-primary: #ffffff;
|
||||
--background-secondary: #f0f0f0;
|
||||
--text-primary: #000000;
|
||||
}
|
||||
|
||||
/* Add borders for clarity */
|
||||
.dashlet,
|
||||
.notice,
|
||||
.document {
|
||||
border: 2px solid var(--better-main) !important;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### CSS Custom Properties Override
|
||||
|
||||
You can override CSS variables in your theme:
|
||||
|
||||
```css
|
||||
/* Override a variable */
|
||||
:root {
|
||||
--better-main: #your-color;
|
||||
}
|
||||
|
||||
/* Override conditionally */
|
||||
html.dark {
|
||||
--background-primary: #your-dark-color;
|
||||
}
|
||||
```
|
||||
|
||||
### Animations
|
||||
|
||||
Add smooth transitions:
|
||||
|
||||
```css
|
||||
/* Smooth color transitions */
|
||||
#menu li {
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.dashlet:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
```
|
||||
|
||||
### Pseudo-elements
|
||||
|
||||
Use pseudo-elements for decorative elements:
|
||||
|
||||
```css
|
||||
/* Add decorative border */
|
||||
.notice::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: var(--better-main);
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Theme Not Applying
|
||||
|
||||
1. Check browser console for CSS errors
|
||||
2. Verify CSS syntax is correct
|
||||
3. Ensure selectors are specific enough
|
||||
4. Check if `!important` is needed
|
||||
|
||||
### Colors Not Changing
|
||||
|
||||
1. Verify you're using CSS variables
|
||||
2. Check if `forceDark` is overriding your styles
|
||||
3. Ensure variables are set on `:root` or `html`
|
||||
|
||||
### Images Not Showing
|
||||
|
||||
1. Verify image variable name matches CSS
|
||||
2. Check image format is supported
|
||||
3. Ensure image size is reasonable
|
||||
4. Verify `url()` wrapper in CSS
|
||||
|
||||
### Dark Mode Issues
|
||||
|
||||
1. Test with `forceDark: true` and `forceDark: false`
|
||||
2. Check if transparency effects are interfering
|
||||
3. Verify `html.dark` selector is correct
|
||||
|
||||
## Resources
|
||||
|
||||
- **Theme Creator**: Access via BetterSEQTA+ settings
|
||||
- **CSS Variables Reference**: See [CSS Variables](#css-variables) section above
|
||||
- **SEQTA DOM Structure**: Inspect SEQTA pages in browser DevTools
|
||||
- **BetterSEQTA+ Source**: Check `src/css/injected.scss` for default styles
|
||||
|
||||
## Contributing Themes
|
||||
|
||||
If you create a great theme, consider sharing it:
|
||||
|
||||
1. Export your theme (Share button in theme creator)
|
||||
2. Submit to the BetterSEQTA+ theme store
|
||||
3. Or share on GitHub/Discord
|
||||
|
||||
---
|
||||
|
||||
**Note**: This documentation is based on BetterSEQTA+ v3.4.13. Some details may change in future versions.
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
Having issues with BetterSEQTA+ development? This guide covers the most common problems and their solutions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation Issues](#installation-issues)
|
||||
- [Development Server Issues](#development-server-issues)
|
||||
- [Browser Extension Issues](#browser-extension-issues)
|
||||
- [Plugin Development Issues](#plugin-development-issues)
|
||||
- [Build Issues](#build-issues)
|
||||
- [Still Stuck?](#still-stuck)
|
||||
|
||||
## Installation Issues
|
||||
|
||||
### ❌ "npm install" fails with peer dependency errors
|
||||
|
||||
**Problem**: You see errors about peer dependencies or conflicting packages.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### ❌ "Cannot find module" errors
|
||||
|
||||
**Problem**: Node.js can't find required packages.
|
||||
|
||||
**Solutions**:
|
||||
1. **Clear and reinstall**:
|
||||
```bash
|
||||
rm -rf node_modules
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
2. **Check Node.js version**:
|
||||
```bash
|
||||
node --version # Should be v16 or higher
|
||||
```
|
||||
|
||||
3. **Try with npm cache clean**:
|
||||
```bash
|
||||
npm cache clean --force
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### ❌ Permission errors on macOS/Linux
|
||||
|
||||
**Problem**: "EACCES" or permission denied errors.
|
||||
|
||||
**Solution**:
|
||||
```bash
|
||||
sudo chown -R $(whoami) ~/.npm
|
||||
sudo chown -R $(whoami) /usr/local/lib/node_modules
|
||||
```
|
||||
|
||||
## Development Server Issues
|
||||
|
||||
### ❌ "npm run dev" fails
|
||||
|
||||
**Problem**: Development server won't start.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check if port is in use**:
|
||||
```bash
|
||||
lsof -i :5173 # Kill the process using the port
|
||||
```
|
||||
|
||||
2. **Clear dist folder**:
|
||||
```bash
|
||||
rm -rf dist
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **Check for TypeScript errors**:
|
||||
```bash
|
||||
npx tsc --noEmit # Check for type errors
|
||||
```
|
||||
|
||||
### ❌ Changes not reflecting in browser
|
||||
|
||||
**Problem**: You make code changes but don't see them in the browser.
|
||||
|
||||
**Solutions**:
|
||||
1. **Reload the extension**:
|
||||
- Go to `chrome://extensions`
|
||||
- Find BetterSEQTA+ and click the refresh icon
|
||||
- Refresh your SEQTA page
|
||||
|
||||
2. **Check if dev server is running**:
|
||||
- Look for "Build completed" in your terminal
|
||||
- If not, restart `npm run dev`
|
||||
|
||||
3. **Hard refresh the page**:
|
||||
- Press `Ctrl+Shift+R` (or `Cmd+Shift+R` on Mac)
|
||||
|
||||
## Browser Extension Issues
|
||||
|
||||
### ❌ Extension doesn't load in Chrome
|
||||
|
||||
**Problem**: Extension appears in `chrome://extensions` but doesn't work.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check for errors**:
|
||||
- Go to `chrome://extensions`
|
||||
- Click "Errors" button on BetterSEQTA+
|
||||
- Fix any JavaScript errors shown
|
||||
|
||||
2. **Verify manifest**:
|
||||
- Check if `dist/manifest.json` exists
|
||||
- Ensure it has proper structure
|
||||
|
||||
3. **Check permissions**:
|
||||
- Extension needs permission to access SEQTA pages
|
||||
- Click "Details" → "Site access" → "On all sites"
|
||||
|
||||
### ❌ Extension doesn't appear on SEQTA pages
|
||||
|
||||
**Problem**: Extension loads but doesn't modify SEQTA.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check if you're on a SEQTA Learn page**:
|
||||
- URL should contain "seqta" or "learn"
|
||||
- Page title should include "SEQTA Learn"
|
||||
|
||||
2. **Check browser console**:
|
||||
- Press `F12` → Console tab
|
||||
- Look for "[BetterSEQTA+]" messages
|
||||
- If no messages, extension isn't running
|
||||
|
||||
3. **Verify page detection**:
|
||||
- Extension only runs on actual SEQTA Learn pages
|
||||
- Test on a real SEQTA instance
|
||||
|
||||
### ❌ Settings page won't open
|
||||
|
||||
**Problem**: Clicking the extension icon doesn't open settings.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check popup errors**:
|
||||
- Right-click extension icon → "Inspect popup"
|
||||
- Look for JavaScript errors
|
||||
|
||||
2. **Clear extension storage**:
|
||||
```javascript
|
||||
// In browser console on any page:
|
||||
chrome.storage.local.clear()
|
||||
```
|
||||
|
||||
3. **Reload extension and try again**
|
||||
|
||||
## Plugin Development Issues
|
||||
|
||||
### ❌ My plugin doesn't appear in settings
|
||||
|
||||
**Problem**: Created a plugin but it's not showing up.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check plugin registration**:
|
||||
- Ensure your plugin is imported in `src/plugins/index.ts`
|
||||
- Verify `pluginManager.registerPlugin(yourPlugin)` is called
|
||||
|
||||
2. **Check plugin structure**:
|
||||
```typescript
|
||||
// Ensure your plugin has all required fields
|
||||
const myPlugin: Plugin = {
|
||||
id: "unique-id", // Must be unique
|
||||
name: "Display Name",
|
||||
description: "What it does",
|
||||
version: "1.0.0",
|
||||
run: async (api) => {
|
||||
// Your code here
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. **Check for errors**:
|
||||
- Look in browser console for plugin loading errors
|
||||
|
||||
### ❌ Plugin settings not working
|
||||
|
||||
**Problem**: Plugin settings don't save or load properly.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check settings definition**:
|
||||
```typescript
|
||||
import { defineSettings, booleanSetting } from "@/plugins/core/settingsHelpers";
|
||||
|
||||
const settings = defineSettings({
|
||||
myOption: booleanSetting({
|
||||
default: true,
|
||||
title: "My Option",
|
||||
description: "What this does"
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
2. **Wait for settings to load**:
|
||||
```typescript
|
||||
run: async (api) => {
|
||||
await api.settings.loaded; // Wait for settings to load
|
||||
console.log(api.settings.myOption); // Now you can use settings
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Plugin API functions not working
|
||||
|
||||
**Problem**: `api.seqta.onMount()` or other API functions don't work.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check selector specificity**:
|
||||
```typescript
|
||||
// Be specific with selectors
|
||||
api.seqta.onMount(".home-page", (element) => {
|
||||
// Your code
|
||||
});
|
||||
```
|
||||
|
||||
2. **Wait for elements**:
|
||||
```typescript
|
||||
// Some elements load after page navigation
|
||||
api.seqta.onPageChange((page) => {
|
||||
if (page === "home") {
|
||||
api.seqta.onMount(".home-content", (element) => {
|
||||
// Now element should exist
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Build Issues
|
||||
|
||||
### ❌ "npm run build" fails
|
||||
|
||||
**Problem**: Production build fails with errors.
|
||||
|
||||
**Solutions**:
|
||||
1. **Check TypeScript errors**:
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
2. **Clear cache and rebuild**:
|
||||
```bash
|
||||
rm -rf dist node_modules
|
||||
npm install --legacy-peer-deps
|
||||
npm run build
|
||||
```
|
||||
|
||||
3. **Check for import errors**:
|
||||
- Ensure all imports use correct paths
|
||||
- Check for missing files
|
||||
|
||||
### ❌ Built extension doesn't work
|
||||
|
||||
**Problem**: `npm run build` succeeds but extension doesn't work.
|
||||
|
||||
**Solutions**:
|
||||
1. **Test the built extension**:
|
||||
- Load the `dist` folder as unpacked extension
|
||||
- Check console for errors
|
||||
|
||||
2. **Compare with dev version**:
|
||||
- If dev works but build doesn't, there might be a build configuration issue
|
||||
|
||||
3. **Check manifest generation**:
|
||||
- Verify `dist/manifest.json` looks correct
|
||||
- Compare with working version
|
||||
|
||||
## Common Error Messages
|
||||
|
||||
### "Cannot access contents of the URL"
|
||||
- **Cause**: Extension permissions issue
|
||||
- **Fix**: Go to `chrome://extensions` → BetterSEQTA+ → Details → Site access → "On all sites"
|
||||
|
||||
### "Extension context invalidated"
|
||||
- **Cause**: Extension was reloaded while page was open
|
||||
- **Fix**: Refresh the SEQTA page
|
||||
|
||||
### "Uncaught ReferenceError: browser is not defined"
|
||||
- **Cause**: Missing webextension-polyfill import
|
||||
- **Fix**: Add `import browser from "webextension-polyfill";` at top of file
|
||||
|
||||
### "Module not found: Can't resolve '@/...' "
|
||||
- **Cause**: TypeScript path mapping issue
|
||||
- **Fix**: Check `tsconfig.json` and `vite.config.ts` for path configuration
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Extension makes SEQTA slow
|
||||
1. **Check for memory leaks**:
|
||||
- Use Chrome DevTools → Performance tab
|
||||
- Look for growing memory usage
|
||||
|
||||
2. **Optimize plugin code**:
|
||||
- Remove unnecessary listeners
|
||||
- Clean up intervals/timeouts
|
||||
- Use efficient selectors
|
||||
|
||||
3. **Profile your changes**:
|
||||
- Test with extension disabled vs enabled
|
||||
- Identify which plugin is causing issues
|
||||
|
||||
## Still Stuck?
|
||||
|
||||
If none of these solutions work:
|
||||
|
||||
1. **🔍 Search existing issues**: [GitHub Issues](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues)
|
||||
|
||||
2. **💬 Ask on Discord**: [Join our server](https://discord.gg/YzmbnCDkat) - fastest way to get help!
|
||||
|
||||
3. **📝 Create a new issue**: Include:
|
||||
- Your operating system
|
||||
- Node.js version (`node --version`)
|
||||
- Browser version
|
||||
- Exact error message
|
||||
- Steps to reproduce
|
||||
- What you've already tried
|
||||
|
||||
4. **📧 Email us**: betterseqta.plus@gmail.com for urgent issues
|
||||
|
||||
## Getting More Debug Info
|
||||
|
||||
### Enable verbose logging
|
||||
Add this to your plugin's `run` function:
|
||||
```typescript
|
||||
console.log("[DEBUG] Plugin starting:", api);
|
||||
```
|
||||
|
||||
### Check extension background page
|
||||
1. Go to `chrome://extensions`
|
||||
2. Click "Details" on BetterSEQTA+
|
||||
3. Click "Inspect views: background page"
|
||||
4. Check console for background script errors
|
||||
|
||||
### Export debug info
|
||||
Run this in browser console on a SEQTA page:
|
||||
```javascript
|
||||
console.log("Extension info:", {
|
||||
version: chrome.runtime.getManifest().version,
|
||||
url: window.location.href,
|
||||
userAgent: navigator.userAgent,
|
||||
storage: await chrome.storage.local.get()
|
||||
});
|
||||
```
|
||||
|
||||
Remember: **Don't give up!** Every developer faces these issues. The community is here to help, and solving these problems makes you a better developer. 💪
|
||||
@@ -0,0 +1,335 @@
|
||||
# Example Plugin Template
|
||||
|
||||
This is a complete, working example of a simple BetterSEQTA+ plugin. You can copy this code and modify it to create your own plugin!
|
||||
|
||||
## What This Example Does
|
||||
|
||||
This plugin adds a friendly welcome message to the SEQTA homepage and lets users customize the message through settings.
|
||||
|
||||
## Complete Plugin Code
|
||||
|
||||
Create a new file in `src/plugins/built-in/my-first-plugin/index.ts`:
|
||||
|
||||
```typescript
|
||||
import type { Plugin } from "@/plugins/core/types";
|
||||
import { BasePlugin } from "@/plugins/core/settings";
|
||||
import {
|
||||
defineSettings,
|
||||
booleanSetting,
|
||||
stringSetting
|
||||
} from "@/plugins/core/settingsHelpers";
|
||||
import { Setting } from "@/plugins/core/settingsHelpers";
|
||||
|
||||
// Define the plugin settings
|
||||
const settings = defineSettings({
|
||||
enabled: booleanSetting({
|
||||
default: true,
|
||||
title: "Show Welcome Message",
|
||||
description: "Display a welcome message on the SEQTA homepage"
|
||||
}),
|
||||
customMessage: stringSetting({
|
||||
default: "Welcome to SEQTA! 🎉",
|
||||
title: "Custom Message",
|
||||
description: "The message to display on the homepage",
|
||||
maxLength: 100
|
||||
}),
|
||||
showEmoji: booleanSetting({
|
||||
default: true,
|
||||
title: "Show Emoji",
|
||||
description: "Include emojis in the welcome message"
|
||||
})
|
||||
});
|
||||
|
||||
// Create settings class
|
||||
class MyFirstPluginSettings extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.enabled)
|
||||
enabled!: boolean;
|
||||
|
||||
@Setting(settings.customMessage)
|
||||
customMessage!: string;
|
||||
|
||||
@Setting(settings.showEmoji)
|
||||
showEmoji!: boolean;
|
||||
}
|
||||
|
||||
// Create settings instance
|
||||
const settingsInstance = new MyFirstPluginSettings();
|
||||
|
||||
// Define the plugin
|
||||
const myFirstPlugin: Plugin<typeof settings> = {
|
||||
id: "my-first-plugin",
|
||||
name: "My First Plugin",
|
||||
description: "Adds a customizable welcome message to the SEQTA homepage",
|
||||
version: "1.0.0",
|
||||
|
||||
// Link our settings
|
||||
settings: settingsInstance.settings,
|
||||
|
||||
// Mark as beta (optional)
|
||||
beta: true,
|
||||
|
||||
// Add some CSS styles (optional)
|
||||
styles: `
|
||||
.my-plugin-welcome {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
animation: slideIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.my-plugin-welcome .close-btn {
|
||||
float: right;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.my-plugin-welcome .close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
`,
|
||||
|
||||
// Main plugin function
|
||||
run: async (api) => {
|
||||
console.log("[My First Plugin] Starting up! 🚀");
|
||||
|
||||
// Wait for settings to load
|
||||
await api.settings.loaded;
|
||||
|
||||
let welcomeElement: HTMLElement | null = null;
|
||||
|
||||
// Function to create the welcome message
|
||||
const createWelcomeMessage = () => {
|
||||
// Only show if enabled in settings
|
||||
if (!api.settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing message if it exists
|
||||
if (welcomeElement) {
|
||||
welcomeElement.remove();
|
||||
}
|
||||
|
||||
// Create the message element
|
||||
welcomeElement = document.createElement("div");
|
||||
welcomeElement.className = "my-plugin-welcome";
|
||||
|
||||
// Build the message content
|
||||
let message = api.settings.customMessage;
|
||||
if (!api.settings.showEmoji) {
|
||||
// Remove emojis if disabled
|
||||
message = message.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '');
|
||||
}
|
||||
|
||||
welcomeElement.innerHTML = `
|
||||
<button class="close-btn" onclick="this.parentElement.remove()">×</button>
|
||||
<div>${message}</div>
|
||||
<small style="opacity: 0.8; margin-top: 10px; display: block;">
|
||||
Powered by My First Plugin
|
||||
</small>
|
||||
`;
|
||||
|
||||
return welcomeElement;
|
||||
};
|
||||
|
||||
// Function to add message to homepage
|
||||
const addToHomepage = () => {
|
||||
api.seqta.onMount(".home-page, .dashboard, [class*='home']", (homePage) => {
|
||||
console.log("[My First Plugin] Found homepage, adding welcome message");
|
||||
|
||||
const message = createWelcomeMessage();
|
||||
if (message) {
|
||||
// Add to the top of the homepage
|
||||
homePage.insertBefore(message, homePage.firstChild);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add message when plugin starts
|
||||
addToHomepage();
|
||||
|
||||
// Re-add message when user navigates to homepage
|
||||
api.seqta.onPageChange((page) => {
|
||||
console.log("[My First Plugin] Page changed to:", page);
|
||||
if (page.includes("home") || page.includes("dashboard")) {
|
||||
// Small delay to let the page load
|
||||
setTimeout(addToHomepage, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for settings changes and update the message
|
||||
api.settings.onChange("enabled", (enabled) => {
|
||||
console.log("[My First Plugin] Enabled setting changed:", enabled);
|
||||
if (enabled) {
|
||||
addToHomepage();
|
||||
} else if (welcomeElement) {
|
||||
welcomeElement.remove();
|
||||
welcomeElement = null;
|
||||
}
|
||||
});
|
||||
|
||||
api.settings.onChange("customMessage", (newMessage) => {
|
||||
console.log("[My First Plugin] Message changed:", newMessage);
|
||||
if (welcomeElement && api.settings.enabled) {
|
||||
// Update existing message
|
||||
addToHomepage();
|
||||
}
|
||||
});
|
||||
|
||||
api.settings.onChange("showEmoji", (showEmoji) => {
|
||||
console.log("[My First Plugin] Show emoji changed:", showEmoji);
|
||||
if (welcomeElement && api.settings.enabled) {
|
||||
// Update existing message
|
||||
addToHomepage();
|
||||
}
|
||||
});
|
||||
|
||||
// Return cleanup function (called when plugin is disabled)
|
||||
return () => {
|
||||
console.log("[My First Plugin] Cleaning up...");
|
||||
if (welcomeElement) {
|
||||
welcomeElement.remove();
|
||||
welcomeElement = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default myFirstPlugin;
|
||||
```
|
||||
|
||||
## How to Use This Example
|
||||
|
||||
### Step 1: Create the Plugin File
|
||||
1. Create a new folder: `src/plugins/built-in/my-first-plugin/`
|
||||
2. Create `index.ts` in that folder
|
||||
3. Copy the code above into `index.ts`
|
||||
|
||||
### Step 2: Register the Plugin
|
||||
Add this to `src/plugins/index.ts`:
|
||||
|
||||
```typescript
|
||||
// Add this import at the top
|
||||
import myFirstPlugin from "./built-in/my-first-plugin";
|
||||
|
||||
// Add this line where other plugins are registered
|
||||
pluginManager.registerPlugin(myFirstPlugin);
|
||||
```
|
||||
|
||||
### Step 3: Test It
|
||||
1. Run `npm run dev`
|
||||
2. Reload your extension in Chrome
|
||||
3. Visit a SEQTA page
|
||||
4. You should see your welcome message!
|
||||
5. Open BetterSEQTA+ settings to customize it
|
||||
|
||||
## Key Concepts Explained
|
||||
|
||||
### 1. Plugin Structure
|
||||
```typescript
|
||||
const myPlugin: Plugin = {
|
||||
id: "unique-id", // Must be unique across all plugins
|
||||
name: "Display Name", // Shown in settings
|
||||
description: "What it does", // Shown in settings
|
||||
version: "1.0.0", // Plugin version
|
||||
settings: settingsObject, // User-configurable options
|
||||
styles: "/* CSS here */", // Optional CSS styles
|
||||
run: async (api) => { // Main plugin code
|
||||
// Your code here
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Settings System
|
||||
```typescript
|
||||
// Define what settings your plugin has
|
||||
const settings = defineSettings({
|
||||
myOption: booleanSetting({
|
||||
default: true,
|
||||
title: "My Option",
|
||||
description: "What this option does"
|
||||
})
|
||||
});
|
||||
|
||||
// Use in your plugin
|
||||
if (api.settings.myOption) {
|
||||
// Do something
|
||||
}
|
||||
```
|
||||
|
||||
### 3. SEQTA Integration
|
||||
```typescript
|
||||
// Wait for elements to appear
|
||||
api.seqta.onMount(".some-selector", (element) => {
|
||||
// Modify the element
|
||||
});
|
||||
|
||||
// Detect page changes
|
||||
api.seqta.onPageChange((page) => {
|
||||
if (page === "home") {
|
||||
// User navigated to homepage
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Cleanup
|
||||
Always return a cleanup function to remove your changes when the plugin is disabled:
|
||||
|
||||
```typescript
|
||||
run: async (api) => {
|
||||
// Add your features
|
||||
|
||||
return () => {
|
||||
// Remove your features
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Customization Ideas
|
||||
|
||||
Want to modify this example? Here are some ideas:
|
||||
|
||||
1. **Change the styling**: Modify the CSS to use different colors, animations, or layouts
|
||||
2. **Add more settings**: Number settings, select dropdowns, hotkeys
|
||||
3. **Different trigger**: Show on different pages, or based on time of day
|
||||
4. **Add interactions**: Buttons that do things when clicked
|
||||
5. **Store data**: Use `api.storage` to remember user preferences
|
||||
6. **Communicate with other plugins**: Use `api.events` to send/receive events
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once you've got this working:
|
||||
|
||||
1. **Experiment**: Try changing things and see what happens
|
||||
2. **Read other plugins**: Look at the built-in plugins for inspiration
|
||||
3. **Check the API docs**: Learn about all available API functions
|
||||
4. **Share your creation**: Show it off in Discord!
|
||||
|
||||
## Need Help?
|
||||
|
||||
- 💬 Ask in our [Discord server](https://discord.gg/YzmbnCDkat)
|
||||
- 📚 Read our [Plugin Development Guide](./README.md)
|
||||
- 🐛 Check the [Troubleshooting Guide](../TROUBLESHOOTING.md)
|
||||
- 📝 Open an issue on GitHub
|
||||
|
||||
Happy coding! 🎉
|
||||
+16
-12
@@ -1,12 +1,14 @@
|
||||
{
|
||||
"name": "betterseqtaplus",
|
||||
"version": "3.4.7",
|
||||
"version": "3.4.15",
|
||||
"type": "module",
|
||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||
"scripts": {
|
||||
"autoaudit": "npm audit && npm audit fix && npm run build",
|
||||
"dev": "cross-env MODE=chrome vite dev",
|
||||
"dev:firefox": "cross-env MODE=firefox vite build --watch",
|
||||
"compile": "npm i && npm run build",
|
||||
"build": "cross-env MODE=chrome vite build && cross-env MODE=firefox vite build",
|
||||
"build:chrome": "cross-env MODE=chrome vite build",
|
||||
"build:firefox": "cross-env MODE=firefox vite build",
|
||||
@@ -28,26 +30,26 @@
|
||||
"keywords": [],
|
||||
"author": {
|
||||
"name": "SethBurkart123",
|
||||
"email": "betterseqta@betterseqta.com",
|
||||
"email": "betterseqta.plus@gmail.com",
|
||||
"url": "https://github.com/BetterSEQTA/BetterSEQTA-plus"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||
"@babel/runtime": "^7.26.9",
|
||||
"@bedframe/cli": "^0.0.91",
|
||||
"@crxjs/vite-plugin": "2.0.0-beta.32",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@bedframe/cli": "^0.0.95",
|
||||
"@crxjs/vite-plugin": "^2.2.0",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"dependency-cruiser": "^16.10.0",
|
||||
"eslint": "9.22.0",
|
||||
"cross-env": "^10.0.0",
|
||||
"dependency-cruiser": "^17.0.1",
|
||||
"eslint": "^9.33.0",
|
||||
"glob": "^11.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"mime-types": "^3.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"process": "^0.11.10",
|
||||
"publish-browser-extension": "^3.0.0",
|
||||
"publish-browser-extension": "^4.0.0",
|
||||
"sass": "^1.85.1",
|
||||
"sass-loader": "^16.0.5",
|
||||
"semver": "^7.7.1",
|
||||
@@ -55,6 +57,7 @@
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bedframe/core": "^0.0.46",
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
@@ -65,10 +68,10 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"@types/chrome": "^0.0.308",
|
||||
"@types/chrome": "^0.1.4",
|
||||
"@types/color": "^4.2.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/sortablejs": "^1.15.8",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/webextension-polyfill": "^0.12.3",
|
||||
@@ -92,6 +95,7 @@
|
||||
"mathjs": "^14.4.0",
|
||||
"million": "^3.1.11",
|
||||
"motion": "^12.4.12",
|
||||
"pdfjs-dist": "^5.4.530",
|
||||
"postcss": "^8.5.3",
|
||||
"react": "17",
|
||||
"react-best-gradient-color-picker": "3.0.11",
|
||||
|
||||
-126
@@ -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
|
||||
+41
-22
@@ -9,6 +9,7 @@ import browser from "webextension-polyfill";
|
||||
import * as plugins from "@/plugins";
|
||||
import { main } from "@/seqta/main";
|
||||
import { delay } from "./seqta/utils/delay";
|
||||
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
|
||||
|
||||
export let MenuOptionsOpen = false;
|
||||
|
||||
@@ -24,24 +25,8 @@ if (document.childNodes[1]) {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes BetterSEQTA+ on a SEQTA page.
|
||||
*
|
||||
* This function performs the following steps:
|
||||
* 1. Verifies that the current page is a SEQTA page.
|
||||
* 2. Injects CSS styles for document loading.
|
||||
* 3. Changes the page's favicon.
|
||||
* 4. Initializes the extension's settings state.
|
||||
* 5. Sets default storage if settings are not already defined.
|
||||
* 6. Calls the main function to apply core BetterSEQTA+ modifications.
|
||||
* 7. Initializes legacy and new plugins if the extension is enabled.
|
||||
* 8. Logs success or error messages during initialization.
|
||||
*/
|
||||
async function init() {
|
||||
const hasSEQTATitle = document.title.includes("SEQTA Learn");
|
||||
|
||||
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
|
||||
// Verify we are on a SEQTA page
|
||||
if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) {
|
||||
IsSEQTAPage = true;
|
||||
console.info("[BetterSEQTA+] Verified SEQTA Page");
|
||||
|
||||
@@ -49,10 +34,30 @@ async function init() {
|
||||
documentLoadStyle.textContent = documentLoadCSS;
|
||||
document.head.appendChild(documentLoadStyle);
|
||||
|
||||
const icon = document.querySelector(
|
||||
'link[rel*="icon"]',
|
||||
)! as HTMLLinkElement;
|
||||
icon.href = icon48; // Change the icon
|
||||
replaceIcons();
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
|
||||
if (
|
||||
mutation.type === "attributes" &&
|
||||
mutation.target instanceof HTMLLinkElement &&
|
||||
mutation.target.rel.includes("icon") &&
|
||||
mutation.attributeName === "href"
|
||||
) {
|
||||
replaceIcons();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.head, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["href"],
|
||||
});
|
||||
|
||||
|
||||
|
||||
try {
|
||||
await initializeSettingsState();
|
||||
@@ -70,11 +75,25 @@ async function init() {
|
||||
await plugins.initializePlugins();
|
||||
}
|
||||
|
||||
if (settingsState.devMode) {
|
||||
initializeHideSensitiveToggle();
|
||||
}
|
||||
|
||||
console.info(
|
||||
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
||||
);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceIcons() {
|
||||
document
|
||||
.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
|
||||
.forEach((link) => {
|
||||
if (link.href !== icon48) {
|
||||
link.href = icon48;
|
||||
}
|
||||
});
|
||||
}
|
||||
+16
-71
@@ -49,7 +49,7 @@ browser.runtime.onMessage.addListener(
|
||||
break;
|
||||
|
||||
case "setDefaultStorage":
|
||||
SetStorageValue(DefaultValues);
|
||||
SetStorageValue(getDefaultValues());
|
||||
break;
|
||||
|
||||
case "sendNews":
|
||||
@@ -64,7 +64,18 @@ browser.runtime.onMessage.addListener(
|
||||
},
|
||||
);
|
||||
|
||||
const DefaultValues: SettingsState = {
|
||||
function detectLowEndDevice(): boolean {
|
||||
// Check for low-end hardware indicators
|
||||
const lowCoreCount = navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4;
|
||||
const lowMemory = (navigator as any).deviceMemory && (navigator as any).deviceMemory <= 2;
|
||||
|
||||
return lowCoreCount || lowMemory;
|
||||
}
|
||||
|
||||
function getDefaultValues(): SettingsState {
|
||||
const isLowEndDevice = detectLowEndDevice();
|
||||
|
||||
return {
|
||||
onoff: true,
|
||||
animatedbk: true,
|
||||
bksliderinput: "50",
|
||||
@@ -96,8 +107,8 @@ const DefaultValues: SettingsState = {
|
||||
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
|
||||
originalSelectedColor: "",
|
||||
DarkMode: true,
|
||||
animations: true,
|
||||
assessmentsAverage: true,
|
||||
animations: !isLowEndDevice,
|
||||
assessmentsAverage: false,
|
||||
defaultPage: "home",
|
||||
shortcuts: [
|
||||
{
|
||||
@@ -117,6 +128,7 @@ const DefaultValues: SettingsState = {
|
||||
lettergrade: false,
|
||||
newsSource: "australia",
|
||||
};
|
||||
}
|
||||
|
||||
function SetStorageValue(object: any) {
|
||||
for (var i in object) {
|
||||
@@ -124,78 +136,11 @@ function SetStorageValue(object: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function convertBksliderToSpeed(bksliderinput: number): number {
|
||||
const minBase = 50;
|
||||
const maxBase = 150;
|
||||
|
||||
const scaledValue =
|
||||
2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
|
||||
const baseSpeed = 3;
|
||||
|
||||
const speed = baseSpeed / scaledValue;
|
||||
return speed;
|
||||
}
|
||||
|
||||
async function migrateLegacySettings() {
|
||||
const storage = (await browser.storage.local.get(
|
||||
null,
|
||||
)) as unknown as SettingsState;
|
||||
|
||||
// Animated Background Migration
|
||||
if ("animatedbk" in storage || "bksliderinput" in storage) {
|
||||
const animatedSettings = {
|
||||
enabled: storage.animatedbk ?? true,
|
||||
speed: storage.bksliderinput
|
||||
? convertBksliderToSpeed(parseFloat(storage.bksliderinput))
|
||||
: 1,
|
||||
};
|
||||
await browser.storage.local.set({
|
||||
"plugin.animated-background.settings": animatedSettings,
|
||||
});
|
||||
}
|
||||
|
||||
// Assessments Average Migration
|
||||
if ("assessmentsAverage" in storage || "lettergrade" in storage) {
|
||||
const assessmentsSettings = {
|
||||
enabled: storage.assessmentsAverage ?? true,
|
||||
lettergrade: storage.lettergrade ?? false,
|
||||
};
|
||||
await browser.storage.local.set({
|
||||
"plugin.assessments-average.settings": assessmentsSettings,
|
||||
});
|
||||
}
|
||||
|
||||
if ("selectedTheme" in storage) {
|
||||
const themesSettings = { enabled: true };
|
||||
await browser.storage.local.set({
|
||||
"plugin.themes.settings": themesSettings,
|
||||
});
|
||||
}
|
||||
if (storage.notificationCollector !== false) {
|
||||
await browser.storage.local.set({
|
||||
"plugin.notificationCollector.settings": { enabled: true },
|
||||
});
|
||||
} else {
|
||||
await browser.storage.local.set({
|
||||
"plugin.notificationCollector.settings": { enabled: false },
|
||||
});
|
||||
}
|
||||
|
||||
const keysToRemove = [
|
||||
"animatedbk",
|
||||
"bksliderinput",
|
||||
"assessmentsAverage",
|
||||
"lettergrade",
|
||||
];
|
||||
await browser.storage.local.remove(keysToRemove);
|
||||
}
|
||||
|
||||
browser.runtime.onInstalled.addListener(function (event) {
|
||||
browser.storage.local.remove(["justupdated"]);
|
||||
browser.storage.local.remove(["data"]);
|
||||
|
||||
if (event.reason == "install" || event.reason == "update") {
|
||||
browser.storage.local.set({ justupdated: true });
|
||||
migrateLegacySettings();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -17,10 +17,19 @@
|
||||
|
||||
@use "injected/popup.scss";
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src: url("https://fonts.gstatic.com/s/roboto/v50/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3yUBA.woff2")
|
||||
format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
html {
|
||||
background: #161616 !important;
|
||||
background-color: #161616;
|
||||
font-family: Rubik, Roboto !important;
|
||||
font-family: Roboto, system-ui, -apple-system, sans-serif !important;
|
||||
}
|
||||
|
||||
.tooltip svg {
|
||||
@@ -94,3 +103,57 @@ body:has(.outside-container:not(.hide))
|
||||
background: var(--text-primary) !important;
|
||||
color: var(--theme-primary) !important;
|
||||
}
|
||||
.fixed-tooltip {
|
||||
display: inline-block;
|
||||
z-index: 5 !important;
|
||||
width: 28px;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
.fixed-tooltip svg {
|
||||
fill: var(--theme-primary);
|
||||
}
|
||||
.tooltiptext-fixed {
|
||||
width: 120px;
|
||||
transform: scale(0);
|
||||
transition: transform 0.2s;
|
||||
transform-origin: top;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin-left: -62px;
|
||||
}
|
||||
.tooltiptext-fixed::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent var(--text-primary) transparent;
|
||||
}
|
||||
.tooltiptext-fixed.show {
|
||||
transform: scale(1);
|
||||
transform-origin: top;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.tooltiptext-fixed p:hover {
|
||||
cursor: pointer;
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
transition: 200ms;
|
||||
}
|
||||
.tooltiptext-fixed p {
|
||||
border-radius: 8px !important;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ body {
|
||||
}
|
||||
|
||||
.cke_panel_listItem > a {
|
||||
|
||||
&:hover {
|
||||
background: #3d3d3e !important;
|
||||
}
|
||||
|
||||
+305
-307
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@ html.transparencyEffects {
|
||||
}
|
||||
|
||||
.filter-select,
|
||||
.uiShortText.search,
|
||||
.report {
|
||||
backdrop-filter: blur(10px) !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"last_updated": "2024-06-15T12:00:00Z",
|
||||
"whatsnew_html": "<div class=\"whatsnewTextContainer\" style=\"overflow-y: auto; font-size: 1.3rem; line-height: 1.6;\"><p>It has come to our attention that several schools have expressed concerns about BetterSEQTA+. This is very disheartening, so we have decided to release a statement on the situation.</p><p>To view our privacy policy, please click the <strong>shield icon</strong> in the settings menu, or <a href=\"https://betterseqta.org/privacy\" target=\"_blank\" rel=\"noopener noreferrer\" id=\"privacy-link\" style=\"color: inherit; text-decoration: underline; cursor: pointer; white-space: nowrap;\">click here</a>.</p><p style=\"font-weight: bold; margin-top: 15px;\">We never collect any information from you, and aim to provide the best features possible.</p></div>"
|
||||
}
|
||||
@@ -2,6 +2,16 @@ div:has(> #rbgcp-wrapper) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
#rbgcp-inputs-wrap {
|
||||
padding-top: 4px !important;
|
||||
margin-bottom: -8px;
|
||||
|
||||
#rbgcp-hex-input,
|
||||
#rbgcp-input {
|
||||
height: 28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
#rbgcp-wrapper {
|
||||
div[style="padding-top: 11px; position: relative;"] div {
|
||||
|
||||
@@ -108,7 +108,6 @@ export default function Picker({
|
||||
<ColorPicker
|
||||
disableDarkMode={true}
|
||||
presets={presets}
|
||||
hideInputs={customOnChange ? false : true}
|
||||
value={customThemeColor ?? ""}
|
||||
onChange={(color: string) => {
|
||||
if (customOnChange) {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { animate } from 'motion';
|
||||
|
||||
let { onConfirm, onCancel, title, message } = $props<{
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
}>();
|
||||
|
||||
let modalElement: HTMLElement;
|
||||
|
||||
$effect(() => {
|
||||
if (modalElement) {
|
||||
animate(
|
||||
modalElement,
|
||||
{ scale: [0.9, 1], opacity: [0, 1] },
|
||||
{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 25
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex fixed inset-0 z-[10000] justify-center items-center bg-black/50"
|
||||
style="position: fixed; top: 0; left: 0; right: 0; bottom: 0;"
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget) onCancel();
|
||||
}}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Escape') onCancel();
|
||||
}}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={modalElement}
|
||||
class="p-4 mx-4 w-full max-w-md bg-white rounded-2xl shadow-2xl dark:bg-zinc-800"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 class="mb-3 text-xl font-bold text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<div class="mb-6 text-lg text-gray-700 whitespace-pre-line dark:text-gray-300">
|
||||
{message}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
onclick={onCancel}
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg transition-colors hover:bg-gray-200 dark:bg-zinc-700 dark:text-gray-200 dark:hover:bg-zinc-600"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={onConfirm}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg shadow-inner transition-colors hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-600"
|
||||
>
|
||||
Enable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
let select: HTMLSelectElement;
|
||||
</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">
|
||||
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-xl w-full overflow-clip">
|
||||
<select
|
||||
bind:this={select}
|
||||
value={state}
|
||||
onchange={() => onChange(select.value)}
|
||||
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"
|
||||
class="px-4 py-2 pr-9 text-[0.875rem] font-medium text-black dark:text-white w-full border-none bg-white/80 dark:bg-zinc-800/70 hover:bg-white/90 dark:hover:bg-zinc-800/80 focus:bg-white/90 dark:focus:bg-zinc-800/80 focus:ring-0 rounded-md appearance-none transition-colors"
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option.value}>
|
||||
@@ -22,3 +22,19 @@
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Make native dropdown list readable on Windows */
|
||||
select option {
|
||||
background-color: #ffffff;
|
||||
color: #111827; /* zinc-900 */
|
||||
}
|
||||
:global(.dark) select option {
|
||||
background-color: #1f2937; /* zinc-800 */
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
:global(.dark) div::after {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
<script lang="ts">
|
||||
import TabbedContainer from '../components/TabbedContainer.svelte';
|
||||
import Settings from './settings/general.svelte';
|
||||
import Shortcuts from './settings/shortcuts.svelte';
|
||||
import Theme from './settings/theme.svelte';
|
||||
import browser from 'webextension-polyfill';
|
||||
import TabbedContainer from "../components/TabbedContainer.svelte";
|
||||
import Settings from "./settings/general.svelte";
|
||||
import Shortcuts from "./settings/shortcuts.svelte";
|
||||
import Theme from "./settings/theme.svelte";
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
import { standalone as StandaloneStore } from '../utils/standalone.svelte';
|
||||
import { onMount } from 'svelte'
|
||||
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
|
||||
import { standalone as StandaloneStore } from "../utils/standalone.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
|
||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
||||
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage"
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew"
|
||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
|
||||
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage";
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
|
||||
//import { OpenMinecraftServerPopup } from "@/seqta/utils/Openers/OpenMinecraftServerPopup";
|
||||
|
||||
import ColourPicker from '../components/ColourPicker.svelte'
|
||||
import { settingsPopup } from '../hooks/SettingsPopup'
|
||||
import ColourPicker from "../components/ColourPicker.svelte";
|
||||
import DisclaimerModal from "../components/DisclaimerModal.svelte";
|
||||
import { settingsPopup } from "../hooks/SettingsPopup";
|
||||
|
||||
let devModeSequence = '';
|
||||
let devModeSequence = "";
|
||||
let showDisclaimerModal = $state(false);
|
||||
let disclaimerCallbacks = $state<{ onConfirm: () => void, onCancel: () => void } | null>(null);
|
||||
|
||||
const handleDevModeToggle = () => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
devModeSequence += event.key.toLowerCase();
|
||||
if (devModeSequence.includes('dev')) {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
if (devModeSequence.includes("dev")) {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
settingsState.devMode = true;
|
||||
alert('Dev mode is now enabled');
|
||||
alert("Dev mode is now enabled");
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
setTimeout(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
devModeSequence = '';
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
devModeSequence = "";
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
const openColourPicker = () => {
|
||||
showColourPicker = true;
|
||||
}
|
||||
};
|
||||
|
||||
const openChangelog = () => {
|
||||
OpenWhatsNewPopup();
|
||||
@@ -49,9 +53,24 @@
|
||||
closeExtensionPopup();
|
||||
};
|
||||
|
||||
/* const openMinecraftServer = () => {
|
||||
OpenMinecraftServerPopup();
|
||||
closeExtensionPopup();
|
||||
}; */
|
||||
|
||||
const openPrivacyStatement = () => {
|
||||
window.open("https://betterseqta.org/privacy", "_blank");
|
||||
closeExtensionPopup();
|
||||
};
|
||||
|
||||
let { standalone } = $props<{ standalone?: boolean }>();
|
||||
let showColourPicker = $state<boolean>(false);
|
||||
|
||||
const showDisclaimer = (onConfirm: () => void, onCancel: () => void) => {
|
||||
disclaimerCallbacks = { onConfirm, onCancel };
|
||||
showDisclaimerModal = true;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
settingsPopup.addListener(() => {
|
||||
showColourPicker = false;
|
||||
@@ -62,30 +81,241 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<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="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40">
|
||||
<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="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40"
|
||||
>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" onclick={handleDevModeToggle} />
|
||||
<img
|
||||
src={browser.runtime.getURL(
|
||||
"resources/icons/betterseqta-dark-full.png",
|
||||
)}
|
||||
class="w-4/5 dark:hidden"
|
||||
alt="Light logo"
|
||||
onclick={handleDevModeToggle}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<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}
|
||||
<button onclick={openChangelog} class="absolute top-1 right-1 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
|
||||
<button onclick={openAbout} class="absolute top-1 right-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</button>
|
||||
<div class="flex absolute top-1 right-1 gap-1 items-center">
|
||||
<button
|
||||
onclick={openAbout}
|
||||
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
|
||||
>
|
||||
{"\ueb73"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={openChangelog}
|
||||
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
|
||||
>
|
||||
{"\ue929"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={openPrivacyStatement}
|
||||
class="flex justify-center items-center w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
|
||||
aria-label="Privacy Statement"
|
||||
>
|
||||
{"\uecba"}
|
||||
</button>
|
||||
|
||||
<!-- <button
|
||||
onclick={openMinecraftServer}
|
||||
class="flex justify-center items-center p-1 w-8 h-8 rounded-xl bg-zinc-100 dark:bg-zinc-700"
|
||||
aria-label="Open Minecraft Server"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 64 70"
|
||||
fill="none"
|
||||
class="w-full h-full"
|
||||
>
|
||||
<path
|
||||
d="M0 0 C3.96 0 7.92 0 12 0 C12 3.96 12 7.92 12 12 C10.68 12 9.36 12 8 12 C8 10.68 8 9.36 8 8 C6.68 8 5.36 8 4 8 C4 6.68 4 5.36 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(42,10)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 6.6 4 13.2 4 20 C2.68 20 1.36 20 0 20 C0 13.4 0 6.8 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(54,22)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C6.6 0 13.2 0 20 0 C20 1.32 20 2.64 20 4 C13.4 4 6.8 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(22,6)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 5.28 4 10.56 4 16 C2.68 16 1.36 16 0 16 C0 10.72 0 5.44 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(46,26)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C5.28 0 10.56 0 16 0 C16 1.32 16 2.64 16 4 C10.72 4 5.44 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(22,14)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C5.32 4 6.64 4 8 4 C8 5.32 8 6.64 8 8 C5.36 8 2.72 8 0 8 C0 5.36 0 2.72 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(6,50)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(14,50)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(18,46)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(10,46)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(50,42)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(22,42)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(14,42)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(26,38)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(18,38)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(30,34)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(22,34)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(34,30)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(26,30)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(38,26)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(30,26)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(42,22)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(34,22)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(38,18)"
|
||||
/>
|
||||
<path
|
||||
d="M0 0 C1.32 0 2.64 0 4 0 C4 1.32 4 2.64 4 4 C2.68 4 1.36 4 0 4 C0 2.68 0 1.36 0 0 Z "
|
||||
fill="currentColor"
|
||||
transform="translate(18,10)"
|
||||
/>
|
||||
</svg>
|
||||
</button> -->
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<TabbedContainer tabs={[
|
||||
{ title: 'Settings', Content: Settings, props: { showColourPicker: openColourPicker } },
|
||||
{ title: 'Shortcuts', Content: Shortcuts },
|
||||
{ title: 'Themes', Content: Theme },
|
||||
]} />
|
||||
<TabbedContainer
|
||||
tabs={[
|
||||
{
|
||||
title: "Settings",
|
||||
Content: Settings,
|
||||
props: { showColourPicker: openColourPicker, showDisclaimer },
|
||||
},
|
||||
{ title: "Shortcuts", Content: Shortcuts },
|
||||
{ title: "Themes", Content: Theme },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if showColourPicker}
|
||||
<ColourPicker hidePicker={() => { showColourPicker = false }} />
|
||||
<ColourPicker
|
||||
hidePicker={() => {
|
||||
showColourPicker = false;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if showDisclaimerModal && disclaimerCallbacks}
|
||||
<DisclaimerModal
|
||||
title="Assessment Averages Disclaimer"
|
||||
message="This feature calculates a simple average of your assessment grades. It does not take into account:
|
||||
• Assessment weightings
|
||||
• Different grading scales
|
||||
• Other factors used in official reports
|
||||
|
||||
The displayed average may be inaccurate compared to your actual marks found in reports.
|
||||
|
||||
Do you want to enable this feature?"
|
||||
onConfirm={() => {
|
||||
disclaimerCallbacks?.onConfirm();
|
||||
showDisclaimerModal = false;
|
||||
disclaimerCallbacks = null;
|
||||
}}
|
||||
onCancel={() => {
|
||||
disclaimerCallbacks?.onCancel();
|
||||
showDisclaimerModal = false;
|
||||
disclaimerCallbacks = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
import type { SettingsList } from "@/interface/types/SettingsProps"
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
||||
import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification"
|
||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
||||
|
||||
import { getAllPluginSettings } from "@/plugins"
|
||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
||||
@@ -91,7 +92,10 @@
|
||||
loadPluginSettings();
|
||||
})
|
||||
|
||||
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
||||
const { showColourPicker, showDisclaimer } = $props<{
|
||||
showColourPicker: () => void;
|
||||
showDisclaimer: (onConfirm: () => void, onCancel: () => void) => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
||||
@@ -187,15 +191,16 @@
|
||||
options: [
|
||||
{ value: "australia", label: "Australia" },
|
||||
{ value: "usa", label: "USA" },
|
||||
{ value: "uk", label: "UK" },
|
||||
{ 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}
|
||||
@@ -222,7 +227,20 @@
|
||||
<div>
|
||||
<Switch
|
||||
state={pluginSettingsValues[plugin.pluginId]?.enabled ?? true}
|
||||
onChange={(value) => updatePluginSetting(plugin.pluginId, 'enabled', value)}
|
||||
onChange={async (value) => {
|
||||
if (plugin.pluginId === 'assessments-average' && value === true) {
|
||||
showDisclaimer(
|
||||
async () => {
|
||||
await updatePluginSetting(plugin.pluginId, 'enabled', true);
|
||||
},
|
||||
() => {
|
||||
// Do nothing on cancel
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
await updatePluginSetting(plugin.pluginId, 'enabled', value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -322,9 +340,9 @@
|
||||
<p class="text-xs">Replace sensitive content with mock data</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => hideSensitiveContent()}
|
||||
text="Hide"
|
||||
<Switch
|
||||
state={$settingsState.hideSensitiveContent ?? false}
|
||||
onChange={(isOn: boolean) => settingsState.hideSensitiveContent = isOn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,6 +358,25 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<div class="pr-4">
|
||||
<h2 class="text-sm font-bold">Show Privacy Notification</h2>
|
||||
<p class="text-xs">Show the privacy notification popup on next page load</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
settingsState.privacyStatementShown = false;
|
||||
settingsState.privacyStatementLastUpdated = undefined;
|
||||
closeExtensionPopup();
|
||||
// Small delay to ensure popup is closed before showing notification
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await showPrivacyNotification();
|
||||
}}
|
||||
text="Show Now"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -24,12 +24,18 @@
|
||||
});
|
||||
|
||||
const switchChange = (shortcut: any) => {
|
||||
const value = $settingsState.shortcuts.find(s => s.name === shortcut);
|
||||
if (value) {
|
||||
value.enabled = !value.enabled;
|
||||
settingsState.shortcuts = settingsState.shortcuts;
|
||||
const idx = $settingsState.shortcuts.findIndex(s => s.name === shortcut);
|
||||
if (idx !== -1) {
|
||||
// Create a new array with the toggled value to ensure reactivity
|
||||
const updated = settingsState.shortcuts.map(s =>
|
||||
s.name === shortcut ? { ...s, enabled: !s.enabled } : s
|
||||
);
|
||||
settingsState.shortcuts = updated;
|
||||
} else {
|
||||
settingsState.shortcuts = [...settingsState.shortcuts, { name: shortcut, enabled: true }];
|
||||
settingsState.shortcuts = [
|
||||
...settingsState.shortcuts,
|
||||
{ name: shortcut, enabled: true }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,16 +202,6 @@
|
||||
</MotionDiv>
|
||||
</div>
|
||||
|
||||
{#each Object.entries(Shortcuts) as shortcut}
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<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}
|
||||
|
||||
<!-- Custom Shortcuts Section -->
|
||||
{#each $settingsState.customshortcuts as shortcut, index}
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
@@ -217,6 +213,16 @@
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#each Object.entries(Shortcuts) as shortcut}
|
||||
<div class="flex justify-between items-center px-4 py-3">
|
||||
<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}
|
||||
{:else}
|
||||
<div class="p-4 text-center">
|
||||
Loading shortcuts...
|
||||
|
||||
@@ -21,13 +21,16 @@
|
||||
<div class="relative w-full">
|
||||
<button
|
||||
onclick={() => editMode = !editMode}
|
||||
class="absolute top-0 right-0 z-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{editMode ? '\ue9e4' : '\uec38'}</button>
|
||||
class="absolute top-0 right-0 z-10 px-2 h-8 text-lg rounded-xl bg-zinc-100 dark:bg-zinc-700">
|
||||
<span class="mr-2">{editMode ? 'Done' : 'Edit'}</span>
|
||||
<span class="font-IconFamily">{editMode ? '\ue9e4' : '\uec38'}</span>
|
||||
</button>
|
||||
|
||||
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
|
||||
<ThemeSelector isEditMode={editMode} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center w-full h-full">
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
<div class="text-lg">
|
||||
Open SEQTA and use the embedded settings to access theme settings. 🫠
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ const updatedFirefoxManifest = {
|
||||
},
|
||||
browser_specific_settings: {
|
||||
gecko: {
|
||||
id: pkg.author.email,
|
||||
id: "betterseqta@betterseqta.com",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
+400
-22
@@ -10,6 +10,20 @@ class ReactFiber {
|
||||
console.log("Selected Nodes:", this.nodes);
|
||||
console.log("🔍 Found Fibers:", this.fibers);
|
||||
console.log("🛠 Found Components:", this.components);
|
||||
|
||||
// Debug fiber info
|
||||
this.fibers.forEach((fiber, index) => {
|
||||
if (fiber) {
|
||||
console.log(`Fiber ${index}:`, {
|
||||
tag: fiber.tag,
|
||||
type: fiber.type?.name || fiber.type,
|
||||
elementType: fiber.elementType,
|
||||
stateNode: fiber.stateNode,
|
||||
hasState: !!fiber.stateNode?.state,
|
||||
hasMemoizedState: !!fiber.memoizedState
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +33,27 @@ class ReactFiber {
|
||||
|
||||
getFiberNode(node) {
|
||||
if (!node) return null;
|
||||
|
||||
// Try multiple property name patterns for different React versions
|
||||
const possibleKeys = [
|
||||
'__reactFiber$', // React 16+
|
||||
'__reactInternalFiber$', // React 15
|
||||
'__reactInternalInstance$', // Older versions
|
||||
'__reactFiber',
|
||||
'__reactInternalInstance'
|
||||
];
|
||||
|
||||
// Check for exact matches first
|
||||
for (const key of possibleKeys) {
|
||||
if (node[key]) return node[key];
|
||||
}
|
||||
|
||||
// Fall back to pattern matching
|
||||
const fiberKey = Object.getOwnPropertyNames(node).find(
|
||||
(name) =>
|
||||
name.startsWith("__reactFiber") ||
|
||||
name.startsWith("__reactInternalInstance"),
|
||||
name.startsWith("__reactInternalInstance") ||
|
||||
name.startsWith("__reactInternalFiber")
|
||||
);
|
||||
return fiberKey ? node[fiberKey] : null;
|
||||
}
|
||||
@@ -30,20 +61,71 @@ class ReactFiber {
|
||||
getOwnerComponent(fiberNode) {
|
||||
let current = fiberNode;
|
||||
while (current) {
|
||||
// Use React's internal tag system to identify component types
|
||||
// Based on React's WorkTags: ClassComponent = 1, FunctionComponent = 0
|
||||
if (current.tag === 1) { // ClassComponent
|
||||
return current.stateNode; // For class components, stateNode is the component instance
|
||||
}
|
||||
|
||||
// For function components, look for hooks in memoizedState
|
||||
if (current.tag === 0 || current.tag === 15) { // FunctionComponent or MemoComponent
|
||||
// Function components don't have setState, but we can still track them
|
||||
if (current.memoizedState && current.type) {
|
||||
return {
|
||||
type: 'function',
|
||||
hooks: current.memoizedState,
|
||||
fiber: current,
|
||||
forceUpdate: () => {
|
||||
// Trigger re-render by updating fiber
|
||||
if (current.alternate) {
|
||||
current.alternate.expirationTime = 1;
|
||||
}
|
||||
current.expirationTime = 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback: check if stateNode has React component methods
|
||||
if (
|
||||
current.stateNode &&
|
||||
current.stateNode !== null &&
|
||||
typeof current.stateNode === 'object' &&
|
||||
(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 (!this.components.length && !this.fibers.length) return null;
|
||||
|
||||
const component = this.components[0];
|
||||
const fiber = this.fibers[0];
|
||||
let state = null;
|
||||
|
||||
// Handle class components
|
||||
if (component?.state) {
|
||||
state = component.state;
|
||||
}
|
||||
// Handle function components with hooks - look directly at fiber
|
||||
else if (fiber?.memoizedState) {
|
||||
if (this.debug) {
|
||||
console.log("🔍 Raw fiber.memoizedState:", fiber.memoizedState);
|
||||
}
|
||||
// Extract useState values from the hook chain
|
||||
const states = this.extractStateFromHooks(fiber.memoizedState);
|
||||
state = states.length === 1 ? states[0] : states;
|
||||
}
|
||||
// Fallback: try component hooks if available
|
||||
else if (component?.type === 'function' && component?.hooks) {
|
||||
const states = this.extractStateFromHooks(component.hooks);
|
||||
state = states.length === 1 ? states[0] : states;
|
||||
}
|
||||
|
||||
if (key === undefined) {
|
||||
return state;
|
||||
@@ -61,8 +143,137 @@ class ReactFiber {
|
||||
return null;
|
||||
}
|
||||
|
||||
extractStateFromHooks(hookChain) {
|
||||
const states = [];
|
||||
let mainStateFound = false;
|
||||
let currentHook = hookChain;
|
||||
let hookIndex = 0;
|
||||
|
||||
if (this.debug) {
|
||||
console.log("🔍 Hook chain analysis:");
|
||||
}
|
||||
|
||||
while (currentHook) {
|
||||
if (this.debug) {
|
||||
console.log(`Hook ${hookIndex}:`, {
|
||||
type: currentHook.tag || 'unknown',
|
||||
memoizedState: currentHook.memoizedState,
|
||||
queue: currentHook.queue,
|
||||
next: !!currentHook.next
|
||||
});
|
||||
}
|
||||
|
||||
// Try different approaches to extract state
|
||||
if (currentHook.memoizedState !== undefined && currentHook.memoizedState !== null) {
|
||||
const state = currentHook.memoizedState;
|
||||
|
||||
// Priority 1: Check for useRef hooks with complex state in .current
|
||||
if (!currentHook.queue &&
|
||||
typeof state === 'object' &&
|
||||
state !== null &&
|
||||
state.current !== undefined &&
|
||||
typeof state.current === 'object' &&
|
||||
state.current !== null) {
|
||||
|
||||
// Check if this looks like a substantial state object (has multiple properties)
|
||||
const currentKeys = Object.keys(state.current);
|
||||
if (currentKeys.length > 2) {
|
||||
states.push(state.current);
|
||||
mainStateFound = true;
|
||||
if (this.debug) console.log(` 🎯 Found main state in useRef:`, state.current);
|
||||
}
|
||||
}
|
||||
// Priority 2: useState hooks with queue
|
||||
else if (currentHook.queue && typeof state !== 'function') {
|
||||
states.push(state);
|
||||
if (this.debug) console.log(` ✅ Found useState state:`, state);
|
||||
}
|
||||
// Priority 3: Other potential state objects (only if we haven't found main state)
|
||||
else if (!mainStateFound && !currentHook.queue && typeof state === 'object' && state !== null) {
|
||||
// Skip useEffect hooks (they have tag 36)
|
||||
if (!(state.tag === 36 && state.create)) {
|
||||
states.push(state);
|
||||
if (this.debug) console.log(` 📦 Found potential state object:`, state);
|
||||
}
|
||||
}
|
||||
// Priority 4: Simple primitive state
|
||||
else if (typeof state !== 'function' && typeof state !== 'object') {
|
||||
states.push(state);
|
||||
if (this.debug) console.log(` 🔹 Found primitive state:`, state);
|
||||
}
|
||||
}
|
||||
|
||||
currentHook = currentHook.next;
|
||||
hookIndex++;
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`🎯 Extracted ${states.length} state values:`, states);
|
||||
}
|
||||
|
||||
// If we found main state objects, prioritize and deduplicate them
|
||||
if (mainStateFound && states.length > 1) {
|
||||
const mainStates = states.filter(state =>
|
||||
typeof state === 'object' &&
|
||||
state !== null &&
|
||||
Object.keys(state).length > 2
|
||||
);
|
||||
|
||||
if (mainStates.length > 1) {
|
||||
// If we have multiple main state objects, find the most comprehensive one
|
||||
// or merge them if they seem complementary
|
||||
const largestState = mainStates.reduce((largest, current) => {
|
||||
const largestKeys = Object.keys(largest).length;
|
||||
const currentKeys = Object.keys(current).length;
|
||||
|
||||
// Prefer the one with more properties
|
||||
if (currentKeys > largestKeys) return current;
|
||||
|
||||
// If same number of properties, prefer the one with more complex data
|
||||
if (currentKeys === largestKeys) {
|
||||
const largestComplexity = this.calculateStateComplexity(largest);
|
||||
const currentComplexity = this.calculateStateComplexity(current);
|
||||
return currentComplexity > largestComplexity ? current : largest;
|
||||
}
|
||||
|
||||
return largest;
|
||||
});
|
||||
|
||||
if (this.debug) {
|
||||
console.log(`🎯 Selected most comprehensive state from ${mainStates.length} candidates:`, largestState);
|
||||
}
|
||||
|
||||
return [largestState];
|
||||
}
|
||||
|
||||
return mainStates;
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
calculateStateComplexity(state) {
|
||||
if (!state || typeof state !== 'object') return 0;
|
||||
|
||||
let complexity = 0;
|
||||
for (const [key, value] of Object.entries(state)) {
|
||||
complexity += 1; // Base point for each property
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
complexity += value.length * 0.1; // Arrays get points based on length
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
complexity += Object.keys(value).length * 0.5; // Nested objects get points
|
||||
} else if (typeof value === 'function') {
|
||||
complexity += 2; // Functions are valuable
|
||||
}
|
||||
}
|
||||
|
||||
return complexity;
|
||||
}
|
||||
|
||||
setState(update) {
|
||||
this.components.forEach((component) => {
|
||||
// Handle class components
|
||||
if (component?.setState) {
|
||||
if (typeof update === "function") {
|
||||
// Functional update
|
||||
@@ -85,6 +296,13 @@ class ReactFiber {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Handle function components - force re-render since we can't directly update hooks
|
||||
else if (component?.type === 'function' && component?.forceUpdate) {
|
||||
if (this.debug) {
|
||||
console.log("⚠️ Function component detected - triggering re-render. Direct state update not possible.");
|
||||
}
|
||||
component.forceUpdate();
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
@@ -99,7 +317,7 @@ class ReactFiber {
|
||||
return this.fibers[0]?.memoizedProps?.[propName];
|
||||
}
|
||||
|
||||
setProp(propName) {
|
||||
setProp(propName, value) {
|
||||
this.fibers.forEach((fiber) => {
|
||||
if (fiber?.memoizedProps) {
|
||||
fiber.memoizedProps[propName] = value;
|
||||
@@ -119,38 +337,176 @@ class ReactFiber {
|
||||
}
|
||||
}
|
||||
|
||||
function makeSerializable(obj) {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
function makeSerializable(obj, visited = new WeakSet(), depth = 0, maxDepth = 10) {
|
||||
// Handle primitives first
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => makeSerializable(item));
|
||||
// Catch ALL functions early
|
||||
if (typeof obj === "function") {
|
||||
return `[Function: ${obj.name || 'anonymous'}]`;
|
||||
}
|
||||
|
||||
if (typeof obj !== "object") {
|
||||
// Handle other primitives
|
||||
if (typeof obj === "symbol") return obj.toString();
|
||||
if (typeof obj === "bigint") return obj.toString() + "n";
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Prevent infinite recursion - depth limit
|
||||
if (depth > maxDepth) {
|
||||
return "[Max Depth Reached]";
|
||||
}
|
||||
|
||||
// Prevent circular references
|
||||
if (visited.has(obj)) {
|
||||
return "[Circular Reference]";
|
||||
}
|
||||
visited.add(obj);
|
||||
|
||||
try {
|
||||
// Handle special objects first
|
||||
if (obj instanceof HTMLElement) {
|
||||
return {
|
||||
type: "HTMLElement",
|
||||
tagName: obj.tagName,
|
||||
id: obj.id || null,
|
||||
className: obj.className || null,
|
||||
attributes: obj.attributes ? Array.from(obj.attributes).map(attr => ({ name: attr.name, value: attr.value })) : []
|
||||
};
|
||||
}
|
||||
|
||||
if (obj instanceof Event) {
|
||||
return {
|
||||
type: "Event",
|
||||
eventType: obj.type,
|
||||
target: obj.target?.tagName || null
|
||||
};
|
||||
}
|
||||
|
||||
if (obj instanceof Date) {
|
||||
return { type: "Date", value: obj.toISOString() };
|
||||
}
|
||||
|
||||
if (obj instanceof RegExp) {
|
||||
return { type: "RegExp", source: obj.source, flags: obj.flags };
|
||||
}
|
||||
|
||||
if (obj instanceof Error) {
|
||||
return { type: "Error", message: obj.message, name: obj.name };
|
||||
}
|
||||
|
||||
// Handle React Fiber nodes - these are super circular
|
||||
if (obj.tag !== undefined && obj.elementType !== undefined) {
|
||||
return {
|
||||
type: "ReactFiber",
|
||||
tag: obj.tag,
|
||||
elementType: typeof obj.elementType === 'function' ? obj.elementType.name || 'AnonymousComponent' : String(obj.elementType),
|
||||
key: obj.key,
|
||||
hasState: !!obj.stateNode?.state,
|
||||
hasMemoizedState: !!obj.memoizedState,
|
||||
hasProps: !!obj.memoizedProps
|
||||
};
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.slice(0, 50).map((item, index) => {
|
||||
if (index >= 25) return "[...truncated]"; // Smaller limit
|
||||
return makeSerializable(item, visited, depth + 1, maxDepth);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle regular objects
|
||||
const serializableObj = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
|
||||
// Get own enumerable properties only to avoid prototype pollution
|
||||
const ownKeys = Object.getOwnPropertyNames(obj).filter(key => {
|
||||
try {
|
||||
return obj.propertyIsEnumerable(key);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Limit number of properties to avoid huge objects
|
||||
const maxKeys = 30; // Smaller limit for safety
|
||||
const processedKeys = ownKeys.slice(0, maxKeys);
|
||||
|
||||
for (const key of processedKeys) {
|
||||
try {
|
||||
// Skip problematic keys early
|
||||
const dangerousKeys = [
|
||||
'parentNode', 'parentElement', 'ownerDocument', 'children', 'childNodes',
|
||||
'return', 'child', 'sibling', 'alternate', 'ref', // React Fiber circular refs
|
||||
'_owner', '_source', '_self', '_debugOwner', '_debugSource', // React internals
|
||||
'window', 'document', 'global', 'self', 'top', 'parent', // Global objects
|
||||
'constructor', 'prototype', '__proto__', // Constructor/prototype chains
|
||||
'addEventListener', 'removeEventListener', // Event handlers
|
||||
'setState', 'forceUpdate', 'render' // React methods that might be functions
|
||||
];
|
||||
|
||||
if (dangerousKeys.includes(key)) {
|
||||
serializableObj[key] = `[Skipped: ${key}]`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
|
||||
if (descriptor && (descriptor.get || descriptor.set)) {
|
||||
serializableObj[key] = "[Getter/Setter]";
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
// Handle symbols specifically (React context symbols)
|
||||
if (typeof value === "symbol") {
|
||||
value = `[Symbol: ${value.description || 'anonymous'}]`;
|
||||
}
|
||||
// Extra function check
|
||||
else if (typeof value === "function") {
|
||||
value = `[Function: ${value.name || 'anonymous'}]`;
|
||||
} else if (value && typeof value === "object") {
|
||||
value = makeSerializable(value, visited, depth + 1, maxDepth);
|
||||
}
|
||||
|
||||
serializableObj[key] = value;
|
||||
} catch (error) {
|
||||
serializableObj[key] = `[Error: ${error.message}]`;
|
||||
}
|
||||
}
|
||||
|
||||
if (ownKeys.length > maxKeys) {
|
||||
serializableObj['...'] = `[${ownKeys.length - maxKeys} more properties]`;
|
||||
}
|
||||
|
||||
return serializableObj;
|
||||
} catch (error) {
|
||||
return `[Serialization Error: ${error.message}]`;
|
||||
} finally {
|
||||
visited.delete(obj); // Clean up for potential reuse
|
||||
}
|
||||
}
|
||||
|
||||
// Final safety check - recursively scan for any remaining functions
|
||||
function deepFunctionCheck(obj, path = "") {
|
||||
if (typeof obj === "function") {
|
||||
throw new Error(`Found function at path: ${path}`);
|
||||
}
|
||||
|
||||
if (obj && typeof obj === "object") {
|
||||
if (Array.isArray(obj)) {
|
||||
obj.forEach((item, index) => {
|
||||
deepFunctionCheck(item, `${path}[${index}]`);
|
||||
});
|
||||
} else {
|
||||
Object.keys(obj).forEach(key => {
|
||||
deepFunctionCheck(obj[key], path ? `${path}.${key}` : key);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
@@ -196,6 +552,28 @@ window.addEventListener("message", (event) => {
|
||||
response = makeSerializable(response);
|
||||
}
|
||||
|
||||
// Final safety check before postMessage
|
||||
try {
|
||||
deepFunctionCheck(response);
|
||||
} catch (functionError) {
|
||||
console.warn("[pageState] Function detected in response, cleaning:", functionError.message);
|
||||
response = `[Cleaned Response - Function found at: ${functionError.message}]`;
|
||||
}
|
||||
|
||||
// Additional structured clone test
|
||||
try {
|
||||
// Test if the object can be cloned (same algorithm as postMessage)
|
||||
if (typeof structuredClone === 'function') {
|
||||
structuredClone(response);
|
||||
} else {
|
||||
// Fallback for older browsers - try JSON round-trip
|
||||
JSON.parse(JSON.stringify(response));
|
||||
}
|
||||
} catch (cloneError) {
|
||||
console.warn("[pageState] Response not cloneable, fallback:", cloneError.message);
|
||||
response = `[Uncloneable Response: ${cloneError.message}]`;
|
||||
}
|
||||
|
||||
window.postMessage(
|
||||
{
|
||||
type: "reactFiberResponse",
|
||||
|
||||
@@ -7,6 +7,20 @@ import {
|
||||
import { type Plugin } from "@/plugins/core/types";
|
||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||
import {
|
||||
clearStuck,
|
||||
getClassByPattern,
|
||||
initStorage,
|
||||
letterToNumber,
|
||||
parseAssessments,
|
||||
processAssessments,
|
||||
} from "./utils.ts";
|
||||
|
||||
interface weightingsStorage {
|
||||
weightings: Record<string, string>;
|
||||
assessments: Record<string, string>;
|
||||
}
|
||||
|
||||
const settings = defineSettings({
|
||||
lettergrade: booleanSetting({
|
||||
@@ -23,7 +37,7 @@ class AssessmentsAveragePluginClass extends BasePlugin<typeof settings> {
|
||||
|
||||
const instance = new AssessmentsAveragePluginClass();
|
||||
|
||||
const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
||||
id: "assessments-average",
|
||||
name: "Assessment Averages",
|
||||
description: "Adds an average grade to the Assessments page",
|
||||
@@ -32,8 +46,10 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
settings: instance.settings,
|
||||
|
||||
run: async (api) => {
|
||||
await initStorage(api);
|
||||
clearStuck(api);
|
||||
|
||||
api.seqta.onMount(".assessmentsWrapper", async () => {
|
||||
// Wait for any assessment item to load first
|
||||
await waitForElm(
|
||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
||||
true,
|
||||
@@ -41,26 +57,13 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
1000,
|
||||
);
|
||||
|
||||
// Helper function to find actual class names by their base pattern
|
||||
const getClassByPattern = (
|
||||
element: Element | Document,
|
||||
basePattern: string,
|
||||
): string => {
|
||||
// Find all classes on the element
|
||||
const classes = Array.from(element.querySelectorAll("*"))
|
||||
.flatMap((el) => Array.from(el.classList))
|
||||
.filter((className) => className.startsWith(basePattern));
|
||||
await parseAssessments(api);
|
||||
|
||||
return classes.length ? classes[0] : "";
|
||||
};
|
||||
|
||||
// Find actual class names from the DOM
|
||||
const sampleAssessmentItem = document.querySelector(
|
||||
"[class*='AssessmentItem__AssessmentItem___']",
|
||||
);
|
||||
if (!sampleAssessmentItem) return;
|
||||
|
||||
// Extract all necessary class patterns from a sample assessment item
|
||||
const assessmentItemClass =
|
||||
Array.from(sampleAssessmentItem.classList).find((c) =>
|
||||
c.startsWith("AssessmentItem__AssessmentItem___"),
|
||||
@@ -83,7 +86,6 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
"AssessmentItem__title___",
|
||||
);
|
||||
|
||||
// Get Thermoscore classes
|
||||
const thermoscoreElement = document.querySelector(
|
||||
"[class*='Thermoscore__Thermoscore___']",
|
||||
);
|
||||
@@ -102,62 +104,34 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
"Thermoscore__text___",
|
||||
);
|
||||
|
||||
// Find assessment list
|
||||
const assessmentsList = document.querySelector(
|
||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
|
||||
);
|
||||
if (!assessmentsList) return;
|
||||
|
||||
const gradeElements = document.querySelectorAll(
|
||||
"[class*='Thermoscore__text___']",
|
||||
const state = await ReactFiber.find(
|
||||
"[class*='AssessmentList__items___']",
|
||||
).getState();
|
||||
const marks = state["marks"];
|
||||
if (!marks || !marks.length) return;
|
||||
|
||||
const assessmentItems = Array.from(
|
||||
assessmentsList.querySelectorAll(
|
||||
`[class*='AssessmentItem__AssessmentItem___']`,
|
||||
),
|
||||
).filter(
|
||||
(item) =>
|
||||
!item
|
||||
.querySelector(`[class*='AssessmentItem__title___']`)
|
||||
?.textContent?.includes("Subject Average"),
|
||||
);
|
||||
if (!gradeElements.length) return;
|
||||
|
||||
// Parse and average grades
|
||||
const letterToNumber: Record<string, number> = {
|
||||
"A+": 100,
|
||||
A: 95,
|
||||
"A-": 90,
|
||||
"B+": 85,
|
||||
B: 80,
|
||||
"B-": 75,
|
||||
"C+": 70,
|
||||
C: 65,
|
||||
"C-": 60,
|
||||
"D+": 55,
|
||||
D: 50,
|
||||
"D-": 45,
|
||||
"E+": 40,
|
||||
E: 35,
|
||||
"E-": 30,
|
||||
F: 0,
|
||||
};
|
||||
const { weightedTotal, totalWeight, hasInaccurateWeighting, count } =
|
||||
await processAssessments(api, assessmentItems);
|
||||
|
||||
function parseGrade(text: string): number {
|
||||
const str = text.trim().toUpperCase();
|
||||
if (str.includes("/")) {
|
||||
const [raw, max] = str.split("/").map((n) => parseFloat(n));
|
||||
return (raw / max) * 100;
|
||||
}
|
||||
if (str.includes("%")) {
|
||||
return parseFloat(str.replace("%", "")) || 0;
|
||||
}
|
||||
return letterToNumber[str] ?? 0;
|
||||
}
|
||||
if (!count || totalWeight === 0) return;
|
||||
|
||||
let total = 0;
|
||||
let count = 0;
|
||||
gradeElements.forEach((el) => {
|
||||
const grade = parseGrade(el.textContent || "");
|
||||
if (grade > 0) {
|
||||
total += grade;
|
||||
count++;
|
||||
}
|
||||
});
|
||||
|
||||
if (!count) return;
|
||||
|
||||
const avg = total / count;
|
||||
const avg = weightedTotal / totalWeight;
|
||||
const rounded = Math.ceil(avg / 5) * 5;
|
||||
const numberToLetter = Object.entries(letterToNumber).reduce(
|
||||
(acc, [k, v]) => {
|
||||
@@ -172,31 +146,40 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
||||
? letterAvg
|
||||
: `${avg.toFixed(2)}%`;
|
||||
|
||||
// Prevent duplicate
|
||||
const existing = assessmentsList.querySelector(
|
||||
`[class*='AssessmentItem__title___']`,
|
||||
);
|
||||
if (existing?.textContent === "Subject Average") return;
|
||||
|
||||
// Use the dynamic class names in the HTML template
|
||||
const averageElement = stringToHTML(/* html */ `
|
||||
let warningHTML = "";
|
||||
if (hasInaccurateWeighting) {
|
||||
warningHTML = /* html */ `
|
||||
<div style="margin-top: 4px; font-size: 11px; color: rgba(255, 255, 255, 0.6); opacity: 0.8; line-height: 1.3;">
|
||||
⚠ Some weightings unavailable
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
assessmentsList.insertBefore(
|
||||
stringToHTML(/* html */ `
|
||||
<div class="${assessmentItemClass}">
|
||||
<div class="${metaContainerClass}">
|
||||
<div class="${metaClass}">
|
||||
<div class="${simpleResultClass}">
|
||||
<div class="${titleClass}">Subject Average</div>
|
||||
${warningHTML}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="${thermoscoreClass}">
|
||||
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
|
||||
<div class="${textClass}" title="${display}">${display}</div>
|
||||
<div class="${textClass}" title="${hasInaccurateWeighting ? display + " (some weightings unavailable)" : display}">${display}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
|
||||
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
|
||||
`).firstChild!,
|
||||
assessmentsList.firstChild,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,572 @@
|
||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
||||
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||
import * as pdfjs from "pdfjs-dist";
|
||||
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||
"https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs";
|
||||
|
||||
export async function initStorage(api: any) {
|
||||
await api.storage.loaded;
|
||||
|
||||
if (!api.storage.weightings) {
|
||||
api.storage.weightings = {};
|
||||
}
|
||||
if (!api.storage.assessments) {
|
||||
api.storage.assessments = {};
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStuck(api: any) {
|
||||
let hasStuckProcessing = false;
|
||||
for (const key in api.storage.weightings) {
|
||||
if (api.storage.weightings[key] === "processing") {
|
||||
delete api.storage.weightings[key];
|
||||
hasStuckProcessing = true;
|
||||
}
|
||||
}
|
||||
if (hasStuckProcessing) {
|
||||
api.storage.weightings = { ...api.storage.weightings };
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to find actual class names by their base pattern
|
||||
export const getClassByPattern = (
|
||||
element: Element | Document,
|
||||
basePattern: string,
|
||||
): string => {
|
||||
const classes = Array.from(element.querySelectorAll("*"))
|
||||
.flatMap((el) => Array.from(el.classList))
|
||||
.filter((className) => className.startsWith(basePattern));
|
||||
|
||||
return classes.length ? classes[0] : "";
|
||||
};
|
||||
|
||||
export const letterToNumber: Record<string, number> = {
|
||||
"A+": 100,
|
||||
A: 95,
|
||||
"A-": 90,
|
||||
"B+": 85,
|
||||
B: 80,
|
||||
"B-": 75,
|
||||
"C+": 70,
|
||||
C: 65,
|
||||
"C-": 60,
|
||||
"D+": 55,
|
||||
D: 50,
|
||||
"D-": 45,
|
||||
"E+": 40,
|
||||
E: 35,
|
||||
"E-": 30,
|
||||
F: 0,
|
||||
};
|
||||
|
||||
function parseGrade(text: string): number {
|
||||
const str = text.trim().toUpperCase();
|
||||
if (str.includes("/")) {
|
||||
const [raw, max] = str.split("/").map((n) => parseFloat(n));
|
||||
return (raw / max) * 100;
|
||||
}
|
||||
if (str.includes("%")) {
|
||||
return parseFloat(str.replace("%", "")) || 0;
|
||||
}
|
||||
return letterToNumber[str] ?? 0;
|
||||
}
|
||||
|
||||
function createWeightLabel(
|
||||
assessmentItem: Element,
|
||||
weighting: string | undefined,
|
||||
) {
|
||||
const statsContainer = assessmentItem.querySelector(
|
||||
`[class*='AssessmentItem__stats___']`,
|
||||
) as HTMLElement;
|
||||
|
||||
if (
|
||||
!statsContainer ||
|
||||
statsContainer.querySelector(".betterseqta-weight-label")
|
||||
)
|
||||
return;
|
||||
|
||||
const label = statsContainer.querySelector(
|
||||
`[class*='Label__Label___']`,
|
||||
) as HTMLElement;
|
||||
|
||||
if (!label) return;
|
||||
|
||||
const weightLabel = label.cloneNode(true) as HTMLElement;
|
||||
weightLabel.classList.add("betterseqta-weight-label");
|
||||
|
||||
const innerTextDiv = weightLabel.querySelector(
|
||||
`[class*='Label__innerText___']`,
|
||||
);
|
||||
if (innerTextDiv) innerTextDiv.textContent = "Weight";
|
||||
|
||||
const textNodes = Array.from(weightLabel.childNodes).filter(
|
||||
(node) => node.nodeType === Node.TEXT_NODE,
|
||||
);
|
||||
|
||||
if (textNodes.length) {
|
||||
textNodes[0].textContent =
|
||||
weighting && weighting !== "processing"
|
||||
? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%`
|
||||
: "N/A";
|
||||
}
|
||||
|
||||
statsContainer.style.position = "relative";
|
||||
weightLabel.style.position = "absolute";
|
||||
weightLabel.style.right = "0";
|
||||
weightLabel.style.top = "50%";
|
||||
weightLabel.style.transform = "translateY(-50%)";
|
||||
|
||||
statsContainer.appendChild(weightLabel);
|
||||
}
|
||||
|
||||
export const isFirefox =
|
||||
navigator.userAgent.toLowerCase().indexOf("firefox") > -1 &&
|
||||
!navigator.userAgent.toLowerCase().includes("seamonkey") &&
|
||||
!navigator.userAgent.toLowerCase().includes("waterfox");
|
||||
|
||||
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
|
||||
const isBlobUrl = url.startsWith("blob:");
|
||||
|
||||
if (isBlobUrl || isFirefox) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
|
||||
const escapedUrl = url.replace(/'/g, "\\'");
|
||||
|
||||
script.textContent = `
|
||||
(function() {
|
||||
fetch('${escapedUrl}')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
})
|
||||
.then(arrayBuffer => {
|
||||
window.postMessage({
|
||||
type: '${requestId}',
|
||||
success: true,
|
||||
data: Array.from(new Uint8Array(arrayBuffer))
|
||||
}, '*');
|
||||
})
|
||||
.catch(error => {
|
||||
window.postMessage({
|
||||
type: '${requestId}',
|
||||
success: false,
|
||||
error: error.message || String(error)
|
||||
}, '*');
|
||||
});
|
||||
})();
|
||||
`;
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
if (event.data?.type === requestId) {
|
||||
window.removeEventListener("message", messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
|
||||
if (event.data.success) {
|
||||
resolve(new Uint8Array(event.data.data).buffer);
|
||||
} else {
|
||||
reject(new Error(event.data.error || "Failed to fetch PDF"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageHandler);
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener("message", messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
reject(new Error("Timeout fetching PDF"));
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
credentials: "include",
|
||||
redirect: "follow",
|
||||
});
|
||||
|
||||
if (response.url && response.url.startsWith("blob:")) {
|
||||
return await fetchPDFAsArrayBuffer(response.url);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch PDF: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await response.arrayBuffer();
|
||||
} catch (error: any) {
|
||||
if (
|
||||
error?.message?.includes("blob") ||
|
||||
error?.message?.includes("Security") ||
|
||||
error?.message?.includes("CSP")
|
||||
) {
|
||||
return await fetchPDFAsArrayBuffer(url);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractPDFText(url: string): Promise<string> {
|
||||
try {
|
||||
if (isFirefox) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement("script");
|
||||
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const escapedUrl = url
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/"/g, '\\"');
|
||||
|
||||
script.textContent = `
|
||||
(function() {
|
||||
const requestId = '${requestId}';
|
||||
const url = '${escapedUrl}';
|
||||
|
||||
if (window.pdfjsLib) {
|
||||
extractPDF();
|
||||
} else {
|
||||
const pdfjsScript = document.createElement('script');
|
||||
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js';
|
||||
pdfjsScript.type = 'text/javascript';
|
||||
|
||||
pdfjsScript.onload = function() {
|
||||
extractPDF();
|
||||
};
|
||||
pdfjsScript.onerror = function() {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Failed to load pdfjs library'
|
||||
}, '*');
|
||||
};
|
||||
|
||||
document.head.appendChild(pdfjsScript);
|
||||
}
|
||||
|
||||
function extractPDF() {
|
||||
try {
|
||||
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.withCredentials = true;
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status !== 200) {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
|
||||
}, '*');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = xhr.response;
|
||||
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
|
||||
throw new Error('PDF response is empty');
|
||||
}
|
||||
|
||||
window.pdfjsLib.getDocument({
|
||||
data: arrayBuffer,
|
||||
useSystemFonts: true,
|
||||
verbosity: 0,
|
||||
useWorkerFetch: false,
|
||||
isEvalSupported: false
|
||||
}).promise
|
||||
.then(pdf => {
|
||||
const pagePromises = [];
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
pagePromises.push(
|
||||
pdf.getPage(i).then(page => {
|
||||
return page.getTextContent().then(content => {
|
||||
return content.items.map(item => item.str).join(' ');
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.all(pagePromises);
|
||||
})
|
||||
.then(pages => {
|
||||
const text = pages.join('\\n');
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: true,
|
||||
text: text
|
||||
}, '*');
|
||||
})
|
||||
.catch(error => {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'PDF parsing error: ' + (error.message || String(error))
|
||||
}, '*');
|
||||
});
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'ArrayBuffer error: ' + (error.message || String(error))
|
||||
}, '*');
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Network error fetching PDF'
|
||||
}, '*');
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Timeout fetching PDF'
|
||||
}, '*');
|
||||
};
|
||||
|
||||
xhr.timeout = 30000;
|
||||
xhr.send();
|
||||
} catch (error) {
|
||||
window.postMessage({
|
||||
type: requestId,
|
||||
success: false,
|
||||
error: 'Setup error: ' + (error.message || String(error))
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
})();
|
||||
`;
|
||||
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
if (event.data?.type === requestId) {
|
||||
window.removeEventListener("message", messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
|
||||
if (event.data.success) {
|
||||
resolve(event.data.text);
|
||||
} else {
|
||||
reject(
|
||||
new Error(event.data.error || "Failed to extract PDF text"),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", messageHandler);
|
||||
(document.head || document.documentElement).appendChild(script);
|
||||
|
||||
setTimeout(() => {
|
||||
window.removeEventListener("message", messageHandler);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
reject(new Error("Timeout extracting PDF text"));
|
||||
}, 60000);
|
||||
});
|
||||
}
|
||||
|
||||
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
|
||||
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
throw new Error("PDF response is empty");
|
||||
}
|
||||
|
||||
const pdf = await pdfjs.getDocument({
|
||||
data: arrayBuffer,
|
||||
useSystemFonts: true,
|
||||
}).promise;
|
||||
|
||||
let text = "";
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const content = await page.getTextContent();
|
||||
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
||||
}
|
||||
|
||||
return text;
|
||||
} catch (error) {
|
||||
console.error("[BetterSEQTA+] Failed to extract PDF text:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWeightings(mark: any, api: any) {
|
||||
const assessmentID = mark.id;
|
||||
const metaclassID = mark.metaclassID;
|
||||
const userInfo = await getUserInfo();
|
||||
const userID = userInfo.id;
|
||||
const title = mark.title;
|
||||
|
||||
if (
|
||||
api.storage.weightings[assessmentID] != undefined &&
|
||||
api.storage.weightings[assessmentID] !== "processing"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: "processing",
|
||||
};
|
||||
|
||||
api.storage.assessments = {
|
||||
...api.storage.assessments,
|
||||
[title.trim()]: assessmentID,
|
||||
};
|
||||
|
||||
try {
|
||||
const filename =
|
||||
"BetterSEQTA-" +
|
||||
String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
||||
|
||||
const printResponse = await fetch(
|
||||
`${location.origin}/seqta/student/print/assessment`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
fileName: filename,
|
||||
id: assessmentID,
|
||||
metaclass: metaclassID,
|
||||
student: userID,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!printResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
||||
|
||||
if (pdfUrl.startsWith("blob:")) {
|
||||
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
|
||||
}
|
||||
|
||||
let text: string;
|
||||
try {
|
||||
text = await extractPDFText(pdfUrl);
|
||||
} catch (error: any) {
|
||||
if (
|
||||
isFirefox &&
|
||||
(error?.message?.includes("blob") ||
|
||||
error?.message?.includes("Security") ||
|
||||
error?.message?.includes("CSP"))
|
||||
) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
text = await extractPDFText(pdfUrl);
|
||||
} else {
|
||||
throw new Error(`PDF extraction failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const match = text.match(/weight:\s*(\d+\.?\d*)/i);
|
||||
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: match ? match[1] : "N/A",
|
||||
};
|
||||
} catch (error: any) {
|
||||
api.storage.weightings = {
|
||||
...api.storage.weightings,
|
||||
[assessmentID]: "N/A",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseAssessments(api: any) {
|
||||
const state = await ReactFiber.find(
|
||||
"[class*='AssessmentList__items___']",
|
||||
).getState();
|
||||
|
||||
const marks = state["marks"];
|
||||
if (!marks) return;
|
||||
|
||||
await Promise.all(marks.map((mark: any) => handleWeightings(mark, api)));
|
||||
}
|
||||
|
||||
export async function processAssessments(api: any, assessmentItems: Element[]) {
|
||||
let weightedTotal = 0;
|
||||
let totalWeight = 0;
|
||||
let hasInaccurateWeighting = false;
|
||||
let count = 0;
|
||||
|
||||
for (const assessmentItem of assessmentItems) {
|
||||
const gradeElement = assessmentItem.querySelector(
|
||||
`[class*='Thermoscore__text___']`,
|
||||
);
|
||||
|
||||
if (!gradeElement) continue;
|
||||
|
||||
const grade = parseGrade(gradeElement.textContent || "");
|
||||
if (grade <= 0) continue;
|
||||
|
||||
const titleEl = assessmentItem.querySelector(
|
||||
`[class*='AssessmentItem__title___']`,
|
||||
);
|
||||
if (!titleEl) continue;
|
||||
|
||||
const title = titleEl.textContent?.trim();
|
||||
if (!title) continue;
|
||||
|
||||
const assessmentID = api.storage.assessments?.[title];
|
||||
const weighting = assessmentID
|
||||
? api.storage.weightings?.[assessmentID]
|
||||
: undefined;
|
||||
|
||||
createWeightLabel(assessmentItem, weighting);
|
||||
|
||||
if (
|
||||
weighting === null ||
|
||||
weighting === undefined ||
|
||||
weighting === "N/A" ||
|
||||
weighting === "processing"
|
||||
) {
|
||||
hasInaccurateWeighting = true;
|
||||
weightedTotal += grade;
|
||||
totalWeight += 1;
|
||||
} else {
|
||||
const weight = parseFloat(weighting);
|
||||
|
||||
if (!isNaN(weight) && weight >= 0) {
|
||||
weightedTotal += grade * weight;
|
||||
totalWeight += weight;
|
||||
} else {
|
||||
weightedTotal += grade;
|
||||
totalWeight += 1;
|
||||
hasInaccurateWeighting = true;
|
||||
}
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
return {
|
||||
weightedTotal,
|
||||
totalWeight,
|
||||
hasInaccurateWeighting,
|
||||
count,
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,9 @@ interface PrefItem {
|
||||
value: string;
|
||||
}
|
||||
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { getMockAssessmentsData } from "@/seqta/ui/dev/hideSensitiveContent";
|
||||
|
||||
let cache: { time: number; data: any } | null = null;
|
||||
const CACHE_MS = 10 * 60 * 1000;
|
||||
const student = 69;
|
||||
@@ -102,6 +105,10 @@ async function loadSubmissions(student: number, assessments: any[]) {
|
||||
}
|
||||
|
||||
export async function getAssessmentsData() {
|
||||
if (settingsState.mockNotices) {
|
||||
return getMockAssessmentsData();
|
||||
}
|
||||
|
||||
if (cache && Date.now() - cache.time < CACHE_MS) return cache.data;
|
||||
const [subjects, colors, upcoming] = await Promise.all([
|
||||
loadSubjects(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Plugin } from "../../core/types";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import { getAssessmentsData } from "./api";
|
||||
import { renderSkeletonLoader, renderErrorState } from "./ui";
|
||||
import { renderErrorState, renderSkeletonLoader } from "./ui";
|
||||
import styles from "./styles.css?inline";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
|
||||
|
||||
@@ -106,7 +106,6 @@
|
||||
max-height: 100%;
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0 0 2px #e2e8f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
@@ -340,7 +339,7 @@
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
margin: 0 0 0.75rem 0;
|
||||
margin: 0 0 0.75rem;
|
||||
line-height: 1.4;
|
||||
padding-right: 2rem; /* Make room for menu button */
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import localforage from 'localforage'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
let fileInput = $state<HTMLInputElement | undefined>(undefined)
|
||||
let dragging = $state(false)
|
||||
let filename = $state<string | undefined>(undefined)
|
||||
let durationText = $state<string | undefined>(undefined)
|
||||
|
||||
const store = localforage.createInstance({
|
||||
name: 'background-music-store',
|
||||
storeName: 'music',
|
||||
})
|
||||
|
||||
async function loadExisting() {
|
||||
const name = await store.getItem<string>('audio-name')
|
||||
filename = name ?? undefined
|
||||
}
|
||||
|
||||
onMount(() => { loadExisting() })
|
||||
|
||||
function triggerSelect() { fileInput?.click() }
|
||||
|
||||
async function handleFiles(files: FileList | null) {
|
||||
const file = files?.[0]
|
||||
if (!file) return
|
||||
// Accept WAV and MP3 files
|
||||
const isSupported = file.type === 'audio/wav' || file.type === 'audio/mpeg' ||
|
||||
file.name.toLowerCase().endsWith('.wav') || file.name.toLowerCase().endsWith('.mp3')
|
||||
if (!isSupported) {
|
||||
alert('Please select a .wav or .mp3 audio file')
|
||||
return
|
||||
}
|
||||
|
||||
await store.setItem('audio-blob', file)
|
||||
await store.setItem('audio-name', file.name)
|
||||
filename = file.name
|
||||
|
||||
// Probe duration
|
||||
try {
|
||||
const url = URL.createObjectURL(file)
|
||||
const audio = new Audio(url)
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
audio.onloadedmetadata = () => resolve()
|
||||
audio.onerror = () => reject()
|
||||
})
|
||||
if (!isNaN(audio.duration) && audio.duration !== Infinity) {
|
||||
const minutes = Math.floor(audio.duration / 60)
|
||||
const seconds = Math.round(audio.duration % 60)
|
||||
durationText = `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
} else {
|
||||
durationText = undefined
|
||||
}
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
durationText = undefined
|
||||
}
|
||||
|
||||
window.dispatchEvent(new Event('betterseqta-background-music-updated'))
|
||||
}
|
||||
|
||||
function onFileChange() { handleFiles(fileInput?.files || null) }
|
||||
|
||||
function onDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
dragging = false
|
||||
handleFiles(event.dataTransfer?.files || null)
|
||||
}
|
||||
|
||||
async function removeAudio() {
|
||||
await store.removeItem('audio-blob')
|
||||
await store.removeItem('audio-name')
|
||||
filename = undefined
|
||||
durationText = undefined
|
||||
window.dispatchEvent(new Event('betterseqta-background-music-stop'))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative cursor-pointer select-none"
|
||||
onclick={() => triggerSelect()}
|
||||
ondragover={(e) => { e.stopPropagation(); dragging = true }}
|
||||
ondragleave={() => dragging = false}
|
||||
ondrop={onDrop}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
triggerSelect()
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="flex gap-3 items-center">
|
||||
{#if filename}
|
||||
<div class="flex items-center px-3 py-1 rounded-lg bg-zinc-200 dark:bg-zinc-800">
|
||||
<div class="text-xs text-zinc-600 dark:text-zinc-300">
|
||||
{filename}
|
||||
<p>{durationText}</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex justify-center items-center m-1 text-lg dark:text-white size-7"
|
||||
onclick={(e) => { e.stopPropagation(); removeAudio() }}
|
||||
aria-label="Remove audio"
|
||||
>×</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-2 items-center px-3 py-1 text-xs rounded-lg border border-dashed transition border-zinc-300 dark:border-zinc-600 text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 text-nowrap">
|
||||
<span class="text-lg font-IconFamily">{'\ued47'}</span>
|
||||
<span>Upload audio</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<input type="file" accept="audio/wav,audio/mpeg" class="hidden" bind:this={fileInput} onchange={onFileChange} />
|
||||
{#if dragging}
|
||||
<div class="absolute inset-0 rounded-lg bg-zinc-200/40 dark:bg-zinc-700/40"></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import type { Plugin } from "@/plugins/core/types";
|
||||
import { booleanSetting, componentSetting, defineSettings, numberSetting } from "@/plugins/core/settingsHelpers";
|
||||
import styles from "./styles.css?inline";
|
||||
import BackgroundMusicSetting from "./BackgroundMusicSetting.svelte";
|
||||
import localforage from "localforage";
|
||||
|
||||
const settings = defineSettings({
|
||||
uploader: componentSetting({
|
||||
title: "Background Music",
|
||||
description: "Upload a .wav or .mp3 audio file to play in the background.",
|
||||
component: BackgroundMusicSetting,
|
||||
}),
|
||||
volume: numberSetting({
|
||||
title: "Volume",
|
||||
description: "Set background music volume",
|
||||
default: 0.5,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
}),
|
||||
pauseOnHidden: booleanSetting({
|
||||
title: "Pause when tab hidden",
|
||||
description: "Pause music when switching to another tab or minimizing the browser",
|
||||
default: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const store = localforage.createInstance({
|
||||
name: "background-music-store",
|
||||
storeName: "music",
|
||||
});
|
||||
|
||||
let currentAudio: HTMLAudioElement | null = null;
|
||||
let currentObjectUrl: string | null = null;
|
||||
let cleanupRegistered = false;
|
||||
let pendingGestureCancel: (() => void) | null = null;
|
||||
let visibilityResumeTimeout: number | null = null;
|
||||
|
||||
async function loadAudioBlob(): Promise<Blob | null> {
|
||||
const blob = await store.getItem<Blob>("audio-blob");
|
||||
return blob && blob instanceof Blob ? blob : null;
|
||||
}
|
||||
|
||||
function stopAndCleanupAudio(): void {
|
||||
if (currentAudio) {
|
||||
currentAudio.pause();
|
||||
currentAudio.src = "";
|
||||
currentAudio.remove();
|
||||
currentAudio = null;
|
||||
}
|
||||
if (currentObjectUrl) {
|
||||
URL.revokeObjectURL(currentObjectUrl);
|
||||
currentObjectUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureGestureStart(handler: () => void): () => void {
|
||||
const eventTypes = ["pointerdown", "keydown", "touchstart"]; // broad user gesture coverage
|
||||
const listener = () => {
|
||||
handler();
|
||||
for (const type of eventTypes) {
|
||||
window.removeEventListener(type, listener);
|
||||
}
|
||||
};
|
||||
for (const type of eventTypes) {
|
||||
window.addEventListener(type, listener, { once: true, passive: true });
|
||||
}
|
||||
return () => {
|
||||
for (const type of eventTypes) {
|
||||
window.removeEventListener(type, listener);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function startPlayback(volume: number): Promise<void> {
|
||||
const blob = await loadAudioBlob();
|
||||
if (!blob) return;
|
||||
|
||||
stopAndCleanupAudio();
|
||||
|
||||
currentObjectUrl = URL.createObjectURL(blob);
|
||||
const audio = new Audio(currentObjectUrl);
|
||||
audio.loop = true;
|
||||
audio.volume = Math.max(0, Math.min(1, volume));
|
||||
audio.preload = "auto";
|
||||
audio.crossOrigin = "anonymous";
|
||||
audio.style.display = "none";
|
||||
document.body.appendChild(audio);
|
||||
currentAudio = audio;
|
||||
|
||||
try {
|
||||
// Attempt immediate play; may be blocked until gesture
|
||||
await audio.play();
|
||||
} catch {
|
||||
// Ignore; will be started after gesture if enabled
|
||||
}
|
||||
}
|
||||
|
||||
const backgroundMusicPlugin: Plugin<typeof settings> = {
|
||||
id: "background-music",
|
||||
name: "Background Music",
|
||||
description: "Play your own music in the background while SEQTA is open.",
|
||||
version: "1.0.0",
|
||||
settings,
|
||||
styles,
|
||||
disableToggle: true,
|
||||
defaultEnabled: false,
|
||||
|
||||
run: async (api) => {
|
||||
await api.storage.loaded;
|
||||
|
||||
// react to specific setting changes
|
||||
api.settings.onChange("volume" as any, (value: any) => {
|
||||
const vol = (typeof value === "number" ? value : 0.5) as number;
|
||||
if (currentAudio) currentAudio.volume = Math.max(0, Math.min(1, vol));
|
||||
});
|
||||
|
||||
api.settings.onChange("pauseOnHidden" as any, (value: any) => {
|
||||
const pauseOnHidden = (typeof value === "boolean" ? value : true) as boolean;
|
||||
// If the setting is disabled and audio is currently paused due to tab being hidden, resume it
|
||||
if (!pauseOnHidden && currentAudio && currentAudio.paused && document.visibilityState === "hidden") {
|
||||
currentAudio.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Stop button/event removed by user; no stop handling needed
|
||||
|
||||
// Start if we have audio and autoplay is enabled
|
||||
const tryStart = async () => {
|
||||
const vol = (api.settings as any).volume ?? 0.5;
|
||||
await startPlayback(vol);
|
||||
};
|
||||
|
||||
// Always arm gesture start and attempt immediate start
|
||||
const cancel = ensureGestureStart(() => { tryStart(); });
|
||||
cleanupRegistered = true;
|
||||
(window as any).__betterseqta_bg_music_cancel__ = cancel;
|
||||
tryStart();
|
||||
|
||||
// Pause on tab hide, resume on show with a small delay (if enabled)
|
||||
const visHandler = () => {
|
||||
if (!currentAudio) return;
|
||||
const pauseOnHidden = (api.settings as any).pauseOnHidden ?? true;
|
||||
if (!pauseOnHidden) return;
|
||||
|
||||
if (document.visibilityState === "hidden") {
|
||||
if (visibilityResumeTimeout !== null) {
|
||||
clearTimeout(visibilityResumeTimeout);
|
||||
visibilityResumeTimeout = null;
|
||||
}
|
||||
currentAudio.pause();
|
||||
} else if (document.visibilityState === "visible") {
|
||||
if (visibilityResumeTimeout !== null) {
|
||||
clearTimeout(visibilityResumeTimeout);
|
||||
}
|
||||
visibilityResumeTimeout = window.setTimeout(() => {
|
||||
visibilityResumeTimeout = null;
|
||||
currentAudio?.play().catch(() => {});
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
document.addEventListener("visibilitychange", visHandler);
|
||||
|
||||
// Allow uploads to trigger refresh
|
||||
const uploadedHandler = () => {
|
||||
const vol = (api.settings as any).volume ?? 0.5;
|
||||
startPlayback(vol);
|
||||
};
|
||||
window.addEventListener("betterseqta-background-music-updated", uploadedHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", visHandler);
|
||||
window.removeEventListener("betterseqta-background-music-updated", uploadedHandler);
|
||||
if (cleanupRegistered && (window as any).__betterseqta_bg_music_cancel__) {
|
||||
(window as any).__betterseqta_bg_music_cancel__();
|
||||
(window as any).__betterseqta_bg_music_cancel__ = undefined;
|
||||
}
|
||||
if (pendingGestureCancel) { pendingGestureCancel(); pendingGestureCancel = null; }
|
||||
if (visibilityResumeTimeout !== null) { clearTimeout(visibilityResumeTimeout); visibilityResumeTimeout = null; }
|
||||
stopAndCleanupAudio();
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default backgroundMusicPlugin;
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.background-music-hidden{display:none}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { defineLazyPlugin } from "../../core/dynamicLoader";
|
||||
import {
|
||||
booleanSetting,
|
||||
buttonSetting,
|
||||
defineSettings,
|
||||
hotkeySetting,
|
||||
} from "../../core/settingsHelpers";
|
||||
import styles from "./src/core/styles.css?inline";
|
||||
|
||||
// Platform-aware default hotkey
|
||||
const getDefaultHotkey = () => {
|
||||
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
return isMac ? "cmd+k" : "ctrl+k";
|
||||
};
|
||||
|
||||
const settings = defineSettings({
|
||||
searchHotkey: hotkeySetting({
|
||||
default: getDefaultHotkey(),
|
||||
title: "Search Hotkey",
|
||||
description: "Keyboard shortcut to open the search",
|
||||
}),
|
||||
showRecentFirst: booleanSetting({
|
||||
default: true,
|
||||
title: "Show Recent First",
|
||||
description: "Sort dynamic content by most recent first",
|
||||
}),
|
||||
transparencyEffects: booleanSetting({
|
||||
default: true,
|
||||
title: "Transparency Effects",
|
||||
description: "Enable transparency effects for the search bar",
|
||||
}),
|
||||
runIndexingOnLoad: booleanSetting({
|
||||
default: true,
|
||||
title: "Index on Page Load",
|
||||
description: "Run content indexing when SEQTA loads",
|
||||
}),
|
||||
resetIndex: buttonSetting({
|
||||
title: "Reset Index",
|
||||
description: "Reset the search index and storage",
|
||||
trigger: async () => {
|
||||
const confirmed = confirm("Are you sure you want to reset the search index and storage?");
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
// Dynamically import modules to avoid loading heavy dependencies
|
||||
const { VectorWorkerManager } = await import("./src/indexing/worker/vectorWorkerManager");
|
||||
const { resetDatabase } = await import("./src/indexing/db");
|
||||
|
||||
// Reset vector worker first
|
||||
try {
|
||||
const workerManager = VectorWorkerManager.getInstance();
|
||||
await workerManager.resetWorker();
|
||||
console.log("Vector worker reset successfully");
|
||||
} catch (e) {
|
||||
console.warn("Failed to reset vector worker:", e);
|
||||
}
|
||||
|
||||
// Close all database connections properly before deletion
|
||||
try {
|
||||
await resetDatabase();
|
||||
console.log("betterseqta-index database closed and reset");
|
||||
} catch (e) {
|
||||
console.warn("Failed to reset betterseqta-index database:", e);
|
||||
}
|
||||
|
||||
// Wait a bit for connections to fully close
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Delete embeddiaDB (vector search database)
|
||||
const deleteDb = (dbName: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const req = indexedDB.deleteDatabase(dbName);
|
||||
req.onsuccess = () => {
|
||||
console.log(`Successfully deleted database: ${dbName}`);
|
||||
resolve();
|
||||
};
|
||||
req.onerror = () => {
|
||||
console.error(`Error deleting database ${dbName}:`, req.error);
|
||||
reject(req.error);
|
||||
};
|
||||
req.onblocked = () => {
|
||||
console.warn(`Database ${dbName} deletion blocked - connections still open`);
|
||||
// Wait and retry once
|
||||
setTimeout(() => {
|
||||
const retryReq = indexedDB.deleteDatabase(dbName);
|
||||
retryReq.onsuccess = () => {
|
||||
console.log(`Successfully deleted database on retry: ${dbName}`);
|
||||
resolve();
|
||||
};
|
||||
retryReq.onerror = () => reject(retryReq.error);
|
||||
retryReq.onblocked = () => {
|
||||
reject(new Error(`One database is open, failed to remove: ${dbName}. Please close other tabs and try again.`));
|
||||
};
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await deleteDb("embeddiaDB");
|
||||
await deleteDb("betterseqta-index");
|
||||
alert("Search index and storage have been reset successfully.");
|
||||
} catch (e) {
|
||||
alert("Failed to reset one or more databases: " + String(e) + "\n\nTry closing other browser tabs and try again.");
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Failed to reset index: " + String(e));
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Create the lazy plugin definition - this loads immediately but doesn't import heavy dependencies
|
||||
export default defineLazyPlugin({
|
||||
id: "global-search",
|
||||
name: "Global Search",
|
||||
description: "Quick search for everything in SEQTA",
|
||||
version: "1.0.0",
|
||||
settings,
|
||||
disableToggle: true,
|
||||
defaultEnabled: false,
|
||||
beta: true,
|
||||
styles: styles,
|
||||
|
||||
// Lazy loader - only imports the heavy plugin when actually needed
|
||||
loader: () => import("./src/core/index")
|
||||
});
|
||||
@@ -35,6 +35,8 @@
|
||||
let isIndexing = $state(false);
|
||||
let completedJobs = $state(0);
|
||||
let totalJobs = $state(0);
|
||||
let indexingStatus = $state<string | null>(null);
|
||||
let indexingDetail = $state<string | null>(null);
|
||||
|
||||
let commandPalleteOpen = $state(false);
|
||||
let searchTerm = $state('');
|
||||
@@ -110,10 +112,12 @@
|
||||
|
||||
onMount(() => {
|
||||
const progressHandler = (event: CustomEvent) => {
|
||||
const { completed, total, indexing } = event.detail;
|
||||
const { completed, total, indexing, status, detail } = event.detail;
|
||||
completedJobs = completed;
|
||||
totalJobs = total;
|
||||
isIndexing = indexing;
|
||||
indexingStatus = status || null;
|
||||
indexingDetail = detail || null;
|
||||
};
|
||||
|
||||
window.addEventListener('indexing-progress', progressHandler as EventListener);
|
||||
@@ -168,6 +172,9 @@
|
||||
term,
|
||||
commandsFuse,
|
||||
commandIdToItemMap,
|
||||
dynamicContentFuse,
|
||||
dynamicIdToItemMap,
|
||||
true, // sortByRecent
|
||||
);
|
||||
} else {
|
||||
combinedResults = [];
|
||||
@@ -176,13 +183,19 @@
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
const debouncedPerformSearch = debounce(performSearch, 20);
|
||||
// Optimized debounce: shorter delay for better responsiveness
|
||||
const debouncedPerformSearch = debounce(performSearch, 50);
|
||||
|
||||
$effect(() => {
|
||||
if (commandPalleteOpen) {
|
||||
if (searchTerm === '') {
|
||||
// Immediate search for empty query (shows recent items)
|
||||
performSearch();
|
||||
} else if (searchTerm.length <= 2) {
|
||||
// Immediate search for very short queries
|
||||
performSearch();
|
||||
} else {
|
||||
// Debounced search for longer queries
|
||||
debouncedPerformSearch();
|
||||
}
|
||||
tick().then(() => searchbar?.focus());
|
||||
@@ -389,19 +402,6 @@
|
||||
{@render Shortcut({ text: 'Select', keybind: ['↵']})}
|
||||
{/if}
|
||||
</div>
|
||||
{#if isIndexing}
|
||||
<div class="inset-x-0 top-0">
|
||||
<div class="absolute right-2 -bottom-4 text-[10px] text-zinc-500 dark:text-zinc-400">
|
||||
Indexing
|
||||
</div>
|
||||
<div class="overflow-hidden h-0.5 bg-zinc-200 dark:bg-zinc-700">
|
||||
<div
|
||||
class="h-full bg-blue-500 transition-all duration-300 ease-out"
|
||||
style="width: {(completedJobs / totalJobs) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
booleanSetting,
|
||||
buttonSetting,
|
||||
defineSettings,
|
||||
Setting,
|
||||
hotkeySetting,
|
||||
Setting,
|
||||
} from "@/plugins/core/settingsHelpers";
|
||||
import styles from "./styles.css?inline";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
@@ -14,6 +14,7 @@ import { initVectorSearch } from "../search/vector/vectorSearch";
|
||||
import { cleanupSearchBar, mountSearchBar } from "./mountSearchBar";
|
||||
import { IndexedDbManager } from "embeddia";
|
||||
import { VectorWorkerManager } from "../indexing/worker/vectorWorkerManager";
|
||||
import { checkAndHandleUpdate } from "../utils/versionCheck";
|
||||
|
||||
// Platform-aware default hotkey
|
||||
const getDefaultHotkey = () => {
|
||||
@@ -50,7 +51,11 @@ const settings = defineSettings({
|
||||
|
||||
if (confirmed) {
|
||||
try {
|
||||
// Import resetDatabase function to properly close connections
|
||||
const { resetDatabase } = await import("../indexing/db");
|
||||
|
||||
// Reset the vector worker first
|
||||
try {
|
||||
const workerManager = VectorWorkerManager.getInstance();
|
||||
await workerManager.resetWorker();
|
||||
console.log("Vector worker reset successfully");
|
||||
@@ -58,23 +63,55 @@ const settings = defineSettings({
|
||||
console.warn("Failed to reset vector worker:", e);
|
||||
}
|
||||
|
||||
// Delete both 'embeddiaDB' and 'betterseqta-index' using native IndexedDB APIs
|
||||
// Close all database connections properly before deletion
|
||||
try {
|
||||
await resetDatabase();
|
||||
} catch (e) {
|
||||
console.warn("Failed to reset betterseqta-index database:", e);
|
||||
}
|
||||
|
||||
// Wait a bit for connections to fully close
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Delete embeddiaDB (vector search database)
|
||||
const deleteDb = (dbName: string) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const req = indexedDB.deleteDatabase(dbName);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onsuccess = () => {
|
||||
console.log(`Successfully deleted database: ${dbName}`);
|
||||
resolve();
|
||||
};
|
||||
req.onerror = () => {
|
||||
console.error(`Error deleting database ${dbName}:`, req.error);
|
||||
reject(req.error);
|
||||
};
|
||||
req.onblocked = () => {
|
||||
reject(new Error(`One database is open, failed to remove: ${dbName}`));
|
||||
console.warn(`Database ${dbName} deletion blocked - connections still open`);
|
||||
// Wait and retry once
|
||||
setTimeout(() => {
|
||||
const retryReq = indexedDB.deleteDatabase(dbName);
|
||||
retryReq.onsuccess = () => {
|
||||
console.log(`Successfully deleted database on retry: ${dbName}`);
|
||||
resolve();
|
||||
};
|
||||
retryReq.onerror = () => reject(retryReq.error);
|
||||
retryReq.onblocked = () => {
|
||||
reject(new Error(`One database is open, failed to remove: ${dbName}. Please close other tabs and try again.`));
|
||||
};
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await deleteDb("embeddiaDB");
|
||||
await deleteDb("betterseqta-index");
|
||||
alert("Search index and storage have been reset.");
|
||||
alert("Search index and storage have been reset successfully.");
|
||||
} catch (e) {
|
||||
alert("Failed to reset one or more databases: " + String(e));
|
||||
alert("Failed to reset one or more databases: " + String(e) + "\n\nTry closing other browser tabs and try again.");
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Failed to reset index: " + String(e));
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -114,6 +151,27 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
||||
run: async (api) => {
|
||||
const appRef = { current: null };
|
||||
|
||||
// Check for extension updates and clear caches if needed
|
||||
// Use a timeout to avoid blocking initialization
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const wasUpdated = await checkAndHandleUpdate();
|
||||
if (wasUpdated) {
|
||||
console.log("[Global Search] Extension updated - caches cleared");
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Handle CSS preload errors and other failures gracefully
|
||||
// These can happen in Firefox or when assets aren't available
|
||||
if (error?.message?.includes("preload CSS") ||
|
||||
error?.message?.includes("MIME type") ||
|
||||
error?.message?.includes("NS_ERROR_CORRUPTED_CONTENT")) {
|
||||
console.debug("[Global Search] Version check skipped due to asset loading restrictions:", error.message);
|
||||
} else {
|
||||
console.warn("[Global Search] Failed to check for updates:", error);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
await IndexedDbManager.create("embeddiaDB", "embeddiaObjectStore", {
|
||||
primaryKey: "id",
|
||||
@@ -126,10 +184,16 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
||||
|
||||
initVectorSearch();
|
||||
|
||||
// Warm up vector worker in background to improve initial response time
|
||||
// Warm up vector worker in background to improve initial response time (skip in Firefox)
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
// Only initialize worker if vector search is supported
|
||||
const { isVectorSearchSupported } = await import("../utils/browserDetection");
|
||||
if (isVectorSearchSupported()) {
|
||||
VectorWorkerManager.getInstance();
|
||||
} else {
|
||||
console.debug("[Global Search] Skipping vector worker warm-up (Firefox detected - using text search only)");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("[Global Search] Vector worker warm-up failed:", error);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import browser from "webextension-polyfill";
|
||||
export function mountSearchBar(
|
||||
titleElement: Element,
|
||||
api: any,
|
||||
appRef: { current: any; storageChangeHandler?: any },
|
||||
appRef: { current: any; storageChangeHandler?: any; progressHandler?: any },
|
||||
) {
|
||||
if (titleElement.querySelector(".search-trigger")) {
|
||||
return;
|
||||
@@ -21,6 +21,72 @@ export function mountSearchBar(
|
||||
const searchButton = document.createElement("div");
|
||||
searchButton.className = "search-trigger";
|
||||
|
||||
// Create progress indicator container
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className = "search-progress-container";
|
||||
progressContainer.style.cssText = "display: flex; align-items: center; gap: 8px; margin-left: 8px; min-width: 120px;";
|
||||
|
||||
// Create progress bar
|
||||
const progressBarWrapper = document.createElement("div");
|
||||
progressBarWrapper.className = "search-progress-bar-wrapper";
|
||||
progressBarWrapper.style.cssText = "flex: 1; height: 4px; background: rgba(0, 0, 0, 0.1); border-radius: 2px; overflow: hidden; display: none;";
|
||||
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.className = "search-progress-bar";
|
||||
progressBar.style.cssText = "height: 100%; background: linear-gradient(90deg, #3b82f6, #2563eb, #3b82f6); transition: width 0.3s ease-out; width: 0%; position: relative;";
|
||||
|
||||
// Add shimmer effect
|
||||
const shimmer = document.createElement("div");
|
||||
shimmer.style.cssText = "position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); animation: shimmer 2s infinite;";
|
||||
progressBar.appendChild(shimmer);
|
||||
progressBarWrapper.appendChild(progressBar);
|
||||
|
||||
// Create progress text
|
||||
const progressText = document.createElement("span");
|
||||
progressText.className = "search-progress-text";
|
||||
progressText.style.cssText = "font-size: 11px; color: #666; white-space: nowrap; display: none;";
|
||||
|
||||
progressContainer.appendChild(progressBarWrapper);
|
||||
progressContainer.appendChild(progressText);
|
||||
|
||||
// Indexing state
|
||||
let isIndexing = false;
|
||||
let completedJobs = 0;
|
||||
let totalJobs = 0;
|
||||
let indexingStatus: string | null = null;
|
||||
|
||||
const updateProgressDisplay = () => {
|
||||
if (isIndexing && totalJobs > 0) {
|
||||
const percentage = Math.round((completedJobs / totalJobs) * 100);
|
||||
progressBar.style.width = `${Math.max(2, percentage)}%`;
|
||||
progressBarWrapper.style.display = "block";
|
||||
|
||||
if (indexingStatus) {
|
||||
progressText.textContent = indexingStatus.length > 20 ? indexingStatus.substring(0, 20) + "..." : indexingStatus;
|
||||
progressText.style.display = "block";
|
||||
} else {
|
||||
progressText.textContent = `${completedJobs}/${totalJobs} (${percentage}%)`;
|
||||
progressText.style.display = "block";
|
||||
}
|
||||
} else {
|
||||
progressBarWrapper.style.display = "none";
|
||||
progressText.style.display = "none";
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for indexing progress events
|
||||
const progressHandler = (event: CustomEvent) => {
|
||||
const { completed, total, indexing, status } = event.detail;
|
||||
completedJobs = completed || 0;
|
||||
totalJobs = total || 0;
|
||||
isIndexing = indexing || false;
|
||||
indexingStatus = status || null;
|
||||
updateProgressDisplay();
|
||||
};
|
||||
|
||||
window.addEventListener('indexing-progress', progressHandler as EventListener);
|
||||
appRef.progressHandler = progressHandler;
|
||||
|
||||
const updateSearchButtonDisplay = () => {
|
||||
searchButton.innerHTML = /* html */ `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -34,6 +100,7 @@ export function mountSearchBar(
|
||||
|
||||
updateSearchButtonDisplay();
|
||||
titleElement.appendChild(searchButton);
|
||||
titleElement.appendChild(progressContainer);
|
||||
|
||||
// Listen for hotkey setting changes
|
||||
const handleStorageChange = (changes: any, area: string) => {
|
||||
@@ -72,7 +139,7 @@ export function mountSearchBar(
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupSearchBar(appRef: { current: any; storageChangeHandler?: any }) {
|
||||
export function cleanupSearchBar(appRef: { current: any; storageChangeHandler?: any; progressHandler?: any }) {
|
||||
if (appRef.current) {
|
||||
try {
|
||||
unmount(appRef.current);
|
||||
@@ -82,12 +149,24 @@ export function cleanupSearchBar(appRef: { current: any; storageChangeHandler?:
|
||||
}
|
||||
}
|
||||
|
||||
// Remove progress event listener
|
||||
if (appRef.progressHandler) {
|
||||
window.removeEventListener('indexing-progress', appRef.progressHandler as EventListener);
|
||||
appRef.progressHandler = null;
|
||||
}
|
||||
|
||||
// Remove search trigger button
|
||||
const searchTrigger = document.querySelector(".search-trigger");
|
||||
if (searchTrigger) {
|
||||
searchTrigger.remove();
|
||||
}
|
||||
|
||||
// Remove progress container
|
||||
const progressContainer = document.querySelector(".search-progress-container");
|
||||
if (progressContainer) {
|
||||
progressContainer.remove();
|
||||
}
|
||||
|
||||
// Remove search root
|
||||
const searchRoot = document.querySelector("div[data-search-root]");
|
||||
if (searchRoot) {
|
||||
|
||||
@@ -69,3 +69,71 @@
|
||||
.dark .highlight {
|
||||
background-color: rgba(255, 230, 100, 0.4);
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
/* Progress indicator next to search trigger */
|
||||
.search-progress-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 8px;
|
||||
min-width: 120px;
|
||||
max-width: 200px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.search-progress-bar-wrapper {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.dark .search-progress-bar-wrapper {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.search-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3b82f6, #2563eb, #3b82f6);
|
||||
transition: width 0.3s ease-out;
|
||||
width: 0%;
|
||||
position: relative;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-progress-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
animation: shimmer 2s infinite;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-progress-text {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
display: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .search-progress-text {
|
||||
color: #999;
|
||||
}
|
||||
@@ -59,17 +59,132 @@ export const actionMap: Record<string, ActionHandler<any>> = {
|
||||
}) as ActionHandler<any>,
|
||||
|
||||
assessment: (async (item: IndexItem & { metadata: AssessmentMetadata }) => {
|
||||
if (item.metadata.isMessageBased) {
|
||||
// Deep clone the entire item to avoid Firefox XrayWrapper issues
|
||||
// Firefox XrayWrapper prevents direct access to nested properties
|
||||
let itemClone: IndexItem & { metadata: AssessmentMetadata };
|
||||
let metadata: AssessmentMetadata;
|
||||
|
||||
try {
|
||||
// First try to clone the entire item
|
||||
itemClone = JSON.parse(JSON.stringify(item));
|
||||
metadata = itemClone.metadata || {};
|
||||
} catch (e) {
|
||||
console.warn("[Assessment Action] Failed to clone item, trying to clone metadata separately:", e);
|
||||
try {
|
||||
// If full clone fails, try cloning just metadata
|
||||
metadata = JSON.parse(JSON.stringify(item.metadata || {}));
|
||||
itemClone = { ...item, metadata };
|
||||
} catch (e2) {
|
||||
console.warn("[Assessment Action] Failed to clone metadata, using direct access:", e2);
|
||||
itemClone = item;
|
||||
metadata = item.metadata || {} as AssessmentMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to extract metadata values using multiple methods to handle XrayWrapper
|
||||
const getMetadataValue = (key: string, altKey?: string): any => {
|
||||
try {
|
||||
// Try direct access first
|
||||
const value = metadata[key];
|
||||
if (value !== undefined && value !== null) {
|
||||
return value;
|
||||
}
|
||||
if (altKey) {
|
||||
const altValue = metadata[altKey];
|
||||
if (altValue !== undefined && altValue !== null) {
|
||||
return altValue;
|
||||
}
|
||||
}
|
||||
// Try accessing via Object.keys iteration (works around XrayWrapper)
|
||||
try {
|
||||
const keys = Object.keys(metadata);
|
||||
for (const k of keys) {
|
||||
if (k === key || k === altKey) {
|
||||
const val = metadata[k];
|
||||
if (val !== undefined && val !== null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Object.keys might fail on XrayWrapper, that's okay
|
||||
}
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
console.warn(`[Assessment Action] Failed to access metadata.${key}:`, e);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (getMetadataValue('isMessageBased')) {
|
||||
window.location.hash = `#?page=/messages`;
|
||||
|
||||
await waitForElm('[class*="Viewer__Viewer___"] > div', true, 20);
|
||||
|
||||
// Select the specific direct message
|
||||
ReactFiber.find('[class*="Viewer__Viewer___"] > div').setState({
|
||||
selected: new Set([item.metadata.messageId]),
|
||||
selected: new Set([getMetadataValue('messageId')]),
|
||||
});
|
||||
} else {
|
||||
window.location.hash = `#?page=/assessments&id=${item.metadata.assessmentId}`;
|
||||
// Extract values - check both camelCase and PascalCase, and try multiple access methods
|
||||
let programmeId = getMetadataValue('programmeId', 'programmeID');
|
||||
let metaclassId = getMetadataValue('metaclassId', 'metaclassID');
|
||||
let assessmentId = getMetadataValue('assessmentId', 'assessmentID');
|
||||
|
||||
// Fallback: try to extract assessmentId from item ID if metadata is missing
|
||||
if ((assessmentId === undefined || assessmentId === null) && itemClone.id && itemClone.id.startsWith('assignment-')) {
|
||||
const extractedId = itemClone.id.replace('assignment-', '');
|
||||
assessmentId = Number(extractedId) || extractedId;
|
||||
console.log("[Assessment Action] Extracted assessmentId from item ID:", assessmentId);
|
||||
}
|
||||
|
||||
// Convert to numbers, but preserve 0 as valid
|
||||
if (programmeId !== undefined && programmeId !== null && programmeId !== '') {
|
||||
const num = Number(programmeId);
|
||||
programmeId = isNaN(num) ? programmeId : num;
|
||||
}
|
||||
if (metaclassId !== undefined && metaclassId !== null && metaclassId !== '') {
|
||||
const num = Number(metaclassId);
|
||||
metaclassId = isNaN(num) ? metaclassId : num;
|
||||
}
|
||||
if (assessmentId !== undefined && assessmentId !== null && assessmentId !== '') {
|
||||
const num = Number(assessmentId);
|
||||
assessmentId = isNaN(num) ? assessmentId : num;
|
||||
}
|
||||
|
||||
// Check if values exist (including 0, which is a valid ID)
|
||||
// Use typeof check to properly handle 0
|
||||
const hasProgrammeId = programmeId !== undefined && programmeId !== null && programmeId !== '' && typeof programmeId === 'number';
|
||||
const hasMetaclassId = metaclassId !== undefined && metaclassId !== null && metaclassId !== '' && typeof metaclassId === 'number';
|
||||
const hasAssessmentId = assessmentId !== undefined && assessmentId !== null && assessmentId !== '' && typeof assessmentId === 'number';
|
||||
|
||||
|
||||
|
||||
if (hasProgrammeId && hasMetaclassId && hasAssessmentId) {
|
||||
const url = `#?page=/assessments/${programmeId}:${metaclassId}&item=${assessmentId}`;
|
||||
console.log("[Assessment Action] ✅ Navigating to:", url);
|
||||
window.location.hash = url;
|
||||
} else {
|
||||
// Fallback: try to navigate to assessments page if metadata is incomplete
|
||||
console.error("[Assessment Action] ❌ Missing required metadata:", {
|
||||
programmeId,
|
||||
metaclassId,
|
||||
assessmentId,
|
||||
hasProgrammeId,
|
||||
hasMetaclassId,
|
||||
hasAssessmentId,
|
||||
metadataKeys: Object.keys(metadata),
|
||||
metadataString: JSON.stringify(metadata),
|
||||
itemId: itemClone.id,
|
||||
});
|
||||
// If we at least have an assessmentId, try to navigate to the general assessments page
|
||||
if (hasAssessmentId) {
|
||||
window.location.hash = `#?page=/assessments/upcoming&item=${assessmentId}`;
|
||||
} else {
|
||||
console.warn("[Assessment Action] No valid assessment ID, redirecting to upcoming");
|
||||
window.location.hash = `#?page=/assessments/upcoming`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}) as ActionHandler<any>,
|
||||
|
||||
|
||||
@@ -213,25 +213,54 @@ export async function clear(store: string): Promise<void> {
|
||||
}
|
||||
|
||||
export async function resetDatabase(): Promise<void> {
|
||||
// Close cached database connection
|
||||
if (cachedDb) {
|
||||
try {
|
||||
cachedDb.close();
|
||||
} catch (e) {
|
||||
console.warn("[DB] Error closing cached database:", e);
|
||||
}
|
||||
cachedDb = null;
|
||||
}
|
||||
|
||||
// Close pending database promise
|
||||
if (dbPromise) {
|
||||
try {
|
||||
const db = await dbPromise;
|
||||
db.close();
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
// Database might not be open yet, that's okay
|
||||
}
|
||||
dbPromise = null;
|
||||
}
|
||||
|
||||
// Wait a bit for connections to fully close
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.deleteDatabase(DB_NAME);
|
||||
req.onsuccess = () => {
|
||||
localStorage.removeItem(VERSION_KEY);
|
||||
resolve();
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onerror = () => {
|
||||
console.error("[DB] Error deleting database:", req.error);
|
||||
reject(req.error);
|
||||
};
|
||||
req.onblocked = () => {
|
||||
console.warn("[DB] Database deletion blocked - waiting for connections to close");
|
||||
// Wait a bit longer and try again
|
||||
setTimeout(() => {
|
||||
const retryReq = indexedDB.deleteDatabase(DB_NAME);
|
||||
retryReq.onsuccess = () => {
|
||||
localStorage.removeItem(VERSION_KEY);
|
||||
resolve();
|
||||
};
|
||||
retryReq.onerror = () => reject(retryReq.error);
|
||||
retryReq.onblocked = () => {
|
||||
reject(new Error(`Database is still open. Please close other tabs/windows and try again.`));
|
||||
};
|
||||
}, 500);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clear, getAll, get, put, remove } from "./db";
|
||||
import { clear, get, getAll, put, remove } from "./db";
|
||||
import { jobs } from "./jobs";
|
||||
import { renderComponentMap } from "./renderComponents";
|
||||
import type { IndexItem, Job, JobContext } from "./types";
|
||||
@@ -396,18 +396,34 @@ export async function runIndexing(): Promise<void> {
|
||||
stopHeartbeat();
|
||||
|
||||
allItemsInPrimaryStores = await loadAllStoredItems();
|
||||
allItemsInPrimaryStores.forEach(item => {
|
||||
// Create new objects to avoid XrayWrapper issues in Firefox
|
||||
const itemsWithComponents = allItemsInPrimaryStores.map(item => {
|
||||
try {
|
||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
||||
let renderComponent = item.renderComponent;
|
||||
if (jobDef) {
|
||||
const renderComponent = renderComponentMap[jobDef.renderComponentId];
|
||||
if (renderComponent) {
|
||||
item.renderComponent = renderComponent;
|
||||
}
|
||||
renderComponent = renderComponentMap[jobDef.renderComponentId] || renderComponent;
|
||||
} else if (renderComponentMap[item.renderComponentId]) {
|
||||
item.renderComponent = renderComponentMap[item.renderComponentId];
|
||||
renderComponent = renderComponentMap[item.renderComponentId];
|
||||
}
|
||||
// Deep clone to avoid Firefox XrayWrapper issues with nested objects like metadata
|
||||
// Use JSON serialization to ensure all nested properties are accessible
|
||||
try {
|
||||
const cloned = JSON.parse(JSON.stringify(item));
|
||||
cloned.renderComponent = renderComponent;
|
||||
return cloned;
|
||||
} catch (e) {
|
||||
// Fallback to shallow copy if deep clone fails
|
||||
console.warn("[Indexer] Failed to deep clone item, using shallow copy:", e);
|
||||
return { ...item, renderComponent };
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: return item as-is if modification fails (Firefox XrayWrapper)
|
||||
console.warn("[Indexer] Failed to add render component to item (Firefox XrayWrapper):", error);
|
||||
return item;
|
||||
}
|
||||
});
|
||||
loadDynamicItems(allItemsInPrimaryStores);
|
||||
loadDynamicItems(itemsWithComponents);
|
||||
window.dispatchEvent(new Event("dynamic-items-updated"));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import { messagesJob } from "./jobs/messages";
|
||||
import { notificationsJob } from "./jobs/notifications";
|
||||
import { forumsJob } from "./jobs/forums";
|
||||
import { subjectsJob } from "./jobs/subjects";
|
||||
import { assignmentsJob } from "./jobs/assignments";
|
||||
|
||||
export const jobs: Record<string, Job> = {
|
||||
messages: messagesJob,
|
||||
notifications: notificationsJob,
|
||||
forums: forumsJob,
|
||||
subjects: subjectsJob,
|
||||
assignments: assignmentsJob,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import type { IndexItem, Job } from "../types";
|
||||
|
||||
const fetchJSON = async (url: string, body: any) => {
|
||||
const res = await fetch(`${location.origin}${url}`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const fetchUpcomingAssessments = async (student: number = 69) => {
|
||||
try {
|
||||
const res = await fetchJSON("/seqta/student/assessment/list/upcoming?", {
|
||||
student,
|
||||
});
|
||||
// Match analytics.rs: payload is an array, return empty array if not found
|
||||
return Array.isArray(res.payload) ? res.payload : [];
|
||||
} catch (e) {
|
||||
console.error("[Assignments job] Failed to fetch upcoming assessments:", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSubjects = async () => {
|
||||
try {
|
||||
const res = await fetchJSON("/seqta/student/load/subjects?", {});
|
||||
return res.payload
|
||||
?.filter((s: any) => s.active === 1)
|
||||
?.flatMap((s: any) => s.subjects) || [];
|
||||
} catch (e) {
|
||||
console.error("[Assignments job] Failed to fetch subjects:", e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const fetchPastAssessments = async (student: number = 69, subjects: any[]) => {
|
||||
const map: Record<number, any> = {};
|
||||
|
||||
// Fetch past assessments for all subjects in parallel (like assessmentsOverview does)
|
||||
// This is much faster than sequential fetching
|
||||
await Promise.all(
|
||||
subjects.map(async (subject) => {
|
||||
try {
|
||||
// Match analytics.rs exactly: parameter order is programme, metaclass, student
|
||||
const res = await fetchJSON("/seqta/student/assessment/list/past?", {
|
||||
programme: subject.programme,
|
||||
metaclass: subject.metaclass,
|
||||
student,
|
||||
});
|
||||
|
||||
// Past assessments API can return data in payload.tasks OR payload.pending (or both)
|
||||
// Based on analytics.rs fetch_past_assessments, we need to check both arrays
|
||||
const processAssessment = (assessment: any) => {
|
||||
if (assessment && assessment.id) {
|
||||
// Ensure programme and metaclass are included from the subject
|
||||
// Use the assessment's IDs if available, otherwise fall back to subject's
|
||||
map[assessment.id] = {
|
||||
...assessment,
|
||||
programme: assessment.programme || assessment.programmeID || subject.programme,
|
||||
programmeID: assessment.programmeID || assessment.programme || subject.programme,
|
||||
metaclass: assessment.metaclass || assessment.metaclassID || subject.metaclass,
|
||||
metaclassID: assessment.metaclassID || assessment.metaclass || subject.metaclass,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Match analytics.rs: Check both pending and tasks arrays
|
||||
// Check for pending array first (matching Rust code order)
|
||||
if (res.payload?.pending && Array.isArray(res.payload.pending)) {
|
||||
res.payload.pending.forEach(processAssessment);
|
||||
}
|
||||
|
||||
// Check for tasks array
|
||||
if (res.payload?.tasks && Array.isArray(res.payload.tasks)) {
|
||||
res.payload.tasks.forEach(processAssessment);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[Assignments job] Failed to fetch past assessments for subject ${subject.code || subject.subject || 'unknown'}:`, e);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return Object.values(map);
|
||||
};
|
||||
|
||||
export const assignmentsJob: Job = {
|
||||
id: "assignments",
|
||||
label: "Assignments",
|
||||
renderComponentId: "assessment",
|
||||
frequency: { type: "expiry", afterMs: 1000 * 60 * 60 * 24 }, // Daily
|
||||
|
||||
boostCriteria: (item, searchTerm) => {
|
||||
if (searchTerm === "") {
|
||||
return -100;
|
||||
}
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Boost upcoming assignments
|
||||
if (item.metadata.dueDate) {
|
||||
const dueDate = new Date(item.metadata.dueDate).getTime();
|
||||
const now = Date.now();
|
||||
const daysUntilDue = (dueDate - now) / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (daysUntilDue >= 0 && daysUntilDue <= 7) {
|
||||
score += 0.05; // Boost assignments due within a week
|
||||
}
|
||||
if (daysUntilDue < 0) {
|
||||
score -= 0.1; // Penalty for overdue assignments
|
||||
}
|
||||
}
|
||||
|
||||
// Boost if submitted
|
||||
if (item.metadata.submitted) {
|
||||
score += 0.02;
|
||||
}
|
||||
|
||||
return score;
|
||||
},
|
||||
|
||||
run: async (ctx) => {
|
||||
// Don't filter by existing IDs - we want to process ALL assessments (both new and old)
|
||||
// to ensure metadata is up-to-date and all past assignments are indexed
|
||||
const existingItems = await ctx.getStoredItems("assignments");
|
||||
const existingIds = new Set(existingItems.map((i) => i.id));
|
||||
|
||||
const student = 69; // TODO: Get from context if available
|
||||
|
||||
console.debug("[Assignments job] Starting indexing - fetching all assessments (upcoming and past)...");
|
||||
|
||||
// Fetch data in parallel
|
||||
const [upcoming, subjects] = await Promise.all([
|
||||
fetchUpcomingAssessments(student),
|
||||
fetchSubjects(),
|
||||
]);
|
||||
|
||||
console.debug(`[Assignments job] Fetched ${upcoming.length} upcoming assessments and ${subjects.length} subjects`);
|
||||
|
||||
// Fetch past assessments for ALL subjects to ensure we get all historical assignments
|
||||
const past = await fetchPastAssessments(student, subjects);
|
||||
|
||||
console.debug(`[Assignments job] Fetched ${past.length} past assessments`);
|
||||
|
||||
// Create a lookup map from subject code to programme/metaclass
|
||||
const subjectLookup = new Map<string, { programme: number; metaclass: number }>();
|
||||
subjects.forEach((s: any) => {
|
||||
if (s.code && s.programme && s.metaclass) {
|
||||
subjectLookup.set(s.code, { programme: s.programme, metaclass: s.metaclass });
|
||||
}
|
||||
});
|
||||
|
||||
// Combine and deduplicate
|
||||
const allAssessments = new Map<number, any>();
|
||||
|
||||
upcoming.forEach((a: any) => {
|
||||
if (a && a.id) {
|
||||
// Prioritize capital ID fields (programmeID, metaclassID) as that's what the API returns
|
||||
let programme = a.programmeID || a.programme;
|
||||
let metaclass = a.metaclassID || a.metaclass;
|
||||
|
||||
// If missing, try to get from subject lookup
|
||||
if ((!programme || !metaclass) && a.code) {
|
||||
const subjectInfo = subjectLookup.get(a.code);
|
||||
if (subjectInfo) {
|
||||
programme = programme || subjectInfo.programme;
|
||||
metaclass = metaclass || subjectInfo.metaclass;
|
||||
}
|
||||
}
|
||||
|
||||
allAssessments.set(a.id, {
|
||||
...a,
|
||||
programme,
|
||||
metaclass,
|
||||
programmeID: programme, // Ensure both formats are available
|
||||
metaclassID: metaclass,
|
||||
isUpcoming: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
past.forEach((a: any) => {
|
||||
if (a && a.id) {
|
||||
// Prioritize capital ID fields (programmeID, metaclassID) as that's what the API returns
|
||||
let programme = a.programmeID || a.programme;
|
||||
let metaclass = a.metaclassID || a.metaclass;
|
||||
|
||||
const existing = allAssessments.get(a.id);
|
||||
if (existing) {
|
||||
// Merge past assessment data, ensuring programme/metaclass are preserved
|
||||
// Use existing values if new ones are missing
|
||||
programme = programme || existing.programme || existing.programmeID;
|
||||
metaclass = metaclass || existing.metaclass || existing.metaclassID;
|
||||
|
||||
Object.assign(existing, {
|
||||
...a,
|
||||
programme,
|
||||
metaclass,
|
||||
programmeID: programme,
|
||||
metaclassID: metaclass,
|
||||
});
|
||||
} else {
|
||||
allAssessments.set(a.id, {
|
||||
...a,
|
||||
programme,
|
||||
metaclass,
|
||||
programmeID: programme,
|
||||
metaclassID: metaclass,
|
||||
isUpcoming: false
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const items: IndexItem[] = [];
|
||||
const processedIds = new Set<string>();
|
||||
|
||||
// Process assessments in batches to avoid overwhelming the API
|
||||
const assessmentArray = Array.from(allAssessments.values());
|
||||
const pastCount = assessmentArray.filter(a => !a.isUpcoming).length;
|
||||
const upcomingCount = assessmentArray.filter(a => a.isUpcoming).length;
|
||||
console.debug(`[Assignments job] Processing ${assessmentArray.length} total assessments (${upcomingCount} upcoming, ${pastCount} past)`);
|
||||
const batchSize = 15; // Increased batch size for better performance
|
||||
|
||||
// Skip fetching assessment details - the API endpoint doesn't exist or returns 404
|
||||
// Details are optional and not critical for search functionality
|
||||
|
||||
// Process ALL assessments (both upcoming and past) to ensure everything is indexed
|
||||
for (let i = 0; i < assessmentArray.length; i += batchSize) {
|
||||
const batch = assessmentArray.slice(i, i + batchSize);
|
||||
|
||||
const batchItems = await Promise.all(
|
||||
batch.map(async (assessment) => {
|
||||
const id = `assignment-${assessment.id}`;
|
||||
|
||||
// Skip if already processed in this batch
|
||||
if (processedIds.has(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
processedIds.add(id);
|
||||
|
||||
// Process ALL assessments (both new and existing, upcoming and past)
|
||||
// This ensures all historical assignments are indexed and metadata is up-to-date
|
||||
|
||||
// Skip fetching details - API endpoint doesn't exist
|
||||
const description = "";
|
||||
|
||||
const subjectName = assessment.subject || assessment.code || "Unknown Subject";
|
||||
const dueDate = assessment.due ? new Date(assessment.due).getTime() : null;
|
||||
|
||||
// Prioritize capital ID fields (programmeID, metaclassID) as that's what the API returns
|
||||
const programmeId = assessment.programmeID || assessment.programme;
|
||||
const metaclassId = assessment.metaclassID || assessment.metaclass;
|
||||
|
||||
// Validate that we have the required IDs for navigation
|
||||
if (!programmeId || !metaclassId || !assessment.id) {
|
||||
console.warn(`[Assignments job] Skipping assignment ${assessment.id} - missing required IDs:`, {
|
||||
programmeId,
|
||||
metaclassId,
|
||||
assessmentId: assessment.id,
|
||||
programmeID: assessment.programmeID,
|
||||
metaclassID: assessment.metaclassID,
|
||||
programme: assessment.programme,
|
||||
metaclass: assessment.metaclass,
|
||||
assessment,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert to numbers, preserving 0 as valid
|
||||
let finalProgrammeId: number | undefined;
|
||||
let finalMetaclassId: number | undefined;
|
||||
|
||||
if (programmeId !== undefined && programmeId !== null && programmeId !== '') {
|
||||
const num = Number(programmeId);
|
||||
finalProgrammeId = isNaN(num) ? undefined : num;
|
||||
}
|
||||
|
||||
if (metaclassId !== undefined && metaclassId !== null && metaclassId !== '') {
|
||||
const num = Number(metaclassId);
|
||||
finalMetaclassId = isNaN(num) ? undefined : num;
|
||||
}
|
||||
|
||||
// Final validation - check for actual numbers (including 0)
|
||||
if (finalProgrammeId === undefined || finalMetaclassId === undefined || !assessment.id) {
|
||||
console.error(`[Assignments job] ❌ Skipping assignment ${assessment.id} - invalid IDs after conversion:`, {
|
||||
programmeId: finalProgrammeId,
|
||||
metaclassId: finalMetaclassId,
|
||||
assessmentId: assessment.id,
|
||||
rawProgrammeId: programmeId,
|
||||
rawMetaclassId: metaclassId,
|
||||
assessment,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const item: IndexItem = {
|
||||
id,
|
||||
text: assessment.title || assessment.name || "Untitled Assignment",
|
||||
category: "assignments",
|
||||
content: `${description}\nSubject: ${subjectName}\nDue: ${assessment.due || "No due date"}`.trim(),
|
||||
dateAdded: dueDate || Date.now(),
|
||||
metadata: {
|
||||
assessmentId: assessment.id,
|
||||
assessmentID: assessment.id, // Store both variants for compatibility
|
||||
subject: subjectName,
|
||||
subjectCode: assessment.code,
|
||||
dueDate: assessment.due,
|
||||
programmeId: finalProgrammeId,
|
||||
programmeID: finalProgrammeId, // Store both variants for compatibility
|
||||
metaclassId: finalMetaclassId,
|
||||
metaclassID: finalMetaclassId, // Store both variants for compatibility
|
||||
submitted: assessment.submitted || false,
|
||||
isUpcoming: assessment.isUpcoming || false,
|
||||
term: assessment.term,
|
||||
timestamp: assessment.due || new Date().toISOString(), // Required by AssessmentMetadata interface
|
||||
},
|
||||
actionId: "assessment",
|
||||
renderComponentId: "assessment",
|
||||
};
|
||||
|
||||
console.debug(`[Assignments job] ✅ Created item for assignment ${assessment.id}:`, {
|
||||
id: item.id,
|
||||
programmeId: item.metadata.programmeId,
|
||||
programmeID: item.metadata.programmeID,
|
||||
metaclassId: item.metadata.metaclassId,
|
||||
metaclassID: item.metadata.metaclassID,
|
||||
assessmentId: item.metadata.assessmentId,
|
||||
assessmentID: item.metadata.assessmentID,
|
||||
});
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
// Filter out nulls and add to items
|
||||
batchItems.forEach(item => {
|
||||
if (item) {
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Small delay between batches to avoid rate limiting
|
||||
if (i + batchSize < assessmentArray.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 50)); // Reduced delay
|
||||
}
|
||||
}
|
||||
|
||||
const newItemsCount = items.filter(item => !existingIds.has(item.id)).length;
|
||||
const updatedItemsCount = items.length - newItemsCount;
|
||||
console.debug(`[Assignments job] Indexed ${items.length} assignment items (${newItemsCount} new, ${updatedItemsCount} updated)`);
|
||||
return items;
|
||||
},
|
||||
|
||||
purge: (items) => {
|
||||
// Keep ALL assignments - don't purge old ones as users may want to search for them
|
||||
// Only remove items that are truly invalid (missing required metadata)
|
||||
return items.filter((i) => {
|
||||
// Keep all items that have valid metadata
|
||||
return i.metadata &&
|
||||
i.metadata.assessmentId &&
|
||||
i.metadata.programmeId !== undefined &&
|
||||
i.metadata.metaclassId !== undefined;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Job, IndexItem } from "../types";
|
||||
import type { IndexItem, Job } from "../types";
|
||||
|
||||
const fetchForums = async () => {
|
||||
const res = await fetch(`${location.origin}/seqta/student/load/forums`, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Job, IndexItem } from "../types";
|
||||
import type { IndexItem, Job } from "../types";
|
||||
import { htmlToPlainText } from "../utils";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
import { VectorWorkerManager } from "../worker/vectorWorkerManager";
|
||||
@@ -604,22 +604,34 @@ export const messagesJob: Job = {
|
||||
if (processedItems.length > 0) {
|
||||
try {
|
||||
const currentItems = await loadAllStoredItems();
|
||||
currentItems.forEach((item) => {
|
||||
// Create new objects to avoid XrayWrapper issues in Firefox
|
||||
const itemsWithComponents = currentItems.map((item) => {
|
||||
try {
|
||||
const jobDef =
|
||||
jobs[item.category] ||
|
||||
Object.values(jobs).find((j) => j.id === item.category) ||
|
||||
jobs[item.renderComponentId];
|
||||
let renderComponent = item.renderComponent;
|
||||
if (jobDef) {
|
||||
const renderComponent =
|
||||
renderComponentMap[jobDef.renderComponentId];
|
||||
if (renderComponent) {
|
||||
item.renderComponent = renderComponent;
|
||||
}
|
||||
renderComponent = renderComponentMap[jobDef.renderComponentId] || renderComponent;
|
||||
} else if (renderComponentMap[item.renderComponentId]) {
|
||||
item.renderComponent = renderComponentMap[item.renderComponentId];
|
||||
renderComponent = renderComponentMap[item.renderComponentId];
|
||||
}
|
||||
// Deep clone to avoid Firefox XrayWrapper issues with nested objects like metadata
|
||||
try {
|
||||
const cloned = JSON.parse(JSON.stringify(item));
|
||||
cloned.renderComponent = renderComponent;
|
||||
return cloned;
|
||||
} catch (e) {
|
||||
// Fallback to shallow copy if deep clone fails
|
||||
return { ...item, renderComponent };
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: return item as-is if modification fails (Firefox XrayWrapper)
|
||||
return item;
|
||||
}
|
||||
});
|
||||
loadDynamicItems(currentItems);
|
||||
loadDynamicItems(itemsWithComponents);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("dynamic-items-updated", {
|
||||
detail: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Job, IndexItem } from "../types";
|
||||
import type { IndexItem, Job } from "../types";
|
||||
import { htmlToPlainText } from "../utils";
|
||||
import { fetchMessageContent } from "./messages";
|
||||
import { delay } from "@/seqta/utils/delay";
|
||||
@@ -372,23 +372,34 @@ export const notificationsJob: Job = {
|
||||
if (items.length > 0) {
|
||||
try {
|
||||
const currentItems = await loadAllStoredItems();
|
||||
currentItems.forEach((item) => {
|
||||
// Create new objects to avoid XrayWrapper issues in Firefox
|
||||
const itemsWithComponents = currentItems.map((item) => {
|
||||
try {
|
||||
const jobDef =
|
||||
jobs[item.category] ||
|
||||
Object.values(jobs).find((j) => j.id === item.category) ||
|
||||
jobs[item.renderComponentId];
|
||||
let renderComponent = item.renderComponent;
|
||||
if (jobDef) {
|
||||
const renderComponent =
|
||||
renderComponentMap[jobDef.renderComponentId];
|
||||
if (renderComponent) {
|
||||
item.renderComponent = renderComponent;
|
||||
}
|
||||
renderComponent = renderComponentMap[jobDef.renderComponentId] || renderComponent;
|
||||
} else if (renderComponentMap[item.renderComponentId]) {
|
||||
item.renderComponent =
|
||||
renderComponentMap[item.renderComponentId];
|
||||
renderComponent = renderComponentMap[item.renderComponentId];
|
||||
}
|
||||
// Deep clone to avoid Firefox XrayWrapper issues with nested objects like metadata
|
||||
try {
|
||||
const cloned = JSON.parse(JSON.stringify(item));
|
||||
cloned.renderComponent = renderComponent;
|
||||
return cloned;
|
||||
} catch (e) {
|
||||
// Fallback to shallow copy if deep clone fails
|
||||
return { ...item, renderComponent };
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: return item as-is if modification fails (Firefox XrayWrapper)
|
||||
return item;
|
||||
}
|
||||
});
|
||||
loadDynamicItems(currentItems);
|
||||
loadDynamicItems(itemsWithComponents);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("dynamic-items-updated", {
|
||||
detail: {
|
||||
|
||||
@@ -3,9 +3,24 @@ import type { IndexItem } from "../types";
|
||||
|
||||
let vectorIndex: EmbeddingIndex | null = null;
|
||||
let isInitialized = false;
|
||||
let initializationFailed = false;
|
||||
let currentAbortController: AbortController | null = null;
|
||||
let loadedItemIds = new Set<string>();
|
||||
|
||||
// Detect Firefox in worker context
|
||||
function isFirefoxWorker(): boolean {
|
||||
try {
|
||||
// Check for Firefox-specific APIs or user agent
|
||||
if (typeof navigator !== "undefined") {
|
||||
return navigator.userAgent.toLowerCase().includes("firefox");
|
||||
}
|
||||
// In worker context, check for Firefox-specific behavior
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let streamingSession: {
|
||||
isActive: boolean;
|
||||
totalExpected: number;
|
||||
@@ -21,6 +36,16 @@ async function initWorker() {
|
||||
console.debug("Vector worker already initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip initialization in Firefox
|
||||
if (isFirefoxWorker()) {
|
||||
console.debug("[Vector Worker] Vector search not supported in Firefox - skipping initialization");
|
||||
isInitialized = true;
|
||||
initializationFailed = true;
|
||||
vectorIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug("Initializing vector worker...");
|
||||
try {
|
||||
await initializeModel();
|
||||
@@ -48,8 +73,9 @@ async function initWorker() {
|
||||
isInitialized = true;
|
||||
console.debug("Vector worker initialized successfully.");
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize vector worker:", e);
|
||||
console.warn("[Vector Worker] Failed to initialize vector worker (will use text search only):", e);
|
||||
isInitialized = true;
|
||||
initializationFailed = true;
|
||||
vectorIndex = null;
|
||||
}
|
||||
}
|
||||
@@ -80,18 +106,29 @@ async function startStreamingSession(
|
||||
totalExpected: number,
|
||||
batchSize: number = 5,
|
||||
) {
|
||||
if (initializationFailed || isFirefoxWorker()) {
|
||||
self.postMessage({
|
||||
type: "progress",
|
||||
data: {
|
||||
status: "complete",
|
||||
message: "Vector search not available in Firefox - using text search only",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vectorIndex) {
|
||||
console.warn(
|
||||
"Streaming requested but vector index not ready. Attempting init.",
|
||||
);
|
||||
await initWorker();
|
||||
if (!vectorIndex) {
|
||||
if (!vectorIndex || initializationFailed) {
|
||||
self.postMessage({
|
||||
type: "progress",
|
||||
data: {
|
||||
status: "error",
|
||||
status: "complete",
|
||||
message:
|
||||
"Vector index not available for streaming after init attempt.",
|
||||
"Vector index not available - using text search only",
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -306,18 +343,29 @@ async function endStreamingSession() {
|
||||
async function processItems(items: IndexItem[], signal: AbortSignal) {
|
||||
console.debug("Worker received process request.");
|
||||
|
||||
if (initializationFailed || isFirefoxWorker()) {
|
||||
self.postMessage({
|
||||
type: "progress",
|
||||
data: {
|
||||
status: "complete",
|
||||
message: "Vector search not available - using text search only",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vectorIndex) {
|
||||
console.warn(
|
||||
"Processing requested but vector index not ready. Attempting init.",
|
||||
);
|
||||
await initWorker();
|
||||
if (!vectorIndex) {
|
||||
if (!vectorIndex || initializationFailed) {
|
||||
self.postMessage({
|
||||
type: "progress",
|
||||
data: {
|
||||
status: "error",
|
||||
status: "complete",
|
||||
message:
|
||||
"Vector index not available for processing after init attempt.",
|
||||
"Vector index not available - using text search only",
|
||||
},
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { refreshVectorCache } from "../../search/vector/vectorSearch";
|
||||
import type { IndexItem } from "../types";
|
||||
import { isVectorSearchSupported } from "../../utils/browserDetection";
|
||||
import vectorWorker from "./vectorWorker.ts?inlineWorker";
|
||||
|
||||
export type ProgressCallback = (data: {
|
||||
@@ -42,6 +43,13 @@ export class VectorWorkerManager {
|
||||
}
|
||||
|
||||
private async initWorker(): Promise<void> {
|
||||
// Skip initialization if vector search is not supported (e.g., Firefox)
|
||||
if (!isVectorSearchSupported()) {
|
||||
console.debug("[VectorWorkerManager] Vector search not supported - skipping worker initialization");
|
||||
this.isInitialized = false;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.isInitialized) return Promise.resolve();
|
||||
if (this.readyPromise) return this.readyPromise;
|
||||
|
||||
@@ -234,6 +242,17 @@ export class VectorWorkerManager {
|
||||
}
|
||||
|
||||
async processItems(items: IndexItem[], onProgress?: ProgressCallback) {
|
||||
// Skip if vector search is not supported
|
||||
if (!isVectorSearchSupported()) {
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
status: "complete",
|
||||
message: "Vector search not available - using text search only"
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only initialize worker if we actually have items to process
|
||||
if (items.length === 0) {
|
||||
if (onProgress) {
|
||||
@@ -298,6 +317,18 @@ export class VectorWorkerManager {
|
||||
batchSize: number = 10,
|
||||
jobId?: string,
|
||||
): Promise<void> {
|
||||
// Skip if vector search is not supported
|
||||
if (!isVectorSearchSupported()) {
|
||||
console.debug("[VectorWorker] Vector search not supported - skipping streaming session");
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
status: "complete",
|
||||
message: "Vector search not available - using text search only",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only initialize if we expect items to process
|
||||
if (totalExpectedItems === 0) {
|
||||
console.debug("[VectorWorker] No items expected, not starting streaming session");
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import type { IndexItem } from "../indexing/types";
|
||||
import type { CombinedResult } from "../core/types";
|
||||
import { searchVectors, type VectorSearchResult } from "./vector/vectorSearch";
|
||||
import { jobs } from "../indexing/jobs";
|
||||
|
||||
/**
|
||||
* Hybrid Search Implementation
|
||||
*
|
||||
* Flow:
|
||||
* 1. BM25 (Fuse.js) gets top N results fast
|
||||
* 2. Vector search reranks by semantic similarity
|
||||
* 3. Apply optional boosting (recency, popularity, tags)
|
||||
*/
|
||||
|
||||
export interface HybridSearchOptions {
|
||||
/** Maximum number of BM25 results to retrieve before reranking */
|
||||
bm25TopK?: number;
|
||||
/** Maximum number of final results to return */
|
||||
finalLimit?: number;
|
||||
/** Whether to apply recency boost */
|
||||
recencyBoost?: boolean;
|
||||
/** Weight for BM25 scores (0-1) */
|
||||
bm25Weight?: number;
|
||||
/** Weight for vector similarity scores (0-1) */
|
||||
vectorWeight?: number;
|
||||
/** Weight for recency boost */
|
||||
recencyWeight?: number;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: Required<HybridSearchOptions> = {
|
||||
bm25TopK: 50, // Get top 50 from BM25, then rerank
|
||||
finalLimit: 10,
|
||||
recencyBoost: true,
|
||||
bm25Weight: 0.4, // 40% BM25, 60% vector
|
||||
vectorWeight: 0.6,
|
||||
recencyWeight: 0.1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a score to 0-1 range
|
||||
*/
|
||||
function normalizeScore(score: number, min: number, max: number): number {
|
||||
if (max === min) return 0.5;
|
||||
return Math.max(0, Math.min(1, (score - min) / (max - min)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates recency boost based on item age
|
||||
*/
|
||||
function calculateRecencyBoost(item: IndexItem, now: number): number {
|
||||
const ageInDays = (now - item.dateAdded) / (1000 * 60 * 60 * 24);
|
||||
// Exponential decay: newer items get higher boost
|
||||
// Items from today get boost of 1, items from 30 days ago get ~0.03
|
||||
return 1 / (1 + ageInDays / 7); // Half-life of 7 days
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates popularity boost (can be extended with click tracking, etc.)
|
||||
*/
|
||||
function calculatePopularityBoost(item: IndexItem): number {
|
||||
// For now, boost based on category and metadata
|
||||
let boost = 0;
|
||||
|
||||
// Boost assignments/assessments
|
||||
if (item.category === "assignments") {
|
||||
boost += 0.1;
|
||||
}
|
||||
|
||||
// Boost upcoming items
|
||||
if (item.metadata?.isUpcoming) {
|
||||
boost += 0.15;
|
||||
}
|
||||
|
||||
// Boost items with subject codes (more structured)
|
||||
if (item.metadata?.subjectCode) {
|
||||
boost += 0.05;
|
||||
}
|
||||
|
||||
return Math.min(boost, 0.3); // Cap at 0.3
|
||||
}
|
||||
|
||||
/**
|
||||
* Reranks BM25 results using vector search
|
||||
*/
|
||||
export async function hybridSearch(
|
||||
bm25Results: CombinedResult[],
|
||||
query: string,
|
||||
options: HybridSearchOptions = {},
|
||||
): Promise<CombinedResult[]> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const trimmedQuery = query.trim().toLowerCase();
|
||||
|
||||
// If no BM25 results, return empty
|
||||
if (bm25Results.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Limit BM25 results to top K
|
||||
const topBm25Results = bm25Results.slice(0, opts.bm25TopK);
|
||||
|
||||
// Get vector search results for reranking
|
||||
// We'll search the full index and then filter to our BM25 results
|
||||
let vectorResults: VectorSearchResult[] = [];
|
||||
|
||||
if (trimmedQuery.length > 2) {
|
||||
try {
|
||||
// Get more vector results than BM25 results to ensure coverage
|
||||
// This allows us to find semantic matches that BM25 might have missed
|
||||
const vectorSearchResults = await searchVectors(trimmedQuery, opts.bm25TopK * 2);
|
||||
|
||||
// Create a map of item ID to vector similarity
|
||||
const vectorMap = new Map<string, number>();
|
||||
vectorSearchResults.forEach(v => {
|
||||
// Use the highest similarity if item appears multiple times
|
||||
const existing = vectorMap.get(v.object.id);
|
||||
if (!existing || v.similarity > existing) {
|
||||
vectorMap.set(v.object.id, v.similarity);
|
||||
}
|
||||
});
|
||||
|
||||
// Now rerank BM25 results with vector scores
|
||||
const now = Date.now();
|
||||
|
||||
const rerankedResults = topBm25Results.map(result => {
|
||||
const item = result.item;
|
||||
|
||||
// Normalize BM25 score to 0-1
|
||||
// Fuse.js scores: lower is better (0 = perfect match)
|
||||
// We need to invert: higher score = better match
|
||||
// Result.score is typically 0-100, where higher = better
|
||||
// So we normalize it to 0-1
|
||||
const normalizedBm25Score = Math.max(0, Math.min(1, result.score / 100));
|
||||
|
||||
// Get vector similarity (0-1, already normalized)
|
||||
// If item wasn't in vector results, use a default low score
|
||||
const vectorSimilarity = vectorMap.get(item.id) || 0.3; // Default to 0.3 if not found
|
||||
|
||||
// Calculate recency boost (0-1 range)
|
||||
const recencyBoost = opts.recencyBoost
|
||||
? calculateRecencyBoost(item, now) * opts.recencyWeight
|
||||
: 0;
|
||||
|
||||
// Calculate popularity boost (0-1 range)
|
||||
const popularityBoost = calculatePopularityBoost(item);
|
||||
|
||||
// Apply job-specific boost if available
|
||||
const job = jobs[item.category];
|
||||
let jobBoost = 0;
|
||||
if (job && typeof job.boostCriteria === 'function') {
|
||||
const boost = job.boostCriteria(item, trimmedQuery);
|
||||
if (boost) {
|
||||
jobBoost = boost / 100; // Normalize boost to 0-1
|
||||
}
|
||||
}
|
||||
|
||||
// Combine scores using weighted average
|
||||
// BM25 and vector are weighted, boosts are additive
|
||||
const hybridScore =
|
||||
(normalizedBm25Score * opts.bm25Weight) +
|
||||
(vectorSimilarity * opts.vectorWeight) +
|
||||
recencyBoost +
|
||||
popularityBoost +
|
||||
jobBoost;
|
||||
|
||||
return {
|
||||
...result,
|
||||
score: hybridScore * 100, // Scale back to 0-100 for consistency
|
||||
// Store component scores for debugging (optional, can be removed in production)
|
||||
_hybridScores: {
|
||||
bm25: normalizedBm25Score,
|
||||
vector: vectorSimilarity,
|
||||
recency: recencyBoost,
|
||||
popularity: popularityBoost,
|
||||
jobBoost: jobBoost,
|
||||
final: hybridScore,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by hybrid score descending
|
||||
rerankedResults.sort((a, b) => b.score - a.score);
|
||||
|
||||
// Return top results
|
||||
return rerankedResults.slice(0, opts.finalLimit);
|
||||
|
||||
} catch (e) {
|
||||
console.warn("[Hybrid Search] Vector reranking failed, using BM25 only:", e);
|
||||
// Fallback to BM25 only
|
||||
return topBm25Results.slice(0, opts.finalLimit);
|
||||
}
|
||||
}
|
||||
|
||||
// If query is too short for vector search, just return BM25 results
|
||||
return topBm25Results.slice(0, opts.finalLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced hybrid search that also includes vector-only results not found by BM25
|
||||
*/
|
||||
export async function hybridSearchWithExpansion(
|
||||
bm25Results: CombinedResult[],
|
||||
query: string,
|
||||
allItems: IndexItem[],
|
||||
options: HybridSearchOptions = {},
|
||||
): Promise<CombinedResult[]> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const trimmedQuery = query.trim().toLowerCase();
|
||||
|
||||
// First, rerank BM25 results
|
||||
const rerankedBm25 = await hybridSearch(bm25Results, query, options);
|
||||
|
||||
// If query is too short, skip vector expansion
|
||||
if (trimmedQuery.length <= 2) {
|
||||
return rerankedBm25;
|
||||
}
|
||||
|
||||
// Get vector search results
|
||||
let vectorResults: VectorSearchResult[] = [];
|
||||
try {
|
||||
vectorResults = await searchVectors(trimmedQuery, opts.bm25TopK);
|
||||
} catch (e) {
|
||||
console.warn("[Hybrid Search] Vector search failed:", e);
|
||||
return rerankedBm25;
|
||||
}
|
||||
|
||||
// Find vector results that weren't in BM25 results
|
||||
const bm25Ids = new Set(bm25Results.map(r => r.item.id));
|
||||
const vectorOnlyResults: CombinedResult[] = [];
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
vectorResults.forEach(v => {
|
||||
if (!bm25Ids.has(v.object.id)) {
|
||||
// This is a semantic match that BM25 missed
|
||||
const item = v.object;
|
||||
|
||||
// Calculate boosts
|
||||
const recencyBoost = opts.recencyBoost
|
||||
? calculateRecencyBoost(item, now) * opts.recencyWeight
|
||||
: 0;
|
||||
const popularityBoost = calculatePopularityBoost(item);
|
||||
|
||||
// Vector-only results get lower base score but high vector similarity
|
||||
const vectorScore = v.similarity * opts.vectorWeight + recencyBoost + popularityBoost;
|
||||
|
||||
// Apply job-specific boost if available
|
||||
const job = jobs[item.category];
|
||||
let jobBoost = 0;
|
||||
if (job && typeof job.boostCriteria === 'function') {
|
||||
const boost = job.boostCriteria(item, trimmedQuery);
|
||||
if (boost) {
|
||||
jobBoost = boost / 100; // Normalize boost
|
||||
}
|
||||
}
|
||||
|
||||
vectorOnlyResults.push({
|
||||
id: item.id,
|
||||
type: "dynamic" as const,
|
||||
score: (vectorScore + jobBoost) * 100,
|
||||
item,
|
||||
_hybridScores: {
|
||||
bm25: 0,
|
||||
vector: v.similarity,
|
||||
recency: recencyBoost,
|
||||
popularity: popularityBoost,
|
||||
final: vectorScore + jobBoost,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Combine reranked BM25 results with vector-only results
|
||||
const allResults = [...rerankedBm25, ...vectorOnlyResults];
|
||||
|
||||
// Sort by score and return top results
|
||||
allResults.sort((a, b) => b.score - a.score);
|
||||
|
||||
return allResults.slice(0, opts.finalLimit);
|
||||
}
|
||||
|
||||
@@ -6,32 +6,79 @@ import type { IndexItem } from "../indexing/types";
|
||||
import { searchVectors } from "./vector/vectorSearch";
|
||||
import type { VectorSearchResult } from "./vector/vectorTypes";
|
||||
import { jobs } from "../indexing/jobs";
|
||||
import { hybridSearchWithExpansion } from "./hybridSearch";
|
||||
|
||||
// Search result cache for better performance
|
||||
const searchCache = new Map<string, { results: CombinedResult[]; timestamp: number }>();
|
||||
const CACHE_TTL = 1000 * 60 * 5; // 5 minutes
|
||||
const MAX_CACHE_SIZE = 100;
|
||||
|
||||
function getCachedResults(query: string): CombinedResult[] | null {
|
||||
const cached = searchCache.get(query);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.results;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCachedResults(query: string, results: CombinedResult[]) {
|
||||
// Limit cache size
|
||||
if (searchCache.size >= MAX_CACHE_SIZE) {
|
||||
const firstKey = searchCache.keys().next().value;
|
||||
searchCache.delete(firstKey);
|
||||
}
|
||||
searchCache.set(query, { results, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the search result cache
|
||||
*/
|
||||
export function clearSearchCache(): void {
|
||||
searchCache.clear();
|
||||
console.debug("[Search] Search result cache cleared");
|
||||
}
|
||||
|
||||
// Listen for cache clear events (e.g., on extension update)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('betterseqta-clear-search-cache', () => {
|
||||
clearSearchCache();
|
||||
});
|
||||
}
|
||||
|
||||
export function createSearchIndexes() {
|
||||
const commands = getStaticCommands();
|
||||
const dynamicItems = getDynamicItems();
|
||||
|
||||
// Optimized command search options
|
||||
const commandOptions = {
|
||||
keys: ["text", "category", "keywords"],
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
threshold: 0.4,
|
||||
threshold: 0.35, // Slightly more permissive for better recall
|
||||
minMatchCharLength: 2,
|
||||
useExtendedSearch: false,
|
||||
ignoreLocation: false,
|
||||
findAllMatches: false, // Performance optimization
|
||||
};
|
||||
|
||||
// Optimized dynamic content search options
|
||||
const dynamicOptions = {
|
||||
keys: [
|
||||
{ name: "text", weight: 2 },
|
||||
{ name: "text", weight: 3 }, // Increased weight for title matches
|
||||
{ name: "content", weight: 1 },
|
||||
{ name: "category", weight: 1 },
|
||||
{ name: "category", weight: 0.5 }, // Lower weight for category
|
||||
{ name: "metadata.subjectName", weight: 1.5 }, // Boost subject name matches
|
||||
{ name: "metadata.subjectCode", weight: 1.5 }, // Boost subject code matches
|
||||
],
|
||||
includeScore: true,
|
||||
includeMatches: true,
|
||||
threshold: 0.4,
|
||||
minMatchCharLength: 2,
|
||||
distance: 100,
|
||||
threshold: 0.5, // More permissive for better partial word matching (increased from 0.4)
|
||||
minMatchCharLength: 2, // Minimum 2 characters for Fuse.js matches (substring fallback handles shorter queries)
|
||||
distance: 100, // Increased to allow matches across longer strings
|
||||
useExtendedSearch: true,
|
||||
ignoreLocation: true, // Allow matches anywhere in the string for better partial word matching
|
||||
findAllMatches: true, // Enable to find all matches for better partial word support
|
||||
shouldSort: true,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -105,18 +152,64 @@ export function searchDynamicItems(
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const searchResults = dynamicContentFuse.search(query, { limit });
|
||||
const queryLower = query.toLowerCase();
|
||||
const queryTrimmed = query.trim();
|
||||
|
||||
return searchResults.map((result: FuseResult<IndexItem>) => {
|
||||
// For short queries (3 chars or less), use a more permissive approach
|
||||
const isShortQuery = queryTrimmed.length <= 3;
|
||||
const searchLimit = Math.min(limit * 3, 50);
|
||||
|
||||
// First, try Fuse.js search
|
||||
const searchResults = dynamicContentFuse.search(query, { limit: searchLimit });
|
||||
|
||||
// For short queries, always do a simple substring match to supplement Fuse.js results
|
||||
// This ensures we catch partial word matches like "SAT" in "SAT 1: Differential Calculus"
|
||||
let additionalMatches: IndexItem[] = [];
|
||||
if (isShortQuery) {
|
||||
// Always do substring search for short queries to catch partial word matches
|
||||
for (const item of dynamicIdToItemMap.values()) {
|
||||
const textLower = item.text.toLowerCase();
|
||||
const contentLower = (item.content || '').toLowerCase();
|
||||
const subjectNameLower = (item.metadata?.subjectName || '').toLowerCase();
|
||||
const subjectCodeLower = (item.metadata?.subjectCode || '').toLowerCase();
|
||||
|
||||
// Check if query appears anywhere in the text, content, or metadata
|
||||
if (textLower.includes(queryLower) ||
|
||||
contentLower.includes(queryLower) ||
|
||||
subjectNameLower.includes(queryLower) ||
|
||||
subjectCodeLower.includes(queryLower)) {
|
||||
// Only add if not already in Fuse.js results
|
||||
if (!searchResults.find(r => r.item.id === item.id)) {
|
||||
additionalMatches.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results = searchResults.map((result: FuseResult<IndexItem>) => {
|
||||
const item = result.item;
|
||||
const fuseScore = 10 * (1 - (result.score || 0.5));
|
||||
|
||||
let score = fuseScore;
|
||||
|
||||
// Recency boost
|
||||
const ageInDays = (now - item.dateAdded) / (1000 * 60 * 60 * 24);
|
||||
const recencyBoost = sortByRecent ? 1 / (ageInDays + 1) : 0;
|
||||
score += recencyBoost;
|
||||
|
||||
// Boost for exact text matches (especially at the start)
|
||||
const textLower = item.text.toLowerCase();
|
||||
if (textLower.startsWith(queryLower)) {
|
||||
score += 5; // Strong boost for prefix matches
|
||||
} else if (textLower.includes(queryLower)) {
|
||||
score += 2; // Boost for substring matches
|
||||
}
|
||||
|
||||
// Boost for category matches
|
||||
if (item.category.toLowerCase().includes(queryLower)) {
|
||||
score += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
type: "dynamic" as const,
|
||||
@@ -125,60 +218,124 @@ export function searchDynamicItems(
|
||||
matches: result.matches,
|
||||
};
|
||||
});
|
||||
|
||||
// Add additional matches from simple substring search
|
||||
additionalMatches.forEach((item) => {
|
||||
// Check if already in results
|
||||
if (!results.find(r => r.id === item.id)) {
|
||||
const textLower = item.text.toLowerCase();
|
||||
let score = 5; // Base score for substring matches
|
||||
|
||||
// Boost for prefix matches
|
||||
if (textLower.startsWith(queryLower)) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// Recency boost
|
||||
const ageInDays = (now - item.dateAdded) / (1000 * 60 * 60 * 24);
|
||||
const recencyBoost = sortByRecent ? 1 / (ageInDays + 1) : 0;
|
||||
score += recencyBoost;
|
||||
|
||||
results.push({
|
||||
id: item.id,
|
||||
type: "dynamic" as const,
|
||||
score,
|
||||
item,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by score and return top results
|
||||
return results.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||
}
|
||||
|
||||
export async function performSearch(
|
||||
query: string,
|
||||
commandsFuse: Fuse<StaticCommandItem>,
|
||||
commandIdToItemMap: Map<string, StaticCommandItem>,
|
||||
dynamicContentFuse?: Fuse<IndexItem>,
|
||||
dynamicIdToItemMap?: Map<string, IndexItem>,
|
||||
sortByRecent: boolean = true,
|
||||
): Promise<CombinedResult[]> {
|
||||
// Get all results first
|
||||
const trimmedQuery = query.trim().toLowerCase();
|
||||
|
||||
// Check cache first
|
||||
if (trimmedQuery.length > 2) {
|
||||
const cached = getCachedResults(trimmedQuery);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1: Get command results (these don't need hybrid search)
|
||||
const commandResults = searchCommands(
|
||||
commandsFuse,
|
||||
query,
|
||||
trimmedQuery,
|
||||
commandIdToItemMap,
|
||||
);
|
||||
|
||||
// Get vector results in parallel
|
||||
let vectorResults: VectorSearchResult[] = [];
|
||||
// Step 2: Get BM25 results for dynamic items
|
||||
let dynamicResults: CombinedResult[] = [];
|
||||
if (dynamicContentFuse && dynamicIdToItemMap) {
|
||||
// Get BM25 results first (fast text-based search)
|
||||
const bm25Results = searchDynamicItems(
|
||||
dynamicContentFuse,
|
||||
trimmedQuery,
|
||||
dynamicIdToItemMap,
|
||||
50, // Get top 50 for reranking
|
||||
sortByRecent,
|
||||
);
|
||||
|
||||
// Step 3: Apply hybrid search (BM25 + Vector reranking + boosting)
|
||||
if (trimmedQuery.length > 2 && bm25Results.length > 0) {
|
||||
try {
|
||||
vectorResults = await searchVectors(query);
|
||||
} catch (e) {}
|
||||
// Get all items for expansion
|
||||
const allItems = Array.from(dynamicIdToItemMap.values());
|
||||
|
||||
// Create a map to store our final results, using ID as key to avoid duplicates
|
||||
const resultMap = new Map<string, CombinedResult>();
|
||||
|
||||
// Add command results first (they keep their original scores)
|
||||
commandResults.forEach((r) => resultMap.set(r.id, r));
|
||||
|
||||
// Process dynamic results and vector results together
|
||||
const seenIds = new Set<string>();
|
||||
|
||||
vectorResults.forEach((v) => {
|
||||
const id = v.object.id;
|
||||
|
||||
if (!seenIds.has(id)) {
|
||||
// This is a semantic match that Fuse missed - add it with the vector similarity as score
|
||||
let score = v.similarity * 0.5; // High base score for semantic matches
|
||||
const job = jobs[v.object.category];
|
||||
if (job && typeof job.boostCriteria === 'function') {
|
||||
const boost = job.boostCriteria(v.object, query);
|
||||
if (boost) {
|
||||
score += boost;
|
||||
// Apply hybrid search with expansion
|
||||
dynamicResults = await hybridSearchWithExpansion(
|
||||
bm25Results,
|
||||
trimmedQuery,
|
||||
allItems,
|
||||
{
|
||||
bm25TopK: 50,
|
||||
finalLimit: 20, // Return top 20 after reranking
|
||||
recencyBoost: sortByRecent,
|
||||
bm25Weight: 0.4, // 40% BM25, 60% vector
|
||||
vectorWeight: 0.6,
|
||||
recencyWeight: 0.1,
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn("[Search] Hybrid search failed, using BM25 only:", e);
|
||||
// Fallback to BM25 only
|
||||
dynamicResults = bm25Results.slice(0, 20);
|
||||
}
|
||||
} else {
|
||||
// For very short queries or no BM25 results, use BM25 only
|
||||
dynamicResults = bm25Results.slice(0, 20);
|
||||
}
|
||||
}
|
||||
resultMap.set(id, {
|
||||
id,
|
||||
type: "dynamic" as const,
|
||||
score,
|
||||
item: v.object,
|
||||
});
|
||||
|
||||
// Step 4: Combine command and dynamic results
|
||||
const allResults = [...commandResults, ...dynamicResults];
|
||||
|
||||
// Sort by score (commands typically have higher priority)
|
||||
allResults.sort((a, b) => {
|
||||
// Commands always come first if scores are similar
|
||||
if (a.type === "command" && b.type === "dynamic") {
|
||||
return b.score - a.score - 10; // Commands get +10 boost
|
||||
}
|
||||
if (a.type === "dynamic" && b.type === "command") {
|
||||
return b.score - a.score + 10; // Commands get +10 boost
|
||||
}
|
||||
return b.score - a.score;
|
||||
});
|
||||
|
||||
// Convert to array and sort by score
|
||||
const results = Array.from(resultMap.values());
|
||||
results.sort((a, b) => b.score - a.score);
|
||||
|
||||
return results;
|
||||
// Cache results for queries longer than 2 chars
|
||||
if (trimmedQuery.length > 2) {
|
||||
setCachedResults(trimmedQuery, allResults);
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
import { EmbeddingIndex, getEmbedding, initializeModel } from "embeddia";
|
||||
import type { IndexItem } from "../../indexing/types";
|
||||
import type { SearchResult } from "embeddia";
|
||||
import { isVectorSearchSupported } from "../../utils/browserDetection";
|
||||
|
||||
let vectorIndex: EmbeddingIndex | null = null;
|
||||
let initializationAttempted = false;
|
||||
let initializationFailed = false;
|
||||
|
||||
export async function initVectorSearch() {
|
||||
// Skip initialization if already attempted and failed, or if not supported
|
||||
if (initializationFailed || !isVectorSearchSupported()) {
|
||||
if (!isVectorSearchSupported()) {
|
||||
console.debug("[Vector Search] Vector search not supported in Firefox - using text search only");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (initializationAttempted) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializationAttempted = true;
|
||||
|
||||
try {
|
||||
await initializeModel();
|
||||
vectorIndex = new EmbeddingIndex([]);
|
||||
vectorIndex.preloadIndexedDB();
|
||||
console.debug("[Vector Search] Initialized successfully");
|
||||
} catch (e) {
|
||||
console.error("Error initializing vector search", e);
|
||||
console.warn("[Vector Search] Failed to initialize vector search (will use text search only):", e);
|
||||
initializationFailed = true;
|
||||
vectorIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,28 +38,111 @@ export interface VectorSearchResult extends SearchResult {
|
||||
object: IndexItem & { embedding: number[] };
|
||||
}
|
||||
|
||||
// Cache for query embeddings to avoid recomputing
|
||||
const embeddingCache = new Map<string, number[]>();
|
||||
const EMBEDDING_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
|
||||
const MAX_EMBEDDING_CACHE_SIZE = 50;
|
||||
|
||||
function getCachedEmbedding(query: string): number[] | null {
|
||||
const cached = embeddingCache.get(query);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setCachedEmbedding(query: string, embedding: number[]) {
|
||||
// Limit cache size
|
||||
if (embeddingCache.size >= MAX_EMBEDDING_CACHE_SIZE) {
|
||||
const firstKey = embeddingCache.keys().next().value;
|
||||
embeddingCache.delete(firstKey);
|
||||
}
|
||||
embeddingCache.set(query, embedding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the embedding cache
|
||||
*/
|
||||
export function clearEmbeddingCache(): void {
|
||||
embeddingCache.clear();
|
||||
console.debug("[Vector Search] Embedding cache cleared");
|
||||
}
|
||||
|
||||
// Listen for cache clear events (e.g., on extension update)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('betterseqta-clear-embedding-cache', () => {
|
||||
clearEmbeddingCache();
|
||||
});
|
||||
}
|
||||
|
||||
export async function searchVectors(
|
||||
query: string,
|
||||
topK: number = 20,
|
||||
): Promise<VectorSearchResult[]> {
|
||||
if (!vectorIndex) await initVectorSearch();
|
||||
// Return empty array if vector search is not supported or failed to initialize
|
||||
if (!isVectorSearchSupported() || initializationFailed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const queryEmbedding = await getEmbedding(query.slice(0, 100));
|
||||
if (!vectorIndex) {
|
||||
await initVectorSearch();
|
||||
if (!vectorIndex) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize query for caching
|
||||
const normalizedQuery = query.trim().toLowerCase().slice(0, 100);
|
||||
|
||||
// Check cache first
|
||||
let queryEmbedding = getCachedEmbedding(normalizedQuery);
|
||||
|
||||
if (!queryEmbedding) {
|
||||
try {
|
||||
queryEmbedding = await getEmbedding(normalizedQuery);
|
||||
setCachedEmbedding(normalizedQuery, queryEmbedding);
|
||||
} catch (e) {
|
||||
console.warn("[Vector Search] Failed to get embedding:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await vectorIndex!.search(queryEmbedding, {
|
||||
topK,
|
||||
topK: Math.min(topK * 2, 30), // Get more results, filter later
|
||||
useStorage: "indexedDB",
|
||||
dedupeEntries: true,
|
||||
});
|
||||
|
||||
// filter results with a similarity below 0.81
|
||||
const filteredResults = results.filter((r) => r.similarity > 0.81);
|
||||
// Filter results with a similarity below 0.80 (slightly more permissive)
|
||||
// and sort by similarity descending
|
||||
const filteredResults = results
|
||||
.filter((r) => r.similarity > 0.80)
|
||||
.sort((a, b) => b.similarity - a.similarity)
|
||||
.slice(0, topK);
|
||||
|
||||
return filteredResults as VectorSearchResult[];
|
||||
} catch (e) {
|
||||
console.warn("[Vector Search] Search failed:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshVectorCache() {
|
||||
if (!vectorIndex) await initVectorSearch();
|
||||
vectorIndex!.clearIndexedDBCache();
|
||||
vectorIndex!.preloadIndexedDB();
|
||||
if (!isVectorSearchSupported() || initializationFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vectorIndex) {
|
||||
await initVectorSearch();
|
||||
}
|
||||
|
||||
if (vectorIndex) {
|
||||
try {
|
||||
vectorIndex.clearIndexedDBCache();
|
||||
vectorIndex.preloadIndexedDB();
|
||||
} catch (e) {
|
||||
console.warn("[Vector Search] Failed to refresh cache:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
/**
|
||||
* Detects if the current browser is Firefox
|
||||
*/
|
||||
export function isFirefox(): boolean {
|
||||
try {
|
||||
// Firefox-specific API
|
||||
if (typeof (browser.runtime as any).getBrowserInfo === "function") {
|
||||
return true;
|
||||
}
|
||||
// Fallback: check user agent
|
||||
if (typeof navigator !== "undefined") {
|
||||
return navigator.userAgent.toLowerCase().includes("firefox");
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
// If we can't detect, assume not Firefox (safer for Chrome/Edge)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if vector search is supported in the current browser
|
||||
* Currently disabled for Firefox due to security restrictions
|
||||
*/
|
||||
export function isVectorSearchSupported(): boolean {
|
||||
return !isFirefox();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
const VERSION_STORAGE_KEY = "betterseqta-global-search-version";
|
||||
const VERSION_CACHE_KEY = "betterseqta-global-search-cache-version";
|
||||
|
||||
/**
|
||||
* Gets the current extension version from the manifest
|
||||
*/
|
||||
export function getCurrentVersion(): string {
|
||||
try {
|
||||
return browser.runtime.getManifest().version;
|
||||
} catch (e) {
|
||||
console.warn("[Version Check] Failed to get manifest version:", e);
|
||||
return "0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last stored version from localStorage
|
||||
*/
|
||||
export function getStoredVersion(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(VERSION_STORAGE_KEY);
|
||||
} catch (e) {
|
||||
console.warn("[Version Check] Failed to get stored version:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the current version in localStorage
|
||||
*/
|
||||
export function storeVersion(version: string): void {
|
||||
try {
|
||||
localStorage.setItem(VERSION_STORAGE_KEY, version);
|
||||
localStorage.setItem(VERSION_CACHE_KEY, version);
|
||||
} catch (e) {
|
||||
console.warn("[Version Check] Failed to store version:", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the extension has been updated and clears caches if needed
|
||||
* Returns true if an update was detected
|
||||
*/
|
||||
export async function checkAndHandleUpdate(): Promise<boolean> {
|
||||
const currentVersion = getCurrentVersion();
|
||||
const storedVersion = getStoredVersion();
|
||||
|
||||
// If no stored version, this is first run - store current version
|
||||
if (!storedVersion) {
|
||||
console.debug(`[Version Check] First run detected, storing version ${currentVersion}`);
|
||||
storeVersion(currentVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If versions match, no update
|
||||
if (storedVersion === currentVersion) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Version mismatch detected - extension was updated
|
||||
console.log(`[Version Check] Extension updated from ${storedVersion} to ${currentVersion}, clearing caches...`);
|
||||
|
||||
// Clear all caches
|
||||
await clearAllCaches();
|
||||
|
||||
// Store new version
|
||||
storeVersion(currentVersion);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all search-related caches
|
||||
*/
|
||||
export async function clearAllCaches(): Promise<void> {
|
||||
try {
|
||||
// Clear search result cache (in-memory Map)
|
||||
if (typeof window !== 'undefined') {
|
||||
// Dispatch event to clear caches in other modules
|
||||
window.dispatchEvent(new CustomEvent('betterseqta-clear-search-cache'));
|
||||
window.dispatchEvent(new CustomEvent('betterseqta-clear-embedding-cache'));
|
||||
}
|
||||
|
||||
// Also try to directly clear caches if modules are already loaded
|
||||
// Use setTimeout to avoid blocking and handle CSS preload errors
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const { clearSearchCache } = await import("../search/searchUtils");
|
||||
clearSearchCache();
|
||||
} catch (e: any) {
|
||||
// Module might not be loaded yet, or CSS preload error - that's okay
|
||||
if (!e?.message?.includes("preload CSS") && !e?.message?.includes("MIME type")) {
|
||||
console.debug("[Version Check] Could not clear search cache:", e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { clearEmbeddingCache } = await import("../search/vector/vectorSearch");
|
||||
clearEmbeddingCache();
|
||||
} catch (e: any) {
|
||||
// Module might not be loaded yet, or CSS preload error - that's okay
|
||||
if (!e?.message?.includes("preload CSS") && !e?.message?.includes("MIME type")) {
|
||||
console.debug("[Version Check] Could not clear embedding cache:", e);
|
||||
}
|
||||
}
|
||||
}, 50);
|
||||
|
||||
console.debug("[Version Check] All caches cleared");
|
||||
} catch (e) {
|
||||
console.error("[Version Check] Error clearing caches:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||
"[class*='notifications__bubble___']",
|
||||
) as HTMLElement;
|
||||
|
||||
if (api.storage.lastNotificationCount !== 0) {
|
||||
if (alertDiv && api.storage.lastNotificationCount !== 0) {
|
||||
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
||||
}
|
||||
|
||||
@@ -74,13 +74,17 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[BetterSEQTA+] Error fetching notifications:", error);
|
||||
api.storage.consecutiveErrors = (api.storage.consecutiveErrors || 0) + 1;
|
||||
api.storage.consecutiveErrors =
|
||||
(api.storage.consecutiveErrors || 0) + 1;
|
||||
}
|
||||
};
|
||||
|
||||
const getNextInterval = () => {
|
||||
// Exponential backoff on errors, max 5 minutes
|
||||
const errorMultiplier = Math.min(Math.pow(2, api.storage.consecutiveErrors || 0), 10);
|
||||
const errorMultiplier = Math.min(
|
||||
Math.pow(2, api.storage.consecutiveErrors || 0),
|
||||
10,
|
||||
);
|
||||
return Math.min(baseInterval * errorMultiplier, maxInterval);
|
||||
};
|
||||
|
||||
@@ -92,7 +96,8 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||
const interval = getNextInterval();
|
||||
pollInterval = window.setTimeout(() => {
|
||||
checkNotifications().then(() => {
|
||||
if (pollInterval) { // Only continue if not stopped
|
||||
if (pollInterval) {
|
||||
// Only continue if not stopped
|
||||
scheduleNext();
|
||||
}
|
||||
});
|
||||
@@ -124,14 +129,16 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||
isVisible = !document.hidden;
|
||||
if (isVisible && !pollInterval) {
|
||||
// Resume polling when tab becomes visible
|
||||
const alertDiv = document.querySelector("[class*='notifications__bubble___']");
|
||||
const alertDiv = document.querySelector(
|
||||
"[class*='notifications__bubble___']",
|
||||
);
|
||||
if (alertDiv) {
|
||||
startPolling();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
|
||||
api.seqta.onMount("[class*='notifications__bubble___']", (_) => {
|
||||
startPolling();
|
||||
@@ -139,7 +146,7 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||
|
||||
return () => {
|
||||
stopPolling();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Plugin } from "@/plugins/core/types";
|
||||
import { defineSettings, componentSetting } from "@/plugins/core/settingsHelpers";
|
||||
import { componentSetting, defineSettings } from "@/plugins/core/settingsHelpers";
|
||||
import ProfilePictureSetting from "./ProfilePictureSetting.svelte";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import styles from "./styles.css?inline";
|
||||
|
||||
@@ -8,8 +8,16 @@
|
||||
object-fit: cover;
|
||||
z-index: 4;
|
||||
box-shadow: 0 0 0 3px #000000;
|
||||
transition: box-shadow 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.dark .userInfoImg {
|
||||
box-shadow: 0 0 0 3px #ffffff;
|
||||
transition: box-shadow 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.userInfoImg {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
@@ -147,14 +147,21 @@ export class ThemeManager {
|
||||
public async initialize(): Promise<void> {
|
||||
console.debug("[ThemeManager] Starting initialization");
|
||||
try {
|
||||
// Check if theme creator was open during reload
|
||||
const neumorphicThemeId = "9a9786d1-b5fc-4a91-8c7a-f8bf7f7679ad";
|
||||
const migrationCSS = "#title {\nbackground: transparent !important;\n}";
|
||||
|
||||
const theme = (await localforage.getItem(neumorphicThemeId)) as CustomTheme | null;
|
||||
if (theme && theme.CustomCSS && !theme.CustomCSS.includes("#title {\nbackground: transparent !important;\n}")) {
|
||||
theme.CustomCSS = theme.CustomCSS + "\n" + migrationCSS;
|
||||
await localforage.setItem(neumorphicThemeId, theme);
|
||||
}
|
||||
|
||||
const themeCreatorOpen = localStorage.getItem("themeCreatorOpen");
|
||||
if (themeCreatorOpen === "true") {
|
||||
console.debug(
|
||||
"[ThemeManager] Theme creator was open, clearing preview state",
|
||||
);
|
||||
this.clearPreview();
|
||||
// Clean up the flag
|
||||
localStorage.removeItem("themeCreatorOpen");
|
||||
}
|
||||
|
||||
|
||||
@@ -39,43 +39,14 @@ const zoomHandlers = new WeakMap<
|
||||
>();
|
||||
|
||||
function resetTimetableStyles(): void {
|
||||
const firstDayColumn = document.querySelector(
|
||||
".dailycal .content .days td",
|
||||
) as HTMLElement;
|
||||
if (!firstDayColumn) return;
|
||||
|
||||
const baseContainerHeight =
|
||||
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
|
||||
|
||||
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
|
||||
dayColumns.forEach((td: Element) => {
|
||||
(td as HTMLElement).style.height = `${baseContainerHeight}px`;
|
||||
});
|
||||
|
||||
const timeColumn = document.querySelector(".times");
|
||||
if (timeColumn) {
|
||||
const times = timeColumn.querySelectorAll(".time");
|
||||
const timeHeight = baseContainerHeight / times.length;
|
||||
times.forEach((time: Element) => {
|
||||
(time as HTMLElement).style.height = `${timeHeight}px`;
|
||||
});
|
||||
}
|
||||
|
||||
const lessons = document.querySelectorAll(".dailycal .lesson");
|
||||
lessons.forEach((lesson: Element) => {
|
||||
const lessonEl = lesson as HTMLElement;
|
||||
const originalHeight = lessonEl.getAttribute("data-original-height");
|
||||
if (originalHeight) {
|
||||
lessonEl.style.height = `${originalHeight}px`;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset entry opacity (for assessment hide feature)
|
||||
const entries = document.querySelectorAll(".entry");
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement;
|
||||
entryEl.style.opacity = "1";
|
||||
});
|
||||
|
||||
// Clean up zoom control event handlers
|
||||
const zoomControls = document.querySelector(".timetable-zoom-controls");
|
||||
if (zoomControls) {
|
||||
const handlers = zoomHandlers.get(zoomControls);
|
||||
@@ -94,19 +65,9 @@ function resetTimetableStyles(): void {
|
||||
async function handleTimetable(): Promise<void> {
|
||||
await waitForElm(".time", true, 10);
|
||||
|
||||
// Store original heights when timetable loads
|
||||
const lessons = document.querySelectorAll(".dailycal .lesson");
|
||||
lessons.forEach((lesson: Element) => {
|
||||
const lessonEl = lesson as HTMLElement;
|
||||
lessonEl.setAttribute(
|
||||
"data-original-height",
|
||||
lessonEl.offsetHeight.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
// Existing time format code
|
||||
// Convert time format if needed
|
||||
if (settingsState.timeFormat == "12") {
|
||||
const times = document.querySelectorAll(".timetablepage .times .time");
|
||||
const times = document.querySelectorAll(".timetablepage .times .time, .timetablepage .entry.new");
|
||||
for (const time of times) {
|
||||
if (!time.textContent) continue;
|
||||
time.textContent = convertTo12HourFormat(time.textContent, true);
|
||||
@@ -120,14 +81,6 @@ async function handleTimetable(): Promise<void> {
|
||||
function handleTimetableZoom(): void {
|
||||
console.log("Initializing timetable zoom controls");
|
||||
|
||||
// Lazy initialize state variables only when function is first called
|
||||
let timetableZoomLevel = 1;
|
||||
let baseContainerHeight: number | null = null;
|
||||
const originalEntryPositions = new Map<
|
||||
Element,
|
||||
{ topRatio: number; heightRatio: number }
|
||||
>();
|
||||
|
||||
// Create zoom controls
|
||||
const zoomControls = document.createElement("div");
|
||||
zoomControls.className = "timetable-zoom-controls";
|
||||
@@ -148,16 +101,16 @@ function handleTimetableZoom(): void {
|
||||
|
||||
// Store event listener references
|
||||
const zoomInHandler = () => {
|
||||
if (timetableZoomLevel < 2) {
|
||||
timetableZoomLevel += 0.2;
|
||||
updateZoom();
|
||||
const seqtaZoomIn = document.querySelector('.uiButton.zoom.in') as HTMLElement;
|
||||
if (seqtaZoomIn) {
|
||||
seqtaZoomIn.click();
|
||||
}
|
||||
};
|
||||
|
||||
const zoomOutHandler = () => {
|
||||
if (timetableZoomLevel > 0.6) {
|
||||
timetableZoomLevel -= 0.2;
|
||||
updateZoom();
|
||||
const seqtaZoomOut = document.querySelector('.uiButton.zoom.out') as HTMLElement;
|
||||
if (seqtaZoomOut) {
|
||||
seqtaZoomOut.click();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -169,84 +122,6 @@ function handleTimetableZoom(): void {
|
||||
zoomIn: zoomInHandler,
|
||||
zoomOut: zoomOutHandler,
|
||||
});
|
||||
|
||||
const initializePositions = () => {
|
||||
// Get the base container height from the first TD
|
||||
const firstDayColumn = document.querySelector(
|
||||
".dailycal .content .days td",
|
||||
) as HTMLElement;
|
||||
if (!firstDayColumn) return false;
|
||||
|
||||
baseContainerHeight =
|
||||
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
|
||||
|
||||
// Store original ratios
|
||||
const entries = document.querySelectorAll(".entriesWrapper .entry");
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement;
|
||||
|
||||
// Calculate ratios relative to detected base height
|
||||
if (baseContainerHeight === null) return;
|
||||
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight;
|
||||
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight;
|
||||
|
||||
originalEntryPositions.set(entry, { topRatio, heightRatio });
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateZoom = () => {
|
||||
// Initialize positions if not already done
|
||||
if (baseContainerHeight === null && !initializePositions()) {
|
||||
console.error("Failed to initialize positions");
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`Updating zoom level to: ${timetableZoomLevel}`);
|
||||
|
||||
// Calculate new container height
|
||||
if (baseContainerHeight === null) return;
|
||||
const newContainerHeight = baseContainerHeight * timetableZoomLevel;
|
||||
|
||||
// Update all day columns (TDs)
|
||||
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
|
||||
dayColumns.forEach((td: Element) => {
|
||||
(td as HTMLElement).style.height = `${newContainerHeight}px`;
|
||||
});
|
||||
|
||||
// Update all entries using stored ratios
|
||||
const entries = document.querySelectorAll(".entriesWrapper .entry");
|
||||
entries.forEach((entry: Element) => {
|
||||
const entryEl = entry as HTMLElement;
|
||||
const originalRatios = originalEntryPositions.get(entry);
|
||||
|
||||
if (originalRatios) {
|
||||
// Calculate new positions from original ratios
|
||||
const newTop = originalRatios.topRatio * newContainerHeight;
|
||||
const newHeight = originalRatios.heightRatio * newContainerHeight;
|
||||
|
||||
// Apply new values
|
||||
entryEl.style.top = `${Math.round(newTop)}px`;
|
||||
entryEl.style.height = `${Math.round(newHeight)}px`;
|
||||
}
|
||||
});
|
||||
|
||||
// Update time column to match
|
||||
const timeColumn = document.querySelector(".times");
|
||||
if (timeColumn) {
|
||||
const times = timeColumn.querySelectorAll(".time");
|
||||
const timeHeight = newContainerHeight / times.length;
|
||||
times.forEach((time: Element) => {
|
||||
(time as HTMLElement).style.height = `${timeHeight}px`;
|
||||
});
|
||||
}
|
||||
|
||||
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
|
||||
behavior: "instant",
|
||||
block: "center",
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function handleTimetableAssessmentHide(): void {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Plugin, PluginSettings } from "./types";
|
||||
|
||||
/**
|
||||
* Interface for lazy-loaded plugin definitions
|
||||
*/
|
||||
export interface LazyPlugin<T extends PluginSettings = PluginSettings, S = any> {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
settings: T;
|
||||
styles?: string;
|
||||
disableToggle?: boolean;
|
||||
defaultEnabled?: boolean;
|
||||
beta?: boolean;
|
||||
|
||||
// Instead of a run function, we have a loader that imports the actual plugin
|
||||
loader: () => Promise<{ default: Plugin<T, S> }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a lazy plugin into a regular plugin by wrapping the run function
|
||||
* with dynamic import logic
|
||||
*/
|
||||
export function createLazyPlugin<T extends PluginSettings = PluginSettings, S = any>(
|
||||
lazyPlugin: LazyPlugin<T, S>
|
||||
): Plugin<T, S> {
|
||||
return {
|
||||
id: lazyPlugin.id,
|
||||
name: lazyPlugin.name,
|
||||
description: lazyPlugin.description,
|
||||
version: lazyPlugin.version,
|
||||
settings: lazyPlugin.settings,
|
||||
styles: lazyPlugin.styles,
|
||||
disableToggle: lazyPlugin.disableToggle,
|
||||
defaultEnabled: lazyPlugin.defaultEnabled,
|
||||
beta: lazyPlugin.beta,
|
||||
|
||||
run: async (api) => {
|
||||
console.info(`[BetterSEQTA+] Dynamically loading plugin "${lazyPlugin.id}"...`);
|
||||
|
||||
try {
|
||||
// Dynamically import the actual plugin implementation
|
||||
const { default: actualPlugin } = await lazyPlugin.loader();
|
||||
|
||||
console.info(`[BetterSEQTA+] Successfully loaded plugin "${lazyPlugin.id}"`);
|
||||
|
||||
// Execute the actual plugin's run function
|
||||
return await actualPlugin.run(api);
|
||||
} catch (error: any) {
|
||||
// Handle Firefox MIME type errors gracefully
|
||||
if (error?.message?.includes("MIME type") || error?.message?.includes("NS_ERROR_CORRUPTED_CONTENT")) {
|
||||
console.error(
|
||||
`[BetterSEQTA+] Failed to load plugin "${lazyPlugin.id}" due to Firefox module loading restrictions. ` +
|
||||
`This may be a build configuration issue. Error:`,
|
||||
error
|
||||
);
|
||||
// Don't throw - allow the extension to continue functioning without this plugin
|
||||
return;
|
||||
}
|
||||
console.error(`[BetterSEQTA+] Failed to dynamically load plugin "${lazyPlugin.id}":`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to create a lazy plugin definition
|
||||
*/
|
||||
export function defineLazyPlugin<T extends PluginSettings = PluginSettings, S = any>(
|
||||
config: LazyPlugin<T, S>
|
||||
): Plugin<T, S> {
|
||||
return createLazyPlugin(config);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type {
|
||||
BooleanSetting,
|
||||
ButtonSetting,
|
||||
ComponentSetting,
|
||||
HotkeySetting,
|
||||
NumberSetting,
|
||||
Plugin,
|
||||
PluginSettings,
|
||||
SelectSetting,
|
||||
StringSetting,
|
||||
ButtonSetting,
|
||||
HotkeySetting,
|
||||
ComponentSetting,
|
||||
} from "./types";
|
||||
import { createPluginAPI } from "./createAPI";
|
||||
import browser from "webextension-polyfill";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type {
|
||||
BooleanSetting,
|
||||
ButtonSetting,
|
||||
ComponentSetting,
|
||||
HotkeySetting,
|
||||
NumberSetting,
|
||||
PluginSettings,
|
||||
SelectSetting,
|
||||
StringSetting,
|
||||
HotkeySetting,
|
||||
PluginSettings,
|
||||
ComponentSetting,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { PluginManager } from "./core/manager";
|
||||
|
||||
// plugins
|
||||
// Lightweight plugins (load immediately)
|
||||
import timetablePlugin from "./built-in/timetable";
|
||||
import notificationCollectorPlugin from "./built-in/notificationCollector";
|
||||
import themesPlugin from "./built-in/themes";
|
||||
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
||||
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
||||
import globalSearchPlugin from "./built-in/globalSearch/src/core";
|
||||
import profilePicturePlugin from "./built-in/profilePicture";
|
||||
import assessmentsOverviewPlugin from "./built-in/assessmentsOverview";
|
||||
import backgroundMusicPlugin from "./built-in/backgroundMusic";
|
||||
//import testPlugin from './built-in/test';
|
||||
|
||||
// Heavy plugins (lazy-loaded only when enabled)
|
||||
import globalSearchPluginLazy from "./built-in/globalSearch/lazy";
|
||||
|
||||
// Initialize plugin manager
|
||||
const pluginManager = PluginManager.getInstance();
|
||||
|
||||
@@ -20,11 +23,14 @@ pluginManager.registerPlugin(animatedBackgroundPlugin);
|
||||
pluginManager.registerPlugin(assessmentsAveragePlugin);
|
||||
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||
pluginManager.registerPlugin(timetablePlugin);
|
||||
pluginManager.registerPlugin(globalSearchPlugin);
|
||||
pluginManager.registerPlugin(profilePicturePlugin);
|
||||
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
||||
pluginManager.registerPlugin(backgroundMusicPlugin);
|
||||
//pluginManager.registerPlugin(testPlugin);
|
||||
|
||||
// Register heavy plugins with lazy loading
|
||||
pluginManager.registerPlugin(globalSearchPluginLazy);
|
||||
|
||||
export { init as Monofile } from "./monofile";
|
||||
|
||||
export async function initializePlugins(): Promise<void> {
|
||||
|
||||
+10
-5
@@ -23,10 +23,10 @@ import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
||||
import loading from "@/seqta/ui/Loading";
|
||||
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
||||
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
|
||||
import {
|
||||
updateTimetableTimes,
|
||||
} from "@/seqta/utils/updateTimetableTimes";
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
|
||||
import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification";
|
||||
|
||||
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
|
||||
|
||||
// JSON content
|
||||
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
||||
@@ -94,7 +94,12 @@ export async function finishLoad() {
|
||||
console.error("Error during loading cleanup:", err);
|
||||
}
|
||||
|
||||
if (settingsState.justupdated && !document.getElementById("whatsnewbk")) {
|
||||
// Check and show privacy statement notification (before what's new)
|
||||
if (!document.getElementById("privacy-notification")) {
|
||||
await showPrivacyNotification();
|
||||
}
|
||||
|
||||
if (settingsState.justupdated && !document.getElementById("whatsnewbk") && !document.getElementById("privacy-notification")) {
|
||||
OpenWhatsNewPopup();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+2
-2
@@ -16,9 +16,9 @@ export async function main() {
|
||||
if (settingsState.onoff) {
|
||||
injectPageState();
|
||||
|
||||
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
||||
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
||||
if (import.meta.env.MODE === "development") {
|
||||
import("../css/injected.scss");
|
||||
import("@/css/injected.scss");
|
||||
} else {
|
||||
const injectedStyle = document.createElement("style");
|
||||
injectedStyle.textContent = injectedCSS;
|
||||
|
||||
@@ -14,7 +14,7 @@ let cachedUserInfo: any = null;
|
||||
|
||||
let LightDarkModeSnakeEggButton = 0;
|
||||
|
||||
async function getUserInfo() {
|
||||
export async function getUserInfo() {
|
||||
if (cachedUserInfo) return cachedUserInfo;
|
||||
|
||||
try {
|
||||
@@ -30,11 +30,10 @@ async function getUserInfo() {
|
||||
}),
|
||||
});
|
||||
|
||||
const responseData = await response.json();
|
||||
cachedUserInfo = responseData.payload;
|
||||
cachedUserInfo = (await response.json()).payload;
|
||||
return cachedUserInfo;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user info:", error);
|
||||
console.error("[BetterSEQTA+] Failed to get user info:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +60,7 @@ export async function AddBetterSEQTAElements() {
|
||||
handleStudentData(),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Error initializing UI elements:", error);
|
||||
console.error("[BetterSEQTA+] Failed to initialize UI elements:", error);
|
||||
}
|
||||
|
||||
setupEventListeners();
|
||||
@@ -80,20 +79,18 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
|
||||
div.classList.add("titlebar");
|
||||
container.append(div);
|
||||
|
||||
const NewButton = stringToHTML(
|
||||
/* html */`<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>`
|
||||
fragment.appendChild(
|
||||
stringToHTML(
|
||||
/* html */ `<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>`,
|
||||
).firstChild!,
|
||||
);
|
||||
if (NewButton.firstChild) {
|
||||
fragment.appendChild(NewButton.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUserInfo() {
|
||||
try {
|
||||
const info = await getUserInfo();
|
||||
updateUserInfo(info);
|
||||
updateUserInfo(await getUserInfo());
|
||||
} catch (error) {
|
||||
console.error("Error fetching and processing student data:", error);
|
||||
console.error("[BetterSEQTA+] Failed to handle user info:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,15 +114,17 @@ function updateUserInfo(info: {
|
||||
}) {
|
||||
const titlebar = document.getElementsByClassName("titlebar")[0];
|
||||
|
||||
const userInfo = stringToHTML(/* html */ `
|
||||
titlebar.append(
|
||||
stringToHTML(/* html */ `
|
||||
<div class="userInfosvgdiv tooltip">
|
||||
<svg class="userInfosvg" viewBox="0 0 24 24"><path fill="var(--text-primary)" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path></svg>
|
||||
<div class="tooltiptext topmenutooltip" id="logouttooltip"></div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
titlebar.append(userInfo!);
|
||||
`).firstChild!,
|
||||
);
|
||||
|
||||
const userinfo = stringToHTML(/* html */ `
|
||||
titlebar.append(
|
||||
stringToHTML(/* html */ `
|
||||
<div class="userInfo">
|
||||
<div class="userInfoText">
|
||||
<div style="display: flex; align-items: center;">
|
||||
@@ -135,12 +134,12 @@ function updateUserInfo(info: {
|
||||
<p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p>
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
titlebar.append(userinfo!);
|
||||
`).firstChild!,
|
||||
);
|
||||
|
||||
var logoutbutton = document.getElementsByClassName("logout")[0];
|
||||
var userInfosvgdiv = document.getElementById("logouttooltip")!;
|
||||
userInfosvgdiv.appendChild(logoutbutton);
|
||||
document
|
||||
.getElementById("logouttooltip")!
|
||||
.appendChild(document.getElementsByClassName("logout")[0]);
|
||||
}
|
||||
|
||||
async function handleStudentData() {
|
||||
@@ -156,57 +155,54 @@ async function handleStudentData() {
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
let students = responseData.payload;
|
||||
await updateStudentInfo(students);
|
||||
await updateStudentInfo((await response.json()).payload);
|
||||
} catch (error) {
|
||||
console.error("Error fetching and processing student data:", error);
|
||||
console.error("[BetterSEQTA+] Failed to handle student data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStudentInfo(students: any) {
|
||||
const info = await getUserInfo();
|
||||
var index = students.findIndex(function (person: any) {
|
||||
return (
|
||||
const index = students.findIndex(
|
||||
(person: any) =>
|
||||
person.firstname == info.userDesc.split(" ")[0] &&
|
||||
person.surname == info.userDesc.split(" ")[1]
|
||||
person.surname == info.userDesc.split(" ")[1],
|
||||
);
|
||||
});
|
||||
|
||||
let houseelement1 = document.getElementsByClassName("userInfohouse")[0];
|
||||
const houseelement = houseelement1 as HTMLElement;
|
||||
const houseelement = document.getElementsByClassName(
|
||||
"userInfohouse",
|
||||
)[0] as HTMLElement;
|
||||
const student = students[index] ?? {};
|
||||
let text = "N/A";
|
||||
|
||||
if (students[index]?.house) {
|
||||
if (students[index]?.house_colour) {
|
||||
houseelement.style.background = students[index].house_colour;
|
||||
if (student.house) {
|
||||
text = `${student.year ?? ""}${student.house}`;
|
||||
|
||||
if (student.house_colour) {
|
||||
houseelement.style.background = student.house_colour;
|
||||
try {
|
||||
let colorresult = GetThresholdOfColor(students[index]?.house_colour);
|
||||
const colorresult = GetThresholdOfColor(student.house_colour);
|
||||
houseelement.style.color =
|
||||
colorresult && colorresult > 300 ? "black" : "white";
|
||||
houseelement.innerText = students[index].year + students[index].house;
|
||||
} catch (error) {
|
||||
houseelement.innerText = students[index].house;
|
||||
} catch {
|
||||
// Invalid color format, leave text color as default
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
houseelement.innerText = students[index].year;
|
||||
} catch (err) {
|
||||
houseelement.innerText = "N/A";
|
||||
}
|
||||
} else if (student.year) {
|
||||
text = student.year;
|
||||
}
|
||||
|
||||
houseelement.innerText = text;
|
||||
}
|
||||
|
||||
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
|
||||
const NewsButtonStr =
|
||||
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>';
|
||||
const NewsButton = stringToHTML(NewsButtonStr);
|
||||
fragment.appendChild(
|
||||
stringToHTML(
|
||||
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>',
|
||||
).firstChild!,
|
||||
);
|
||||
|
||||
if (NewsButton.firstChild) {
|
||||
fragment.appendChild(NewsButton.firstChild);
|
||||
}
|
||||
|
||||
let iconCover = document.createElement("div");
|
||||
const iconCover = document.createElement("div");
|
||||
iconCover.classList.add("icon-cover");
|
||||
iconCover.id = "icon-cover";
|
||||
menu.appendChild(iconCover);
|
||||
@@ -245,46 +241,42 @@ function setupEventListeners() {
|
||||
}
|
||||
|
||||
async function createSettingsButton() {
|
||||
let SettingsButton = stringToHTML(/* html */ `
|
||||
document.getElementById("content")!.append(
|
||||
stringToHTML(/* html */ `
|
||||
<button class="addedButton tooltip" id="AddedSettings">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g>
|
||||
</svg>
|
||||
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
|
||||
</button>
|
||||
`);
|
||||
let ContentDiv = document.getElementById("content");
|
||||
ContentDiv!.append(SettingsButton.firstChild!);
|
||||
`).firstChild!,
|
||||
);
|
||||
}
|
||||
|
||||
function GetLightDarkModeString() {
|
||||
if (settingsState.DarkMode) {
|
||||
return "Switch to light theme";
|
||||
} else {
|
||||
return "Switch to dark theme";
|
||||
}
|
||||
return settingsState.DarkMode
|
||||
? "Switch to light theme"
|
||||
: "Switch to dark theme";
|
||||
}
|
||||
|
||||
async function addDarkLightToggle() {
|
||||
const tooltipString = GetLightDarkModeString();
|
||||
const SUN_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
||||
const MOON_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
||||
|
||||
const initialSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
|
||||
|
||||
const LightDarkModeButton = stringToHTML(/* html */ `
|
||||
document.getElementById("content")!.append(
|
||||
stringToHTML(/* html */ `
|
||||
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">${initialSvgContent}</svg>
|
||||
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg">${settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG}</svg>
|
||||
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${GetLightDarkModeString()}</div>
|
||||
</button>
|
||||
`);
|
||||
|
||||
let ContentDiv = document.getElementById("content");
|
||||
ContentDiv!.append(LightDarkModeButton.firstChild!);
|
||||
`).firstChild!,
|
||||
);
|
||||
|
||||
updateAllColors();
|
||||
|
||||
const lightDarkModeButtonElement = document.getElementById("LightDarkModeButton")!;
|
||||
const lightDarkModeButtonElement = document.getElementById(
|
||||
"LightDarkModeButton",
|
||||
)!;
|
||||
|
||||
lightDarkModeButtonElement.addEventListener("click", async () => {
|
||||
const darklightText = document.getElementById("darklighttooliptext");
|
||||
@@ -296,7 +288,6 @@ async function addDarkLightToggle() {
|
||||
LightDarkModeSnakeEggButton = 0;
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
settingsState.originalDarkMode !== undefined &&
|
||||
settingsState.selectedTheme
|
||||
@@ -307,38 +298,24 @@ async function addDarkLightToggle() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!document.startViewTransition || !settingsState.animations || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
||||
settingsState.DarkMode = !settingsState.DarkMode;
|
||||
updateAllColors();
|
||||
|
||||
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
|
||||
const svgElement = lightDarkModeButtonElement.querySelector("svg");
|
||||
if (svgElement) svgElement.innerHTML = newSvgContent;
|
||||
darklightText!.innerText = GetLightDarkModeString();
|
||||
return;
|
||||
}
|
||||
|
||||
settingsState.DarkMode = !settingsState.DarkMode;
|
||||
|
||||
updateAllColors();
|
||||
|
||||
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
|
||||
const svgElement = lightDarkModeButtonElement.querySelector("svg");
|
||||
if (svgElement) svgElement.innerHTML = newSvgContent;
|
||||
|
||||
const svgElement = lightDarkModeButtonElement.querySelector("svg")!;
|
||||
svgElement.innerHTML = settingsState.DarkMode
|
||||
? SUN_ICON_SVG
|
||||
: MOON_ICON_SVG;
|
||||
darklightText!.innerText = GetLightDarkModeString();
|
||||
});
|
||||
}
|
||||
|
||||
function customizeMenuToggle() {
|
||||
const menuToggle = document.getElementById("menuToggle");
|
||||
if (menuToggle) {
|
||||
const menuToggle = document.getElementById("menuToggle")!;
|
||||
menuToggle.innerHTML = "";
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const line = document.createElement("div");
|
||||
line.className = "hamburger-line";
|
||||
menuToggle!.appendChild(line);
|
||||
menuToggle.appendChild(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,21 @@ interface ContentConfig {
|
||||
[key: string]: ElementConfig;
|
||||
}
|
||||
|
||||
// Track processed elements to avoid re-randomizing
|
||||
const processedElements = new WeakSet<Element>();
|
||||
|
||||
function debounce(func: Function, wait: number): Function {
|
||||
let timeout: NodeJS.Timeout;
|
||||
return function executedFunction(...args: any[]) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
function getRandomElement(array: string[]): string {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
@@ -164,9 +179,32 @@ const contentConfig: ContentConfig = {
|
||||
},
|
||||
},
|
||||
forumTopics: {
|
||||
selector: "#menu .sub ul li label",
|
||||
selector: "#menu .sub ul li:not([data-colour]):not(.hasChildren) label",
|
||||
action: (element) => {
|
||||
// Only redact if not in assessments section
|
||||
const assessmentsSection = element.closest('[data-key="assessments"]');
|
||||
if (!assessmentsSection) {
|
||||
element.textContent = "Forum Topic Redacted";
|
||||
}
|
||||
},
|
||||
},
|
||||
assessmentSubjects: {
|
||||
selector: '[data-key="assessments"] .sub ul li[data-colour] label',
|
||||
action: (element) => {
|
||||
element.textContent = getRandomElement(mockData.subjects);
|
||||
},
|
||||
},
|
||||
assessmentYearGroups: {
|
||||
selector: '[data-key="assessments"] .sub ul li.hasChildren:not([data-colour]) label',
|
||||
action: (element) => {
|
||||
const yearGroup = Math.floor(Math.random() * 5) + 8; // Years 8-12
|
||||
element.textContent = `Year ${yearGroup}`;
|
||||
},
|
||||
},
|
||||
assessmentSubYearGroups: {
|
||||
selector: '[data-key="assessments"] .sub .sub ul li[data-colour] label',
|
||||
action: (element) => {
|
||||
element.textContent = getRandomElement(mockData.subjects);
|
||||
},
|
||||
},
|
||||
courseNames: {
|
||||
@@ -541,11 +579,168 @@ export function getMockNotices() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function hideSensitiveContent() {
|
||||
export function getMockAssessmentsData() {
|
||||
const subjects = mockData.subjects.slice(0, 5).map((title, i) => ({
|
||||
code: `SUBJ${i + 1}`,
|
||||
programme: i + 1,
|
||||
metaclass: i + 1,
|
||||
title,
|
||||
}));
|
||||
|
||||
const colors: Record<string, string> = {};
|
||||
subjects.forEach((s) => {
|
||||
colors[s.code] = `hsl(${Math.floor(Math.random() * 360)},70%,60%)`;
|
||||
});
|
||||
|
||||
const statusTemplates = [
|
||||
// Marked with scores (70-90%) - goes to MARKS_RELEASED
|
||||
{ submitted: true, score: () => Math.floor(Math.random() * 21) + 70, dayOffset: () => Math.floor(Math.random() * -30) - 7 }, // Past due, marked with score
|
||||
{ submitted: true, score: () => Math.floor(Math.random() * 21) + 70, dayOffset: () => Math.floor(Math.random() * -14) - 1 }, // Recently marked with score
|
||||
{ submitted: true, score: () => Math.floor(Math.random() * 21) + 70, dayOffset: () => Math.floor(Math.random() * -7) }, // Very recently marked with score
|
||||
|
||||
// Submitted but unmarked - goes to SUBMITTED
|
||||
{ submitted: true, score: null, dayOffset: () => Math.floor(Math.random() * -5) - 1 }, // Recently submitted, awaiting marking
|
||||
{ submitted: true, score: null, dayOffset: () => Math.floor(Math.random() * -3) }, // Very recently submitted, awaiting marking
|
||||
{ submitted: true, score: null, dayOffset: () => Math.floor(Math.random() * -2) }, // Just submitted, awaiting marking
|
||||
|
||||
// Due soon (not submitted) - only a couple
|
||||
{ submitted: false, score: null, dayOffset: () => 0 }, // Due today
|
||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 3) + 2 }, // Due in next few days
|
||||
|
||||
// Due later (not submitted) - most assessments
|
||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 7) + 8 }, // Due in 1-2 weeks
|
||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 14) + 14 }, // Due in 2-4 weeks
|
||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 21) + 21 }, // Due in 3-6 weeks
|
||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * 14) + 35 }, // Due in 5-7 weeks
|
||||
|
||||
// Few overdue (not submitted) - less common
|
||||
{ submitted: false, score: null, dayOffset: () => Math.floor(Math.random() * -3) - 1 }, // Recently overdue
|
||||
];
|
||||
|
||||
const assessments = Array.from({ length: 12 }, (_, i) => {
|
||||
const subj = subjects[i % subjects.length];
|
||||
const template = statusTemplates[i % statusTemplates.length];
|
||||
const due = new Date();
|
||||
due.setDate(due.getDate() + template.dayOffset());
|
||||
|
||||
const assessment: any = {
|
||||
id: i + 1,
|
||||
title: mockData.assessmentTitles[i % mockData.assessmentTitles.length],
|
||||
code: subj.code,
|
||||
programmeID: subj.programme,
|
||||
metaclassID: subj.metaclass,
|
||||
due: due.toISOString(),
|
||||
submitted: template.submitted,
|
||||
};
|
||||
|
||||
if (template.score && typeof template.score === 'function') {
|
||||
assessment.percentage = template.score(); // This triggers MARKS_RELEASED
|
||||
assessment.results = {
|
||||
percentage: template.score() // This displays the thermometer
|
||||
};
|
||||
}
|
||||
|
||||
return assessment;
|
||||
});
|
||||
|
||||
return { assessments, subjects, colors };
|
||||
}
|
||||
|
||||
// Create a debounced processing function
|
||||
const debouncedProcessElements = debounce(processNewElements, 1);
|
||||
|
||||
function processNewElements() {
|
||||
Object.entries(contentConfig).forEach(([_, { selector, action }]) => {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element: Element) => {
|
||||
// Only process elements that haven't been processed before
|
||||
if (!processedElements.has(element)) {
|
||||
action(element);
|
||||
processedElements.add(element);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let observer: MutationObserver | null = null;
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
export default function hideSensitiveContent() {
|
||||
// Initial processing of existing elements
|
||||
processNewElements();
|
||||
|
||||
// Set up MutationObserver if not already created
|
||||
if (!observer) {
|
||||
observer = new MutationObserver((mutations) => {
|
||||
let shouldProcess = false;
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
// Check for both childList and subtree changes
|
||||
if (mutation.type === 'childList') {
|
||||
// Check added nodes
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const element = node as Element;
|
||||
// Check if the added element or its children match any of our selectors
|
||||
for (const config of Object.values(contentConfig)) {
|
||||
if (element.matches?.(config.selector) || element.querySelector?.(config.selector)) {
|
||||
shouldProcess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also trigger on large DOM replacements (like page navigation)
|
||||
if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) {
|
||||
shouldProcess = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for attribute changes that might affect our selectors
|
||||
if (mutation.type === 'attributes') {
|
||||
const target = mutation.target as Element;
|
||||
for (const config of Object.values(contentConfig)) {
|
||||
if (target.matches?.(config.selector)) {
|
||||
shouldProcess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldProcess) {
|
||||
debouncedProcessElements();
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing with more comprehensive options
|
||||
observer.observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'id'] // Watch for class/id changes that might affect our selectors
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback: periodic check for new elements (especially useful for SPA navigation)
|
||||
if (!intervalId) {
|
||||
intervalId = setInterval(() => {
|
||||
debouncedProcessElements();
|
||||
}, 500); // Check every 500ms as a fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Function to stop observing (useful for cleanup)
|
||||
export function stopHidingSensitiveContent() {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import renderSvelte from "@/interface/main";
|
||||
import { SettingsResizer } from "@/seqta/ui/SettingsResizer";
|
||||
import Settings from "@/interface/pages/settings.svelte";
|
||||
|
||||
let isSettingsRendered = false;
|
||||
|
||||
export function addExtensionSettings() {
|
||||
const extensionPopup = document.createElement("div");
|
||||
extensionPopup.classList.add("outside-container", "hide");
|
||||
@@ -17,14 +19,6 @@ export function addExtensionSettings() {
|
||||
) as HTMLDivElement;
|
||||
if (extensionContainer) extensionContainer.appendChild(extensionPopup);
|
||||
|
||||
// create shadow dom and render svelte app
|
||||
try {
|
||||
const shadow = extensionPopup.attachShadow({ mode: "open" });
|
||||
requestIdleCallback(() => renderSvelte(Settings, shadow));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const container = document.getElementById("container");
|
||||
|
||||
new SettingsResizer();
|
||||
@@ -38,3 +32,22 @@ export function addExtensionSettings() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function renderSettingsIfNeeded() {
|
||||
if (isSettingsRendered) return;
|
||||
|
||||
const extensionPopup = document.getElementById("ExtensionPopup");
|
||||
if (!extensionPopup) return;
|
||||
|
||||
try {
|
||||
const shadow = extensionPopup.attachShadow({ mode: "open" });
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => renderSvelte(Settings, shadow));
|
||||
} else {
|
||||
renderSvelte(Settings, shadow);
|
||||
}
|
||||
isSettingsRendered = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ export function addShortcuts(shortcuts: any) {
|
||||
|
||||
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
||||
// Creates the stucture and element information for each seperate shortcut
|
||||
const container = document.getElementById("shortcuts");
|
||||
if (!container) return;
|
||||
|
||||
let shortcut = document.createElement("a");
|
||||
shortcut.setAttribute("href", link);
|
||||
shortcut.setAttribute("target", "_blank");
|
||||
@@ -42,5 +45,5 @@ function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
||||
shortcutdiv.append(text);
|
||||
shortcut.append(shortcutdiv);
|
||||
|
||||
document.getElementById("shortcuts")!.appendChild(shortcut);
|
||||
container.appendChild(shortcut);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import stringToHTML from "../stringToHTML";
|
||||
|
||||
export function CreateCustomShortcutDiv(element: any) {
|
||||
// Creates the stucture and element information for each seperate shortcut
|
||||
const container = document.getElementById("shortcuts");
|
||||
if (!container) return;
|
||||
|
||||
var shortcut = document.createElement("a");
|
||||
shortcut.setAttribute("href", element.url);
|
||||
shortcut.setAttribute("target", "_blank");
|
||||
@@ -45,5 +48,5 @@ export function CreateCustomShortcutDiv(element: any) {
|
||||
shortcutdiv.append(text);
|
||||
shortcut.append(shortcutdiv);
|
||||
|
||||
document.getElementById("shortcuts")!.append(shortcut);
|
||||
container.append(shortcut);
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import links from "@/seqta/content/links.json";
|
||||
|
||||
export function RemoveShortcutDiv(elements: any) {
|
||||
if (elements.length === 0) return;
|
||||
|
||||
elements.forEach((element: any) => {
|
||||
const shortcuts = document.querySelectorAll(".shortcut");
|
||||
shortcuts.forEach((shortcut) => {
|
||||
const anchorElement = shortcut.parentElement; // the <a> element is the parent
|
||||
const textElement = shortcut.querySelector("p"); // <p> is a direct child of .shortcut
|
||||
const title = textElement ? textElement.textContent : "";
|
||||
|
||||
const elementName = links[element.name as keyof typeof links]?.DisplayName || element.name;
|
||||
|
||||
let shouldRemove = title === elementName;
|
||||
|
||||
// Check href only if element.url exists
|
||||
if (element.url) {
|
||||
shouldRemove =
|
||||
shouldRemove && anchorElement!.getAttribute("href") === element.url;
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
anchorElement!.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
|
||||
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
|
||||
import coursesicon from "@/seqta/icons/coursesIcon";
|
||||
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
||||
import { addShortcuts } from "../Adders/AddShortcuts";
|
||||
import { convertTo12HourFormat } from "../convertTo12HourFormat";
|
||||
import { delay } from "../delay";
|
||||
import { settingsState } from "../listeners/SettingsState";
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
||||
import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
|
||||
import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement";
|
||||
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
||||
import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent";
|
||||
import { setupFixedTooltips } from "@/seqta/utils/fixedTooltip";
|
||||
|
||||
let LessonInterval: any;
|
||||
let currentSelectedDate = new Date();
|
||||
@@ -30,20 +30,17 @@ export async function loadHomePage() {
|
||||
element?.classList.add("active");
|
||||
|
||||
const main = document.getElementById("main");
|
||||
if (!main) {
|
||||
console.error("[BetterSEQTA+] Main element not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const homeRoot = stringToHTML(`<div id="home-root" class="home-root"></div>`);
|
||||
if (!main) return;
|
||||
|
||||
main.innerHTML = "";
|
||||
main.appendChild(homeRoot?.firstChild!);
|
||||
main.appendChild(
|
||||
stringToHTML(`<div id="home-root" class="home-root"></div>`).firstChild!,
|
||||
);
|
||||
|
||||
const homeContainer = document.getElementById("home-root");
|
||||
if (!homeContainer) return;
|
||||
|
||||
const skeletonStructure = stringToHTML(`
|
||||
const skeletonStructure = stringToHTML(/* html */ `
|
||||
<div class="home-container" id="home-container">
|
||||
<div class="border shortcut-container">
|
||||
<div class="border shortcuts" id="shortcuts"></div>
|
||||
@@ -99,97 +96,21 @@ export async function loadHomePage() {
|
||||
|
||||
const cleanup = setupTimetableListeners();
|
||||
|
||||
try {
|
||||
addShortcuts(settingsState.shortcuts);
|
||||
} catch (err: any) {
|
||||
console.error("[BetterSEQTA+] Error adding shortcuts:", err.message || err);
|
||||
}
|
||||
AddCustomShortcutsToPage();
|
||||
renderShortcuts();
|
||||
|
||||
const date = new Date();
|
||||
const TodayFormatted = formatDate(date);
|
||||
|
||||
const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [
|
||||
fetch(`${location.origin}/seqta/student/load/timetable?`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
from: TodayFormatted,
|
||||
until: TodayFormatted,
|
||||
student: 69,
|
||||
}),
|
||||
}).then((res) => res.json()),
|
||||
const TodayFormatted = formatDate(new Date());
|
||||
|
||||
const [assessments, classes, prefs] = await Promise.all([
|
||||
GetUpcomingAssessments(),
|
||||
|
||||
GetActiveClasses(),
|
||||
|
||||
fetch(`${location.origin}/seqta/student/load/prefs?`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ asArray: true, request: "userPrefs" }),
|
||||
}).then((res) => res.json()),
|
||||
];
|
||||
|
||||
const [timetableData, assessments, classes, prefs] = await Promise.all([
|
||||
timetablePromise,
|
||||
assessmentsPromise,
|
||||
classesPromise,
|
||||
prefsPromise,
|
||||
]);
|
||||
|
||||
const dayContainer = document.getElementById("day-container");
|
||||
if (dayContainer && timetableData.payload.items.length > 0) {
|
||||
const lessonArray = timetableData.payload.items.sort((a: any, b: any) =>
|
||||
a.from.localeCompare(b.from),
|
||||
);
|
||||
const colours = await GetLessonColours();
|
||||
|
||||
dayContainer.innerHTML = "";
|
||||
for (let i = 0; i < lessonArray.length; i++) {
|
||||
const lesson = lessonArray[i];
|
||||
const subjectname = `timetable.subject.colour.${lesson.code}`;
|
||||
const subject = colours.find(
|
||||
(element: any) => element.name === subjectname,
|
||||
);
|
||||
|
||||
lesson.colour = subject
|
||||
? `--item-colour: ${subject.value};`
|
||||
: "--item-colour: #8e8e8e;";
|
||||
lesson.from = lesson.from.substring(0, 5);
|
||||
lesson.until = lesson.until.substring(0, 5);
|
||||
|
||||
if (settingsState.timeFormat === "12") {
|
||||
lesson.from = convertTo12HourFormat(lesson.from);
|
||||
lesson.until = convertTo12HourFormat(lesson.until);
|
||||
}
|
||||
|
||||
lesson.attendanceTitle = CheckUnmarkedAttendance(lesson.attendance);
|
||||
|
||||
const div = makeLessonDiv(lesson, i + 1);
|
||||
if (GetThresholdOfColor(subject?.value) > 300) {
|
||||
const firstChild = div.firstChild as HTMLElement;
|
||||
if (firstChild) {
|
||||
firstChild.classList.add("day-inverted");
|
||||
}
|
||||
}
|
||||
dayContainer.appendChild(div.firstChild!);
|
||||
}
|
||||
|
||||
if (currentSelectedDate.getDate() === date.getDate()) {
|
||||
for (let i = 0; i < lessonArray.length; i++) {
|
||||
CheckCurrentLesson(lessonArray[i], i + 1);
|
||||
}
|
||||
CheckCurrentLessonAll(lessonArray);
|
||||
}
|
||||
} else if (dayContainer) {
|
||||
dayContainer.innerHTML = `
|
||||
<div class="day-empty">
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" />
|
||||
<p>No lessons available.</p>
|
||||
</div>`;
|
||||
}
|
||||
dayContainer?.classList.remove("loading");
|
||||
callHomeTimetable(TodayFormatted, true);
|
||||
|
||||
const activeClass = classes.find((c: any) => c.hasOwnProperty("active"));
|
||||
const activeSubjects = activeClass?.subjects || [];
|
||||
@@ -226,20 +147,20 @@ export async function loadHomePage() {
|
||||
}
|
||||
|
||||
async function GetUpcomingAssessments() {
|
||||
let func = fetch(
|
||||
`${location.origin}/seqta/student/assessment/list/upcoming?`,
|
||||
{
|
||||
try {
|
||||
return fetch(`${location.origin}/seqta/student/assessment/list/upcoming?`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: JSON.stringify({ student: 69 }),
|
||||
},
|
||||
);
|
||||
|
||||
return func
|
||||
})
|
||||
.then((result) => result.json())
|
||||
.then((response) => response.payload);
|
||||
} catch (error) {
|
||||
console.error("[BetterSEQTA+] Failed to get upcoming assessments:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function setupTimetableListeners() {
|
||||
@@ -297,15 +218,10 @@ async function GetActiveClasses() {
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.payload;
|
||||
return (await response.json()).payload;
|
||||
} catch (error) {
|
||||
console.error("Oops! There was a problem fetching active classes:", error);
|
||||
console.error("[BetterSEQTA+] Failed to get active classes:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,28 +231,25 @@ function setupNotices(labelArray: string[], date: string) {
|
||||
) as HTMLInputElement;
|
||||
|
||||
const fetchNotices = async (date: string) => {
|
||||
let data;
|
||||
|
||||
if (settingsState.mockNotices) {
|
||||
data = getMockNotices();
|
||||
} else {
|
||||
const response = await fetch(
|
||||
`${location.origin}/seqta/student/load/notices?`,
|
||||
{
|
||||
try {
|
||||
const data = settingsState.mockNotices
|
||||
? getMockNotices()
|
||||
: await (
|
||||
await fetch(`${location.origin}/seqta/student/load/notices?`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({ date }),
|
||||
},
|
||||
);
|
||||
data = await response.json();
|
||||
}
|
||||
})
|
||||
).json();
|
||||
|
||||
processNotices(data, labelArray);
|
||||
} catch (error) {
|
||||
console.error("[BetterSEQTA+] Failed to fetch notices:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedInputChange = debounce((e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
fetchNotices(target.value);
|
||||
fetchNotices((e.target as HTMLInputElement).value);
|
||||
}, 250);
|
||||
|
||||
dateControl?.addEventListener("input", debouncedInputChange);
|
||||
@@ -357,25 +270,8 @@ function debounce<T extends (...args: any[]) => any>(
|
||||
}
|
||||
|
||||
function comparedate(obj1: any, obj2: any) {
|
||||
if (obj1.date < obj2.date) {
|
||||
return -1;
|
||||
return obj1.date < obj2.date ? -1 : obj1.date > obj2.date ? 1 : 0;
|
||||
}
|
||||
if (obj1.date > obj2.date) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async function AddCustomShortcutsToPage() {
|
||||
let customshortcuts: any = settingsState.customshortcuts;
|
||||
if (customshortcuts.length > 0) {
|
||||
for (let i = 0; i < customshortcuts.length; i++) {
|
||||
const element = customshortcuts[i];
|
||||
CreateCustomShortcutDiv(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function processNotices(response: any, labelArray: string[]) {
|
||||
const NoticeContainer = document.getElementById("notice-container");
|
||||
if (!NoticeContainer) return;
|
||||
@@ -419,9 +315,14 @@ function processNoticeColor(colour: string): string | undefined {
|
||||
}
|
||||
|
||||
function createNoticeElement(notice: any, colour: string | undefined): Node {
|
||||
const cleanContent = notice.contents
|
||||
const textPreview =
|
||||
notice.contents
|
||||
.replace(/<[^>]*>/g, "")
|
||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||
.replace(/ +/, " ");
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.substring(0, 150) + (notice.contents.length > 150 ? "..." : "");
|
||||
|
||||
const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const htmlContent = `
|
||||
@@ -436,16 +337,14 @@ function createNoticeElement(notice: any, colour: string | undefined): Node {
|
||||
<button class="notice-close-btn" style="opacity: 0; pointer-events: none;">×</button>
|
||||
</div>
|
||||
<h2 class="notice-content-title">${notice.title}</h2>
|
||||
<div class="notice-content-body">${cleanContent}</div>
|
||||
<div class="notice-content-body">${textPreview}</div>
|
||||
</div>`;
|
||||
|
||||
const element = stringToHTML(htmlContent).firstChild as HTMLElement;
|
||||
if (element) {
|
||||
element.addEventListener("click", () =>
|
||||
openNoticeModal(notice, colour, element),
|
||||
);
|
||||
}
|
||||
return element!;
|
||||
return element;
|
||||
}
|
||||
|
||||
function openNoticeModal(
|
||||
@@ -457,15 +356,11 @@ function openNoticeModal(
|
||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||
.replace(/ +/, " ");
|
||||
|
||||
const existingModal = document.getElementById("notice-modal");
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
document.getElementById("notice-modal")?.remove();
|
||||
|
||||
const sourceRect = sourceElement.getBoundingClientRect();
|
||||
let scrollY = Math.round(window.scrollY);
|
||||
let scrollX = Math.round(window.scrollX);
|
||||
|
||||
let sourceLeft = sourceRect.left;
|
||||
let sourceTop = sourceRect.top;
|
||||
let sourceWidth = sourceRect.width;
|
||||
@@ -547,7 +442,6 @@ function openNoticeModal(
|
||||
let targetHeight = Math.round(
|
||||
Math.min(Math.max(measuredHeight, 200), viewportHeight * 0.85),
|
||||
);
|
||||
|
||||
let targetLeft = Math.round((viewportWidth - targetWidth) / 2);
|
||||
let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY;
|
||||
|
||||
@@ -628,12 +522,13 @@ function openNoticeModal(
|
||||
// Get the current scale applied to the source element and compensate for it
|
||||
const computedStyle = getComputedStyle(sourceElement);
|
||||
const transform = computedStyle.transform;
|
||||
let scaleX = 1, scaleY = 1;
|
||||
let scaleX = 1,
|
||||
scaleY = 1;
|
||||
|
||||
if (transform && transform !== 'none') {
|
||||
if (transform && transform !== "none") {
|
||||
const matrix = transform.match(/matrix.*\((.+)\)/);
|
||||
if (matrix) {
|
||||
const values = matrix[1].split(', ');
|
||||
const values = matrix[1].split(", ");
|
||||
scaleX = parseFloat(values[0]);
|
||||
scaleY = parseFloat(values[3]);
|
||||
}
|
||||
@@ -655,13 +550,10 @@ function openNoticeModal(
|
||||
const newTargetWidth = Math.round(
|
||||
Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40),
|
||||
);
|
||||
|
||||
// Just measure the existing modal content
|
||||
const currentHeight = unifiedContent.getBoundingClientRect().height;
|
||||
const newTargetHeight = Math.round(
|
||||
Math.min(Math.max(currentHeight, 200), newViewportHeight * 0.85),
|
||||
);
|
||||
|
||||
const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2);
|
||||
const newTargetTop =
|
||||
Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY;
|
||||
@@ -726,7 +618,8 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.readyState !== 4) return;
|
||||
|
||||
if (loadingTimeout) {
|
||||
clearTimeout(loadingTimeout);
|
||||
loadingTimeout = null;
|
||||
@@ -734,7 +627,6 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
|
||||
const DayContainer = document.getElementById("day-container")!;
|
||||
|
||||
try {
|
||||
var serverResponse = JSON.parse(xhr.response);
|
||||
let lessonArray: Array<any> = [];
|
||||
|
||||
@@ -748,20 +640,20 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
});
|
||||
|
||||
GetLessonColours().then((colours) => {
|
||||
let subjects = colours;
|
||||
for (let i = 0; i < lessonArray.length; i++) {
|
||||
let subjectname = `timetable.subject.colour.${lessonArray[i].code}`;
|
||||
|
||||
let subject = subjects.find(
|
||||
let subjectname =
|
||||
lessonArray[i].type == "tutorial"
|
||||
? `timetable.tutor.${lessonArray[i].tutorID}`
|
||||
: `timetable.subject.colour.${lessonArray[i].code}`;
|
||||
let subject = colours.find(
|
||||
(element: any) => element.name === subjectname,
|
||||
);
|
||||
|
||||
if (!subject) {
|
||||
lessonArray[i].colour = "--item-colour: #8e8e8e;";
|
||||
} else {
|
||||
lessonArray[i].colour = `--item-colour: ${subject.value};`;
|
||||
let result = GetThresholdOfColor(subject.value);
|
||||
|
||||
if (result > 300) {
|
||||
if (GetThresholdOfColor(subject.value) > 300) {
|
||||
lessonArray[i].invert = true;
|
||||
}
|
||||
}
|
||||
@@ -770,9 +662,7 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
lessonArray[i].until = lessonArray[i].until.substring(0, 5);
|
||||
|
||||
if (settingsState.timeFormat === "12") {
|
||||
lessonArray[i].from = convertTo12HourFormat(
|
||||
lessonArray[i].from,
|
||||
);
|
||||
lessonArray[i].from = convertTo12HourFormat(lessonArray[i].from);
|
||||
lessonArray[i].until = convertTo12HourFormat(
|
||||
lessonArray[i].until,
|
||||
);
|
||||
@@ -785,13 +675,10 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
|
||||
DayContainer.innerText = "";
|
||||
for (let i = 0; i < lessonArray.length; i++) {
|
||||
var div = makeLessonDiv(lessonArray[i], i + 1);
|
||||
|
||||
const div = makeLessonDiv(lessonArray[i], i + 1);
|
||||
if (lessonArray[i].invert) {
|
||||
const div1 = div.firstChild! as HTMLElement;
|
||||
div1.classList.add("day-inverted");
|
||||
(div.firstChild! as HTMLElement).classList.add("day-inverted");
|
||||
}
|
||||
|
||||
DayContainer.append(div.firstChild as HTMLElement);
|
||||
}
|
||||
|
||||
@@ -802,40 +689,22 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
for (let i = 0; i < lessonArray.length; i++) {
|
||||
CheckCurrentLesson(lessonArray[i], i + 1);
|
||||
}
|
||||
|
||||
CheckCurrentLessonAll(lessonArray);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
DayContainer.innerHTML = "";
|
||||
var dummyDay = document.createElement("div");
|
||||
const dummyDay = document.createElement("div");
|
||||
dummyDay.classList.add("day-empty");
|
||||
let img = document.createElement("img");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
let text = document.createElement("p");
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No lessons available.";
|
||||
dummyDay.append(img);
|
||||
dummyDay.append(text);
|
||||
dummyDay.append(img, text);
|
||||
DayContainer.append(dummyDay);
|
||||
|
||||
DayContainer.classList.remove("loading");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading timetable data:", error);
|
||||
|
||||
DayContainer.classList.remove("loading");
|
||||
|
||||
DayContainer.innerHTML = "";
|
||||
const errorDiv = document.createElement("div");
|
||||
errorDiv.classList.add("day-empty");
|
||||
errorDiv.innerHTML = `
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" />
|
||||
<p>Error loading lessons. Please try again.</p>
|
||||
`;
|
||||
DayContainer.append(errorDiv);
|
||||
}
|
||||
}
|
||||
};
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
@@ -924,8 +793,6 @@ async function CheckCurrentLesson(lesson: any, num: number) {
|
||||
}
|
||||
|
||||
function makeLessonDiv(lesson: any, num: number) {
|
||||
if (!lesson) throw new Error("No lesson provided.");
|
||||
|
||||
const {
|
||||
code,
|
||||
colour,
|
||||
@@ -938,17 +805,19 @@ function makeLessonDiv(lesson: any, num: number) {
|
||||
programmeID,
|
||||
metaID,
|
||||
assessments,
|
||||
type,
|
||||
} = lesson;
|
||||
|
||||
let lessonString = `
|
||||
<div class="day" id="${code + num}" style="${colour}">
|
||||
<h2>${description || "Unknown"}</h2>
|
||||
<h2>${type == "class" ? description : type == "tutorial" ? "Tutorial" : "Unknown"}</h2>
|
||||
<h3>${staff || "Unknown"}</h3>
|
||||
<h3>${room || "Unknown"}</h3>
|
||||
<h3>${room || (type == "tutorial" ? "Unknown" : "N/A")}</h3>
|
||||
<h4>${from || "Unknown"} - ${until || "Unknown"}</h4>
|
||||
<h5>${attendanceTitle || "Unknown"}</h5>
|
||||
`;
|
||||
|
||||
if (type == "class") {
|
||||
if (programmeID !== 0) {
|
||||
lessonString += `
|
||||
<div class="day-button clickable" style="right: 5px;" onclick="location.href='${buildAssessmentURL(programmeID, metaID)}'">${assessmentsicon}</div>
|
||||
@@ -965,7 +834,7 @@ function makeLessonDiv(lesson: any, num: number) {
|
||||
.join("");
|
||||
|
||||
lessonString += `
|
||||
<div class="tooltip assessmenttooltip">
|
||||
<div class="fixed-tooltip assessmenttooltip">
|
||||
<svg style="width:28px;height:28px;border-radius:0;" viewBox="0 0 24 24">
|
||||
<path fill="#ed3939" d="M16 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H16C17.11 22 18 21.11 18 20V4C18 2.9 17.11 2 16 2M16 20H4V4H6V12L8.5 9.75L11 12V4H16V20M20 15H22V17H20V15M22 7V13H20V7H22Z" />
|
||||
</svg>
|
||||
@@ -973,10 +842,12 @@ function makeLessonDiv(lesson: any, num: number) {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
lessonString += "</div>";
|
||||
|
||||
return stringToHTML(lessonString);
|
||||
const element = stringToHTML(lessonString);
|
||||
setupFixedTooltips(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") {
|
||||
@@ -987,64 +858,48 @@ function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") {
|
||||
}
|
||||
|
||||
function CheckUnmarkedAttendance(lessonattendance: any) {
|
||||
if (lessonattendance) {
|
||||
var lesson = lessonattendance.label;
|
||||
} else {
|
||||
lesson = " ";
|
||||
}
|
||||
return lesson;
|
||||
return lessonattendance ? lessonattendance.label : " ";
|
||||
}
|
||||
|
||||
async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
||||
let upcomingitemcontainer = document.querySelector("#upcoming-items");
|
||||
let overdueDates = [];
|
||||
let upcomingDates = {};
|
||||
|
||||
var Today = new Date();
|
||||
const upcomingitemcontainer = document.querySelector("#upcoming-items");
|
||||
const overdueDates = [];
|
||||
const upcomingDates = {};
|
||||
const Today = new Date();
|
||||
|
||||
for (let i = 0; i < assessments.length; i++) {
|
||||
const assessment = assessments[i];
|
||||
let assessmentdue = new Date(assessment.due);
|
||||
|
||||
CheckSpecialDay(Today, assessmentdue);
|
||||
if (assessmentdue < Today) {
|
||||
if (!CheckSpecialDay(Today, assessmentdue)) {
|
||||
overdueDates.push(assessment);
|
||||
const assessmentdue = new Date(assessments[i].due);
|
||||
if (assessmentdue < Today && !CheckSpecialDay(Today, assessmentdue)) {
|
||||
overdueDates.push(assessments[i]);
|
||||
assessments.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var TomorrowDate = new Date();
|
||||
TomorrowDate.setDate(TomorrowDate.getDate() + 1);
|
||||
|
||||
const colours = await GetLessonColours();
|
||||
|
||||
let subjects = colours;
|
||||
for (let i = 0; i < assessments.length; i++) {
|
||||
let subjectname = `timetable.subject.colour.${assessments[i].code}`;
|
||||
|
||||
let subject = subjects.find((element: any) => element.name === subjectname);
|
||||
|
||||
const subject = colours.find(
|
||||
(element: any) =>
|
||||
element.name === `timetable.subject.colour.${assessments[i].code}`,
|
||||
);
|
||||
if (!subject) {
|
||||
assessments[i].colour = "--item-colour: #8e8e8e;";
|
||||
} else {
|
||||
assessments[i].colour = `--item-colour: ${subject.value};`;
|
||||
GetThresholdOfColor(subject.value);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < activeSubjects.length; i++) {
|
||||
const element = activeSubjects[i];
|
||||
let subjectname = `timetable.subject.colour.${element.code}`;
|
||||
let colour = colours.find((element: any) => element.name === subjectname);
|
||||
const colour = colours.find(
|
||||
(c: any) => c.name === `timetable.subject.colour.${element.code}`,
|
||||
);
|
||||
if (!colour) {
|
||||
element.colour = "--item-colour: #8e8e8e;";
|
||||
} else {
|
||||
element.colour = `--item-colour: ${colour.value};`;
|
||||
let result = GetThresholdOfColor(colour.value);
|
||||
if (result > 300) {
|
||||
if (GetThresholdOfColor(colour.value) > 300) {
|
||||
element.invert = true;
|
||||
}
|
||||
}
|
||||
@@ -1052,52 +907,34 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
||||
|
||||
CreateFilters(activeSubjects);
|
||||
|
||||
let type;
|
||||
let class_;
|
||||
|
||||
for (let i = 0; i < assessments.length; i++) {
|
||||
const element: any = assessments[i];
|
||||
if (!upcomingDates[element.due as keyof typeof upcomingDates]) {
|
||||
let dateObj: any = new Object();
|
||||
dateObj.div = CreateElement(
|
||||
(type = "div"),
|
||||
(class_ = "upcoming-date-container"),
|
||||
);
|
||||
dateObj.assessments = [];
|
||||
const dateObj: any = {
|
||||
div: CreateElement("div", "upcoming-date-container"),
|
||||
assessments: [],
|
||||
};
|
||||
(upcomingDates[element.due as keyof typeof upcomingDates] as any) =
|
||||
dateObj;
|
||||
}
|
||||
let assessmentDateDiv =
|
||||
const assessmentDateDiv =
|
||||
upcomingDates[element.due as keyof typeof upcomingDates];
|
||||
|
||||
if (assessmentDateDiv) {
|
||||
(assessmentDateDiv as any).assessments.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
for (var date in upcomingDates) {
|
||||
let assessmentdue = new Date(
|
||||
(
|
||||
upcomingDates[date as keyof typeof upcomingDates] as any
|
||||
).assessments[0].due,
|
||||
const assessmentdue = new Date(
|
||||
(upcomingDates[date as keyof typeof upcomingDates] as any).assessments[0]
|
||||
.due,
|
||||
);
|
||||
let specialcase = CheckSpecialDay(Today, assessmentdue);
|
||||
let assessmentDate;
|
||||
|
||||
if (specialcase) {
|
||||
let datecase: string = specialcase!;
|
||||
assessmentDate = createAssessmentDateDiv(
|
||||
const specialcase = CheckSpecialDay(Today, assessmentdue);
|
||||
const assessmentDate = createAssessmentDateDiv(
|
||||
date,
|
||||
upcomingDates[date as keyof typeof upcomingDates],
|
||||
|
||||
datecase,
|
||||
specialcase,
|
||||
);
|
||||
} else {
|
||||
assessmentDate = createAssessmentDateDiv(
|
||||
date,
|
||||
upcomingDates[date as keyof typeof upcomingDates],
|
||||
);
|
||||
}
|
||||
|
||||
if (specialcase === "Yesterday") {
|
||||
upcomingitemcontainer!.insertBefore(
|
||||
@@ -1109,80 +946,79 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
||||
}
|
||||
}
|
||||
FilterUpcomingAssessments(settingsState.subjectfilters);
|
||||
|
||||
if (assessments.length === 0) {
|
||||
upcomingitemcontainer!.innerHTML = `
|
||||
<div class="day-empty">
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" />
|
||||
<p>No assessments available.</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
|
||||
var options = {
|
||||
const options = {
|
||||
weekday: "long" as "long",
|
||||
month: "long" as "long",
|
||||
day: "numeric" as "numeric",
|
||||
};
|
||||
const FormattedDate = new Date(date);
|
||||
|
||||
const assessments = value.assessments;
|
||||
const container = value.div;
|
||||
|
||||
let DateTitleDiv = document.createElement("div");
|
||||
const DateTitleDiv = document.createElement("div");
|
||||
DateTitleDiv.classList.add("upcoming-date-title");
|
||||
|
||||
if (datecase) {
|
||||
let datetitle = document.createElement("h5");
|
||||
const datetitle = document.createElement("h5");
|
||||
datetitle.classList.add("upcoming-special-day");
|
||||
datetitle.innerText = datecase;
|
||||
DateTitleDiv.append(datetitle);
|
||||
container.setAttribute("data-day", datecase);
|
||||
}
|
||||
|
||||
let DateTitle = document.createElement("h5");
|
||||
const DateTitle = document.createElement("h5");
|
||||
DateTitle.innerText = FormattedDate.toLocaleDateString("en-AU", options);
|
||||
DateTitleDiv.append(DateTitle);
|
||||
|
||||
container.append(DateTitleDiv);
|
||||
|
||||
let assessmentContainer = document.createElement("div");
|
||||
const assessmentContainer = document.createElement("div");
|
||||
assessmentContainer.classList.add("upcoming-date-assessments");
|
||||
|
||||
for (let i = 0; i < assessments.length; i++) {
|
||||
const element = assessments[i];
|
||||
let item = document.createElement("div");
|
||||
const item = document.createElement("div");
|
||||
item.classList.add("upcoming-assessment");
|
||||
item.setAttribute("data-subject", element.code);
|
||||
item.id = `assessment${element.id}`;
|
||||
|
||||
item.style.cssText = element.colour;
|
||||
|
||||
let titlediv = document.createElement("div");
|
||||
const titlediv = document.createElement("div");
|
||||
titlediv.classList.add("upcoming-subject-title");
|
||||
|
||||
let titlesvg =
|
||||
titlediv.append(
|
||||
stringToHTML(`<svg viewBox="0 0 24 24" style="width:35px;height:35px;fill:white;">
|
||||
<path d="M6 20H13V22H6C4.89 22 4 21.11 4 20V4C4 2.9 4.89 2 6 2H18C19.11 2 20 2.9 20 4V12.54L18.5 11.72L18 12V4H13V12L10.5 9.75L8 12V4H6V20M24 17L18.5 14L13 17L18.5 20L24 17M15 19.09V21.09L18.5 23L22 21.09V19.09L18.5 21L15 19.09Z"></path>
|
||||
</svg>`).firstChild;
|
||||
titlediv.append(titlesvg!);
|
||||
</svg>`).firstChild!,
|
||||
);
|
||||
|
||||
let detailsdiv = document.createElement("div");
|
||||
const detailsdiv = document.createElement("div");
|
||||
detailsdiv.classList.add("upcoming-details");
|
||||
let detailstitle = document.createElement("h5");
|
||||
const detailstitle = document.createElement("h5");
|
||||
detailstitle.innerText = `${element.subject} assessment`;
|
||||
let subject = document.createElement("p");
|
||||
const subject = document.createElement("p");
|
||||
subject.innerText = element.title;
|
||||
subject.classList.add("upcoming-assessment-title");
|
||||
subject.onclick = function () {
|
||||
document.querySelector("#menu ul")!.classList.add("noscroll");
|
||||
location.href = `../#?page=/assessments/${element.programmeID}:${element.metaclassID}&item=${element.id}`;
|
||||
};
|
||||
detailsdiv.append(detailstitle);
|
||||
detailsdiv.append(subject);
|
||||
|
||||
item.append(titlediv);
|
||||
item.append(detailsdiv);
|
||||
detailsdiv.append(detailstitle, subject);
|
||||
item.append(titlediv, detailsdiv);
|
||||
assessmentContainer.append(item);
|
||||
|
||||
fetch(`${location.origin}/seqta/student/assessment/submissions/get`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({
|
||||
assessment: element.id,
|
||||
metaclass: element.metaclassID,
|
||||
@@ -1193,8 +1029,7 @@ function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
|
||||
.then((response) => {
|
||||
if (response.payload.length > 0) {
|
||||
const assessment = document.querySelector(`#assessment${element.id}`);
|
||||
|
||||
let submittedtext = document.createElement("div");
|
||||
const submittedtext = document.createElement("div");
|
||||
submittedtext.classList.add("upcoming-submittedtext");
|
||||
submittedtext.innerText = "Submitted";
|
||||
assessment!.append(submittedtext);
|
||||
@@ -1232,36 +1067,37 @@ function CheckSpecialDay(date1: Date, date2: Date) {
|
||||
}
|
||||
|
||||
async function GetLessonColours() {
|
||||
let func = fetch(`${location.origin}/seqta/student/load/prefs?`, {
|
||||
try {
|
||||
return fetch(`${location.origin}/seqta/student/load/prefs?`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
body: JSON.stringify({ request: "userPrefs", asArray: true, user: 69 }),
|
||||
});
|
||||
return func
|
||||
})
|
||||
.then((result) => result.json())
|
||||
.then((response) => response.payload);
|
||||
} catch (error) {
|
||||
console.error("[BetterSEQTA+] Failed to get lesson colours:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function CreateFilters(subjects: any) {
|
||||
let filteroptions = settingsState.subjectfilters;
|
||||
const filteroptions = settingsState.subjectfilters;
|
||||
const filterdiv = document.querySelector("#upcoming-filters");
|
||||
|
||||
let filterdiv = document.querySelector("#upcoming-filters");
|
||||
for (let i = 0; i < subjects.length; i++) {
|
||||
const element = subjects[i];
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) {
|
||||
filteroptions[element.code] = true;
|
||||
settingsState.subjectfilters = filteroptions;
|
||||
}
|
||||
let elementdiv = CreateSubjectFilter(
|
||||
filterdiv!.append(
|
||||
CreateSubjectFilter(
|
||||
element.code,
|
||||
element.colour,
|
||||
filteroptions[element.code],
|
||||
),
|
||||
);
|
||||
|
||||
filterdiv!.append(elementdiv);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1270,23 +1106,20 @@ function CreateSubjectFilter(
|
||||
itemcolour: string,
|
||||
checked: any,
|
||||
) {
|
||||
let label = CreateElement("label", "upcoming-checkbox-container");
|
||||
const label = CreateElement("label", "upcoming-checkbox-container");
|
||||
label.innerText = subjectcode;
|
||||
let input1 = CreateElement("input");
|
||||
const input = input1 as HTMLInputElement;
|
||||
const input = CreateElement("input") as HTMLInputElement;
|
||||
input.type = "checkbox";
|
||||
input.checked = checked;
|
||||
input.id = `filter-${subjectcode}`;
|
||||
label.style.cssText = itemcolour;
|
||||
let span = CreateElement("span", "upcoming-checkmark");
|
||||
label.append(input);
|
||||
label.append(span);
|
||||
const span = CreateElement("span", "upcoming-checkmark");
|
||||
label.append(input, span);
|
||||
|
||||
input.addEventListener("change", function (change) {
|
||||
let filters = settingsState.subjectfilters;
|
||||
let id = (change.target as HTMLInputElement)!.id.split("-")[1];
|
||||
filters[id] = (change.target as HTMLInputElement)!.checked;
|
||||
|
||||
const filters = settingsState.subjectfilters;
|
||||
const id = (change.target as HTMLInputElement).id.split("-")[1];
|
||||
filters[id] = (change.target as HTMLInputElement).checked;
|
||||
settingsState.subjectfilters = filters;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import browser from "webextension-polyfill";
|
||||
import { settingsState } from "../listeners/SettingsState";
|
||||
import { animate, stagger } from "motion";
|
||||
import { DeleteWhatsNew } from "../Whatsnew";
|
||||
import { openPopup } from "./PopupManager";
|
||||
|
||||
export function OpenAboutPage() {
|
||||
const background = document.createElement("div");
|
||||
background.id = "whatsnewbk";
|
||||
background.classList.add("whatsnewBackground");
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("whatsnewContainer");
|
||||
|
||||
var header: any = stringToHTML(
|
||||
const header = stringToHTML(
|
||||
/* html */
|
||||
`<div class="whatsnewHeader">
|
||||
<h1>About</h1>
|
||||
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
|
||||
<p>About the extension</p>
|
||||
</div>`,
|
||||
).firstChild;
|
||||
).firstChild as HTMLElement;
|
||||
|
||||
let text = stringToHTML(/* html */ `
|
||||
const text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer" style="overflow-y: hidden;">
|
||||
<img src="${settingsState.DarkMode ? "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/dark.jpg" : "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/light.jpg"}" class="aboutImg" />
|
||||
<p>BetterSEQTA+ is a fork of BetterSEQTA (originally developed by Nulkem), which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features. </p>
|
||||
@@ -34,83 +25,44 @@ export function OpenAboutPage() {
|
||||
</h1>
|
||||
<div style="max-width: 600px; margin: auto;">
|
||||
<img
|
||||
src="https://contrib.rocks/image?repo=BetterSEQTA/BetterSEQTA-Plus&columns=13"
|
||||
style="width: 100%; max-width: 500px; height: auto; object-fit: contain; display: block; margin: -110px auto 0;">
|
||||
src="https://contrib.rocks/image?repo=BetterSEQTA/BetterSEQTA-Plus&columns=10"
|
||||
style="width: 100%; max-width: 500px; height: auto; object-fit: contain; display: block; margin: -80px auto 0;">
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
let footer = stringToHTML(/* html */ `
|
||||
const footer = stringToHTML(/* html */ `
|
||||
<div class="whatsnewFooter">
|
||||
<div>
|
||||
Report bugs and feedback:
|
||||
Resources and Feedback:
|
||||
<a class="socials" href="https://betterseqta.org" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 658 656" version="1.1">
|
||||
<path d="M 296 6.079 C 222.099 14.147, 156.177 44.962, 103.631 96 C 75.901 122.933, 55.863 150.195, 39.039 183.877 C 6.713 248.596, -2.990 322.811, 11.567 394 C 24.458 457.036, 54.499 512.622, 100.472 558.501 C 152.711 610.633, 218.648 642.109, 294.500 651.123 C 308.578 652.796, 349.167 652.807, 363.500 651.143 C 457.686 640.203, 538.776 592.815, 592.980 517.037 C 642.593 447.677, 662.695 361.034, 648.904 276 C 633.968 183.904, 580.183 103.524, 499.640 52.932 C 470.832 34.836, 435.045 20.244, 400.531 12.522 C 375.717 6.970, 364.646 5.804, 333.500 5.466 C 317.550 5.293, 300.675 5.568, 296 6.079 M 300.500 148.106 C 261.812 152.166, 225.171 169.425, 197.296 196.717 C 171.447 222.025, 154.115 255.340, 147.986 291.500 C 146.044 302.958, 145.844 306.932, 146.301 325 C 147.060 355.042, 151.117 371.665, 163.998 397.500 C 187.801 445.243, 230.082 477.905, 283.388 489.727 L 295.500 492.414 411.250 492.742 L 527 493.071 527 469.536 L 527 446 482.433 446 L 437.866 446 445.596 437.554 C 457.097 424.987, 465.208 413.133, 473.002 397.500 C 485.883 371.665, 489.940 355.042, 490.699 325 C 491.154 307.015, 490.951 302.933, 489.050 291.729 C 473.693 201.254, 391.395 138.565, 300.500 148.106 M 304.500 195.620 C 270.564 200.792, 243.575 215.251, 223.612 238.956 C 203.303 263.071, 193.650 289.377, 193.690 320.500 C 193.770 381.750, 237.341 433.004, 298.364 443.631 C 311.912 445.990, 335.206 445.075, 348.221 441.672 C 361.455 438.211, 373.637 433.094, 383.671 426.781 C 413.787 407.833, 433.890 379.189, 441.066 345 C 443.682 332.536, 444.161 311.707, 442.101 300 C 434.241 255.323, 402.917 217.681, 361 202.541 C 347.818 197.780, 337.607 195.947, 322 195.540 C 314.025 195.333, 306.150 195.369, 304.500 195.620" fill="currentColor" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 256 250" preserveAspectRatio="xMidYMid" style="vertical-align: middle;">
|
||||
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://www.youtube.com/@BetterSEQTAPlus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"/></svg>
|
||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
let exitbutton = document.createElement("div");
|
||||
exitbutton.id = "whatsnewclosebutton";
|
||||
|
||||
container.append(header);
|
||||
container.append(text as ChildNode);
|
||||
container.append(footer as ChildNode);
|
||||
container.append(exitbutton);
|
||||
|
||||
background.append(container);
|
||||
|
||||
document.getElementById("container")!.append(background);
|
||||
|
||||
let bkelement = document.getElementById("whatsnewbk");
|
||||
let popup = document.getElementsByClassName("whatsnewContainer")[0];
|
||||
|
||||
if (settingsState.animations) {
|
||||
animate(
|
||||
[popup, bkelement as HTMLElement],
|
||||
{ scale: [0, 1] },
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 220,
|
||||
damping: 18,
|
||||
},
|
||||
);
|
||||
|
||||
animate(
|
||||
".whatsnewTextContainer *",
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.05, { startDelay: 0.1 }),
|
||||
duration: 0.5,
|
||||
ease: [0.22, 0.03, 0.26, 1],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
delete settingsState.justupdated;
|
||||
|
||||
bkelement!.addEventListener("click", function (event) {
|
||||
// Check if the click event originated from the element itself and not any of its children
|
||||
if (event.target === bkelement) {
|
||||
DeleteWhatsNew();
|
||||
}
|
||||
});
|
||||
|
||||
var closeelement = document.getElementById("whatsnewclosebutton");
|
||||
closeelement!.addEventListener("click", function () {
|
||||
DeleteWhatsNew();
|
||||
openPopup({
|
||||
header,
|
||||
content: [text, footer],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import { openPopup } from "./PopupManager";
|
||||
|
||||
export function OpenMinecraftServerPopup() {
|
||||
if (!document.querySelector('link[href*="minecraftia"]')) {
|
||||
const fontLink = document.createElement("link");
|
||||
fontLink.href = "https://fonts.cdnfonts.com/css/minecraftia";
|
||||
fontLink.rel = "stylesheet";
|
||||
document.head.appendChild(fontLink);
|
||||
}
|
||||
|
||||
const header = stringToHTML(
|
||||
/* html */
|
||||
`<div class="whatsnewHeader">
|
||||
<h1>Minecraft Server</h1>
|
||||
<p>The official BetterSEQTA+ Minecraft Server</p>
|
||||
</div>`,
|
||||
).firstChild as HTMLElement;
|
||||
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.classList.add("whatsnewImgContainer");
|
||||
|
||||
const video = document.createElement("video");
|
||||
video.style.aspectRatio = "16/9";
|
||||
video.style.background = "black";
|
||||
|
||||
const source = document.createElement("source");
|
||||
source.setAttribute(
|
||||
"src",
|
||||
"https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/server-video.mp4",
|
||||
);
|
||||
|
||||
video.autoplay = true;
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.appendChild(source);
|
||||
video.classList.add("whatsnewImg");
|
||||
imageContainer.appendChild(video);
|
||||
|
||||
const text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer" style="height: 50%; overflow-y: hidden;">
|
||||
<h1>Join our community in Minecraft!</h1>
|
||||
<p style="margin-left: 0;">Join the official BetterSEQTA+ Minecraft Server community now!</p>
|
||||
|
||||
<h1>Server Features</h1>
|
||||
<ul>
|
||||
<li>SMP as our first release gamemode</li>
|
||||
<li>Community events and competitions</li>
|
||||
<li>Custom world generation</li>
|
||||
<li>Shop system with buying and selling</li>
|
||||
<li>Regular updates and maintenance</li>
|
||||
<li>The End dimension will be enabled during an upcoming live event</li>
|
||||
</ul>
|
||||
|
||||
<p style="
|
||||
font-family: 'Minecraftia', sans-serif;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 34px;
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.1em;
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;">
|
||||
mc.betterseqta.org
|
||||
</p>
|
||||
<p style="
|
||||
font-family: 'Minecraftia', sans-serif;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 0;
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000;">
|
||||
Version: 1.21.4
|
||||
</p>
|
||||
</div>
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
const footer = stringToHTML(/* html */ `
|
||||
<div class="whatsnewFooter">
|
||||
<div>
|
||||
Resources and Feedback:
|
||||
<a class="socials" href="https://betterseqta.org" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 658 656" version="1.1">
|
||||
<path d="M 296 6.079 C 222.099 14.147, 156.177 44.962, 103.631 96 C 75.901 122.933, 55.863 150.195, 39.039 183.877 C 6.713 248.596, -2.990 322.811, 11.567 394 C 24.458 457.036, 54.499 512.622, 100.472 558.501 C 152.711 610.633, 218.648 642.109, 294.500 651.123 C 308.578 652.796, 349.167 652.807, 363.500 651.143 C 457.686 640.203, 538.776 592.815, 592.980 517.037 C 642.593 447.677, 662.695 361.034, 648.904 276 C 633.968 183.904, 580.183 103.524, 499.640 52.932 C 470.832 34.836, 435.045 20.244, 400.531 12.522 C 375.717 6.970, 364.646 5.804, 333.500 5.466 C 317.550 5.293, 300.675 5.568, 296 6.079 M 300.500 148.106 C 261.812 152.166, 225.171 169.425, 197.296 196.717 C 171.447 222.025, 154.115 255.340, 147.986 291.500 C 146.044 302.958, 145.844 306.932, 146.301 325 C 147.060 355.042, 151.117 371.665, 163.998 397.500 C 187.801 445.243, 230.082 477.905, 283.388 489.727 L 295.500 492.414 411.250 492.742 L 527 493.071 527 469.536 L 527 446 482.433 446 L 437.866 446 445.596 437.554 C 457.097 424.987, 465.208 413.133, 473.002 397.500 C 485.883 371.665, 489.940 355.042, 490.699 325 C 491.154 307.015, 490.951 302.933, 489.050 291.729 C 473.693 201.254, 391.395 138.565, 300.500 148.106 M 304.500 195.620 C 270.564 200.792, 243.575 215.251, 223.612 238.956 C 203.303 263.071, 193.650 289.377, 193.690 320.500 C 193.770 381.750, 237.341 433.004, 298.364 443.631 C 311.912 445.990, 335.206 445.075, 348.221 441.672 C 361.455 438.211, 373.637 433.094, 383.671 426.781 C 413.787 407.833, 433.890 379.189, 441.066 345 C 443.682 332.536, 444.161 311.707, 442.101 300 C 434.241 255.323, 402.917 217.681, 361 202.541 C 347.818 197.780, 337.607 195.947, 322 195.540 C 314.025 195.333, 306.150 195.369, 304.500 195.620" fill="currentColor" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 256 250" preserveAspectRatio="xMidYMid" style="vertical-align: middle;">
|
||||
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://www.youtube.com/@BetterSEQTAPlus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"/></svg>
|
||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
openPopup({
|
||||
header,
|
||||
content: [imageContainer, text, footer],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import { settingsState } from "../listeners/SettingsState";
|
||||
import { openPopup } from "./PopupManager";
|
||||
|
||||
export function showPrivacyNotification() {
|
||||
const lastUpdated = "2025-12-19";
|
||||
|
||||
if (document.getElementById("whatsnewbk")) return;
|
||||
if (settingsState.privacyStatementShown) return;
|
||||
if (settingsState.privacyStatementLastUpdated && new Date(settingsState.privacyStatementLastUpdated) > new Date(lastUpdated)) return;
|
||||
|
||||
const header = stringToHTML(
|
||||
/* html */
|
||||
`<div class="whatsnewHeader">
|
||||
<h1>Privacy Statement</h1>
|
||||
<p>Important Information</p>
|
||||
</div>`,
|
||||
).firstChild as HTMLElement;
|
||||
|
||||
const text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer privacyStatement" style="overflow-y: auto; font-size: 1.2rem; line-height: 1.6;">
|
||||
<img style="aspect-ratio: 16/5.8;" src="${settingsState.DarkMode ? "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/dark.jpg" : "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/branding/light.jpg"}" class="aboutImg" />
|
||||
<p>
|
||||
<strong>Addressing Recent Concerns About BetterSEQTA+</strong><br>
|
||||
We appreciate the feedback we've received from several schools regarding BetterSEQTA+. Transparency and trust are core to our mission, and we want to address these concerns directly.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Our Commitment to Privacy:</strong><br>
|
||||
<span style="display: block; margin-left: 1em;">
|
||||
• We do not collect, store, or share any personal information<br>
|
||||
• All data processing happens locally on your device<br>
|
||||
• Our code is open source and available for review
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>What We're Doing:</strong><br>
|
||||
We're willing to actively work with school administrators to ensure BetterSEQTA+ meets both student needs and institutional requirements. If your school has specific concerns, we encourage them to contact us at <a href="mailto:betterseqta.plus@gmail.com" style="color: inherit; text-decoration: underline;">betterseqta.plus@gmail.com</a> or via github at <a href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">github.com/BetterSEQTA/BetterSEQTA-Plus</a>.
|
||||
</p>
|
||||
<p>
|
||||
For complete details about our privacy practices, visit our <a href="https://betterseqta.org/privacy" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: underline;">privacy policy</a> or click the shield icon in settings.
|
||||
</p>
|
||||
</div>
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
settingsState.privacyStatementLastUpdated = "2025-12-20";
|
||||
settingsState.privacyStatementShown = true;
|
||||
|
||||
openPopup({
|
||||
header,
|
||||
content: [text],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import { openPopup } from "./PopupManager";
|
||||
|
||||
export function OpenPrivacyStatement() {
|
||||
const header = stringToHTML(
|
||||
/* html */
|
||||
`<div class="whatsnewHeader">
|
||||
<h1>Privacy Statement</h1>
|
||||
<p>Our commitment to your privacy</p>
|
||||
</div>`,
|
||||
).firstChild as HTMLElement;
|
||||
|
||||
const text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer" style="overflow-y: auto; max-height: 60vh;">
|
||||
<h2 style="margin-top: 0;">Privacy Policy</h2>
|
||||
<p>At BetterSEQTA+, we take your privacy seriously. We want to be completely transparent about how we handle your data.</p>
|
||||
|
||||
<h3>Data Collection</h3>
|
||||
<p><strong>We never collect any information from you.</strong> BetterSEQTA+ is designed to work entirely on your device. All processing happens locally in your browser, and we do not send any data to external servers.</p>
|
||||
|
||||
<h3>What We Don't Do</h3>
|
||||
<ul style="text-align: left; margin: 10px 0;">
|
||||
<li>We do not track your browsing activity</li>
|
||||
<li>We do not collect personal information</li>
|
||||
<li>We do not store your SEQTA credentials</li>
|
||||
<li>We do not send data to third-party services</li>
|
||||
<li>We do not use analytics or tracking cookies</li>
|
||||
</ul>
|
||||
|
||||
<h3>Local Storage</h3>
|
||||
<p>BetterSEQTA+ uses your browser's local storage to save your preferences and settings. This data remains on your device and is never transmitted anywhere. You can clear this data at any time through your browser's settings.</p>
|
||||
|
||||
<h3>Open Source</h3>
|
||||
<p>BetterSEQTA+ is an open-source project. You can review our code on <a href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" target="_blank" style="color: inherit; text-decoration: underline;">GitHub</a> to verify our privacy practices. We believe in transparency and encourage you to inspect the code yourself.</p>
|
||||
|
||||
<h3>Our Commitment</h3>
|
||||
<p>We are committed to providing the best features possible while respecting your privacy. We understand that schools and students have concerns about data privacy, and we want to assure you that BetterSEQTA+ is designed with privacy as a core principle.</p>
|
||||
|
||||
<p style="margin-top: 20px; font-weight: bold;">If you have any questions or concerns about our privacy practices, please reach out to us through our <a href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" target="_blank" style="color: inherit; text-decoration: underline;">GitHub repository</a>.</p>
|
||||
</div>
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
openPopup({
|
||||
header,
|
||||
content: [text],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,48 +1,22 @@
|
||||
import { settingsState } from "./listeners/SettingsState";
|
||||
import { animate, stagger } from "motion";
|
||||
import stringToHTML from "./stringToHTML";
|
||||
import stringToHTML from "../stringToHTML";
|
||||
import browser from "webextension-polyfill";
|
||||
import kofi from "@/resources/kofi.png?base64";
|
||||
|
||||
export async function DeleteWhatsNew() {
|
||||
const bkelement = document.getElementById("whatsnewbk");
|
||||
const popup = document.getElementsByClassName("whatsnewContainer")[0];
|
||||
|
||||
if (!settingsState.animations) {
|
||||
bkelement?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
animate(
|
||||
[popup, bkelement!],
|
||||
{ opacity: [1, 0], scale: [1, 0] },
|
||||
{ ease: [0.22, 0.03, 0.26, 1] },
|
||||
).then(() => {
|
||||
bkelement?.remove();
|
||||
});
|
||||
}
|
||||
import { openPopup } from "./PopupManager";
|
||||
|
||||
export function OpenWhatsNewPopup() {
|
||||
const background = document.createElement("div");
|
||||
background.id = "whatsnewbk";
|
||||
background.classList.add("whatsnewBackground");
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("whatsnewContainer");
|
||||
|
||||
var header: any = stringToHTML(
|
||||
const header = stringToHTML(
|
||||
/* html */
|
||||
`<div class="whatsnewHeader">
|
||||
<h1>What's New</h1>
|
||||
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
|
||||
</div>`,
|
||||
).firstChild;
|
||||
).firstChild as HTMLElement;
|
||||
|
||||
let imagecont = document.createElement("div");
|
||||
imagecont.classList.add("whatsnewImgContainer");
|
||||
const imageContainer = document.createElement("div");
|
||||
imageContainer.classList.add("whatsnewImgContainer");
|
||||
|
||||
/* let video = document.createElement("video");
|
||||
let source = document.createElement("source");
|
||||
const video = document.createElement("video");
|
||||
const source = document.createElement("source");
|
||||
|
||||
source.setAttribute(
|
||||
"src",
|
||||
@@ -53,19 +27,76 @@ export function OpenWhatsNewPopup() {
|
||||
video.loop = true;
|
||||
video.appendChild(source);
|
||||
video.classList.add("whatsnewImg");
|
||||
imagecont.appendChild(video); */
|
||||
imageContainer.appendChild(video);
|
||||
|
||||
let whatsnewimg = document.createElement("img");
|
||||
//whatsnewimg.src = "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/update-image.webp";
|
||||
whatsnewimg.src = browser.runtime.getURL('../../resources/update-image.webp');
|
||||
whatsnewimg.classList.add("whatsnewImg");
|
||||
imagecont.appendChild(whatsnewimg);
|
||||
const text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
|
||||
|
||||
let textcontainer = document.createElement("div");
|
||||
textcontainer.classList.add("whatsnewTextContainer");
|
||||
<h1>3.4.15 - SEQTA New UI Patch</h1>
|
||||
<li>Fixed compatibility issues caused by the new SEQTA UI update</li>
|
||||
<li>Adjusted styling to match updated SEQTA layout changes</li>
|
||||
<li>Other minor bug fixes and stability improvements</li>
|
||||
|
||||
<h1>3.4.14 - Search & Assessments Update</h1>
|
||||
<li>Global Search improvements: indexing progress, more accurate results, and now includes past assessments/assignments</li>
|
||||
<li>Assessment Averages now parse weightings when possible for more accurate subject averages</li>
|
||||
<li>Added weight labels to assessment items (including proper handling of 0% and missing weights)</li>
|
||||
<li>Fixed homepage tutor lesson colours and assessments/courses visibility issues</li>
|
||||
<li>Fixed upcoming lessons tutorial room not displaying</li>
|
||||
<li>Fixed favicon not showing / race condition issues</li>
|
||||
<li>Other minor styling and stability improvements</li>
|
||||
|
||||
<h1>3.4.13 - Bug Fixes & Styling Improvements</h1>
|
||||
<li>Fixed house/year box hard failing when house_colour does not exist</li>
|
||||
<li>Fixed message of the day being unreadable in light mode</li>
|
||||
<li>Fixed global font styling issues due to SEQTA updates</li>
|
||||
<li>Fixed styling issues with title bar and other elements</li>
|
||||
<li>Other minor bug fixes and improvements</li>
|
||||
|
||||
<h1>3.4.12 - Privacy Updates & Bug Fixes</h1>
|
||||
<li>Added privacy statement</li>
|
||||
<li>Added disclaimer modal to assessment averages switch</li>
|
||||
<li>Improved popup management system</li>
|
||||
<li>Other minor bug fixes and improvements</li>
|
||||
|
||||
<h1>3.4.11 - New Features & Bug Fixes</h1>
|
||||
<li>Added Background Music plugin</li>
|
||||
<li>Added empty state for assessments on homepage</li>
|
||||
<li>Added Colour Picker hex/rgba controls</li>
|
||||
<li>Fixed custom shortcuts positioning (moved above regular shortcuts)</li>
|
||||
<li>Fixed Go to popup not scrolling properly</li>
|
||||
<li>Made theme edit mode more plain</li>
|
||||
<li>Other minor bug fixes and improvements</li>
|
||||
|
||||
<h1>3.4.10 - Minor bug fixes</h1>
|
||||
<li>Fixed UI file styling incorrectly applying to documents</li>
|
||||
<li>Fixed missing styles in global search</li>
|
||||
<li>Added icons for image files in file viewer</li>
|
||||
<li>Added rounded corners when dragging calendar events</li>
|
||||
<li>Improved performance of element scanning</li>
|
||||
<li>Other minor improvements</li>
|
||||
|
||||
<h1>3.4.9 - Bug Fixes and Performance Improvements</h1>
|
||||
<li>Fixed performance issues with large notices on the homepage</li>
|
||||
<li>Improved performance when global search is disabled</li>
|
||||
<li>Improved performance of storage handling</li>
|
||||
<li>Other bug fixes and improvements</li>
|
||||
|
||||
<h1>3.4.8 - Improvements!</h1>
|
||||
<li>Added new assessments kanban overview</li>
|
||||
<li>Added custom profile pictures</li>
|
||||
<li>Added custom shortcut icons</li>
|
||||
<li>Added modern and animated notices on homepage</li>
|
||||
<li>Improved global search performance and bug fixes</li>
|
||||
<li>Fixed sidebar icons reverting to old style after reload</li>
|
||||
<li>Fixed settings popup not appearing on disabled pages</li>
|
||||
<li>Fixed 12-hour time not applying correctly in timetable</li>
|
||||
<li>Fixed background flickering on page load</li>
|
||||
<li>Fixed homepage lessons not properly changing days</li>
|
||||
<li>Performance improvements for global search</li>
|
||||
<li>Performance improvements across the extension</li>
|
||||
<li>Other bug fixes and improvements</li>
|
||||
|
||||
let text = stringToHTML(/* html */ `
|
||||
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: scroll;">
|
||||
<h1>3.4.7 - Global Search</h1>
|
||||
<li>Added a new global search bar (enable in settings)
|
||||
<span class="beta">beta</span>
|
||||
@@ -252,27 +283,34 @@ export function OpenWhatsNewPopup() {
|
||||
<h1>Create Custom Shortcuts</h1>
|
||||
<li>Found in the BetterSEQTA+ Settings menu, custom shortcuts can now be created with a name and URL of your choice.</li>
|
||||
</div>
|
||||
`).firstChild;
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
let footer = stringToHTML(/* html */ `
|
||||
const footer = stringToHTML(/* html */ `
|
||||
<div class="whatsnewFooter">
|
||||
<div>
|
||||
Report bugs and feedback:
|
||||
Resources and Feedback:
|
||||
<a class="socials" href="https://betterseqta.org" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 658 656" version="1.1">
|
||||
<path d="M 296 6.079 C 222.099 14.147, 156.177 44.962, 103.631 96 C 75.901 122.933, 55.863 150.195, 39.039 183.877 C 6.713 248.596, -2.990 322.811, 11.567 394 C 24.458 457.036, 54.499 512.622, 100.472 558.501 C 152.711 610.633, 218.648 642.109, 294.500 651.123 C 308.578 652.796, 349.167 652.807, 363.500 651.143 C 457.686 640.203, 538.776 592.815, 592.980 517.037 C 642.593 447.677, 662.695 361.034, 648.904 276 C 633.968 183.904, 580.183 103.524, 499.640 52.932 C 470.832 34.836, 435.045 20.244, 400.531 12.522 C 375.717 6.970, 364.646 5.804, 333.500 5.466 C 317.550 5.293, 300.675 5.568, 296 6.079 M 300.500 148.106 C 261.812 152.166, 225.171 169.425, 197.296 196.717 C 171.447 222.025, 154.115 255.340, 147.986 291.500 C 146.044 302.958, 145.844 306.932, 146.301 325 C 147.060 355.042, 151.117 371.665, 163.998 397.500 C 187.801 445.243, 230.082 477.905, 283.388 489.727 L 295.500 492.414 411.250 492.742 L 527 493.071 527 469.536 L 527 446 482.433 446 L 437.866 446 445.596 437.554 C 457.097 424.987, 465.208 413.133, 473.002 397.500 C 485.883 371.665, 489.940 355.042, 490.699 325 C 491.154 307.015, 490.951 302.933, 489.050 291.729 C 473.693 201.254, 391.395 138.565, 300.500 148.106 M 304.500 195.620 C 270.564 200.792, 243.575 215.251, 223.612 238.956 C 203.303 263.071, 193.650 289.377, 193.690 320.500 C 193.770 381.750, 237.341 433.004, 298.364 443.631 C 311.912 445.990, 335.206 445.075, 348.221 441.672 C 361.455 438.211, 373.637 433.094, 383.671 426.781 C 413.787 407.833, 433.890 379.189, 441.066 345 C 443.682 332.536, 444.161 311.707, 442.101 300 C 434.241 255.323, 402.917 217.681, 361 202.541 C 347.818 197.780, 337.607 195.947, 322 195.540 C 314.025 195.333, 306.150 195.369, 304.500 195.620" fill="currentColor" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="25px" height="25px" viewBox="0 0 256 250" preserveAspectRatio="xMidYMid" style="vertical-align: middle;">
|
||||
<g><path d="M128.00106,0 C57.3172926,0 0,57.3066942 0,128.00106 C0,184.555281 36.6761997,232.535542 87.534937,249.460899 C93.9320223,250.645779 96.280588,246.684165 96.280588,243.303333 C96.280588,240.251045 96.1618878,230.167899 96.106777,219.472176 C60.4967585,227.215235 52.9826207,204.369712 52.9826207,204.369712 C47.1599584,189.574598 38.770408,185.640538 38.770408,185.640538 C27.1568785,177.696113 39.6458206,177.859325 39.6458206,177.859325 C52.4993419,178.762293 59.267365,191.04987 59.267365,191.04987 C70.6837675,210.618423 89.2115753,204.961093 96.5158685,201.690482 C97.6647155,193.417512 100.981959,187.77078 104.642583,184.574357 C76.211799,181.33766 46.324819,170.362144 46.324819,121.315702 C46.324819,107.340889 51.3250588,95.9223682 59.5132437,86.9583937 C58.1842268,83.7344152 53.8029229,70.715562 60.7532354,53.0843636 C60.7532354,53.0843636 71.5019501,49.6441813 95.9626412,66.2049595 C106.172967,63.368876 117.123047,61.9465949 128.00106,61.8978432 C138.879073,61.9465949 149.837632,63.368876 160.067033,66.2049595 C184.49805,49.6441813 195.231926,53.0843636 195.231926,53.0843636 C202.199197,70.715562 197.815773,83.7344152 196.486756,86.9583937 C204.694018,95.9223682 209.660343,107.340889 209.660343,121.315702 C209.660343,170.478725 179.716133,181.303747 151.213281,184.472614 C155.80443,188.444828 159.895342,196.234518 159.895342,208.176593 C159.895342,225.303317 159.746968,239.087361 159.746968,243.303333 C159.746968,246.709601 162.05102,250.70089 168.53925,249.443941 C219.370432,232.499507 256,184.536204 256,128.00106 C256,57.3066942 198.691187,0 128.00106,0 Z M47.9405593,182.340212 C47.6586465,182.976105 46.6581745,183.166873 45.7467277,182.730227 C44.8183235,182.312656 44.2968914,181.445722 44.5978808,180.80771 C44.8734344,180.152739 45.876026,179.97045 46.8023103,180.409216 C47.7328342,180.826786 48.2627451,181.702199 47.9405593,182.340212 Z M54.2367892,187.958254 C53.6263318,188.524199 52.4329723,188.261363 51.6232682,187.366874 C50.7860088,186.474504 50.6291553,185.281144 51.2480912,184.70672 C51.8776254,184.140775 53.0349512,184.405731 53.8743302,185.298101 C54.7115892,186.201069 54.8748019,187.38595 54.2367892,187.958254 Z M58.5562413,195.146347 C57.7719732,195.691096 56.4895886,195.180261 55.6968417,194.042013 C54.9125733,192.903764 54.9125733,191.538713 55.713799,190.991845 C56.5086651,190.444977 57.7719732,190.936735 58.5753181,192.066505 C59.3574669,193.22383 59.3574669,194.58888 58.5562413,195.146347 Z M65.8613592,203.471174 C65.1597571,204.244846 63.6654083,204.03712 62.5716717,202.981538 C61.4524999,201.94927 61.1409122,200.484596 61.8446341,199.710926 C62.5547146,198.935137 64.0575422,199.15346 65.1597571,200.200564 C66.2704506,201.230712 66.6095936,202.705984 65.8613592,203.471174 Z M75.3025151,206.281542 C74.9930474,207.284134 73.553809,207.739857 72.1039724,207.313809 C70.6562556,206.875043 69.7087748,205.700761 70.0012857,204.687571 C70.302275,203.678621 71.7478721,203.20382 73.2083069,203.659543 C74.6539041,204.09619 75.6035048,205.261994 75.3025151,206.281542 Z M86.046947,207.473627 C86.0829806,208.529209 84.8535871,209.404622 83.3316829,209.4237 C81.8013,209.457614 80.563428,208.603398 80.5464708,207.564772 C80.5464708,206.498591 81.7483088,205.631657 83.2786917,205.606221 C84.8005962,205.576546 86.046947,206.424403 86.046947,207.473627 Z M96.6021471,207.069023 C96.7844366,208.099171 95.7267341,209.156872 94.215428,209.438785 C92.7295577,209.710099 91.3539086,209.074206 91.1652603,208.052538 C90.9808515,206.996955 92.0576306,205.939253 93.5413813,205.66582 C95.054807,205.402984 96.4092596,206.021919 96.6021471,207.069023 Z" fill="currentColor" /></g>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="socials" href="https://www.youtube.com/@BetterSEQTAPlus" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" width="50px" height="50px"><path d="M 44.898438 14.5 C 44.5 12.300781 42.601563 10.699219 40.398438 10.199219 C 37.101563 9.5 31 9 24.398438 9 C 17.800781 9 11.601563 9.5 8.300781 10.199219 C 6.101563 10.699219 4.199219 12.199219 3.800781 14.5 C 3.398438 17 3 20.5 3 25 C 3 29.5 3.398438 33 3.898438 35.5 C 4.300781 37.699219 6.199219 39.300781 8.398438 39.800781 C 11.898438 40.5 17.898438 41 24.5 41 C 31.101563 41 37.101563 40.5 40.601563 39.800781 C 42.800781 39.300781 44.699219 37.800781 45.101563 35.5 C 45.5 33 46 29.398438 46.101563 25 C 45.898438 20.5 45.398438 17 44.898438 14.5 Z M 19 32 L 19 18 L 31.199219 25 Z"/></svg>
|
||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding: 0; display: flex; align-items: center;">
|
||||
<svg style="width:25px; height:25px; vertical-align: middle;" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://ko-fi.com/sethburkart" target="_blank" style="background: none !important; margin:0;margin-left:6px;padding:0; display: flex; align-items: center;">
|
||||
@@ -280,58 +318,10 @@ export function OpenWhatsNewPopup() {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`).firstChild;
|
||||
`).firstChild as HTMLElement;
|
||||
|
||||
let exitbutton = document.createElement("div");
|
||||
exitbutton.id = "whatsnewclosebutton";
|
||||
|
||||
container.append(header);
|
||||
container.append(imagecont);
|
||||
container.append(textcontainer);
|
||||
container.append(text as ChildNode);
|
||||
container.append(footer as ChildNode);
|
||||
container.append(exitbutton);
|
||||
|
||||
background.append(container);
|
||||
|
||||
document.getElementById("container")!.append(background);
|
||||
|
||||
let bkelement = document.getElementById("whatsnewbk");
|
||||
let popup = document.getElementsByClassName("whatsnewContainer")[0];
|
||||
|
||||
if (settingsState.animations) {
|
||||
animate(
|
||||
[popup, bkelement as HTMLElement],
|
||||
{ scale: [0, 1] },
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 220,
|
||||
damping: 18,
|
||||
},
|
||||
);
|
||||
|
||||
animate(
|
||||
".whatsnewTextContainer *",
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.05, { startDelay: 0.1 }),
|
||||
duration: 0.5,
|
||||
ease: [0.22, 0.03, 0.26, 1],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
delete settingsState.justupdated;
|
||||
|
||||
bkelement!.addEventListener("click", function (event) {
|
||||
// Check if the click event originated from the element itself and not any of its children
|
||||
if (event.target === bkelement) {
|
||||
DeleteWhatsNew();
|
||||
}
|
||||
});
|
||||
|
||||
var closeelement = document.getElementById("whatsnewclosebutton");
|
||||
closeelement!.addEventListener("click", function () {
|
||||
DeleteWhatsNew();
|
||||
openPopup({
|
||||
header,
|
||||
content: [imageContainer, text, footer],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { settingsState } from "../listeners/SettingsState";
|
||||
import { animate as motionAnimate, stagger } from "motion";
|
||||
|
||||
type AnimationTarget = string | Element | Element[] | NodeList | null;
|
||||
|
||||
let isClosing = false;
|
||||
|
||||
export async function closePopup() {
|
||||
if (isClosing) return;
|
||||
isClosing = true;
|
||||
|
||||
const background = document.getElementById("whatsnewbk");
|
||||
const popup = document.getElementsByClassName("whatsnewContainer")[0] as
|
||||
| HTMLElement
|
||||
| undefined;
|
||||
|
||||
if (!background || !popup) {
|
||||
isClosing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settingsState.animations) {
|
||||
background.remove();
|
||||
isClosing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await (motionAnimate as any)(
|
||||
[popup, background],
|
||||
{ opacity: [1, 0], scale: [1, 0.95] },
|
||||
{ duration: 0.25, easing: [0.22, 0.03, 0.26, 1] },
|
||||
);
|
||||
|
||||
background.remove();
|
||||
isClosing = false;
|
||||
}
|
||||
|
||||
interface OpenPopupOptions {
|
||||
header?: Node | null;
|
||||
content?: (Node | null | undefined)[];
|
||||
animateSelector?: AnimationTarget;
|
||||
}
|
||||
|
||||
export function openPopup({
|
||||
header,
|
||||
content = [],
|
||||
animateSelector = ".whatsnewTextContainer *",
|
||||
}: OpenPopupOptions = {}) {
|
||||
const background = document.createElement("div");
|
||||
background.id = "whatsnewbk";
|
||||
background.classList.add("whatsnewBackground");
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("whatsnewContainer");
|
||||
|
||||
if (header) container.append(header);
|
||||
for (const node of content) if (node) container.append(node);
|
||||
|
||||
const closeButton = document.createElement("div");
|
||||
closeButton.id = "whatsnewclosebutton";
|
||||
container.append(closeButton);
|
||||
|
||||
background.append(container);
|
||||
document.getElementById("container")!.append(background);
|
||||
|
||||
if (settingsState.animations) {
|
||||
(motionAnimate as any)(
|
||||
[container, background],
|
||||
{ scale: [0, 1] },
|
||||
{ type: "spring", stiffness: 220, damping: 18 },
|
||||
);
|
||||
|
||||
if (animateSelector) {
|
||||
const targets =
|
||||
typeof animateSelector === "string"
|
||||
? document.querySelectorAll(animateSelector)
|
||||
: animateSelector;
|
||||
|
||||
(motionAnimate as any)(
|
||||
targets!,
|
||||
{ opacity: [0, 1], y: [10, 0] },
|
||||
{
|
||||
delay: stagger(0.05, { startDelay: 0.1 }),
|
||||
duration: 0.5,
|
||||
easing: [0.22, 0.03, 0.26, 1],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
delete settingsState.justupdated;
|
||||
|
||||
background.addEventListener("click", (event) => {
|
||||
if (event.target === background) void closePopup();
|
||||
});
|
||||
|
||||
closeButton.addEventListener("click", () => void closePopup());
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts";
|
||||
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
||||
|
||||
export function renderShortcuts() {
|
||||
const container = document.getElementById("shortcuts");
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
try {
|
||||
addShortcuts(settingsState.shortcuts || []);
|
||||
} catch (err: any) {
|
||||
console.error("[BetterSEQTA+] Error adding built-in shortcuts:", err?.message || err);
|
||||
}
|
||||
|
||||
const custom = settingsState.customshortcuts || [];
|
||||
for (const element of custom) {
|
||||
try {
|
||||
CreateCustomShortcutDiv(element);
|
||||
} catch (err: any) {
|
||||
console.error("[BetterSEQTA+] Error adding custom shortcut:", element?.name, err?.message || err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,25 @@ export async function SendNewsPage() {
|
||||
const main = document.getElementById("main");
|
||||
main!.innerHTML = "";
|
||||
|
||||
const displayCountry = (() => {
|
||||
switch (settingsState.newsSource?.toLowerCase()) {
|
||||
case "usa": return "the USA";
|
||||
case "uk": return "the UK";
|
||||
case "netherlands": return "the Netherlands";
|
||||
default:
|
||||
return settingsState.newsSource
|
||||
? settingsState.newsSource
|
||||
.split("_")
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
: "Australia";
|
||||
}
|
||||
})();
|
||||
|
||||
const html = stringToHTML(/* html */ `
|
||||
<div class="home-root">
|
||||
<div class="home-container" id="news-container">
|
||||
<h1 class="border">Latest Headlines in ${settingsState.newsSource ? settingsState.newsSource.charAt(0).toUpperCase() + settingsState.newsSource.slice(1) : "Australia"}</h1>
|
||||
<h1 class="border">Latest Headlines in ${displayCountry}</h1>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
export function setupFixedTooltips(root: Document | HTMLElement = document) {
|
||||
const elements = root.querySelectorAll<HTMLElement>(".fixed-tooltip");
|
||||
elements.forEach((tooltip) => {
|
||||
const text = tooltip.querySelector<HTMLElement>(".tooltiptext");
|
||||
if (!text) return;
|
||||
tooltip.removeChild(text);
|
||||
text.classList.add("tooltiptext-fixed");
|
||||
|
||||
let hideTimeout: number | undefined;
|
||||
|
||||
const show = () => {
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = undefined;
|
||||
}
|
||||
document.body.appendChild(text);
|
||||
const rect = tooltip.getBoundingClientRect();
|
||||
text.style.left = `${rect.left + rect.width / 2}px`;
|
||||
text.style.top = `${rect.bottom + 5}px`;
|
||||
requestAnimationFrame(() => text.classList.add("show"));
|
||||
};
|
||||
|
||||
const scheduleHide = () => {
|
||||
hideTimeout = window.setTimeout(() => {
|
||||
text.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
if (text.parentElement === document.body) {
|
||||
document.body.removeChild(text);
|
||||
}
|
||||
}, 200);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
tooltip.addEventListener("mouseenter", show);
|
||||
tooltip.addEventListener("mouseleave", scheduleHide);
|
||||
tooltip.addEventListener("blur", scheduleHide);
|
||||
tooltip.addEventListener("click", scheduleHide);
|
||||
|
||||
text.addEventListener("mouseenter", () => {
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = undefined;
|
||||
}
|
||||
});
|
||||
text.addEventListener("mouseleave", scheduleHide);
|
||||
text.addEventListener("click", () => {
|
||||
if (hideTimeout) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = undefined;
|
||||
}
|
||||
text.classList.remove("show");
|
||||
setTimeout(() => {
|
||||
if (text.parentElement === document.body) {
|
||||
document.body.removeChild(text);
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { settingsState } from "./listeners/SettingsState";
|
||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent";
|
||||
|
||||
function maybeHide() {
|
||||
if (settingsState.hideSensitiveContent) {
|
||||
hideSensitiveContent();
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeHideSensitiveToggle() {
|
||||
maybeHide();
|
||||
window.addEventListener("hashchange", maybeHide);
|
||||
settingsState.register("hideSensitiveContent", (val) => {
|
||||
if (val) {
|
||||
maybeHide();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
import ReactFiber from "../ReactFiber";
|
||||
import { delay } from "../delay";
|
||||
|
||||
const handleNotificationClick = async (target: HTMLElement) => {
|
||||
const notificationItem = target.closest(
|
||||
@@ -17,7 +18,7 @@ const handleNotificationClick = async (target: HTMLElement) => {
|
||||
if (!buttonId) return;
|
||||
|
||||
const matchingNotification =
|
||||
notificationList.storeState.notifications.items.find(
|
||||
notificationList.items.find(
|
||||
(item: any) => item.notificationID === parseInt(buttonId),
|
||||
);
|
||||
|
||||
@@ -33,6 +34,24 @@ const handleNotificationClick = async (target: HTMLElement) => {
|
||||
'[class*="notifications__notifications___"] > button',
|
||||
) as HTMLButtonElement | null;
|
||||
notificationButton?.click();
|
||||
|
||||
await delay(10);
|
||||
|
||||
const button = document.querySelector('[class*="MessageList__selected___"]');
|
||||
if (button) {
|
||||
(button as HTMLElement).click();
|
||||
}
|
||||
|
||||
// send a network request to mark as read
|
||||
fetch('/seqta/student/save/message', {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
items: [matchingNotification.message.messageID],
|
||||
mode: 'x-read',
|
||||
read: true,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const clickListeners = [
|
||||
|
||||
@@ -57,13 +57,37 @@ class EventManager {
|
||||
return { unregister };
|
||||
}
|
||||
|
||||
private buildSelector(options: EventListenerOptions): string | null {
|
||||
if (options.textContent || options.customCheck) return null;
|
||||
|
||||
let selector = options.elementType || "";
|
||||
if (options.id) {
|
||||
selector += `#${CSS.escape(options.id)}`;
|
||||
}
|
||||
if (options.className) {
|
||||
selector += `.${CSS.escape(options.className)}`;
|
||||
}
|
||||
|
||||
return selector.trim() || null;
|
||||
}
|
||||
|
||||
private async scanExistingElements(
|
||||
options: EventListenerOptions,
|
||||
callback: (element: Element) => void,
|
||||
): Promise<void> {
|
||||
const root = options.parentElement || document.documentElement;
|
||||
const elements = Array.from(root.getElementsByTagName("*"));
|
||||
const selector = this.buildSelector(options);
|
||||
let elements: Element[] = [];
|
||||
|
||||
if (selector) {
|
||||
elements = Array.from(root.querySelectorAll(selector));
|
||||
if (selector && root.matches && root.matches(selector)) {
|
||||
elements.unshift(root);
|
||||
}
|
||||
} else {
|
||||
elements = Array.from(root.getElementsByTagName("*"));
|
||||
elements.unshift(root);
|
||||
}
|
||||
|
||||
for (let i = 0; i < elements.length; i += this.chunkSize) {
|
||||
const chunk = elements.slice(i, i + this.chunkSize);
|
||||
|
||||
@@ -8,15 +8,17 @@ type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void;
|
||||
class StorageManager {
|
||||
private static instance: StorageManager;
|
||||
private data: SettingsState;
|
||||
private listeners: { [key: string]: ChangeListener[] };
|
||||
private globalListeners: GlobalChangeListener[];
|
||||
private listeners: Map<string, Set<ChangeListener>>;
|
||||
private globalListeners: Set<GlobalChangeListener>;
|
||||
private subscribers: Set<Subscriber<SettingsState>> = new Set();
|
||||
private saveTimeout: NodeJS.Timeout | null = null;
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {
|
||||
this.data = {} as SettingsState;
|
||||
this.listeners = {};
|
||||
this.globalListeners = [];
|
||||
this.loadFromStorage();
|
||||
this.listeners = new Map();
|
||||
this.globalListeners = new Set();
|
||||
// Don't call async loadFromStorage in constructor
|
||||
|
||||
const handler: ProxyHandler<StorageManager> = {
|
||||
get: (target, prop: keyof SettingsState | "register" | "initialize") => {
|
||||
@@ -26,8 +28,13 @@ class StorageManager {
|
||||
return Reflect.get(target.data, prop);
|
||||
},
|
||||
set: (target, prop: keyof SettingsState, value) => {
|
||||
const oldValue = target.data[prop];
|
||||
|
||||
// Only save if the reference actually changed
|
||||
if (oldValue !== value) {
|
||||
Reflect.set(target.data, prop, value);
|
||||
target.saveToStorage();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
deleteProperty: (target, prop: keyof SettingsState) => {
|
||||
@@ -35,8 +42,9 @@ class StorageManager {
|
||||
if (oldValue !== undefined) {
|
||||
delete target.data[prop];
|
||||
target.removeFromStorage(prop);
|
||||
if (target.listeners[prop]) {
|
||||
for (const listener of target.listeners[prop]) {
|
||||
const listeners = target.listeners.get(prop as string);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(undefined, oldValue);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +67,10 @@ class StorageManager {
|
||||
|
||||
public static async initialize(): Promise<StorageManager & SettingsState> {
|
||||
const instance = StorageManager.getInstance();
|
||||
if (!instance.initialized) {
|
||||
await instance.loadFromStorage();
|
||||
instance.initialized = true;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -67,8 +78,19 @@ class StorageManager {
|
||||
key: K,
|
||||
value: SettingsState[K],
|
||||
): void {
|
||||
const oldValue = this.data[key];
|
||||
if (oldValue !== value) {
|
||||
this.data[key] = value;
|
||||
this.saveToStorage();
|
||||
|
||||
// Notify listeners
|
||||
const listeners = this.listeners.get(key as string);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(value, oldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadFromStorage(): Promise<void> {
|
||||
@@ -78,7 +100,11 @@ class StorageManager {
|
||||
});
|
||||
}
|
||||
|
||||
private async saveToStorage(): Promise<void> {
|
||||
public async saveToStorage(): Promise<void> {
|
||||
if (this.saveTimeout) {
|
||||
clearTimeout(this.saveTimeout);
|
||||
this.saveTimeout = null;
|
||||
}
|
||||
// @ts-expect-error
|
||||
await browser.storage.local.set(this.data);
|
||||
this.notifySubscribers();
|
||||
@@ -91,22 +117,38 @@ class StorageManager {
|
||||
private initStorageListener(): void {
|
||||
browser.storage.onChanged.addListener((changes, areaName) => {
|
||||
if (areaName === "local") {
|
||||
const actualChanges: string[] = [];
|
||||
|
||||
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
|
||||
// Only process if value actually changed
|
||||
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
|
||||
if (newValue !== undefined) {
|
||||
(this.data as any)[key] = newValue;
|
||||
} else {
|
||||
delete (this.data as any)[key];
|
||||
}
|
||||
if (this.listeners[key]) {
|
||||
for (const listener of this.listeners[key]) {
|
||||
actualChanges.push(key);
|
||||
|
||||
// Notify specific listeners
|
||||
const listeners = this.listeners.get(key);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(newValue, oldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only notify global listeners if there were actual changes
|
||||
if (actualChanges.length > 0 && this.globalListeners.size > 0) {
|
||||
for (const listener of this.globalListeners) {
|
||||
for (const key of actualChanges) {
|
||||
const { oldValue, newValue } = changes[key];
|
||||
listener(newValue, oldValue, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,18 +158,36 @@ class StorageManager {
|
||||
* @param listener The listener to call when the setting changes -> takes two arguments, (newValue, oldValue)
|
||||
*/
|
||||
public register(prop: keyof SettingsState, listener: ChangeListener): void {
|
||||
if (!this.listeners[prop]) {
|
||||
this.listeners[prop] = [];
|
||||
const key = prop as string;
|
||||
if (!this.listeners.has(key)) {
|
||||
this.listeners.set(key, new Set());
|
||||
}
|
||||
this.listeners[prop].push(listener);
|
||||
this.listeners.get(key)!.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a listener for a setting.
|
||||
* @param prop The setting to stop listening to.
|
||||
* @param listener The listener to remove.
|
||||
*/
|
||||
public unregister(prop: keyof SettingsState, listener: ChangeListener): void {
|
||||
this.listeners.get(prop as string)?.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a listener for any setting.
|
||||
* @param listener The listener to call when any setting changes -> takes two arguments, (newValue, oldValue)
|
||||
* @param listener The listener to call when any setting changes -> takes three arguments, (newValue, oldValue, key)
|
||||
*/
|
||||
public registerGlobal(listener: GlobalChangeListener): void {
|
||||
this.globalListeners.push(listener);
|
||||
this.globalListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a global listener.
|
||||
* @param listener The listener to remove.
|
||||
*/
|
||||
public unregisterGlobal(listener: GlobalChangeListener): void {
|
||||
this.globalListeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { settingsState } from "./SettingsState";
|
||||
import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
||||
|
||||
import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts";
|
||||
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
||||
// Shortcuts rendering
|
||||
import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
|
||||
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
||||
import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv";
|
||||
|
||||
import browser from "webextension-polyfill";
|
||||
import type { CustomShortcut } from "@/types/storage";
|
||||
@@ -46,52 +45,16 @@ export class StorageChangeHandler {
|
||||
newValue: CustomShortcut[],
|
||||
oldValue: CustomShortcut[],
|
||||
) {
|
||||
if (newValue) {
|
||||
if (newValue.length > oldValue.length) {
|
||||
CreateCustomShortcutDiv(newValue[oldValue.length]);
|
||||
} else if (newValue.length < oldValue.length) {
|
||||
const removedElement = oldValue.find(
|
||||
(oldItem: any) =>
|
||||
!newValue.some(
|
||||
(newItem: any) =>
|
||||
JSON.stringify(oldItem) === JSON.stringify(newItem),
|
||||
),
|
||||
);
|
||||
|
||||
if (removedElement) {
|
||||
RemoveShortcutDiv([removedElement]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!newValue || !oldValue) return;
|
||||
renderShortcuts();
|
||||
}
|
||||
|
||||
private handleShortcutsChange(
|
||||
newValue: { enabled: boolean; name: string }[],
|
||||
oldValue: { enabled: boolean; name: string }[],
|
||||
) {
|
||||
const addedShortcuts = newValue.filter((newItem: any) => {
|
||||
const wasDisabledAndNowEnabled = oldValue.some((oldItem: any) => {
|
||||
return oldItem.name === newItem.name && !oldItem.enabled && newItem.enabled;
|
||||
});
|
||||
|
||||
const isNewShortcut = !oldValue.some((oldItem: any) => oldItem.name === newItem.name);
|
||||
|
||||
return (wasDisabledAndNowEnabled || isNewShortcut) && newItem.enabled;
|
||||
});
|
||||
|
||||
const removedShortcuts = newValue.filter((newItem: any) => {
|
||||
const isRemoved = oldValue.some((oldItem: any) => {
|
||||
const match = oldItem.name === newItem.name;
|
||||
const wasEnabled = oldItem.enabled;
|
||||
const isDisabled = !newItem.enabled;
|
||||
return match && wasEnabled && isDisabled;
|
||||
});
|
||||
|
||||
return isRemoved;
|
||||
});
|
||||
|
||||
addShortcuts(addedShortcuts);
|
||||
RemoveShortcutDiv(removedShortcuts);
|
||||
if (!newValue || !oldValue) return;
|
||||
renderShortcuts();
|
||||
}
|
||||
|
||||
private handleTransparencyEffectsChange(newValue: boolean) {
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
} from "./Closers/closeExtensionPopup";
|
||||
import { animate } from "motion";
|
||||
import { settingsState } from "./listeners/SettingsState";
|
||||
import { renderSettingsIfNeeded } from "./Adders/AddExtensionSettings";
|
||||
import { delay } from "./delay";
|
||||
|
||||
export function setupSettingsButton() {
|
||||
var AddedSettings = document.getElementById("AddedSettings");
|
||||
@@ -14,6 +16,10 @@ export function setupSettingsButton() {
|
||||
if (SettingsClicked) {
|
||||
closeExtensionPopup(extensionPopup as HTMLElement);
|
||||
} else {
|
||||
renderSettingsIfNeeded();
|
||||
|
||||
await delay(30);
|
||||
|
||||
if (settingsState.animations) {
|
||||
animate(0, 1, {
|
||||
onUpdate: (progress) => {
|
||||
|
||||
@@ -37,6 +37,20 @@ function updateTimeElements(): void {
|
||||
const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", "");
|
||||
el.textContent = `${start12}–${end12}`;
|
||||
});
|
||||
|
||||
const quickbarTimes = document.querySelectorAll<HTMLElement>(".quickbar .meta .times");
|
||||
quickbarTimes.forEach((el) => {
|
||||
if (!el.dataset.original) el.dataset.original = el.textContent || "";
|
||||
const original = el.dataset.original || "";
|
||||
|
||||
if (!original.includes("–") && !original.includes("-")) return;
|
||||
const [start, end] = original.split(/[-–]/).map((p) => p.trim());
|
||||
|
||||
if (!start || !end) return;
|
||||
const start12 = convertTo12HourFormat(start).toLowerCase().replace(" ", "");
|
||||
const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", "");
|
||||
el.textContent = `${start12}–${end12}`;
|
||||
});
|
||||
}
|
||||
|
||||
function checkIfOnTimetablePage(): boolean {
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface SettingsState {
|
||||
subjectfilters: Record<string, any>;
|
||||
transparencyEffects: boolean;
|
||||
justupdated?: boolean;
|
||||
privacyStatementShown?: boolean;
|
||||
privacyStatementLastUpdated?: string;
|
||||
timeFormat?: string;
|
||||
animations: boolean;
|
||||
defaultPage: string;
|
||||
@@ -37,6 +39,7 @@ export interface SettingsState {
|
||||
originalDarkMode?: boolean;
|
||||
newsSource?: string;
|
||||
mockNotices?: boolean;
|
||||
hideSensitiveContent?: boolean;
|
||||
|
||||
// depreciated keys
|
||||
animatedbk: boolean;
|
||||
|
||||
@@ -84,6 +84,10 @@ export default defineConfig(({ command }) => ({
|
||||
settings: join(__dirname, "src", "interface", "index.html"),
|
||||
pageState: join(__dirname, "src", "pageState.js"),
|
||||
},
|
||||
onwarn(warning, warn) {
|
||||
if (warning.code === "FILE_NAME_CONFLICT") return;
|
||||
warn(warning);
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user