mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 11:44:40 +00:00
Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d53dc9ff06 | |||
| 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 | |||
| c4140a2a9d | |||
| 566f326dce | |||
| c9fe4e0e1c | |||
| fbd8d9e9e8 | |||
| 3f0d3f87fe | |||
| 2292585e60 | |||
| bb7c27dfea | |||
| 8c1df8f829 | |||
| 7462e6ab5d | |||
| 66ff6e3468 | |||
| 8bd9b1dae7 | |||
| d377329bf9 | |||
| ec38502747 | |||
| 57b4daa9b7 | |||
| fa37fe9d21 | |||
| ccb4354b26 | |||
| 841426d7ec | |||
| b9f0675c4f | |||
| 8ff5fd8d2f | |||
| ff01b44ead | |||
| 72f7eeb935 | |||
| a009f40ac2 | |||
| c202af9688 | |||
| 7a91550de5 | |||
| c4dc4b58b8 | |||
| d59802d4c3 | |||
| 074c2ff4bb | |||
| e94008efba | |||
| 972783eb13 | |||
| 9af3ca4516 | |||
| 60b4438552 | |||
| d3dadad982 | |||
| bf01c0ca7b | |||
| 3821034a5c | |||
| c218f184c0 | |||
| 4b67736da2 | |||
| 8e107800f1 | |||
| f44d28c2b8 | |||
| 058cbb0bfa | |||
| 9b13e7571a | |||
| 6c12f5cf00 | |||
| df385775d9 | |||
| dce112d129 | |||
| f62d712549 | |||
| 148559556a | |||
| 0c2bdf36cf | |||
| ee002991af | |||
| c53de6ed8d | |||
| 98560af0a3 | |||
| f9fa334e40 | |||
| a7e250a86d | |||
| b3db85c565 | |||
| c9d9611e3e | |||
| a1f480855e | |||
| 9c3c63e497 | |||
| 418c3c010e | |||
| 4c55cf4331 | |||
| 0b93223b84 | |||
| 84ab19eee7 | |||
| 94b479519a | |||
| af68eb5534 | |||
| 94e527c73d | |||
| 3b9f8124cf | |||
| 4c69ba7794 | |||
| 6e43be2a18 | |||
| 37a13cba07 | |||
| a855fbe9ec | |||
| 1206fb655d | |||
| 1ae9bd0652 | |||
| 280163111e | |||
| 59e195d2aa | |||
| 37d62cf2a8 | |||
| 6e5c3b4733 | |||
| ac9761286c | |||
| d7fc7582d1 | |||
| 425860c7cc | |||
| 69ac159bad | |||
| 4c93bcd0d7 | |||
| c596e65449 | |||
| afdbfe3190 | |||
| f1137763a6 | |||
| 074e73b0fd | |||
| fc4b121d30 | |||
| 55c48cbe5c | |||
| 72b18dfb7d | |||
| d0cb352e74 | |||
| 8fee6ddb76 | |||
| 599f20e6d0 | |||
| 07af33eb78 | |||
| f03d25f918 | |||
| 1adb18ca42 | |||
| 8f7a9b655a | |||
| 899ba46995 | |||
| ccb465cc2d |
@@ -2,6 +2,7 @@ name: Bug report
|
|||||||
description: Report an issue with the modpack in its unmodified state. For other issues, use Discord.
|
description: Report an issue with the modpack in its unmodified state. For other issues, use Discord.
|
||||||
labels: bug
|
labels: bug
|
||||||
title: "[BUG]"
|
title: "[BUG]"
|
||||||
|
type: "Bug"
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ name: Feature request
|
|||||||
description: Suggest a new Feature to be added or replaced in BetterSeqtaPLUS
|
description: Suggest a new Feature to be added or replaced in BetterSeqtaPLUS
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
title: "[FR]"
|
title: "[FR]"
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -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,
|
Hey there! 👋 Thanks for your interest in contributing to BetterSEQTA+! We're excited to have you join our community of contributors.
|
||||||
email, or any other method with the owners of this repository before making a change.
|
|
||||||
|
## 🚀 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
|
## Community
|
||||||
|
|
||||||
Join our community channels to discuss the project, get help, and connect with other contributors:
|
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 Discussions**: For longer-form conversations
|
||||||
- **GitHub Issues**: For bug reports and feature requests
|
- **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
|
## Pull Request Process
|
||||||
|
|
||||||
1. It is recommended to start by opening an issue to discuss the change you wish to make. This will allow us to discuss the change and ensure it is a good fit for the project.
|
1. It is recommended to start by opening an issue to discuss the change you wish to make. This will allow us to discuss the change and ensure it is a good fit for the project.
|
||||||
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.
|
3. When writing your pull request, make sure to use the pull request template.
|
||||||
|
|
||||||
### Pull Request Template
|
### Pull Request Template
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#
|
|
||||||
|
|
||||||
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
|
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
|
||||||
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
|
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
|
||||||
</a>
|
</a>
|
||||||
@@ -10,7 +8,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<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://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>
|
</p>
|
||||||
|
|
||||||
<div>
|
<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 :)
|
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:
|
||||||
|
|
||||||
```
|
- **👋 New to the project?** Start with our [Getting Started Guide](./docs/GETTING_STARTED_CONTRIBUTING.md)
|
||||||
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
|
- **🏗️ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Install dependencies
|
**2. Install & Run**
|
||||||
|
```bash
|
||||||
You may install the dependencies like below:
|
npm install --legacy-peer-deps
|
||||||
|
npm run dev
|
||||||
```
|
|
||||||
npm install # or your preferred package manager like pnpm or yarn
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
2. Run the dev script (it updates as you save files)
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run dev # or use your perferred package manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building for production
|
|
||||||
|
|
||||||
2. Run the build script
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build # or use your perferred package manager
|
|
||||||
```
|
|
||||||
|
|
||||||
2.1. Package it up (optional)
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your perferred package manager
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Load the extension into chrome
|
|
||||||
|
|
||||||
- Go to `chrome://extensions`
|
|
||||||
- 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
|
## Folder Structure
|
||||||
|
|
||||||
@@ -131,7 +119,7 @@ Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plu
|
|||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development from lead developers [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) with help from other volunteers
|
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development from lead developers [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) with help from other volunteers.
|
||||||
|
|
||||||
## Star History
|
## 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
|
- [Project Overview](./README.md) - This file
|
||||||
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
|
- [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
|
### Plugin System
|
||||||
|
|
||||||
|
|||||||
@@ -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! 🎉
|
||||||
@@ -1,8 +1,41 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import mime from "mime-types";
|
import mime from "mime-types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Vite plugin designed to load files as base64 encoded data URLs.
|
||||||
|
* This plugin intercepts module imports that have a `?base64` query parameter
|
||||||
|
* appended to the file path. It then reads the targeted file, converts its content
|
||||||
|
* to a base64 string, and constructs a data URL which is then exported as the
|
||||||
|
* default export of a new JavaScript module.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // To use this loader, import a file with ?base64 query:
|
||||||
|
* // import myImageBase64 from './path/to/myimage.png?base64';
|
||||||
|
* // myImageBase64 will then be a string like "data:image/png;base64,..."
|
||||||
|
*/
|
||||||
export const base64Loader = {
|
export const base64Loader = {
|
||||||
|
/**
|
||||||
|
* The name of the Vite plugin.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
name: "base64-loader",
|
name: "base64-loader",
|
||||||
|
/**
|
||||||
|
* The core transformation function of the Vite plugin.
|
||||||
|
* It is called by Vite for modules that might need transformation. This function
|
||||||
|
* checks if the module ID includes the `?base64` query. If so, it reads the
|
||||||
|
* specified file, converts it to a base64 data URL, and returns a new
|
||||||
|
* JavaScript module that default exports this data URL.
|
||||||
|
*
|
||||||
|
* @param {any} _ The original code of the file. This parameter is unused by this loader.
|
||||||
|
* @param {string} id The ID of the module being transformed. This string typically
|
||||||
|
* contains the absolute file path and any query parameters
|
||||||
|
* (e.g., "/path/to/file.png?base64").
|
||||||
|
* @returns {string | null} If the module ID does not contain `?base64` query,
|
||||||
|
* it returns `null` to indicate no transformation.
|
||||||
|
* Otherwise, it returns a string of JavaScript code
|
||||||
|
* that default exports the base64 data URL of the file.
|
||||||
|
* For example: `export default 'data:image/png;base64,xxxx';`
|
||||||
|
*/
|
||||||
transform(_: any, id: string) {
|
transform(_: any, id: string) {
|
||||||
const [filePath, query] = id.split("?");
|
const [filePath, query] = id.split("?");
|
||||||
if (query !== "base64") return null;
|
if (query !== "base64") return null;
|
||||||
|
|||||||
+39
-6
@@ -1,25 +1,58 @@
|
|||||||
// ref: https://stackoverflow.com/a/76920975
|
// ref: https://stackoverflow.com/a/76920975
|
||||||
import type { Plugin } from "vite";
|
import type { Plugin } from "vite";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Vite plugin designed to gracefully handle the conclusion of the build process.
|
||||||
|
* This plugin utilizes the `buildEnd` and `closeBundle` hooks provided by Vite.
|
||||||
|
* It checks for errors at the end of the build:
|
||||||
|
* - If an error occurred during the build (`buildEnd` hook receives an error), it logs the error
|
||||||
|
* and explicitly exits the Node.js process with a status code of 1 (indicating failure).
|
||||||
|
* - If the build completes without errors and the bundle is successfully generated
|
||||||
|
* (`closeBundle` hook is called), it logs a success message and exits the process
|
||||||
|
* with a status code of 0 (indicating success).
|
||||||
|
* This explicit process exiting can be useful in CI/CD environments or scripts that
|
||||||
|
* rely on the process status code to determine the build outcome.
|
||||||
|
* The core logic for using these hooks to exit the process is inspired by
|
||||||
|
* a solution found on StackOverflow (https://stackoverflow.com/a/76920975).
|
||||||
|
*
|
||||||
|
* @returns {Plugin} A Vite plugin object configured with `name`, `buildEnd`, and `closeBundle` hooks.
|
||||||
|
*/
|
||||||
export default function ClosePlugin(): Plugin {
|
export default function ClosePlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
* The unique name of this Vite plugin. This name is used by Vite for identification
|
||||||
|
* purposes and will appear in warnings, errors, and logs related to this plugin.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
name: "ClosePlugin", // required, will show up in warnings and errors
|
name: "ClosePlugin", // required, will show up in warnings and errors
|
||||||
|
|
||||||
// use this to catch errors when building
|
/**
|
||||||
|
* A Vite hook that is called when the build process has finished, regardless of
|
||||||
|
* whether it was successful or encountered an error.
|
||||||
|
*
|
||||||
|
* @param {Error} [error] An optional error object. If the build failed, this parameter
|
||||||
|
* will contain the error that occurred. If the build was successful,
|
||||||
|
* this parameter will be undefined or null.
|
||||||
|
*/
|
||||||
buildEnd(error) {
|
buildEnd(error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error("Error bundling");
|
console.error("Error bundling");
|
||||||
console.error(error);
|
console.error(error);
|
||||||
process.exit(1);
|
process.exit(1); // Exit with status 1 indicating an error
|
||||||
} else {
|
} else {
|
||||||
console.log("Build ended");
|
console.log("Build ended"); // Log successful completion of the build phase
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// use this to catch the end of a build without errors
|
/**
|
||||||
|
* A Vite hook that is called after the `buildEnd` hook, but only if the build
|
||||||
|
* was successful (i.e., no errors were passed to `buildEnd`) and all output
|
||||||
|
* files have been generated and written to disk. This signifies the successful
|
||||||
|
* completion of the entire bundling process.
|
||||||
|
*/
|
||||||
closeBundle() {
|
closeBundle() {
|
||||||
console.log("Bundle closed");
|
console.log("Bundle closed"); // Log successful closure of the bundle
|
||||||
process.exit(0);
|
process.exit(0); // Exit with status 0 indicating a successful build
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-10
@@ -1,12 +1,22 @@
|
|||||||
import type { Browser, BuildTarget, Manifest } from "./types";
|
import type { Browser, BuildTarget, Manifest } from "./types";
|
||||||
import type { AnyCase } from "./utils";
|
import type { AnyCase } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Packages a given manifest object with a specific target browser identifier.
|
||||||
|
* This function is typically used in multi-browser extension build processes
|
||||||
|
* to create a configuration object that pairs the manifest data with the browser
|
||||||
|
* it's intended for. The `AnyCase<Browser>` type for the browser parameter
|
||||||
|
* implies that browser names like 'chrome', 'firefox', etc., can be provided
|
||||||
|
* in various casings.
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @param {Manifest} manifest
|
* @param {Manifest} manifest The core manifest data for the extension,
|
||||||
* @param {AnyCase<Browser>} browser
|
* compatible with `chrome.runtime.ManifestV3` as defined by the {@link Manifest} type.
|
||||||
* @return {*} {@link BuildTarget}
|
* @param {AnyCase<Browser>} browser The target browser identifier (e.g., 'chrome', 'firefox', 'CHROME').
|
||||||
|
* Refers to the {@link Browser} type, allowing for flexible casing.
|
||||||
|
* @returns {BuildTarget} An object that pairs the `manifest` with its target `browser`.
|
||||||
|
* The structure is `{ manifest: Manifest; browser: AnyCase<Browser>; }`
|
||||||
|
* as defined by the {@link BuildTarget} type.
|
||||||
*/
|
*/
|
||||||
export function createManifest(
|
export function createManifest(
|
||||||
manifest: Manifest,
|
manifest: Manifest,
|
||||||
@@ -19,14 +29,17 @@ export function createManifest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a base Manifest to inherit from
|
* Defines a base manifest object.
|
||||||
* type Manifest = chrome.runtime.ManifestV3
|
* This function is typically used to establish a common, shared foundation for an extension's manifest
|
||||||
*
|
* (compatible with `chrome.runtime.ManifestV3` as per the {@link Manifest} type).
|
||||||
* use as shared base to extend inBrowser manifests
|
* This base can then be extended or modified for different browsers or specific build configurations.
|
||||||
|
* For example, you might define core permissions and properties here, and then add
|
||||||
|
* browser-specific keys in subsequent steps.
|
||||||
*
|
*
|
||||||
* @export
|
* @export
|
||||||
* @param {Manifest} manifest
|
* @param {Manifest} manifest The core manifest data to be used as a base.
|
||||||
* @return {*} {@link Manifest}
|
* This should conform to the {@link Manifest} type structure.
|
||||||
|
* @returns {Manifest} The provided manifest object, intended to serve as a reusable base.
|
||||||
*/
|
*/
|
||||||
export function createManifestBase(manifest: Manifest): Manifest {
|
export function createManifestBase(manifest: Manifest): Manifest {
|
||||||
return manifest;
|
return manifest;
|
||||||
|
|||||||
+42
-9
@@ -1,27 +1,60 @@
|
|||||||
// vite-plugin-inline-worker-dev.ts
|
// vite-plugin-inline-worker-dev.ts
|
||||||
|
// vite-plugin-inline-worker-dev.ts
|
||||||
import { Plugin } from "vite";
|
import { Plugin } from "vite";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { build, transform } from "esbuild";
|
import { build } from "esbuild";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Vite plugin designed for bundling and inlining web worker scripts during development.
|
||||||
|
* This plugin specifically targets module imports that include a `?inlineWorker` query parameter.
|
||||||
|
* When such an import is encountered, the plugin bundles the worker script using `esbuild`
|
||||||
|
* and then generates JavaScript code that inlines this bundled worker as a Blob,
|
||||||
|
* creating the worker instance via `URL.createObjectURL()`.
|
||||||
|
* The name "vite:inline-worker-dev" suggests it's primarily intended for development builds.
|
||||||
|
*
|
||||||
|
* @returns {Plugin} A Vite plugin object with `name` and `load` properties.
|
||||||
|
*/
|
||||||
export default function InlineWorkerDevPlugin(): Plugin {
|
export default function InlineWorkerDevPlugin(): Plugin {
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
* The unique name of this Vite plugin.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
name: "vite:inline-worker-dev",
|
name: "vite:inline-worker-dev",
|
||||||
|
/**
|
||||||
|
* The Vite hook responsible for loading and transforming modules.
|
||||||
|
* This function intercepts modules imported with `?inlineWorker`.
|
||||||
|
* For such modules, it bundles the worker script and returns JavaScript code
|
||||||
|
* that, when executed, will create an instance of this worker from an inlined Blob.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} id The path or ID of the module Vite is attempting to load,
|
||||||
|
* potentially including query parameters (e.g., "/path/to/worker.ts?inlineWorker").
|
||||||
|
* @returns {Promise<string | null>} A promise that resolves to:
|
||||||
|
* - `null` if the module ID does not include `?inlineWorker`.
|
||||||
|
* - A string of JavaScript code if the module is an inline worker.
|
||||||
|
* This code will define a default export function (e.g., `InlineWorker`)
|
||||||
|
* that, when called, creates and returns a new `Worker` instance
|
||||||
|
* from the bundled and inlined worker script.
|
||||||
|
*/
|
||||||
async load(id) {
|
async load(id) {
|
||||||
if (id.includes("?inlineWorker")) {
|
if (id.includes("?inlineWorker")) {
|
||||||
const [cleanPath] = id.split("?");
|
const [cleanPath] = id.split("?");
|
||||||
console.log("cleanPath", cleanPath);
|
// Note: Original code had `await fs.readFile(cleanPath, "utf-8");` but `code` wasn't used.
|
||||||
const code = await fs.readFile(cleanPath, "utf-8");
|
// `esbuild` directly takes `cleanPath` as an entry point.
|
||||||
const result = await build({
|
const result = await build({
|
||||||
entryPoints: [cleanPath],
|
entryPoints: [cleanPath], // esbuild uses the file path directly
|
||||||
bundle: true,
|
bundle: true,
|
||||||
write: false,
|
write: false, // We want the output in memory, not written to disk
|
||||||
platform: "browser",
|
platform: "browser", // Target environment for the worker code
|
||||||
format: "iife",
|
format: "iife", // Immediately Invoked Function Expression, suitable for workers
|
||||||
target: "esnext",
|
target: "esnext", // Transpile to modern JavaScript
|
||||||
});
|
});
|
||||||
|
|
||||||
const workerCode = result.outputFiles[0].text;
|
const workerCode = result.outputFiles[0].text;
|
||||||
|
|
||||||
|
// Construct JavaScript code that will create the worker from a Blob.
|
||||||
|
// This code is what gets returned to Vite and replaces the original import.
|
||||||
const workerBlobCode = `
|
const workerBlobCode = `
|
||||||
const code = ${JSON.stringify(workerCode)};
|
const code = ${JSON.stringify(workerCode)};
|
||||||
export default function InlineWorker() {
|
export default function InlineWorker() {
|
||||||
@@ -31,7 +64,7 @@ export default function InlineWorkerDevPlugin(): Plugin {
|
|||||||
`;
|
`;
|
||||||
return workerBlobCode;
|
return workerBlobCode;
|
||||||
}
|
}
|
||||||
return null;
|
return null; // Let Vite handle other modules normally
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-17
@@ -1,8 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
* This script is a command-line utility for publishing the BetterSEQTA+ extension.
|
||||||
|
* It automates the process of finding the latest built extension ZIP files for specified
|
||||||
|
* browsers, zipping the project source code (for Firefox), and then invoking the
|
||||||
|
* `publish-extension` tool with the appropriate arguments.
|
||||||
|
*
|
||||||
|
* To use this script, invoke it with Node.js followed by browser arguments:
|
||||||
|
* e.g., `node lib/publish.js --b chrome firefox`
|
||||||
|
* or `node lib/publish.js --b chrome`
|
||||||
|
* or `node lib/publish.js --b firefox`
|
||||||
|
*/
|
||||||
|
|
||||||
const glob = require("glob");
|
const glob = require("glob");
|
||||||
const semver = require("semver");
|
const semver = require("semver");
|
||||||
const { execSync } = require("child_process");
|
const { execSync } = require("child_process");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the latest version string from a list of filenames that include version numbers.
|
||||||
|
* Filenames are expected to follow a pattern like `betterseqtaplus@3.4.5.1-chrome.zip`.
|
||||||
|
* This function handles potential 4-part versions (e.g., `3.4.5.1`) by trimming them
|
||||||
|
* to 3 parts (e.g., `3.4.5`) for comparison using the `semver` library. After identifying
|
||||||
|
* the latest semver-compatible version, it returns the original full version string
|
||||||
|
* (e.g., "3.4.5.1") that corresponds to this latest version.
|
||||||
|
*
|
||||||
|
* @param {string[]} files An array of filenames.
|
||||||
|
* @returns {string | null} The latest version string (e.g., "3.4.5.1") found among the files,
|
||||||
|
* or `null` if no valid version numbers are found or no files are provided.
|
||||||
|
*/
|
||||||
function getLatestVersion(files) {
|
function getLatestVersion(files) {
|
||||||
console.log("Files passed to getLatestVersion:", files);
|
console.log("Files passed to getLatestVersion:", files);
|
||||||
|
|
||||||
@@ -19,32 +44,56 @@ function getLatestVersion(files) {
|
|||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
||||||
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
|
const fullVersion = match[1]; // Original version (e.g., 3.4.5.1)
|
||||||
const semverVersion = fullVersion.split(".").slice(0, 3).join("."); // Trim to 3.4.5
|
// Trim to 3 parts for semver comparison, as semver typically handles X.Y.Z
|
||||||
|
const semverVersion = fullVersion.split(".").slice(0, 3).join(".");
|
||||||
|
|
||||||
return { fullVersion, semverVersion };
|
return { fullVersion, semverVersion };
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean); // Remove null entries if any file didn't match
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"Extracted versions:",
|
"Extracted versions:",
|
||||||
versions.map((v) => v.semverVersion),
|
versions.map((v) => v.semverVersion),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (versions.length === 0) {
|
||||||
|
console.log("No versions extracted.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Find latest version using the trimmed semver format
|
// Find latest version using the trimmed semver format
|
||||||
const latestSemver = semver.maxSatisfying(
|
const latestSemver = semver.maxSatisfying(
|
||||||
versions.map((v) => v.semverVersion),
|
versions.map((v) => v.semverVersion),
|
||||||
"*",
|
"*", // Satisfy any version, effectively finding the max
|
||||||
);
|
);
|
||||||
console.log("Latest SemVer-compatible version:", latestSemver);
|
console.log("Latest SemVer-compatible version:", latestSemver);
|
||||||
|
|
||||||
// Get the full version that matches the latest SemVer version
|
if (!latestSemver) {
|
||||||
const latestVersion =
|
console.log("Could not determine latest semver version.");
|
||||||
versions.find((v) => v.semverVersion === latestSemver)?.fullVersion || null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Final selected latest version:", latestVersion);
|
// Get the original full version string that matches the identified latest SemVer version
|
||||||
return latestVersion;
|
const latestVersionData = versions.find(
|
||||||
|
(v) => v.semverVersion === latestSemver,
|
||||||
|
);
|
||||||
|
const latestFullVersion = latestVersionData ? latestVersionData.fullVersion : null;
|
||||||
|
|
||||||
|
console.log("Final selected latest version:", latestFullVersion);
|
||||||
|
return latestFullVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the path to the latest built ZIP file for a specific browser.
|
||||||
|
* It constructs a glob pattern based on the browser name (e.g., `dist/betterseqtaplus@*-*chrome.zip`),
|
||||||
|
* finds all matching files, and then uses `getLatestVersion` to identify the version string
|
||||||
|
* of the most recent file. Finally, it returns the full path to that specific file.
|
||||||
|
*
|
||||||
|
* @param {string} browser A string indicating the target browser (e.g., "chrome", "firefox").
|
||||||
|
* @returns {string | undefined} The filepath string to the latest ZIP file for the specified browser,
|
||||||
|
* or `undefined` if no matching file is found or if the latest version
|
||||||
|
* cannot be determined.
|
||||||
|
*/
|
||||||
function getLatestFiles(browser) {
|
function getLatestFiles(browser) {
|
||||||
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
const pattern = `dist/betterseqtaplus@*-*${browser}.zip`;
|
||||||
console.log("Glob pattern:", pattern);
|
console.log("Glob pattern:", pattern);
|
||||||
@@ -52,15 +101,32 @@ function getLatestFiles(browser) {
|
|||||||
const files = glob.sync(pattern);
|
const files = glob.sync(pattern);
|
||||||
console.log("Files found for browser", browser, ":", files);
|
console.log("Files found for browser", browser, ":", files);
|
||||||
|
|
||||||
const latestVersion = getLatestVersion(files);
|
if (files.length === 0) {
|
||||||
|
console.log("No files found for browser", browser);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the exact file by matching the original full version
|
const latestVersion = getLatestVersion(files);
|
||||||
|
if (!latestVersion) {
|
||||||
|
console.log("Could not determine latest version for browser", browser);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the exact file by matching the original full version string
|
||||||
const latestFile = files.find((file) => file.includes(`@${latestVersion}-`));
|
const latestFile = files.find((file) => file.includes(`@${latestVersion}-`));
|
||||||
|
|
||||||
console.log("Latest file for browser", browser, ":", latestFile);
|
console.log("Latest file for browser", browser, ":", latestFile);
|
||||||
return latestFile;
|
return latestFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ZIP file of the project's source code, excluding specified development-related
|
||||||
|
* files and directories such as `node_modules`, `dist`, `.git`, etc.
|
||||||
|
* It uses the `7z` command-line tool to perform the archiving.
|
||||||
|
* The output filename is fixed as `dist/betterseqtaplus@latest-sources.zip`.
|
||||||
|
*
|
||||||
|
* @returns {string} The filename of the created ZIP file (e.g., `dist/betterseqtaplus@latest-sources.zip`).
|
||||||
|
*/
|
||||||
function zipSources() {
|
function zipSources() {
|
||||||
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
|
const zipFileName = `dist/betterseqtaplus@latest-sources.zip`;
|
||||||
|
|
||||||
@@ -74,17 +140,31 @@ function zipSources() {
|
|||||||
"LICENSE",
|
"LICENSE",
|
||||||
"package.json",
|
"package.json",
|
||||||
]
|
]
|
||||||
.map((pattern) => `-x!${pattern}`)
|
.map((pattern) => `-x!${pattern}`) // Format for 7z exclude syntax
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
// Command to zip the current directory's contents into zipFileName, applying exclude patterns
|
||||||
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
|
const zipCommand = `7z a ${zipFileName} . ${excludePatterns}`;
|
||||||
|
|
||||||
console.log("Zipping project sources with command:", zipCommand);
|
console.log("Zipping project sources with command:", zipCommand);
|
||||||
execSync(zipCommand, { stdio: "inherit" });
|
execSync(zipCommand, { stdio: "inherit" }); // Execute synchronously and show output
|
||||||
|
|
||||||
return zipFileName;
|
return zipFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orchestrates the extension publishing process for the specified browsers.
|
||||||
|
* This function performs the following steps:
|
||||||
|
* 1. Calls `getLatestFiles` to find the latest built ZIP for Chrome if "chrome" is in `browsers`.
|
||||||
|
* 2. Calls `getLatestFiles` to find the latest built ZIP for Firefox if "firefox" is in `browsers`.
|
||||||
|
* 3. Calls `zipSources` to create a source code ZIP if "firefox" is in `browsers` (required for Mozilla Add-ons).
|
||||||
|
* 4. Validates that all required files were found and that at least one browser was specified. Exits if not.
|
||||||
|
* 5. Constructs the `publish-extension` command-line string with the appropriate arguments
|
||||||
|
* based on the found ZIP files for the specified browsers.
|
||||||
|
* 6. Executes the constructed `publish-extension` command.
|
||||||
|
*
|
||||||
|
* @param {string[]} browsers An array of browser strings (e.g., ["chrome", "firefox"]) for which to publish the extension.
|
||||||
|
*/
|
||||||
function runPublishCommand(browsers) {
|
function runPublishCommand(browsers) {
|
||||||
const chromeZip = browsers.includes("chrome")
|
const chromeZip = browsers.includes("chrome")
|
||||||
? getLatestFiles("chrome")
|
? getLatestFiles("chrome")
|
||||||
@@ -92,6 +172,7 @@ function runPublishCommand(browsers) {
|
|||||||
const firefoxZip = browsers.includes("firefox")
|
const firefoxZip = browsers.includes("firefox")
|
||||||
? getLatestFiles("firefox")
|
? getLatestFiles("firefox")
|
||||||
: null;
|
: null;
|
||||||
|
// Sources are typically only needed for Firefox submissions
|
||||||
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
|
const firefoxSourcesZip = browsers.includes("firefox") ? zipSources() : null;
|
||||||
|
|
||||||
console.log("Chrome zip:", chromeZip);
|
console.log("Chrome zip:", chromeZip);
|
||||||
@@ -100,15 +181,16 @@ function runPublishCommand(browsers) {
|
|||||||
|
|
||||||
if (browsers.length === 0) {
|
if (browsers.length === 0) {
|
||||||
console.log("No browsers specified. Exiting.");
|
console.log("No browsers specified. Exiting.");
|
||||||
process.exit(0);
|
process.exit(0); // Exit gracefully if no action is needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if required files are missing for the specified browsers
|
||||||
if (
|
if (
|
||||||
(browsers.includes("chrome") && !chromeZip) ||
|
(browsers.includes("chrome") && !chromeZip) ||
|
||||||
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
|
(browsers.includes("firefox") && (!firefoxZip || !firefoxSourcesZip))
|
||||||
) {
|
) {
|
||||||
console.error("Could not find required zip files for specified browsers.");
|
console.error("Could not find required zip files for specified browsers.");
|
||||||
process.exit(1);
|
process.exit(1); // Exit with error status
|
||||||
}
|
}
|
||||||
|
|
||||||
let command = "publish-extension";
|
let command = "publish-extension";
|
||||||
@@ -120,12 +202,13 @@ function runPublishCommand(browsers) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log("Running command:", command);
|
console.log("Running command:", command);
|
||||||
execSync(command, { stdio: "inherit" });
|
execSync(command, { stdio: "inherit" }); // Execute and show output
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command-line arguments
|
// Parse command-line arguments to determine which browsers to publish for
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const browserIndex = args.indexOf("--b");
|
const browserIndex = args.indexOf("--b"); // Find the --b flag
|
||||||
|
// If --b is found, take all subsequent arguments as browser names
|
||||||
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
|
const browsers = browserIndex !== -1 ? args.slice(browserIndex + 1) : [];
|
||||||
|
|
||||||
runPublishCommand(browsers);
|
runPublishCommand(browsers);
|
||||||
|
|||||||
+42
-4
@@ -1,17 +1,55 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Vite plugin designed to improve the reliability of Hot Module Replacement (HMR)
|
||||||
|
* for global CSS files.
|
||||||
|
*
|
||||||
|
* When a JavaScript/TypeScript module that imports a CSS file is updated, Vite's HMR
|
||||||
|
* might not always reliably update the styles injected by that global CSS. This plugin
|
||||||
|
* attempts to mitigate this by listening for hot updates. If an updated module
|
||||||
|
* has direct importers that are CSS files (e.g., a JS file imports a global CSS file),
|
||||||
|
* this plugin will "touch" those CSS files by updating their access and modification
|
||||||
|
* timestamps using `fs.utimesSync`. This action can help signal to Vite or the browser
|
||||||
|
* that the CSS file has changed, potentially triggering a more reliable style reload.
|
||||||
|
*
|
||||||
|
* @returns {import('vite').Plugin} A Vite plugin object configured with `name` and `handleHotUpdate` hooks.
|
||||||
|
*/
|
||||||
export default function touchGlobalCSSPlugin() {
|
export default function touchGlobalCSSPlugin() {
|
||||||
return {
|
return {
|
||||||
|
/**
|
||||||
|
* The unique name of this Vite plugin.
|
||||||
|
* This name is used by Vite for identification purposes and will appear in logs.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
name: "touch-global-css",
|
name: "touch-global-css",
|
||||||
|
/**
|
||||||
|
* A Vite hook that is called when a module is hot-updated.
|
||||||
|
* This function inspects the importers of the updated module. If any of these
|
||||||
|
* importers are CSS files, their filesystem timestamps are updated ("touched").
|
||||||
|
*
|
||||||
|
* @param {object} context The context object provided by Vite's `handleHotUpdate` hook.
|
||||||
|
* @param {Array<import('vite').ModuleNode>} context.modules An array of `ModuleNode` instances that have been updated.
|
||||||
|
* This plugin specifically accesses `modules[0]._clientModule.importers`
|
||||||
|
* to find CSS files that import the updated module.
|
||||||
|
*/
|
||||||
handleHotUpdate({ modules }) {
|
handleHotUpdate({ modules }) {
|
||||||
// log all of the staticImportedUrls
|
// It's assumed `modules[0]` is the primary updated module of interest.
|
||||||
const importers = modules[0]._clientModule.importers;
|
// `_clientModule` and `importers` might be internal or less stable Vite APIs.
|
||||||
|
const importers = modules[0]?._clientModule?.importers;
|
||||||
|
if (importers) {
|
||||||
importers.forEach((importer) => {
|
importers.forEach((importer) => {
|
||||||
if (importer.file.includes(".css")) {
|
// Check if the importer is a CSS file
|
||||||
console.log("touching", importer.file);
|
if (importer.file && importer.file.includes(".css")) {
|
||||||
|
console.log("[touch-global-css] touching", importer.file);
|
||||||
|
try {
|
||||||
|
// Update the access and modification times of the CSS file to the current time
|
||||||
fs.utimesSync(importer.file, new Date(), new Date());
|
fs.utimesSync(importer.file, new Date(), new Date());
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[touch-global-css] Error touching file ${importer.file}:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+144
-8
@@ -1,6 +1,9 @@
|
|||||||
import type { ManifestV3Export } from "@crxjs/vite-plugin";
|
import type { ManifestV3Export } from "@crxjs/vite-plugin";
|
||||||
import { type AnyCase, createEnum } from "./utils";
|
import { type AnyCase, createEnum, ObjectValues } from "./utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates supported JavaScript frameworks for project generation or configuration.
|
||||||
|
*/
|
||||||
export const FrameworkEnum = {
|
export const FrameworkEnum = {
|
||||||
React: "React",
|
React: "React",
|
||||||
Vanilla: "Vanilla",
|
Vanilla: "Vanilla",
|
||||||
@@ -10,6 +13,9 @@ export const FrameworkEnum = {
|
|||||||
Vue: "Vue",
|
Vue: "Vue",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates supported web browsers, typically for targeting builds or configurations.
|
||||||
|
*/
|
||||||
export const BrowserEnum = {
|
export const BrowserEnum = {
|
||||||
Chrome: "Chrome",
|
Chrome: "Chrome",
|
||||||
Brave: "Brave",
|
Brave: "Brave",
|
||||||
@@ -19,15 +25,26 @@ export const BrowserEnum = {
|
|||||||
Safari: "Safari",
|
Safari: "Safari",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Enumerates supported programming languages for project setup.
|
||||||
|
* This enum is not exported, suggesting it's for internal use within this module or related modules.
|
||||||
|
*/
|
||||||
const LanguageEnum = {
|
const LanguageEnum = {
|
||||||
TypeScript: "TypeScript",
|
TypeScript: "TypeScript",
|
||||||
JavaScript: "JavaScript",
|
JavaScript: "JavaScript",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates supported styling options or libraries.
|
||||||
|
*/
|
||||||
export const StyleEnum = {
|
export const StyleEnum = {
|
||||||
Tailwind: "Tailwind",
|
Tailwind: "Tailwind",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerates supported package managers.
|
||||||
|
*/
|
||||||
export const PackageManagerEnum = {
|
export const PackageManagerEnum = {
|
||||||
Bun: "Bun",
|
Bun: "Bun",
|
||||||
PnPm: "PnPm",
|
PnPm: "PnPm",
|
||||||
@@ -35,7 +52,21 @@ export const PackageManagerEnum = {
|
|||||||
Yarn: "Yarn",
|
Yarn: "Yarn",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts
|
/**
|
||||||
|
* Defines the structure for browser-specific settings within a web extension manifest.
|
||||||
|
* This is particularly used for Firefox (gecko) extensions to specify properties like
|
||||||
|
* an extension ID, and minimum/maximum supported browser versions.
|
||||||
|
* The structure is based on common manifest extensions for Firefox.
|
||||||
|
* See: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings
|
||||||
|
* The link in the original code (// see: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/firefox-webext-browser/index.d.ts)
|
||||||
|
* also points to type definitions that include this structure.
|
||||||
|
*
|
||||||
|
* @property {object} [browser_specific_settings] - Container for browser-specific settings.
|
||||||
|
* @property {object} [browser_specific_settings.gecko] - Settings specific to Gecko-based browsers (e.g., Firefox).
|
||||||
|
* @property {string} [browser_specific_settings.gecko.id] - The unique identifier for the extension in Firefox.
|
||||||
|
* @property {string} [browser_specific_settings.gecko.strict_min_version] - The minimum version of Firefox the extension is compatible with.
|
||||||
|
* @property {string} [browser_specific_settings.gecko.strict_max_version] - The maximum version of Firefox the extension is compatible with.
|
||||||
|
*/
|
||||||
export type BrowserSpecificSettings = {
|
export type BrowserSpecificSettings = {
|
||||||
browser_specific_settings?: {
|
browser_specific_settings?: {
|
||||||
gecko?: {
|
gecko?: {
|
||||||
@@ -46,59 +77,164 @@ export type BrowserSpecificSettings = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the structure of a Chrome Manifest V3 file.
|
||||||
|
* This type is an alias for `ManifestV3Export` from the `@crxjs/vite-plugin`,
|
||||||
|
* which provides a comprehensive definition for Chrome extension manifests.
|
||||||
|
*/
|
||||||
export type Manifest = ManifestV3Export;
|
export type Manifest = ManifestV3Export;
|
||||||
|
|
||||||
|
/** Alias for the `icons` property within a Chrome Manifest V3. */
|
||||||
export type ManifestIcons = chrome.runtime.ManifestIcons;
|
export type ManifestIcons = chrome.runtime.ManifestIcons;
|
||||||
|
/** Alias for the `background` property within a Chrome Manifest V3. */
|
||||||
export type ManifestBackground = chrome.runtime.ManifestV3["background"];
|
export type ManifestBackground = chrome.runtime.ManifestV3["background"];
|
||||||
|
/** Alias for the `content_scripts` property within a Chrome Manifest V3. */
|
||||||
export type ManifestContentScripts =
|
export type ManifestContentScripts =
|
||||||
chrome.runtime.ManifestV3["content_scripts"];
|
chrome.runtime.ManifestV3["content_scripts"];
|
||||||
|
/** Alias for the `web_accessible_resources` property within a Chrome Manifest V3. */
|
||||||
export type ManifestWebAccessibleResources =
|
export type ManifestWebAccessibleResources =
|
||||||
chrome.runtime.ManifestV3["web_accessible_resources"];
|
chrome.runtime.ManifestV3["web_accessible_resources"];
|
||||||
|
/** Alias for the `commands` property within a Chrome Manifest V3. */
|
||||||
export type ManifestCommands = chrome.runtime.ManifestV3["commands"];
|
export type ManifestCommands = chrome.runtime.ManifestV3["commands"];
|
||||||
|
/** Alias for the `action` property (or `browser_action`/`page_action`) within a Chrome Manifest V3. */
|
||||||
export type ManifestAction = chrome.runtime.ManifestV3["action"];
|
export type ManifestAction = chrome.runtime.ManifestV3["action"];
|
||||||
|
/** Alias for the `permissions` property within a Chrome Manifest V3. */
|
||||||
export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"];
|
export type ManifestPermissions = chrome.runtime.ManifestV3["permissions"];
|
||||||
|
/** Alias for the `options_ui` property within a Chrome Manifest V3. */
|
||||||
export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"];
|
export type ManifestOptionsUI = chrome.runtime.ManifestV3["options_ui"];
|
||||||
|
/** Alias for the `chrome_url_overrides` property within a Chrome Manifest V3. */
|
||||||
export type ManifestURLOverrides =
|
export type ManifestURLOverrides =
|
||||||
chrome.runtime.ManifestV3["chrome_url_overrides"];
|
chrome.runtime.ManifestV3["chrome_url_overrides"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a type that accepts a string literal `T` in either its capitalized or lowercase form.
|
||||||
|
* Useful for defining types that should be case-insensitive for specific known strings.
|
||||||
|
* @template T - A string literal type.
|
||||||
|
*/
|
||||||
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>;
|
export type BrowserName<T extends string> = Capitalize<T> | Lowercase<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a record type where both keys and values are derived from a string literal `T`,
|
||||||
|
* specifically using `BrowserName<T>` which allows for capitalized or lowercase forms.
|
||||||
|
* This could be used to define an object where, for example, keys are 'Chrome' or 'chrome'
|
||||||
|
* and values are also 'Chrome' or 'chrome'.
|
||||||
|
* @template T - A string literal type, typically representing a browser name.
|
||||||
|
*/
|
||||||
export type BrowserEnumType<T extends string> = {
|
export type BrowserEnumType<T extends string> = {
|
||||||
[browser in BrowserName<T>]: BrowserName<T>;
|
[browser in BrowserName<T>]: BrowserName<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the target browser for a build, allowing for various casings of browser names
|
||||||
|
* (e.g., "chrome", "Chrome", "CHROME") through the `AnyCase<Browser>` utility type.
|
||||||
|
* `Browser` itself is a union of specific browser name strings (e.g., "Chrome" | "Firefox").
|
||||||
|
*/
|
||||||
export type BuildMode = AnyCase<Browser>;
|
export type BuildMode = AnyCase<Browser>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an object structure that pairs a web extension `Manifest`
|
||||||
|
* with its target `browser` (represented as `AnyCase<Browser>`).
|
||||||
|
* This is commonly used in build processes to manage configurations for different browsers.
|
||||||
|
*/
|
||||||
export type BuildTarget = {
|
export type BuildTarget = {
|
||||||
manifest: Manifest;
|
manifest: Manifest;
|
||||||
browser: AnyCase<Browser>;
|
browser: AnyCase<Browser>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the configuration options for a build process.
|
||||||
|
* @property {"build" | "serve"} [command] - The type of build command (e.g., 'build' for production, 'serve' for development).
|
||||||
|
* @property {AnyCase<Browser> | string | undefined} [mode] - The target build mode, typically a browser name (allowing various casings)
|
||||||
|
* or potentially other custom mode strings.
|
||||||
|
*/
|
||||||
export type BuildConfig = {
|
export type BuildConfig = {
|
||||||
command?: "build" | "serve";
|
command?: "build" | "serve";
|
||||||
mode?: AnyCase<Browser> | string | undefined;
|
mode?: AnyCase<Browser> | string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the structure for repository information, commonly found in `package.json`.
|
||||||
|
* @property {string} type - The type of the repository (e.g., "git").
|
||||||
|
* @property {string} [url] - The URL of the repository.
|
||||||
|
* @property {Bugs} [bugs] - An object containing information about where to report bugs.
|
||||||
|
*/
|
||||||
export interface Repository {
|
export interface Repository {
|
||||||
type: string;
|
type: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
bugs?: Bugs;
|
bugs?: Bugs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the structure for bug reporting information, often part of the `Repository` interface.
|
||||||
|
* @property {string} [url] - The URL of the issue tracker.
|
||||||
|
* @property {string} [email] - The email address for reporting bugs.
|
||||||
|
*/
|
||||||
export interface Bugs {
|
export interface Bugs {
|
||||||
url?: string;
|
url?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Browser = (typeof BrowserEnum)[keyof typeof BrowserEnum];
|
/**
|
||||||
|
* A string literal union type representing supported browser names, derived from the values of `BrowserEnum`.
|
||||||
|
* e.g., "Chrome" | "Firefox" | ...
|
||||||
|
*/
|
||||||
|
export type Browser = ObjectValues<typeof BrowserEnum>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A constant intended to provide access to browser names, potentially in various casings.
|
||||||
|
* Its type `AnyCase<Browser>` suggests it can be used where case-insensitivity for browser names is needed.
|
||||||
|
* The `createEnum(BrowserEnum)` call aims to produce a representation of browser names from `BrowserEnum`.
|
||||||
|
* Note: `createEnum` from `lib/utils.ts` has a declared return type of `ObjectValues<T>` (a union of values),
|
||||||
|
* while its implementation uses `Object.values()` which returns an array. This constant will hold the
|
||||||
|
* runtime array value, but its JSDoc type refers to the more restrictive `AnyCase<Browser>` union type.
|
||||||
|
*/
|
||||||
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum);
|
export const Browser: AnyCase<Browser> = createEnum(BrowserEnum);
|
||||||
|
|
||||||
export type PackageManager =
|
/**
|
||||||
(typeof PackageManagerEnum)[keyof typeof PackageManagerEnum];
|
* A string literal union type representing supported package managers, derived from the values of `PackageManagerEnum`.
|
||||||
|
* e.g., "Bun" | "PnPm" | "Npm" | "Yarn"
|
||||||
|
*/
|
||||||
|
export type PackageManager = ObjectValues<typeof PackageManagerEnum>;
|
||||||
|
/**
|
||||||
|
* A constant intended to provide access to package manager names, potentially in various casings.
|
||||||
|
* Its type `AnyCase<PackageManager>` suggests it can be used where case-insensitivity for package manager names is needed.
|
||||||
|
* Utilizes `createEnum(PackageManagerEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||||
|
*/
|
||||||
export const PackageManager: AnyCase<PackageManager> =
|
export const PackageManager: AnyCase<PackageManager> =
|
||||||
createEnum(PackageManagerEnum);
|
createEnum(PackageManagerEnum);
|
||||||
|
|
||||||
export type Framework = (typeof FrameworkEnum)[keyof typeof FrameworkEnum];
|
/**
|
||||||
|
* A string literal union type representing supported JavaScript frameworks, derived from the values of `FrameworkEnum`.
|
||||||
|
* e.g., "React" | "Vanilla" | ...
|
||||||
|
*/
|
||||||
|
export type Framework = ObjectValues<typeof FrameworkEnum>;
|
||||||
|
/**
|
||||||
|
* A constant intended to provide access to framework names, potentially in various casings.
|
||||||
|
* Its type `AnyCase<Framework>` suggests it can be used where case-insensitivity for framework names is needed.
|
||||||
|
* Utilizes `createEnum(FrameworkEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||||
|
*/
|
||||||
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum);
|
export const Framework: AnyCase<Framework> = createEnum(FrameworkEnum);
|
||||||
|
|
||||||
export type Style = (typeof StyleEnum)[keyof typeof StyleEnum];
|
/**
|
||||||
|
* A string literal union type representing supported styling options, derived from the values of `StyleEnum`.
|
||||||
|
* e.g., "Tailwind"
|
||||||
|
*/
|
||||||
|
export type Style = ObjectValues<typeof StyleEnum>;
|
||||||
|
/**
|
||||||
|
* A constant intended to provide access to style option names, potentially in various casings.
|
||||||
|
* Its type `AnyCase<Style>` suggests it can be used where case-insensitivity for style names is needed.
|
||||||
|
* Utilizes `createEnum(StyleEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||||
|
*/
|
||||||
export const Style: AnyCase<Style> = createEnum(StyleEnum);
|
export const Style: AnyCase<Style> = createEnum(StyleEnum);
|
||||||
|
|
||||||
export type Language = (typeof LanguageEnum)[keyof typeof LanguageEnum];
|
/**
|
||||||
|
* A string literal union type representing supported programming languages, derived from the values of `LanguageEnum`.
|
||||||
|
* e.g., "TypeScript" | "JavaScript"
|
||||||
|
*/
|
||||||
|
export type Language = ObjectValues<typeof LanguageEnum>;
|
||||||
|
/**
|
||||||
|
* A constant intended to provide access to programming language names, potentially in various casings.
|
||||||
|
* Its type `AnyCase<Language>` suggests it can be used where case-insensitivity for language names is needed.
|
||||||
|
* Utilizes `createEnum(LanguageEnum)`. Refer to notes on `Browser` constant regarding `createEnum` behavior.
|
||||||
|
*/
|
||||||
export const Language: AnyCase<Language> = createEnum(LanguageEnum);
|
export const Language: AnyCase<Language> = createEnum(LanguageEnum);
|
||||||
|
|||||||
@@ -1,21 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Extracts a union type of all values from the properties of an object type `T`.
|
||||||
|
*
|
||||||
|
* @template T - An object type (typically a Record or an enum-like object).
|
||||||
|
* @example
|
||||||
|
* type MyObject = { a: "foo", b: "bar", c: 123 };
|
||||||
|
* type MyObjectValues = ObjectValues<MyObject>; // "foo" | "bar" | 123
|
||||||
|
*/
|
||||||
export type ObjectValues<T> = T[keyof T];
|
export type ObjectValues<T> = T[keyof T];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a union of an object's string values, often used to represent the set of possible values for an enum-like object.
|
||||||
|
* Note: The implementation `Object.values(enumObj) as unknown as ObjectValues<T>` returns an array at runtime,
|
||||||
|
* but the declared return type `ObjectValues<T>` is a union of the object's property values.
|
||||||
|
* This type signature suggests it's intended to represent the set of possible string values from `enumObj`.
|
||||||
|
*
|
||||||
|
* @template T - An object type where keys are strings and values are strings (e.g., `const MyEnum = { VAL_A: "A", VAL_B: "B" }`).
|
||||||
|
* @param {T} enumObj - The object from which to extract values.
|
||||||
|
* @returns {ObjectValues<T>} A union type representing all possible string values of the `enumObj`.
|
||||||
|
* For example, if `enumObj` is `{ A: "valA", B: "valB" }`, the return type is `"valA" | "valB"`.
|
||||||
|
* (Runtime behavior of `Object.values()` is to return an array like `["valA", "valB"]`).
|
||||||
|
*/
|
||||||
export function createEnum<T extends Record<string, string>>(enumObj: T) {
|
export function createEnum<T extends Record<string, string>>(enumObj: T) {
|
||||||
return Object.values(enumObj) as unknown as ObjectValues<T>;
|
return Object.values(enumObj) as unknown as ObjectValues<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a union type that includes various case formats (uppercase, lowercase, capitalized, uncapitalized)
|
||||||
|
* of a given string literal type `T`.
|
||||||
|
*
|
||||||
|
* @template T - A string literal type.
|
||||||
|
* @example
|
||||||
|
* type MyString = "example";
|
||||||
|
* type MyStringAnyCase = AnyCase<MyString>; // "EXAMPLE" | "example" | "Example" | "example" (Uncapitalize<"Example"> is "example")
|
||||||
|
*/
|
||||||
export type AnyCase<T extends string> =
|
export type AnyCase<T extends string> =
|
||||||
| Uppercase<T>
|
| Uppercase<T>
|
||||||
| Lowercase<T>
|
| Lowercase<T>
|
||||||
| Capitalize<T>
|
| Capitalize<T>
|
||||||
| Uncapitalize<T>;
|
| Uncapitalize<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a union type that includes various case formats (uppercase, lowercase, capitalized, uncapitalized)
|
||||||
|
* of the union of two given string literal types `T` and `K`.
|
||||||
|
* This is useful for representing a combined set of related string constants where case variations are permitted for each.
|
||||||
|
*
|
||||||
|
* @template T - A string literal type.
|
||||||
|
* @template K - Another string literal type.
|
||||||
|
* @example
|
||||||
|
* type Lang1 = "english";
|
||||||
|
* type Lang2 = "french";
|
||||||
|
* type CombinedLangsAnyCase = AnyCaseLanguage<Lang1, Lang2>;
|
||||||
|
* // Result includes: "ENGLISH" | "english" | "English" | "FRENCH" | "french" | "French" etc.
|
||||||
|
* // for all case variations of "english" and "french".
|
||||||
|
*/
|
||||||
export type AnyCaseLanguage<T extends string, K extends string> =
|
export type AnyCaseLanguage<T extends string, K extends string> =
|
||||||
| Uppercase<T | K>
|
| Uppercase<T | K>
|
||||||
| Lowercase<T | K>
|
| Lowercase<T | K>
|
||||||
| Capitalize<T | K>
|
| Capitalize<T | K>
|
||||||
| Uncapitalize<T | K>;
|
| Uncapitalize<T | K>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a new object type containing only the keys of `T` whose properties are optional
|
||||||
|
* (i.e., their type includes `undefined`). The values associated with these keys retain their original types.
|
||||||
|
*
|
||||||
|
* @template T - An object type.
|
||||||
|
* @example
|
||||||
|
* type MyObject = {
|
||||||
|
* requiredProp: string;
|
||||||
|
* optionalProp?: number;
|
||||||
|
* anotherOptional?: boolean | undefined;
|
||||||
|
* nullProp: string | null;
|
||||||
|
* };
|
||||||
|
* type MyOptionalProps = OptionalKeys<MyObject>;
|
||||||
|
* // MyOptionalProps would be conceptually equivalent to:
|
||||||
|
* // {
|
||||||
|
* // optionalProp?: number;
|
||||||
|
* // anotherOptional?: boolean | undefined;
|
||||||
|
* // }
|
||||||
|
* // The actual resulting type is an object type with only these optional keys.
|
||||||
|
*/
|
||||||
export type OptionalKeys<T> = {
|
export type OptionalKeys<T> = {
|
||||||
[K in keyof T as undefined extends T[K] ? K : never]: T[K];
|
[K in keyof T as undefined extends T[K] ? K : never]: T[K];
|
||||||
};
|
};
|
||||||
|
|||||||
+15
-19
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "betterseqtaplus",
|
"name": "betterseqtaplus",
|
||||||
"version": "3.4.6.1",
|
"version": "3.4.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
|
||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
@@ -16,10 +16,7 @@
|
|||||||
"dependency-graph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg",
|
"dependency-graph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg",
|
||||||
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
|
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
|
||||||
"publish": "bun lib/publish.js --b",
|
"publish": "bun lib/publish.js --b",
|
||||||
"zip": "bedframe zip",
|
"zip": "bedframe zip"
|
||||||
"test": "jest",
|
|
||||||
"test:watch": "jest --watch",
|
|
||||||
"test:coverage": "jest --coverage"
|
|
||||||
},
|
},
|
||||||
"targets": {
|
"targets": {
|
||||||
"prod": {
|
"prod": {
|
||||||
@@ -31,36 +28,34 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": {
|
"author": {
|
||||||
"name": "SethBurkart123",
|
"name": "SethBurkart123",
|
||||||
"email": "betterseqta@betterseqta.com",
|
"email": "betterseqta.plus@gmail.com",
|
||||||
"url": "https://github.com/BetterSEQTA/BetterSEQTA-plus"
|
"url": "https://github.com/BetterSEQTA/BetterSEQTA-plus"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-transform-runtime": "^7.26.9",
|
"@babel/plugin-transform-runtime": "^7.26.9",
|
||||||
"@babel/runtime": "^7.26.9",
|
"@babel/runtime": "^7.26.9",
|
||||||
"@bedframe/cli": "^0.0.91",
|
"@bedframe/cli": "^0.0.95",
|
||||||
"@crxjs/vite-plugin": "2.0.0-beta.32",
|
"@crxjs/vite-plugin": "^2.2.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/mime-types": "^2.1.4",
|
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^10.0.0",
|
||||||
"dependency-cruiser": "^16.10.0",
|
"dependency-cruiser": "^17.0.1",
|
||||||
"eslint": "9.22.0",
|
"eslint": "^9.33.0",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"jest": "^29.7.0",
|
"mime-types": "^3.0.1",
|
||||||
"mime-types": "^2.1.35",
|
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"publish-browser-extension": "^3.0.0",
|
"publish-browser-extension": "^3.0.1",
|
||||||
"sass": "^1.85.1",
|
"sass": "^1.85.1",
|
||||||
"sass-loader": "^16.0.5",
|
"sass-loader": "^16.0.5",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"tailwindcss": "3",
|
"tailwindcss": "3",
|
||||||
"ts-jest": "^29.3.4",
|
|
||||||
"url": "^0.11.4"
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@bedframe/core": "^0.0.46",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -71,16 +66,17 @@
|
|||||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tsconfig/svelte": "^5.0.4",
|
"@tsconfig/svelte": "^5.0.4",
|
||||||
"@types/chrome": "^0.0.308",
|
"@types/chrome": "^0.1.4",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^4.2.0",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^24.3.0",
|
||||||
"@types/sortablejs": "^1.15.8",
|
"@types/sortablejs": "^1.15.8",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/webextension-polyfill": "^0.12.3",
|
"@types/webextension-polyfill": "^0.12.3",
|
||||||
"@uiw/codemirror-extensions-color": "^4.23.10",
|
"@uiw/codemirror-extensions-color": "^4.23.10",
|
||||||
"@uiw/codemirror-theme-github": "^4.23.10",
|
"@uiw/codemirror-theme-github": "^4.23.10",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"canvas-confetti": "^1.9.3",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"color": "^5.0.0",
|
"color": "^5.0.0",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
|
|||||||
-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
|
|
||||||
+19
-4
@@ -9,6 +9,7 @@ import browser from "webextension-polyfill";
|
|||||||
import * as plugins from "@/plugins";
|
import * as plugins from "@/plugins";
|
||||||
import { main } from "@/seqta/main";
|
import { main } from "@/seqta/main";
|
||||||
import { delay } from "./seqta/utils/delay";
|
import { delay } from "./seqta/utils/delay";
|
||||||
|
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
|
||||||
|
|
||||||
export let MenuOptionsOpen = false;
|
export let MenuOptionsOpen = false;
|
||||||
|
|
||||||
@@ -24,6 +25,19 @@ if (document.childNodes[1]) {
|
|||||||
init();
|
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() {
|
async function init() {
|
||||||
const hasSEQTATitle = document.title.includes("SEQTA Learn");
|
const hasSEQTATitle = document.title.includes("SEQTA Learn");
|
||||||
|
|
||||||
@@ -51,15 +65,16 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|
||||||
if (settingsState.onoff) {
|
|
||||||
// Initialize legacy plugins
|
|
||||||
plugins.Monofile();
|
plugins.Monofile();
|
||||||
|
|
||||||
// Initialize new plugin system
|
if (settingsState.onoff) {
|
||||||
await plugins.initializePlugins();
|
await plugins.initializePlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settingsState.devMode) {
|
||||||
|
initializeHideSensitiveToggle();
|
||||||
|
}
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-72
@@ -49,7 +49,7 @@ browser.runtime.onMessage.addListener(
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "setDefaultStorage":
|
case "setDefaultStorage":
|
||||||
SetStorageValue(DefaultValues);
|
SetStorageValue(getDefaultValues());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "sendNews":
|
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,
|
onoff: true,
|
||||||
animatedbk: true,
|
animatedbk: true,
|
||||||
bksliderinput: "50",
|
bksliderinput: "50",
|
||||||
@@ -96,8 +107,8 @@ const DefaultValues: SettingsState = {
|
|||||||
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
|
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
|
||||||
originalSelectedColor: "",
|
originalSelectedColor: "",
|
||||||
DarkMode: true,
|
DarkMode: true,
|
||||||
animations: true,
|
animations: !isLowEndDevice,
|
||||||
assessmentsAverage: true,
|
assessmentsAverage: false,
|
||||||
defaultPage: "home",
|
defaultPage: "home",
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
@@ -116,7 +127,8 @@ const DefaultValues: SettingsState = {
|
|||||||
customshortcuts: [],
|
customshortcuts: [],
|
||||||
lettergrade: false,
|
lettergrade: false,
|
||||||
newsSource: "australia",
|
newsSource: "australia",
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function SetStorageValue(object: any) {
|
function SetStorageValue(object: any) {
|
||||||
for (var i in object) {
|
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.runtime.onInstalled.addListener(function (event) {
|
||||||
browser.storage.local.remove(["justupdated"]);
|
browser.storage.local.remove(["justupdated"]);
|
||||||
browser.storage.local.remove(["data"]);
|
browser.storage.local.remove(["data"]);
|
||||||
|
|
||||||
if (event.reason == "install" || event.reason == "update") {
|
if (event.reason == "install" || event.reason == "update") {
|
||||||
browser.storage.local.set({ justupdated: true });
|
browser.storage.local.set({ justupdated: true });
|
||||||
migrateLegacySettings();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import Parser from "rss-parser";
|
import Parser from "rss-parser";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches news articles specifically for Australia from the NewsAPI.
|
||||||
|
*
|
||||||
|
* This function handles a specific case for fetching Australian news. It includes a
|
||||||
|
* mechanism to retry the fetch operation by appending "%00" to the URL if a
|
||||||
|
* rate limit error (`response.code == "rateLimited"`) is encountered. This is
|
||||||
|
* likely a workaround for cache-busting or bypassing certain rate-limiting measures.
|
||||||
|
*
|
||||||
|
* @param {string} url The NewsAPI URL to fetch Australian news from.
|
||||||
|
* @param {any} sendResponse A callback function (likely from a browser extension message listener)
|
||||||
|
* to send the fetched news data back to the caller.
|
||||||
|
* It's called with an object like `{ news: responseData }`.
|
||||||
|
*/
|
||||||
const fetchAustraliaNews = async (url: string, sendResponse: any) => {
|
const fetchAustraliaNews = async (url: string, sendResponse: any) => {
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then((result) => result.json())
|
.then((result) => result.json())
|
||||||
@@ -12,6 +25,12 @@ const fetchAustraliaNews = async (url: string, sendResponse: any) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record mapping lowercase country codes (e.g., "usa", "canada") to an array
|
||||||
|
* of RSS feed URLs for news sources in that country.
|
||||||
|
*
|
||||||
|
* @type {Record<string, string[]>}
|
||||||
|
*/
|
||||||
const rssFeedsByCountry: Record<string, string[]> = {
|
const rssFeedsByCountry: Record<string, string[]> = {
|
||||||
usa: [
|
usa: [
|
||||||
"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
|
||||||
@@ -54,6 +73,25 @@ const rssFeedsByCountry: Record<string, string[]> = {
|
|||||||
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
|
netherlands: ["https://www.dutchnews.nl/feed/", "https://www.nrc.nl/rss/"],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches news articles based on a specified source.
|
||||||
|
*
|
||||||
|
* The source can be:
|
||||||
|
* 1. The string "australia": Fetches news from Australian sources via NewsAPI,
|
||||||
|
* handled by the `fetchAustraliaNews` function.
|
||||||
|
* 2. A lowercase country code (e.g., "usa", "canada"): Fetches news from a predefined
|
||||||
|
* list of RSS feeds for that country, as specified in `rssFeedsByCountry`.
|
||||||
|
* 3. A direct RSS feed URL (starting with "http"): Fetches news directly from this URL.
|
||||||
|
*
|
||||||
|
* The fetched articles are then sent back to the caller using the `sendResponse` callback.
|
||||||
|
*
|
||||||
|
* @param {string} source The news source identifier. This can be "australia", a
|
||||||
|
* lowercase country code, or a direct RSS feed URL.
|
||||||
|
* @param {any} sendResponse A callback function (typically from a browser extension
|
||||||
|
* message listener, like `chrome.runtime.onMessage`)
|
||||||
|
* used to send the fetched news data back to the caller.
|
||||||
|
* It's called with an object like `{ news: { articles: [...] } }`.
|
||||||
|
*/
|
||||||
export async function fetchNews(source: string, sendResponse: any) {
|
export async function fetchNews(source: string, sendResponse: any) {
|
||||||
if (source === "australia") {
|
if (source === "australia") {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|||||||
@@ -94,3 +94,57 @@ body:has(.outside-container:not(.hide))
|
|||||||
background: var(--text-primary) !important;
|
background: var(--text-primary) !important;
|
||||||
color: var(--theme-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;
|
||||||
|
}
|
||||||
|
|||||||
+619
-65
@@ -38,11 +38,27 @@ body,
|
|||||||
html {
|
html {
|
||||||
font-family: Rubik, sans-serif !important;
|
font-family: Rubik, sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Ensure native select dropdowns are readable on Windows */
|
||||||
|
select option {
|
||||||
|
background-color: #ffffff !important;
|
||||||
|
color: #111827 !important;
|
||||||
|
}
|
||||||
|
.dark select option {
|
||||||
|
background-color: #1f2937 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Consistent rounded corners for selects */
|
||||||
|
select {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
#container {
|
#container {
|
||||||
transition: 200ms;
|
transition: 200ms;
|
||||||
background: var(--auto-background) !important;
|
background: var(--auto-background) !important;
|
||||||
}
|
}
|
||||||
* {
|
:root * {
|
||||||
|
font-family: Rubik, sans-serif !important;
|
||||||
--theme-fg-parts: white;
|
--theme-fg-parts: white;
|
||||||
}
|
}
|
||||||
.extension-editor {
|
.extension-editor {
|
||||||
@@ -143,6 +159,16 @@ html {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
#main {
|
||||||
|
> .timetablepage {
|
||||||
|
> .quickbar {
|
||||||
|
.gutter {
|
||||||
|
border-bottom-left-radius: 15px;
|
||||||
|
border-bottom-right-radius: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.forums {
|
.forums {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
@@ -379,6 +405,18 @@ ul.magicDelete > li.deleting {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Allow long course/assessment names in the sidebar to wrap and break safely */
|
||||||
|
#menu li > label,
|
||||||
|
#menu section > label {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
text-transform: none;
|
||||||
|
font-size: 16px;
|
||||||
|
hyphens: auto;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
#menu {
|
#menu {
|
||||||
width: 270px;
|
width: 270px;
|
||||||
z-index: 19;
|
z-index: 19;
|
||||||
@@ -451,11 +489,6 @@ ul.magicDelete > li.deleting {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu li > label,
|
|
||||||
#menu section > label {
|
|
||||||
text-transform: none;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
#userActions {
|
#userActions {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -792,7 +825,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
min-height: 128px !important;
|
min-height: 128px !important;
|
||||||
}
|
}
|
||||||
.student #menu > ul::before {
|
.student #menu > ul::before {
|
||||||
background-image: var(--betterseqta-logo);
|
background-image: var(--betterseqta-logo) !important;
|
||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -801,6 +834,18 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.transparencyEffects [class*="BasicPanel__BasicPanel___q92_U"] > ol > li {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.transparencyEffects
|
||||||
|
[class*="BasicPanel__BasicPanel___q92_U"]
|
||||||
|
> ol
|
||||||
|
> li
|
||||||
|
+ li {
|
||||||
|
border-top: 1px solid var(--theme-offset-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.assessmentsWrapper .message {
|
.assessmentsWrapper .message {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -926,6 +971,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
.notices-container {
|
.notices-container {
|
||||||
input {
|
input {
|
||||||
border: none;
|
border: none;
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -998,8 +1044,8 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
#title {
|
#title {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary) !important;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary) !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-right: 56px !important;
|
padding-right: 56px !important;
|
||||||
@@ -1061,6 +1107,7 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
max-width: 1050px;
|
max-width: 1050px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
width: 1050px;
|
width: 1050px;
|
||||||
|
overflow-x: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
@@ -1107,9 +1154,11 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
transition: 200ms;
|
transition: 200ms;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 15em;
|
height: 15em;
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: row;
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: minmax(142px, 1fr);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
.day:first-child {
|
.day:first-child {
|
||||||
border-bottom-left-radius: 16px;
|
border-bottom-left-radius: 16px;
|
||||||
@@ -1172,11 +1221,6 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
.notices-container h2 {
|
|
||||||
margin: 20px;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
.notice {
|
.notice {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -1257,6 +1301,9 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
.customshortcut > svg {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
.colourbar {
|
.colourbar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
@@ -1318,7 +1365,17 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
font-size: 20px !important;
|
font-size: 20px !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
min-height: 46px;
|
min-height: 46px;
|
||||||
height: 36%;
|
/* Let the title expand naturally but clamp to 2 lines to avoid overlap */
|
||||||
|
height: auto;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
.day h3 {
|
.day h3 {
|
||||||
padding: 0px 5px;
|
padding: 0px 5px;
|
||||||
@@ -1347,20 +1404,6 @@ div > ol:has(.uiFileHandlerWrapper) {
|
|||||||
font-size: 10px !important;
|
font-size: 10px !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
.day-empty {
|
|
||||||
font-size: 30px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 16px 0;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.day-empty img {
|
|
||||||
margin: 20px;
|
|
||||||
height: 50%;
|
|
||||||
}
|
|
||||||
.day-empty p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.waitWindow {
|
.waitWindow {
|
||||||
background: var(--better-main);
|
background: var(--better-main);
|
||||||
}
|
}
|
||||||
@@ -1665,7 +1708,9 @@ iframe.userHTML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.programmeNavigator {
|
.programmeNavigator {
|
||||||
box-shadow: 0 0 40px 0px rgba(0,0,0,0.05);
|
box-shadow: 0 0 40px 0px rgba(0, 0, 0, 0.05);
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
.navigator {
|
.navigator {
|
||||||
padding: 6px !important;
|
padding: 6px !important;
|
||||||
@@ -1678,17 +1723,6 @@ iframe.userHTML {
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1;
|
|
||||||
top: 70px;
|
|
||||||
width: 390px;
|
|
||||||
height: 60px;
|
|
||||||
background: linear-gradient(to bottom, var(--background-primary) 50%, rgba(0, 0, 0, 0));
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
@@ -1745,7 +1779,9 @@ iframe.userHTML {
|
|||||||
background: var(--auto-background);
|
background: var(--auto-background);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
scale: 0.95;
|
scale: 0.95;
|
||||||
transition: opacity 0.2s ease-out, scale 0.1s ease-out;
|
transition:
|
||||||
|
opacity 0.2s ease-out,
|
||||||
|
scale 0.1s ease-out;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -1759,7 +1795,6 @@ iframe.userHTML {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1;
|
scale: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1783,7 +1818,9 @@ iframe.userHTML {
|
|||||||
.dark .programmeNavigator .navigator {
|
.dark .programmeNavigator .navigator {
|
||||||
.search {
|
.search {
|
||||||
background: var(--background-secondary) !important;
|
background: var(--background-secondary) !important;
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1), inset 0px 0px 15px 0px rgba(0, 0, 0, 0.1) !important;
|
box-shadow:
|
||||||
|
0px 0px 10px 0px rgba(0, 0, 0, 0.1),
|
||||||
|
inset 0px 0px 15px 0px rgba(0, 0, 0, 0.1) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark #main > .course > .content > h1 {
|
.dark #main > .course > .content > h1 {
|
||||||
@@ -1997,15 +2034,70 @@ div.entry.class[style*="width: 46.5%"] {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.entry.tutorial {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.entry.event {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.entry.new {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.liveEntry {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dailycalMarker {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.uiFileHandler .uiButton {
|
.uiFileHandler .uiButton {
|
||||||
border-radius: 32px !important;
|
border-radius: 32px !important;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
margin-top: 4px !important;
|
margin-top: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uiFile {
|
a.uiFile:not(.rows) {
|
||||||
border-radius: 8px !important;
|
display: flex !important;
|
||||||
transition: all 0.2s ease-in-out;
|
height: auto !important;
|
||||||
|
width: 200px !important;
|
||||||
|
border-radius: 80px !important;
|
||||||
|
place-items: center !important;
|
||||||
|
padding: 0px 9px !important;
|
||||||
|
gap: 6px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
box-shadow: inset 0 0 0px 2px rgba(255, 255, 255, 0.2) !important;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: white;
|
||||||
|
position: unset !important;
|
||||||
|
display: unset !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: 42px !important;
|
||||||
|
z-index: 1 !important;
|
||||||
|
flex: 0.199;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
position: unset !important;
|
||||||
|
background: transparent !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark .title a.uiFile {
|
.dark .title a.uiFile {
|
||||||
@@ -2109,10 +2201,32 @@ div.bar.flat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashlet-motd {
|
.dashlet-motd {
|
||||||
|
padding: 7px !important;
|
||||||
.message {
|
.message {
|
||||||
font-size: 24px !important;
|
font-size: 24px !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: none !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cke_toolbox > .cke_toolbar > .cke_toolgroup > .cke_button {
|
||||||
|
background: var(--background-secondary) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cke_toolbox > .cke_toolbar > .cke_combo > .cke_combo_button {
|
||||||
|
background: var(--background-secondary) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cke_toolbox > .cke_toolbar > .cke_combo > .cke_combo_button {
|
.cke_toolbox > .cke_toolbar > .cke_combo > .cke_combo_button {
|
||||||
@@ -2736,10 +2850,13 @@ body {
|
|||||||
.menuShown #menuToggle .hamburger-line:nth-child(3) {
|
.menuShown #menuToggle .hamburger-line:nth-child(3) {
|
||||||
transform: translateY(-6px) rotate(-45deg);
|
transform: translateY(-6px) rotate(-45deg);
|
||||||
}
|
}
|
||||||
.day-empty {
|
div.day-empty {
|
||||||
font-size: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 15em;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 16px 0;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
@@ -2748,6 +2865,7 @@ body {
|
|||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-size: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.upcoming-submittedtext {
|
.upcoming-submittedtext {
|
||||||
@@ -2957,7 +3075,6 @@ body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.day {
|
.day {
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -2985,18 +3102,6 @@ body {
|
|||||||
.dark .day h3 {
|
.dark .day h3 {
|
||||||
color: #c1bcbc;
|
color: #c1bcbc;
|
||||||
}
|
}
|
||||||
.day-empty {
|
|
||||||
font-size: 30px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.day-empty img {
|
|
||||||
margin: 20px;
|
|
||||||
height: 50%;
|
|
||||||
}
|
|
||||||
.day-empty p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-items {
|
.upcoming-items {
|
||||||
background: var(--background-primary);
|
background: var(--background-primary);
|
||||||
@@ -3254,6 +3359,22 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
transform-origin: center center;
|
transform-origin: center center;
|
||||||
}
|
}
|
||||||
|
.whatsnewTextContainer.privacyStatement p {
|
||||||
|
margin-bottom: 1.5ex;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.whatsnewTextContainer.privacyStatement a {
|
||||||
|
background: rgba(184, 184, 184, 0.1);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
padding: 2px 4px;
|
||||||
|
}
|
||||||
|
.dark .whatsnewTextContainer.privacyStatement a {
|
||||||
|
background: rgba(7, 7, 7, 0.1);
|
||||||
|
}
|
||||||
.whatsnewHeader {
|
.whatsnewHeader {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -3286,8 +3407,9 @@ body {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.whatsnewImg {
|
.whatsnewImg {
|
||||||
margin: 8px auto;
|
margin: 0 auto;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
|
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
@@ -3305,6 +3427,25 @@ body {
|
|||||||
text-indent: -1em;
|
text-indent: -1em;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
|
.whatsnewTextContainer .beta {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
padding-top: 0.125rem;
|
||||||
|
padding-bottom: 0.125rem;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #9a3412;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border: 1px solid rgba(253, 186, 140, 0.3);
|
||||||
|
background-color: #ffedd5;
|
||||||
|
border-color: rgba(253, 186, 140, 0.3);
|
||||||
|
}
|
||||||
|
.dark .whatsnewTextContainer .beta {
|
||||||
|
border-color: rgb(124 45 18 / 0.3);
|
||||||
|
background-color: rgb(124 45 18 / 0.3);
|
||||||
|
color: rgb(253 186 116);
|
||||||
|
}
|
||||||
.whatsnewTextHeader {
|
.whatsnewTextHeader {
|
||||||
font-size: 1.4em !important;
|
font-size: 1.4em !important;
|
||||||
color: #4dd868;
|
color: #4dd868;
|
||||||
@@ -3535,3 +3676,416 @@ body {
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notice-modal-content {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-unified-content.notice-modal-state {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notice card hover effects for main page cards
|
||||||
|
.notice-unified-content.notice-card-state:not([data-transitioning]) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-secondary) !important;
|
||||||
|
border-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-badge {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-staff {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-preview {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal styles
|
||||||
|
.notice-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-transition {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10001;
|
||||||
|
transition: none; // Controlled by motion animations
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-content {
|
||||||
|
background: var(--background-primary);
|
||||||
|
border-radius: 16px;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
&.notice-transitioning {
|
||||||
|
max-width: none;
|
||||||
|
max-height: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-unified-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--background-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .notice-unified-content {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-unified-content {
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
color: inherit !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content-title {
|
||||||
|
font-size: 20px !important; // Nice middle ground - not too big, not too small
|
||||||
|
font-weight: 600 !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
margin: 0 0 12px !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content-body {
|
||||||
|
font-size: 14px !important;
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
flex: 1;
|
||||||
|
display: block;
|
||||||
|
// Force stable layout dimensions - content renders at full size always
|
||||||
|
min-width: 600px; // Ensure tables have consistent width for layout
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ONLY difference between states is clipping!
|
||||||
|
&.notice-card-state {
|
||||||
|
.notice-content-body {
|
||||||
|
// Clip to show only 2 lines but keep full layout
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 3em; // ~2 lines worth of height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notice-modal-state {
|
||||||
|
.notice-close-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content-body {
|
||||||
|
// Show full content with scrolling
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
// Custom scrollbar for long content
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style content elements nicely
|
||||||
|
p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--theme-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-badge-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.notice-close-btn {
|
||||||
|
position: absolute !important;
|
||||||
|
font-size: 20px !important;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-badge-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-badge {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-staff {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-close {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 16px 20px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-body {
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
// Custom scrollbar
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style content elements
|
||||||
|
p {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--theme-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 12px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode adjustments
|
||||||
|
.dark {
|
||||||
|
.notice-card {
|
||||||
|
border-color: rgba(255, 255, 255, 0.05);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-content {
|
||||||
|
border-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile responsiveness
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notice-modal-overlay {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-content {
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-title {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 12px 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-modal-body {
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-card {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-preview {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2.home-subtitle {
|
||||||
|
margin: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ html.transparencyEffects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Blurs */
|
/* Blurs */
|
||||||
|
.search,
|
||||||
|
.document,
|
||||||
|
.border,
|
||||||
.draggable,
|
.draggable,
|
||||||
.notice,
|
.notice,
|
||||||
[class*="BasicPanel__BasicPanel___"],
|
[class*="BasicPanel__BasicPanel___"],
|
||||||
@@ -42,29 +45,23 @@ html.transparencyEffects {
|
|||||||
backdrop-filter: blur(80px);
|
backdrop-filter: blur(80px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-select,
|
||||||
|
.uiShortText.search,
|
||||||
.report {
|
.report {
|
||||||
backdrop-filter: blur(10px) !important;
|
backdrop-filter: blur(10px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu,
|
||||||
|
.kanban-column,
|
||||||
.whatsnewContainer,
|
.whatsnewContainer,
|
||||||
[class*="Message__Message___"] {
|
[class*="Message__Message___"] {
|
||||||
backdrop-filter: blur(50px);
|
backdrop-filter: blur(50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu {
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title > a {
|
.title > a {
|
||||||
backdrop-filter: blur(0px) !important;
|
backdrop-filter: blur(0px) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search,
|
|
||||||
.document,
|
|
||||||
.border {
|
|
||||||
backdrop-filter: blur(80px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#main > .dashboard {
|
#main > .dashboard {
|
||||||
section,
|
section,
|
||||||
.dashlet {
|
.dashlet {
|
||||||
|
|||||||
@@ -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;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#rbgcp-inputs-wrap {
|
||||||
|
padding-top: 4px !important;
|
||||||
|
margin-bottom: -8px;
|
||||||
|
|
||||||
|
#rbgcp-hex-input,
|
||||||
|
#rbgcp-input {
|
||||||
|
height: 28px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
#rbgcp-wrapper {
|
#rbgcp-wrapper {
|
||||||
div[style="padding-top: 11px; position: relative;"] div {
|
div[style="padding-top: 11px; position: relative;"] div {
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export default function Picker({
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
disableDarkMode={true}
|
disableDarkMode={true}
|
||||||
presets={presets}
|
presets={presets}
|
||||||
hideInputs={customOnChange ? false : true}
|
|
||||||
value={customThemeColor ?? ""}
|
value={customThemeColor ?? ""}
|
||||||
onChange={(color: string) => {
|
onChange={(color: string) => {
|
||||||
if (customOnChange) {
|
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;
|
let select: HTMLSelectElement;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-lg w-full overflow-clip">
|
<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
|
<select
|
||||||
bind:this={select}
|
bind:this={select}
|
||||||
value={state}
|
value={state}
|
||||||
onchange={() => onChange(select.value)}
|
onchange={() => onChange(select.value)}
|
||||||
class="px-4 py-1 text-[0.75rem] dark: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}
|
{#each options as option}
|
||||||
<option value={option.value}>
|
<option value={option.value}>
|
||||||
@@ -22,3 +22,19 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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,8 +1,20 @@
|
|||||||
import { type DBSchema, type IDBPDatabase, openDB } from "idb";
|
import { type DBSchema, type IDBPDatabase, openDB } from "idb";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the schema for the IndexedDB database used for storing background image data.
|
||||||
|
*
|
||||||
|
* @interface BackgroundDB
|
||||||
|
* @extends {DBSchema}
|
||||||
|
* @property {object} backgrounds - The object store for background images.
|
||||||
|
* @property {string} backgrounds.key - The type of the key for the object store (in this case, it's `id` as defined in `keyPath`).
|
||||||
|
* @property {object} backgrounds.value - The structure of the objects stored.
|
||||||
|
* @property {string} backgrounds.value.id - The unique identifier for the background image record.
|
||||||
|
* @property {string} backgrounds.value.type - The MIME type of the image (e.g., "image/png", "image/jpeg").
|
||||||
|
* @property {Blob} backgrounds.value.blob - The binary large object (Blob) containing the image data.
|
||||||
|
*/
|
||||||
interface BackgroundDB extends DBSchema {
|
interface BackgroundDB extends DBSchema {
|
||||||
backgrounds: {
|
backgrounds: {
|
||||||
key: string;
|
key: string; // Corresponds to the 'id' property due to keyPath: "id"
|
||||||
value: {
|
value: {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -13,6 +25,14 @@ interface BackgroundDB extends DBSchema {
|
|||||||
|
|
||||||
let db: IDBPDatabase<BackgroundDB> | null = null;
|
let db: IDBPDatabase<BackgroundDB> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes and opens an IndexedDB connection or returns an existing one.
|
||||||
|
* If the database doesn't exist or needs an upgrade, the `upgrade` callback
|
||||||
|
* creates the 'backgrounds' object store with 'id' as the keyPath.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @returns {Promise<IDBPDatabase<BackgroundDB>>} A promise that resolves with the database instance.
|
||||||
|
*/
|
||||||
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
|
export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
|
||||||
if (db) return db;
|
if (db) return db;
|
||||||
|
|
||||||
@@ -25,6 +45,12 @@ export async function openDatabase(): Promise<IDBPDatabase<BackgroundDB>> {
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all background image records from the 'backgrounds' object store in IndexedDB.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @returns {Promise<Array<{id: string, type: string, blob: Blob}>>} A promise that resolves with an array of all background image records.
|
||||||
|
*/
|
||||||
export async function readAllData(): Promise<
|
export async function readAllData(): Promise<
|
||||||
Array<{ id: string; type: string; blob: Blob }>
|
Array<{ id: string; type: string; blob: Blob }>
|
||||||
> {
|
> {
|
||||||
@@ -32,6 +58,16 @@ export async function readAllData(): Promise<
|
|||||||
return db.getAll("backgrounds");
|
return db.getAll("backgrounds");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes or updates a background image record in the 'backgrounds' object store.
|
||||||
|
* If a record with the given `id` already exists, it will be updated. Otherwise, a new record is created.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} id - The unique identifier for the background image record.
|
||||||
|
* @param {string} type - The MIME type of the image (e.g., "image/png").
|
||||||
|
* @param {Blob} blob - The Blob object containing the image data.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the data has been successfully written.
|
||||||
|
*/
|
||||||
export async function writeData(
|
export async function writeData(
|
||||||
id: string,
|
id: string,
|
||||||
type: string,
|
type: string,
|
||||||
@@ -41,16 +77,37 @@ export async function writeData(
|
|||||||
await db.put("backgrounds", { id, type, blob });
|
await db.put("backgrounds", { id, type, blob });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a background image record from the 'backgrounds' object store by its ID.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} id - The unique identifier of the background image record to delete.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the data has been successfully deleted.
|
||||||
|
*/
|
||||||
export async function deleteData(id: string): Promise<void> {
|
export async function deleteData(id: string): Promise<void> {
|
||||||
const db = await openDatabase();
|
const db = await openDatabase();
|
||||||
await db.delete("backgrounds", id);
|
await db.delete("backgrounds", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all records from the 'backgrounds' object store in IndexedDB.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @returns {Promise<void>} A promise that resolves when all data has been successfully cleared.
|
||||||
|
*/
|
||||||
export async function clearAllData(): Promise<void> {
|
export async function clearAllData(): Promise<void> {
|
||||||
const db = await openDatabase();
|
const db = await openDatabase();
|
||||||
await db.clear("backgrounds");
|
await db.clear("backgrounds");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single background image record from the 'backgrounds' object store by its ID.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {string} id - The unique identifier of the background image record to retrieve.
|
||||||
|
* @returns {Promise<{id: string, type: string, blob: Blob} | undefined>} A promise that resolves with the
|
||||||
|
* background image record if found, or undefined otherwise.
|
||||||
|
*/
|
||||||
export async function getDataById(
|
export async function getDataById(
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
|
): Promise<{ id: string; type: string; blob: Blob } | undefined> {
|
||||||
@@ -58,6 +115,10 @@ export async function getDataById(
|
|||||||
return db.get("backgrounds", id);
|
return db.get("backgrounds", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the active IndexedDB connection and nullifies the global `db` variable.
|
||||||
|
* This is important to release resources and allow for proper database management.
|
||||||
|
*/
|
||||||
export function closeDatabase(): void {
|
export function closeDatabase(): void {
|
||||||
if (db) {
|
if (db) {
|
||||||
db.close();
|
db.close();
|
||||||
@@ -65,12 +126,24 @@ export function closeDatabase(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if IndexedDB is supported
|
/**
|
||||||
|
* Checks if IndexedDB is supported by the current browser environment.
|
||||||
|
*
|
||||||
|
* @returns {boolean} True if IndexedDB is supported, false otherwise.
|
||||||
|
*/
|
||||||
export function isIndexedDBSupported(): boolean {
|
export function isIndexedDBSupported(): boolean {
|
||||||
return "indexedDB" in window;
|
return "indexedDB" in window;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to check if there's enough storage space
|
/**
|
||||||
|
* Estimates available storage space and checks if it's sufficient for the specified `requiredSpace`.
|
||||||
|
* Uses the `navigator.storage.estimate()` API if available.
|
||||||
|
* If the API is not available or cannot determine space, it defaults to assuming enough space is available.
|
||||||
|
*
|
||||||
|
* @async
|
||||||
|
* @param {number} requiredSpace - The amount of storage space required, in bytes.
|
||||||
|
* @returns {Promise<boolean>} A promise that resolves with true if enough space is estimated to be available, false otherwise.
|
||||||
|
*/
|
||||||
export async function hasEnoughStorageSpace(
|
export async function hasEnoughStorageSpace(
|
||||||
requiredSpace: number,
|
requiredSpace: number,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
type BackgroundUpdateCallback = () => void;
|
type BackgroundUpdateCallback = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton class used to notify listeners about generic background updates or events.
|
||||||
|
* These updates typically signify that UI components or other parts of the application
|
||||||
|
* might need to refresh or re-evaluate background-related data (e.g., after a new background
|
||||||
|
* image is added, removed, or changed).
|
||||||
|
*/
|
||||||
class BackgroundUpdates {
|
class BackgroundUpdates {
|
||||||
private static instance: BackgroundUpdates;
|
private static instance: BackgroundUpdates;
|
||||||
private listeners: Set<BackgroundUpdateCallback> = new Set();
|
private listeners: Set<BackgroundUpdateCallback> = new Set();
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton instance of the BackgroundUpdates class.
|
||||||
|
* @returns {BackgroundUpdates} The singleton instance.
|
||||||
|
*/
|
||||||
public static getInstance(): BackgroundUpdates {
|
public static getInstance(): BackgroundUpdates {
|
||||||
if (!BackgroundUpdates.instance) {
|
if (!BackgroundUpdates.instance) {
|
||||||
BackgroundUpdates.instance = new BackgroundUpdates();
|
BackgroundUpdates.instance = new BackgroundUpdates();
|
||||||
@@ -13,14 +23,31 @@ class BackgroundUpdates {
|
|||||||
return BackgroundUpdates.instance;
|
return BackgroundUpdates.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback function to be invoked when a background update is triggered.
|
||||||
|
*
|
||||||
|
* @param {BackgroundUpdateCallback} callback The function to call when a background update occurs.
|
||||||
|
* This callback takes no arguments and returns void.
|
||||||
|
*/
|
||||||
public addListener(callback: BackgroundUpdateCallback): void {
|
public addListener(callback: BackgroundUpdateCallback): void {
|
||||||
this.listeners.add(callback);
|
this.listeners.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a previously added callback function.
|
||||||
|
* After calling this method, the provided callback will no longer be invoked when a background update is triggered.
|
||||||
|
*
|
||||||
|
* @param {BackgroundUpdateCallback} callback The callback function to remove from the listeners.
|
||||||
|
*/
|
||||||
public removeListener(callback: BackgroundUpdateCallback): void {
|
public removeListener(callback: BackgroundUpdateCallback): void {
|
||||||
this.listeners.delete(callback);
|
this.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes all registered listener callbacks, signifying that a background update has occurred.
|
||||||
|
* This method should be called whenever a change to background data happens that requires
|
||||||
|
* other parts of the application to be notified.
|
||||||
|
*/
|
||||||
public triggerUpdate(): void {
|
public triggerUpdate(): void {
|
||||||
this.listeners.forEach((callback) => callback());
|
this.listeners.forEach((callback) => callback());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,30 @@ class SettingsPopup {
|
|||||||
return SettingsPopup.instance;
|
return SettingsPopup.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback function to be invoked when the settings popup is closed.
|
||||||
|
*
|
||||||
|
* @param {SettingsPopupCallback} callback The function to call when the settings popup closes.
|
||||||
|
* This callback takes no arguments and returns void.
|
||||||
|
*/
|
||||||
public addListener(callback: SettingsPopupCallback): void {
|
public addListener(callback: SettingsPopupCallback): void {
|
||||||
this.listeners.add(callback);
|
this.listeners.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a previously added callback function.
|
||||||
|
* After calling this method, the provided callback will no longer be invoked when the settings popup closes.
|
||||||
|
*
|
||||||
|
* @param {SettingsPopupCallback} callback The callback function to remove from the listeners.
|
||||||
|
*/
|
||||||
public removeListener(callback: SettingsPopupCallback): void {
|
public removeListener(callback: SettingsPopupCallback): void {
|
||||||
this.listeners.delete(callback);
|
this.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes all registered listener callbacks.
|
||||||
|
* This method should be called when the settings popup is closed to notify all subscribed components or services.
|
||||||
|
*/
|
||||||
public triggerClose(): void {
|
public triggerClose(): void {
|
||||||
this.listeners.forEach((callback) => callback());
|
this.listeners.forEach((callback) => callback());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
type ThemeUpdateCallback = () => void;
|
type ThemeUpdateCallback = () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton class used to notify listeners about theme-related updates.
|
||||||
|
* These updates can include events like theme changes, custom theme modifications,
|
||||||
|
* or any other event that might require UI components to refresh their appearance
|
||||||
|
* or re-apply theme styles.
|
||||||
|
*/
|
||||||
class ThemeUpdates {
|
class ThemeUpdates {
|
||||||
private static instance: ThemeUpdates;
|
private static instance: ThemeUpdates;
|
||||||
private listeners: Set<ThemeUpdateCallback> = new Set();
|
private listeners: Set<ThemeUpdateCallback> = new Set();
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton instance of the ThemeUpdates class.
|
||||||
|
* @returns {ThemeUpdates} The singleton instance.
|
||||||
|
*/
|
||||||
public static getInstance(): ThemeUpdates {
|
public static getInstance(): ThemeUpdates {
|
||||||
if (!ThemeUpdates.instance) {
|
if (!ThemeUpdates.instance) {
|
||||||
ThemeUpdates.instance = new ThemeUpdates();
|
ThemeUpdates.instance = new ThemeUpdates();
|
||||||
@@ -13,14 +23,31 @@ class ThemeUpdates {
|
|||||||
return ThemeUpdates.instance;
|
return ThemeUpdates.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback function to be invoked when a theme update is triggered.
|
||||||
|
*
|
||||||
|
* @param {ThemeUpdateCallback} callback The function to call when a theme update occurs.
|
||||||
|
* This callback takes no arguments and returns void.
|
||||||
|
*/
|
||||||
public addListener(callback: ThemeUpdateCallback): void {
|
public addListener(callback: ThemeUpdateCallback): void {
|
||||||
this.listeners.add(callback);
|
this.listeners.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters a previously added callback function.
|
||||||
|
* After calling this method, the provided callback will no longer be invoked when a theme update is triggered.
|
||||||
|
*
|
||||||
|
* @param {ThemeUpdateCallback} callback The callback function to remove from the listeners.
|
||||||
|
*/
|
||||||
public removeListener(callback: ThemeUpdateCallback): void {
|
public removeListener(callback: ThemeUpdateCallback): void {
|
||||||
this.listeners.delete(callback);
|
this.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes all registered listener callbacks, signifying that a theme-related update has occurred.
|
||||||
|
* This method should be called whenever a change related to themes happens that requires
|
||||||
|
* other parts of the application to be notified.
|
||||||
|
*/
|
||||||
public triggerUpdate(): void {
|
public triggerUpdate(): void {
|
||||||
this.listeners.forEach((callback) => callback());
|
this.listeners.forEach((callback) => callback());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Settings from "./pages/settings.svelte";
|
|||||||
import IconFamily from "@/resources/fonts/IconFamily.woff";
|
import IconFamily from "@/resources/fonts/IconFamily.woff";
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import renderSvelte from "./main";
|
import renderSvelte from "./main";
|
||||||
|
import { initializeSettingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
|
||||||
function InjectCustomIcons() {
|
function InjectCustomIcons() {
|
||||||
console.info("[BetterSEQTA+] Injecting Icons");
|
console.info("[BetterSEQTA+] Injecting Icons");
|
||||||
@@ -26,4 +27,8 @@ if (!mountPoint) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InjectCustomIcons();
|
InjectCustomIcons();
|
||||||
renderSvelte(Settings, mountPoint, { standalone: true });
|
|
||||||
|
(async () => {
|
||||||
|
await initializeSettingsState();
|
||||||
|
renderSvelte(Settings, mountPoint, { standalone: true });
|
||||||
|
})();
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ export default function renderSvelte(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (mountPoint instanceof ShadowRoot) {
|
||||||
const styleElement = document.createElement("style");
|
const styleElement = document.createElement("style");
|
||||||
styleElement.textContent = style;
|
styleElement.textContent = style;
|
||||||
mountPoint.appendChild(styleElement);
|
mountPoint.appendChild(styleElement);
|
||||||
|
}
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,47 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TabbedContainer from '../components/TabbedContainer.svelte';
|
import TabbedContainer from "../components/TabbedContainer.svelte";
|
||||||
import Settings from './settings/general.svelte';
|
import Settings from "./settings/general.svelte";
|
||||||
import Shortcuts from './settings/shortcuts.svelte';
|
import Shortcuts from "./settings/shortcuts.svelte";
|
||||||
import Theme from './settings/theme.svelte';
|
import Theme from "./settings/theme.svelte";
|
||||||
import browser from 'webextension-polyfill';
|
import browser from "webextension-polyfill";
|
||||||
|
|
||||||
import { standalone as StandaloneStore } from '../utils/standalone.svelte';
|
import { standalone as StandaloneStore } from "../utils/standalone.svelte";
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from "svelte";
|
||||||
import { initializeSettingsState, settingsState } from '@/seqta/utils/listeners/SettingsState'
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
|
||||||
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
|
||||||
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage"
|
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage";
|
||||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew"
|
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
|
||||||
|
//import { OpenMinecraftServerPopup } from "@/seqta/utils/Openers/OpenMinecraftServerPopup";
|
||||||
|
|
||||||
import ColourPicker from '../components/ColourPicker.svelte'
|
import ColourPicker from "../components/ColourPicker.svelte";
|
||||||
import { settingsPopup } from '../hooks/SettingsPopup'
|
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 handleDevModeToggle = () => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
devModeSequence += event.key.toLowerCase();
|
devModeSequence += event.key.toLowerCase();
|
||||||
if (devModeSequence.includes('dev')) {
|
if (devModeSequence.includes("dev")) {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
settingsState.devMode = true;
|
settingsState.devMode = true;
|
||||||
alert('Dev mode is now enabled');
|
alert("Dev mode is now enabled");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.removeEventListener('keydown', handleKeyDown);
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
devModeSequence = '';
|
devModeSequence = "";
|
||||||
}, 10000);
|
}, 10000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openColourPicker = () => {
|
const openColourPicker = () => {
|
||||||
showColourPicker = true;
|
showColourPicker = true;
|
||||||
}
|
};
|
||||||
|
|
||||||
const openChangelog = () => {
|
const openChangelog = () => {
|
||||||
OpenWhatsNewPopup();
|
OpenWhatsNewPopup();
|
||||||
@@ -49,44 +53,269 @@
|
|||||||
closeExtensionPopup();
|
closeExtensionPopup();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* const openMinecraftServer = () => {
|
||||||
|
OpenMinecraftServerPopup();
|
||||||
|
closeExtensionPopup();
|
||||||
|
}; */
|
||||||
|
|
||||||
|
const openPrivacyStatement = () => {
|
||||||
|
window.open("https://betterseqta.org/privacy", "_blank");
|
||||||
|
closeExtensionPopup();
|
||||||
|
};
|
||||||
|
|
||||||
let { standalone } = $props<{ standalone?: boolean }>();
|
let { standalone } = $props<{ standalone?: boolean }>();
|
||||||
let showColourPicker = $state<boolean>(false);
|
let showColourPicker = $state<boolean>(false);
|
||||||
|
|
||||||
onMount(() => {
|
const showDisclaimer = (onConfirm: () => void, onCancel: () => void) => {
|
||||||
|
disclaimerCallbacks = { onConfirm, onCancel };
|
||||||
|
showDisclaimerModal = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
settingsPopup.addListener(() => {
|
settingsPopup.addListener(() => {
|
||||||
showColourPicker = false;
|
showColourPicker = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!standalone) return;
|
if (!standalone) return;
|
||||||
initializeSettingsState();
|
|
||||||
StandaloneStore.setStandalone(true);
|
StandaloneStore.setStandalone(true);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode ? 'dark' : ''} { standalone ? 'h-[600px]' : 'h-full rounded-xl' } overflow-clip">
|
<div
|
||||||
<div class="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white">
|
class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode
|
||||||
<div class="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40">
|
? '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_no_noninteractive_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" onclick={handleDevModeToggle} />
|
<img
|
||||||
|
src={browser.runtime.getURL(
|
||||||
|
"resources/icons/betterseqta-dark-full.png",
|
||||||
|
)}
|
||||||
|
class="w-4/5 dark:hidden"
|
||||||
|
alt="Light logo"
|
||||||
|
onclick={handleDevModeToggle}
|
||||||
|
/>
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" onclick={handleDevModeToggle} />
|
<img
|
||||||
|
src={browser.runtime.getURL(
|
||||||
|
"resources/icons/betterseqta-light-full.png",
|
||||||
|
)}
|
||||||
|
class="hidden w-4/5 dark:block"
|
||||||
|
alt="Dark logo"
|
||||||
|
onclick={handleDevModeToggle}
|
||||||
|
/>
|
||||||
|
|
||||||
{#if !standalone}
|
{#if !standalone}
|
||||||
<button onclick={openChangelog} class="absolute top-1 right-1 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
|
<div class="flex absolute top-1 right-1 gap-1 items-center">
|
||||||
<button onclick={openAbout} class="absolute top-1 right-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</button>
|
<button
|
||||||
|
onclick={openAbout}
|
||||||
|
class="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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabbedContainer tabs={[
|
<TabbedContainer
|
||||||
{ title: 'Settings', Content: Settings, props: { showColourPicker: openColourPicker } },
|
tabs={[
|
||||||
{ title: 'Shortcuts', Content: Shortcuts },
|
{
|
||||||
{ title: 'Themes', Content: Theme },
|
title: "Settings",
|
||||||
]} />
|
Content: Settings,
|
||||||
|
props: { showColourPicker: openColourPicker, showDisclaimer },
|
||||||
|
},
|
||||||
|
{ title: "Shortcuts", Content: Shortcuts },
|
||||||
|
{ title: "Themes", Content: Theme },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showColourPicker}
|
{#if showColourPicker}
|
||||||
<ColourPicker hidePicker={() => { showColourPicker = false }} />
|
<ColourPicker
|
||||||
|
hidePicker={() => {
|
||||||
|
showColourPicker = false;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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,10 +10,11 @@
|
|||||||
import type { SettingsList } from "@/interface/types/SettingsProps"
|
import type { SettingsList } from "@/interface/types/SettingsProps"
|
||||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
|
||||||
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
|
||||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification"
|
||||||
|
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
|
||||||
|
|
||||||
import { getAllPluginSettings } from "@/plugins"
|
import { getAllPluginSettings } from "@/plugins"
|
||||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting } from "@/plugins/core/types"
|
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
||||||
|
|
||||||
// Union type representing all possible settings
|
// Union type representing all possible settings
|
||||||
type SettingType =
|
type SettingType =
|
||||||
@@ -32,6 +33,11 @@
|
|||||||
(Omit<HotkeySetting, 'type'> & {
|
(Omit<HotkeySetting, 'type'> & {
|
||||||
type: 'hotkey',
|
type: 'hotkey',
|
||||||
id: string
|
id: string
|
||||||
|
}) |
|
||||||
|
(Omit<ComponentSetting, 'type'> & {
|
||||||
|
type: 'component',
|
||||||
|
id: string,
|
||||||
|
component: any
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Plugin {
|
interface Plugin {
|
||||||
@@ -55,7 +61,11 @@
|
|||||||
pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {};
|
pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {};
|
||||||
|
|
||||||
for (const [key, setting] of Object.entries(plugin.settings)) {
|
for (const [key, setting] of Object.entries(plugin.settings)) {
|
||||||
if (pluginSettingsValues[plugin.pluginId][key] === undefined && setting.type !== 'button') {
|
if (
|
||||||
|
pluginSettingsValues[plugin.pluginId][key] === undefined &&
|
||||||
|
setting.type !== 'button' &&
|
||||||
|
setting.type !== 'component'
|
||||||
|
) {
|
||||||
pluginSettingsValues[plugin.pluginId][key] = setting.default;
|
pluginSettingsValues[plugin.pluginId][key] = setting.default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +92,10 @@
|
|||||||
loadPluginSettings();
|
loadPluginSettings();
|
||||||
})
|
})
|
||||||
|
|
||||||
const { showColourPicker } = $props<{ showColourPicker: () => void }>();
|
const { showColourPicker, showDisclaimer } = $props<{
|
||||||
|
showColourPicker: () => void;
|
||||||
|
showDisclaimer: (onConfirm: () => void, onCancel: () => void) => void;
|
||||||
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
{#snippet Setting({ title, description, Component, props }: SettingsList) }
|
||||||
@@ -178,15 +191,16 @@
|
|||||||
options: [
|
options: [
|
||||||
{ value: "australia", label: "Australia" },
|
{ value: "australia", label: "Australia" },
|
||||||
{ value: "usa", label: "USA" },
|
{ value: "usa", label: "USA" },
|
||||||
|
{ value: "uk", label: "UK" },
|
||||||
{ value: "taiwan", label: "Taiwan" },
|
{ value: "taiwan", label: "Taiwan" },
|
||||||
{ value: "hong_kong", label: "Hong Kong" },
|
{ value: "hong_kong", label: "Hong Kong" },
|
||||||
{ value: "panama", label: "Panama" },
|
{ value: "panama", label: "Panama" },
|
||||||
{ value: "canada", label: "Canada" },
|
{ value: "canada", label: "Canada" },
|
||||||
{ value: "singapore", label: "Singapore" },
|
{ value: "singapore", label: "Singapore" },
|
||||||
{ value: "uk", label: "UK" },
|
|
||||||
{ value: "japan", label: "Japan" },
|
{ value: "japan", label: "Japan" },
|
||||||
{ value: "netherlands", label: "Netherlands" }
|
{ value: "netherlands", label: "Netherlands" }
|
||||||
]
|
]
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
] as option}
|
] as option}
|
||||||
@@ -213,7 +227,20 @@
|
|||||||
<div>
|
<div>
|
||||||
<Switch
|
<Switch
|
||||||
state={pluginSettingsValues[plugin.pluginId]?.enabled ?? true}
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,6 +295,11 @@
|
|||||||
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||||
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||||
/>
|
/>
|
||||||
|
{:else if setting.type === 'component'}
|
||||||
|
{#if setting.component}
|
||||||
|
{@const Component = setting.component}
|
||||||
|
<Component />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,10 +339,41 @@
|
|||||||
<h2 class="text-sm font-bold">Sensitive Hider</h2>
|
<h2 class="text-sm font-bold">Sensitive Hider</h2>
|
||||||
<p class="text-xs">Replace sensitive content with mock data</p>
|
<p class="text-xs">Replace sensitive content with mock data</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
state={$settingsState.hideSensitiveContent ?? false}
|
||||||
|
onChange={(isOn: boolean) => settingsState.hideSensitiveContent = isOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
|
<div class="pr-4">
|
||||||
|
<h2 class="text-sm font-bold">Mock Notices</h2>
|
||||||
|
<p class="text-xs">Use fake notice data on homepage instead of real data</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
state={$settingsState.mockNotices ?? false}
|
||||||
|
onChange={(isOn: boolean) => settingsState.mockNotices = isOn}
|
||||||
|
/>
|
||||||
|
</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>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => hideSensitiveContent()}
|
onClick={async () => {
|
||||||
text="Hide"
|
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>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import Shortcuts from "@/seqta/content/links.json"
|
import Shortcuts from "@/seqta/content/links.json"
|
||||||
|
|
||||||
let isLoaded = $state(false);
|
let isLoaded = $state(false);
|
||||||
|
let fileInput = $state<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Wait for settingsState to be initialized
|
// Wait for settingsState to be initialized
|
||||||
@@ -23,18 +24,43 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const switchChange = (shortcut: any) => {
|
const switchChange = (shortcut: any) => {
|
||||||
const value = $settingsState.shortcuts.find(s => s.name === shortcut);
|
const idx = $settingsState.shortcuts.findIndex(s => s.name === shortcut);
|
||||||
if (value) {
|
if (idx !== -1) {
|
||||||
value.enabled = !value.enabled;
|
// Create a new array with the toggled value to ensure reactivity
|
||||||
settingsState.shortcuts = settingsState.shortcuts;
|
const updated = settingsState.shortcuts.map(s =>
|
||||||
|
s.name === shortcut ? { ...s, enabled: !s.enabled } : s
|
||||||
|
);
|
||||||
|
settingsState.shortcuts = updated;
|
||||||
} else {
|
} else {
|
||||||
settingsState.shortcuts = [...settingsState.shortcuts, { name: shortcut, enabled: true }];
|
settingsState.shortcuts = [
|
||||||
|
...settingsState.shortcuts,
|
||||||
|
{ name: shortcut, enabled: true }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFormVisible = $state(false);
|
let isFormVisible = $state(false);
|
||||||
let newTitle = $state("");
|
let newTitle = $state("");
|
||||||
let newURL = $state("");
|
let newURL = $state("");
|
||||||
|
let newIcon = $state<string | null>(null);
|
||||||
|
|
||||||
|
function handleIconChange(event: Event) {
|
||||||
|
const file = (event.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file && file.type === "image/svg+xml") {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
newIcon = reader.result as string;
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearIcon = () => {
|
||||||
|
newIcon = null;
|
||||||
|
if (fileInput) {
|
||||||
|
fileInput.value = ""; // Clear the file input so the same file can be re-selected
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleForm = () => {
|
const toggleForm = () => {
|
||||||
isFormVisible = !isFormVisible;
|
isFormVisible = !isFormVisible;
|
||||||
@@ -54,11 +80,13 @@
|
|||||||
|
|
||||||
const addNewCustomShortcut = () => {
|
const addNewCustomShortcut = () => {
|
||||||
if (isValidTitle(newTitle) && isValidURL(newURL)) {
|
if (isValidTitle(newTitle) && isValidURL(newURL)) {
|
||||||
const newShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon: newTitle[0] };
|
const icon = newIcon || newTitle[0];
|
||||||
|
const newShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon };
|
||||||
settingsState.customshortcuts = [...settingsState.customshortcuts, newShortcut];
|
settingsState.customshortcuts = [...settingsState.customshortcuts, newShortcut];
|
||||||
|
|
||||||
newTitle = "";
|
newTitle = "";
|
||||||
newURL = "";
|
newURL = "";
|
||||||
|
newIcon = null;
|
||||||
isFormVisible = false;
|
isFormVisible = false;
|
||||||
} else {
|
} else {
|
||||||
alert("Please enter a valid title and URL.");
|
alert("Please enter a valid title and URL.");
|
||||||
@@ -101,7 +129,7 @@
|
|||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.05, duration: 0.2 }}
|
transition={{ delay: 0.05, duration: 0.2 }}
|
||||||
class="w-full"
|
class="flex gap-2 w-full"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="p-2 my-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
class="p-2 my-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
||||||
@@ -109,6 +137,48 @@
|
|||||||
placeholder="URL eg. https://google.com"
|
placeholder="URL eg. https://google.com"
|
||||||
bind:value={newURL}
|
bind:value={newURL}
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
bind:this={fileInput}
|
||||||
|
class="p-2 w-full rounded-lg border-0 transition placeholder-zinc-300 bg-zinc-100 dark:bg-zinc-700 focus:bg-zinc-200/50 dark:focus:bg-zinc-600"
|
||||||
|
type="file"
|
||||||
|
accept=".svg"
|
||||||
|
onchange={handleIconChange}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex justify-between items-center p-2 my-2 text-left rounded-lg border border-dashed transition text-nowrap text-zinc-500 dark:text-zinc-400 bg-zinc-100 dark:bg-zinc-700/50 hover:bg-zinc-200/50 dark:hover:bg-zinc-700/30 focus:bg-zinc-200/50 dark:focus:bg-zinc-600/50 border-zinc-300 dark:border-zinc-600"
|
||||||
|
onclick={() => fileInput?.click()}
|
||||||
|
>
|
||||||
|
{#if newIcon}
|
||||||
|
<div class="flex overflow-hidden items-center">
|
||||||
|
<div class="flex-shrink-0 mr-2 w-6 h-6">
|
||||||
|
<img src={`data:image/svg+xml;base64,${btoa(newIcon)}`} alt="Selected Icon" class="object-contain w-full h-full" />
|
||||||
|
</div>
|
||||||
|
<span class="truncate">Selected Icon</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="p-1 ml-2 rounded hover:bg-zinc-200 dark:hover:bg-zinc-600"
|
||||||
|
aria-label="Clear icon"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onclick={(event) => { event.stopPropagation(); clearIcon(); }}
|
||||||
|
onkeydown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
clearIcon();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="font-IconFamily">{ '\ued47' }</span>
|
||||||
|
<span class="ml-2">SVG icon <span class="text-xs italic text-zinc-400 dark:text-zinc-500">(Optional)</span></span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</MotionDiv>
|
</MotionDiv>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -132,16 +202,6 @@
|
|||||||
</MotionDiv>
|
</MotionDiv>
|
||||||
</div>
|
</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 -->
|
<!-- Custom Shortcuts Section -->
|
||||||
{#each $settingsState.customshortcuts as shortcut, index}
|
{#each $settingsState.customshortcuts as shortcut, index}
|
||||||
<div class="flex justify-between items-center px-4 py-3">
|
<div class="flex justify-between items-center px-4 py-3">
|
||||||
@@ -153,6 +213,16 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/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}
|
{:else}
|
||||||
<div class="p-4 text-center">
|
<div class="p-4 text-center">
|
||||||
Loading shortcuts...
|
Loading shortcuts...
|
||||||
|
|||||||
@@ -21,13 +21,16 @@
|
|||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<button
|
<button
|
||||||
onclick={() => editMode = !editMode}
|
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} />
|
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
|
||||||
<ThemeSelector isEditMode={editMode} />
|
<ThemeSelector isEditMode={editMode} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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">
|
<div class="text-lg">
|
||||||
Open SEQTA and use the embedded settings to access theme settings. 🫠
|
Open SEQTA and use the embedded settings to access theme settings. 🫠
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,27 @@
|
|||||||
import type { LoadedCustomTheme } from "@/types/CustomThemes";
|
import type { LoadedCustomTheme } from "@/types/CustomThemes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random 9-character alphanumeric string to be used as a unique ID for images.
|
||||||
|
* This helps in identifying and managing custom images within a theme.
|
||||||
|
*
|
||||||
|
* @returns {string} A randomly generated unique ID string.
|
||||||
|
*/
|
||||||
export function generateImageId(): string {
|
export function generateImageId(): string {
|
||||||
return Math.random().toString(36).substr(2, 9);
|
return Math.random().toString(36).substr(2, 9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the upload of a new custom image from a file input event.
|
||||||
|
* If a file is selected, it reads the file using FileReader, converts it to a Blob,
|
||||||
|
* generates a unique ID and a default variable name for it, and then adds this new image
|
||||||
|
* to the `CustomImages` array within the provided `theme` object.
|
||||||
|
*
|
||||||
|
* @param {Event} event The file input change event, typically from an `<input type="file">` element.
|
||||||
|
* @param {LoadedCustomTheme} theme The current theme object to which the new image will be added.
|
||||||
|
* @returns {Promise<LoadedCustomTheme> | LoadedCustomTheme} A Promise that resolves with the updated theme object
|
||||||
|
* containing the new image if a file was processed.
|
||||||
|
* Returns the original theme object synchronously if no file was selected.
|
||||||
|
*/
|
||||||
export function handleImageUpload(
|
export function handleImageUpload(
|
||||||
event: Event,
|
event: Event,
|
||||||
theme: LoadedCustomTheme,
|
theme: LoadedCustomTheme,
|
||||||
@@ -34,6 +52,16 @@ export function handleImageUpload(
|
|||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a custom image from the theme based on its ID.
|
||||||
|
* It filters out the image with the specified `imageId` from the `CustomImages` array
|
||||||
|
* in the `theme` object.
|
||||||
|
*
|
||||||
|
* @param {string} imageId The unique ID of the custom image to be removed.
|
||||||
|
* @param {LoadedCustomTheme} theme The current theme object from which the image will be removed.
|
||||||
|
* @returns {LoadedCustomTheme} A new theme object with the specified image removed from its `CustomImages` array.
|
||||||
|
* This function is synchronous.
|
||||||
|
*/
|
||||||
export function handleRemoveImage(
|
export function handleRemoveImage(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
theme: LoadedCustomTheme,
|
theme: LoadedCustomTheme,
|
||||||
@@ -44,6 +72,17 @@ export function handleRemoveImage(
|
|||||||
} as LoadedCustomTheme;
|
} as LoadedCustomTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the CSS variable name associated with a specific custom image in the theme.
|
||||||
|
* It finds the image by `imageId` in the `CustomImages` array of the `theme` object
|
||||||
|
* and updates its `variableName` property.
|
||||||
|
*
|
||||||
|
* @param {string} imageId The unique ID of the custom image whose variable name is to be updated.
|
||||||
|
* @param {string} variableName The new CSS variable name to assign to the image.
|
||||||
|
* @param {LoadedCustomTheme} theme The current theme object containing the image to be updated.
|
||||||
|
* @returns {LoadedCustomTheme} A new theme object with the updated image variable name.
|
||||||
|
* This function is synchronous.
|
||||||
|
*/
|
||||||
export function handleImageVariableChange(
|
export function handleImageVariableChange(
|
||||||
imageId: string,
|
imageId: string,
|
||||||
variableName: string,
|
variableName: string,
|
||||||
@@ -57,6 +96,17 @@ export function handleImageVariableChange(
|
|||||||
} as LoadedCustomTheme;
|
} as LoadedCustomTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the upload of a cover image for the theme from a file input event.
|
||||||
|
* If a file is selected, it reads the file using FileReader, converts it to a Blob,
|
||||||
|
* and then updates the `coverImage` property of the provided `theme` object with this new blob.
|
||||||
|
*
|
||||||
|
* @param {Event} event The file input change event, typically from an `<input type="file">` element.
|
||||||
|
* @param {LoadedCustomTheme} theme The current theme object whose cover image will be updated.
|
||||||
|
* @returns {Promise<LoadedCustomTheme>} A Promise that resolves with the updated theme object
|
||||||
|
* containing the new cover image if a file was processed.
|
||||||
|
* Returns a Promise resolving with the original theme object if no file was selected.
|
||||||
|
*/
|
||||||
export function handleCoverImageUpload(
|
export function handleCoverImageUpload(
|
||||||
event: Event,
|
event: Event,
|
||||||
theme: LoadedCustomTheme,
|
theme: LoadedCustomTheme,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const updatedFirefoxManifest = {
|
|||||||
},
|
},
|
||||||
browser_specific_settings: {
|
browser_specific_settings: {
|
||||||
gecko: {
|
gecko: {
|
||||||
id: pkg.author.email,
|
id: "betterseqta@betterseqta.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
{
|
{
|
||||||
"resources": ["resources/icons/*"],
|
"resources": ["resources/icons/*", "resources/update-image.webp"],
|
||||||
"matches": ["*://*/*"]
|
"matches": ["*://*/*"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+401
-23
@@ -10,6 +10,20 @@ class ReactFiber {
|
|||||||
console.log("Selected Nodes:", this.nodes);
|
console.log("Selected Nodes:", this.nodes);
|
||||||
console.log("🔍 Found Fibers:", this.fibers);
|
console.log("🔍 Found Fibers:", this.fibers);
|
||||||
console.log("🛠 Found Components:", this.components);
|
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) {
|
getFiberNode(node) {
|
||||||
if (!node) return null;
|
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(
|
const fiberKey = Object.getOwnPropertyNames(node).find(
|
||||||
(name) =>
|
(name) =>
|
||||||
name.startsWith("__reactFiber") ||
|
name.startsWith("__reactFiber") ||
|
||||||
name.startsWith("__reactInternalInstance"),
|
name.startsWith("__reactInternalInstance") ||
|
||||||
|
name.startsWith("__reactInternalFiber")
|
||||||
);
|
);
|
||||||
return fiberKey ? node[fiberKey] : null;
|
return fiberKey ? node[fiberKey] : null;
|
||||||
}
|
}
|
||||||
@@ -30,20 +61,71 @@ class ReactFiber {
|
|||||||
getOwnerComponent(fiberNode) {
|
getOwnerComponent(fiberNode) {
|
||||||
let current = fiberNode;
|
let current = fiberNode;
|
||||||
while (current) {
|
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 (
|
if (
|
||||||
current.stateNode &&
|
current.stateNode &&
|
||||||
|
current.stateNode !== null &&
|
||||||
|
typeof current.stateNode === 'object' &&
|
||||||
(current.stateNode.setState || current.stateNode.forceUpdate)
|
(current.stateNode.setState || current.stateNode.forceUpdate)
|
||||||
) {
|
) {
|
||||||
return current.stateNode;
|
return current.stateNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
current = current.return;
|
current = current.return;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getState(key) {
|
getState(key) {
|
||||||
if (!this.components.length) return null;
|
if (!this.components.length && !this.fibers.length) return null;
|
||||||
const state = this.components[0]?.state || 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) {
|
if (key === undefined) {
|
||||||
return state;
|
return state;
|
||||||
@@ -61,8 +143,137 @@ class ReactFiber {
|
|||||||
return null;
|
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) {
|
setState(update) {
|
||||||
this.components.forEach((component) => {
|
this.components.forEach((component) => {
|
||||||
|
// Handle class components
|
||||||
if (component?.setState) {
|
if (component?.setState) {
|
||||||
if (typeof update === "function") {
|
if (typeof update === "function") {
|
||||||
// Functional update
|
// 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;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -99,7 +317,7 @@ class ReactFiber {
|
|||||||
return this.fibers[0]?.memoizedProps?.[propName];
|
return this.fibers[0]?.memoizedProps?.[propName];
|
||||||
}
|
}
|
||||||
|
|
||||||
setProp(propName) {
|
setProp(propName, value) {
|
||||||
this.fibers.forEach((fiber) => {
|
this.fibers.forEach((fiber) => {
|
||||||
if (fiber?.memoizedProps) {
|
if (fiber?.memoizedProps) {
|
||||||
fiber.memoizedProps[propName] = value;
|
fiber.memoizedProps[propName] = value;
|
||||||
@@ -119,38 +337,176 @@ class ReactFiber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSerializable(obj) {
|
function makeSerializable(obj, visited = new WeakSet(), depth = 0, maxDepth = 10) {
|
||||||
if (typeof obj !== "object" || obj === null) {
|
// Handle primitives first
|
||||||
|
if (obj === null || obj === undefined) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
// Catch ALL functions early
|
||||||
return obj.map((item) => makeSerializable(item));
|
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 = {};
|
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];
|
let value = obj[key];
|
||||||
|
|
||||||
if (typeof value === "function") {
|
// Handle symbols specifically (React context symbols)
|
||||||
value = "[Function]";
|
if (typeof value === "symbol") {
|
||||||
} else if (value instanceof HTMLElement) {
|
value = `[Symbol: ${value.description || 'anonymous'}]`;
|
||||||
value = {
|
}
|
||||||
type: "HTMLElement",
|
// Extra function check
|
||||||
id: value.id,
|
else if (typeof value === "function") {
|
||||||
tagName: value.tagName,
|
value = `[Function: ${value.name || 'anonymous'}]`;
|
||||||
}; // Replace DOM node with ID/tag info
|
} else if (value && typeof value === "object") {
|
||||||
} else if (typeof value === "symbol") {
|
value = makeSerializable(value, visited, depth + 1, maxDepth);
|
||||||
value = value.toString();
|
|
||||||
} else if (typeof value === "object" && value !== null) {
|
|
||||||
value = makeSerializable(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serializableObj[key] = value;
|
serializableObj[key] = value;
|
||||||
|
} catch (error) {
|
||||||
|
serializableObj[key] = `[Error: ${error.message}]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ownKeys.length > maxKeys) {
|
||||||
|
serializableObj['...'] = `[${ownKeys.length - maxKeys} more properties]`;
|
||||||
|
}
|
||||||
|
|
||||||
return serializableObj;
|
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) => {
|
window.addEventListener("message", (event) => {
|
||||||
@@ -168,7 +524,7 @@ window.addEventListener("message", (event) => {
|
|||||||
case "setState":
|
case "setState":
|
||||||
// Handle both function and object updates
|
// Handle both function and object updates
|
||||||
if (payload.updateFn) {
|
if (payload.updateFn) {
|
||||||
const updateFn = eval(`(${payload.updateFn})`);
|
const updateFn = new Function('return ' + payload.updateFn)();
|
||||||
fiberInstance.setState(updateFn);
|
fiberInstance.setState(updateFn);
|
||||||
} else {
|
} else {
|
||||||
fiberInstance.setState(payload.updateObject);
|
fiberInstance.setState(payload.updateObject);
|
||||||
@@ -196,6 +552,28 @@ window.addEventListener("message", (event) => {
|
|||||||
response = makeSerializable(response);
|
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(
|
window.postMessage(
|
||||||
{
|
{
|
||||||
type: "reactFiberResponse",
|
type: "reactFiberResponse",
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { determineStatus, formatDate, getGradeValue } from "./utils";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
import confetti from "canvas-confetti";
|
||||||
|
|
||||||
|
export let data: any;
|
||||||
|
|
||||||
|
interface FilterOptions {
|
||||||
|
subject: string;
|
||||||
|
sortBy: "due" | "grade" | "subject" | "title";
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentageToLetter(percentage: number): string {
|
||||||
|
const letterMap: Record<number, string> = {
|
||||||
|
100: "A+",
|
||||||
|
95: "A",
|
||||||
|
90: "A-",
|
||||||
|
85: "B+",
|
||||||
|
80: "B",
|
||||||
|
75: "B-",
|
||||||
|
70: "C+",
|
||||||
|
65: "C",
|
||||||
|
60: "C-",
|
||||||
|
55: "D+",
|
||||||
|
50: "D",
|
||||||
|
45: "D-",
|
||||||
|
40: "E+",
|
||||||
|
35: "E",
|
||||||
|
30: "E-",
|
||||||
|
0: "F",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rounded = Math.ceil(percentage / 5) * 5;
|
||||||
|
return letterMap[rounded] || "F";
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentFilters: FilterOptions = {
|
||||||
|
subject: "all",
|
||||||
|
sortBy: "due",
|
||||||
|
};
|
||||||
|
|
||||||
|
let filteredAssessments: any[] = [];
|
||||||
|
let statusGroups: Record<string, any[]> = {};
|
||||||
|
|
||||||
|
function updateAssessments() {
|
||||||
|
filteredAssessments = data.assessments.filter((a: any) => {
|
||||||
|
const subjectMatch =
|
||||||
|
currentFilters.subject === "all" || a.code === currentFilters.subject;
|
||||||
|
return subjectMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredAssessments.sort((a: any, b: any) => {
|
||||||
|
switch (currentFilters.sortBy) {
|
||||||
|
case "due":
|
||||||
|
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||||
|
case "grade":
|
||||||
|
const gradeA = getGradeValue(a);
|
||||||
|
const gradeB = getGradeValue(b);
|
||||||
|
if (gradeA === null && gradeB === null) return 0;
|
||||||
|
if (gradeA === null) return 1;
|
||||||
|
if (gradeB === null) return -1;
|
||||||
|
return gradeB - gradeA;
|
||||||
|
case "subject":
|
||||||
|
return a.code.localeCompare(b.code);
|
||||||
|
case "title":
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
statusGroups = {
|
||||||
|
UPCOMING: [],
|
||||||
|
DUE_SOON: [],
|
||||||
|
OVERDUE: [],
|
||||||
|
SUBMITTED: [],
|
||||||
|
MARKS_RELEASED: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
filteredAssessments.forEach((assessment) => {
|
||||||
|
const status = determineStatus(assessment);
|
||||||
|
if (statusGroups[status]) {
|
||||||
|
statusGroups[status].push(assessment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDueDateClass(assessment: any): string {
|
||||||
|
const status = determineStatus(assessment);
|
||||||
|
switch (status) {
|
||||||
|
case "OVERDUE":
|
||||||
|
return "overdue";
|
||||||
|
case "DUE_SOON":
|
||||||
|
return "due-soon";
|
||||||
|
case "UPCOMING":
|
||||||
|
return "upcoming";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAssessmentCompleted(assessment: any) {
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
|
||||||
|
if (!completed.includes(assessment.id)) {
|
||||||
|
completed.push(assessment.id);
|
||||||
|
localStorage.setItem(completedKey, JSON.stringify(completed));
|
||||||
|
updateAssessments();
|
||||||
|
checkForCelebration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unmarkAssessmentCompleted(assessment: any) {
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
|
||||||
|
const index = completed.indexOf(assessment.id);
|
||||||
|
if (index > -1) {
|
||||||
|
completed.splice(index, 1);
|
||||||
|
localStorage.setItem(completedKey, JSON.stringify(completed));
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForCelebration() {
|
||||||
|
const overdueCount = statusGroups.OVERDUE?.length || 0;
|
||||||
|
const dueSoonCount = statusGroups.DUE_SOON?.length || 0;
|
||||||
|
|
||||||
|
if (overdueCount === 0 && dueSoonCount === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const duration = 100;
|
||||||
|
const end = Date.now() + duration;
|
||||||
|
|
||||||
|
(function frame() {
|
||||||
|
confetti({
|
||||||
|
particleCount: 17,
|
||||||
|
angle: 60,
|
||||||
|
spread: 65,
|
||||||
|
drift: 0.8,
|
||||||
|
startVelocity: 40,
|
||||||
|
scalar: 2,
|
||||||
|
gravity: 2,
|
||||||
|
decay: 0.97,
|
||||||
|
ticks: 300,
|
||||||
|
origin: { x: 0, y: 1 },
|
||||||
|
disableForReducedMotion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
confetti({
|
||||||
|
particleCount: 17,
|
||||||
|
angle: 120,
|
||||||
|
spread: 65,
|
||||||
|
drift: -0.8,
|
||||||
|
startVelocity: 40,
|
||||||
|
scalar: 2,
|
||||||
|
decay: 0.97,
|
||||||
|
ticks: 300,
|
||||||
|
gravity: 2,
|
||||||
|
origin: { x: 1, y: 1 },
|
||||||
|
disableForReducedMotion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Date.now() < end) {
|
||||||
|
requestAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Confetti celebration failed:", e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} else if (overdueCount === 0 || dueSoonCount === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
confetti({
|
||||||
|
particleCount: 100,
|
||||||
|
spread: 70,
|
||||||
|
origin: { y: 0.6 },
|
||||||
|
scalar: 0.9,
|
||||||
|
disableForReducedMotion: true,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Confetti celebration failed:", e);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManuallyCompleted(assessmentId: string): boolean {
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
return completed.includes(assessmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCardClick(assessment: any, event: Event) {
|
||||||
|
if ((event.target as HTMLElement).closest(".card-menu")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.hash = `#?page=/assessments/${assessment.programmeID}:${assessment.metaclassID}&item=${assessment.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let openMenuId: string | null = null;
|
||||||
|
|
||||||
|
function toggleMenu(assessmentId: string, event: Event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
openMenuId = openMenuId === assessmentId ? null : assessmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllMenus() {
|
||||||
|
openMenuId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (data) {
|
||||||
|
updateAssessments();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "UPCOMING",
|
||||||
|
title: "Upcoming",
|
||||||
|
className: "column-upcoming",
|
||||||
|
icon: "📅",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DUE_SOON",
|
||||||
|
title: "Due Soon",
|
||||||
|
className: "column-due-soon",
|
||||||
|
icon: "⏰",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "OVERDUE",
|
||||||
|
title: "Overdue",
|
||||||
|
className: "column-overdue",
|
||||||
|
icon: "🚨",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "SUBMITTED",
|
||||||
|
title: "Submitted",
|
||||||
|
className: "column-submitted",
|
||||||
|
icon: "📝",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "MARKS_RELEASED",
|
||||||
|
title: "Marked",
|
||||||
|
className: "column-marked",
|
||||||
|
icon: "✅",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:click={closeAllMenus} />
|
||||||
|
|
||||||
|
<div id="grid-view-container">
|
||||||
|
<div class="grid-view-header">
|
||||||
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
|
<div class="grid-view-filters">
|
||||||
|
<select class="filter-select" bind:value={currentFilters.subject}>
|
||||||
|
<option value="all">All Subjects</option>
|
||||||
|
{#each data.subjects as subject}
|
||||||
|
<option value={subject.code}>{subject.code} - {subject.title}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" bind:value={currentFilters.sortBy}>
|
||||||
|
<option value="due">Sort by Due Date</option>
|
||||||
|
<option value="grade">Sort by Grade</option>
|
||||||
|
<option value="subject">Sort by Subject</option>
|
||||||
|
<option value="title">Sort by Title</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-grid-content">
|
||||||
|
{#if filteredAssessments.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">📋</div>
|
||||||
|
<p>No assessments found matching your filters</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="kanban-board">
|
||||||
|
{#each columns as column}
|
||||||
|
{#if statusGroups[column.key]?.length > 0}
|
||||||
|
<div class="kanban-column-parent">
|
||||||
|
<div class="kanban-column {column.className}">
|
||||||
|
<div class="column-header">
|
||||||
|
<div class="column-title">
|
||||||
|
{column.icon} {column.title}
|
||||||
|
<span class="column-count">{statusGroups[column.key].length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column-cards" id="{column.key.toLowerCase()}-cards">
|
||||||
|
{#each statusGroups[column.key] as assessment}
|
||||||
|
{@const status = determineStatus(assessment)}
|
||||||
|
{@const dueDateClass = getDueDateClass(assessment)}
|
||||||
|
{@const isCompleted = isManuallyCompleted(assessment.id)}
|
||||||
|
{@const color = data.colors[assessment.code] || "#6366f1"}
|
||||||
|
<div
|
||||||
|
class="assessment-card"
|
||||||
|
data-subject={assessment.code}
|
||||||
|
data-status={status}
|
||||||
|
style="--subject-color: {color}"
|
||||||
|
on:click={(e) => handleCardClick(assessment, e)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
on:keydown={(e) => e.key === 'Enter' && handleCardClick(assessment, e)}
|
||||||
|
>
|
||||||
|
<div class="card-labels">
|
||||||
|
<span class="card-label label-subject">{assessment.code}</span>
|
||||||
|
{#if assessment.submitted}
|
||||||
|
<span class="card-label label-submitted" style="background: #10b981; color: white;">Submitted</span>
|
||||||
|
{/if}
|
||||||
|
{#if isCompleted && status === "MARKS_RELEASED" && !assessment.results}
|
||||||
|
<span class="card-label label-completed" style="background: #059669; color: white;">Completed</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if status !== "MARKS_RELEASED" || isCompleted}
|
||||||
|
<div class="card-menu">
|
||||||
|
<button
|
||||||
|
class="menu-button"
|
||||||
|
data-assessment-id={assessment.id}
|
||||||
|
on:click={(e) => toggleMenu(assessment.id, e)}
|
||||||
|
aria-label="Open menu"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<circle cx="12" cy="5" r="2"/>
|
||||||
|
<circle cx="12" cy="12" r="2"/>
|
||||||
|
<circle cx="12" cy="19" r="2"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="menu-dropdown" style="display: {openMenuId === assessment.id ? 'block' : 'none'};">
|
||||||
|
{#if status !== "MARKS_RELEASED"}
|
||||||
|
<button class="menu-item mark-completed" on:click={() => markAssessmentCompleted(assessment)}>
|
||||||
|
Mark as Completed
|
||||||
|
</button>
|
||||||
|
{:else if isCompleted}
|
||||||
|
<button class="menu-item mark-not-completed" on:click={() => unmarkAssessmentCompleted(assessment)}>
|
||||||
|
Mark as Not Complete
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<h3 class="assessment-title">{assessment.title}</h3>
|
||||||
|
|
||||||
|
{#if !assessment.results && !isCompleted}
|
||||||
|
<div class="assessment-meta">
|
||||||
|
<div class="due-date {dueDateClass}">
|
||||||
|
📅 {formatDate(assessment.due, assessment.submitted)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if assessment.results}
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="Thermoscore__Thermoscore___WFpL3" style="--fill-colour: {color}">
|
||||||
|
<div style="width: {assessment.results.percentage}%" class="Thermoscore__fill___ojxDI">
|
||||||
|
<div title="{assessment.results.percentage}%" class="Thermoscore__text___XSR_M">
|
||||||
|
{(() => {
|
||||||
|
const allSettings = settingsState.getAll() as unknown as any;
|
||||||
|
const letterGradeSetting = allSettings["plugin.assessments-average.settings"]?.lettergrade;
|
||||||
|
return letterGradeSetting
|
||||||
|
? percentageToLetter(assessment.results.percentage)
|
||||||
|
: `${assessment.results.percentage}%`;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let error: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="error-container">
|
||||||
|
<p class="error-text">Failed to load assessments</p>
|
||||||
|
<p style="color: #94a3b8; font-size: 0.875rem;">{error}</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<div id="grid-view-container">
|
||||||
|
<div class="grid-view-header">
|
||||||
|
<h1 class="grid-view-title">Assessments</h1>
|
||||||
|
<div class="grid-view-filters">
|
||||||
|
<select class="filter-select" disabled>
|
||||||
|
<option value="all">Loading subjects...</option>
|
||||||
|
</select>
|
||||||
|
<select class="filter-select" disabled>
|
||||||
|
<option value="due">Sort by Due Date</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-grid-content">
|
||||||
|
<div class="kanban-board">
|
||||||
|
{#each columns as column}
|
||||||
|
<div class="kanban-column-parent">
|
||||||
|
<div class="kanban-column {column.className}">
|
||||||
|
<div class="column-header">
|
||||||
|
<div class="column-title">
|
||||||
|
{column.icon} {column.title}
|
||||||
|
<span class="column-count">...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column-cards" id="{column.key.toLowerCase()}-cards">
|
||||||
|
{#each Array(column.skeletonCount) as _}
|
||||||
|
<div class="assessment-card">
|
||||||
|
<div class="skeleton-element skeleton-label"></div>
|
||||||
|
<div class="skeleton-element skeleton-title"></div>
|
||||||
|
<div class="skeleton-element skeleton-title-line2"></div>
|
||||||
|
<div class="skeleton-element skeleton-meta"></div>
|
||||||
|
{#if column.key === "MARKS_RELEASED"}
|
||||||
|
<div class="skeleton-footer">
|
||||||
|
<div class="skeleton-element" style="height: 16px; width: 100%;"></div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
key: "UPCOMING",
|
||||||
|
title: "Upcoming",
|
||||||
|
className: "column-upcoming",
|
||||||
|
icon: "📅",
|
||||||
|
skeletonCount: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "DUE_SOON",
|
||||||
|
title: "Due Soon",
|
||||||
|
className: "column-due-soon",
|
||||||
|
icon: "⏰",
|
||||||
|
skeletonCount: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "OVERDUE",
|
||||||
|
title: "Overdue",
|
||||||
|
className: "column-overdue",
|
||||||
|
icon: "🚨",
|
||||||
|
skeletonCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "MARKS_RELEASED",
|
||||||
|
title: "Marked",
|
||||||
|
className: "column-marked",
|
||||||
|
icon: "✅",
|
||||||
|
skeletonCount: 4,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
interface Subject {
|
||||||
|
code: string;
|
||||||
|
programme: number;
|
||||||
|
metaclass: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
interface PrefItem {
|
||||||
|
name: string;
|
||||||
|
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;
|
||||||
|
|
||||||
|
async function fetchJSON(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSubjects() {
|
||||||
|
const res = await fetchJSON("/seqta/student/load/subjects?", {});
|
||||||
|
return res.payload
|
||||||
|
.filter((s: any) => s.active === 1)
|
||||||
|
.flatMap((s: any) => s.subjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPrefs(student: number) {
|
||||||
|
const res = await fetchJSON("/seqta/student/load/prefs?", {
|
||||||
|
request: "userPrefs",
|
||||||
|
asArray: true,
|
||||||
|
user: student,
|
||||||
|
});
|
||||||
|
const colors: Record<string, string> = {};
|
||||||
|
res.payload.forEach((p: PrefItem) => {
|
||||||
|
if (p.name.startsWith("timetable.subject.colour.")) {
|
||||||
|
const code = p.name.replace("timetable.subject.colour.", "");
|
||||||
|
colors[code] = p.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUpcoming(student: number) {
|
||||||
|
const res = await fetchJSON("/seqta/student/assessment/list/upcoming?", {
|
||||||
|
student,
|
||||||
|
});
|
||||||
|
return res.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPast(student: number, subjects: Subject[]) {
|
||||||
|
const map: Record<number, any> = {};
|
||||||
|
await Promise.all(
|
||||||
|
subjects.map(async (s) => {
|
||||||
|
const res = await fetchJSON("/seqta/student/assessment/list/past?", {
|
||||||
|
programme: s.programme,
|
||||||
|
metaclass: s.metaclass,
|
||||||
|
student,
|
||||||
|
});
|
||||||
|
if (res.payload.tasks) {
|
||||||
|
res.payload.tasks.forEach((t: any) => {
|
||||||
|
map[t.id] = t;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadSubmissions(student: number, assessments: any[]) {
|
||||||
|
const submissionMap: Record<number, boolean> = {};
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
assessments.map(async (assessment) => {
|
||||||
|
try {
|
||||||
|
const res = await fetchJSON(
|
||||||
|
"/seqta/student/assessment/submissions/get",
|
||||||
|
{
|
||||||
|
assessment: assessment.id,
|
||||||
|
metaclass: assessment.metaclassID,
|
||||||
|
student,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
submissionMap[assessment.id] = res.payload && res.payload.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to fetch submission for assessment ${assessment.id}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
submissionMap[assessment.id] = false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return submissionMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
loadPrefs(student),
|
||||||
|
loadUpcoming(student),
|
||||||
|
]);
|
||||||
|
const pastMap = await loadPast(student, subjects);
|
||||||
|
const map: Record<number, any> = {};
|
||||||
|
upcoming.forEach((a: any) => {
|
||||||
|
map[a.id] = { ...a };
|
||||||
|
});
|
||||||
|
Object.values(pastMap).forEach((t: any) => {
|
||||||
|
if (map[t.id]) Object.assign(map[t.id], t);
|
||||||
|
else map[t.id] = t;
|
||||||
|
});
|
||||||
|
|
||||||
|
const allAssessments = Object.values(map);
|
||||||
|
const submissions = await loadSubmissions(student, allAssessments);
|
||||||
|
|
||||||
|
allAssessments.forEach((assessment: any) => {
|
||||||
|
assessment.submitted = submissions[assessment.id] || false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = { assessments: allAssessments, subjects, colors };
|
||||||
|
cache = { time: Date.now(), data };
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { Plugin } from "../../core/types";
|
||||||
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
import { getAssessmentsData } from "./api";
|
||||||
|
import { renderSkeletonLoader, renderErrorState } from "./ui";
|
||||||
|
import styles from "./styles.css?inline";
|
||||||
|
import { delay } from "@/seqta/utils/delay";
|
||||||
|
|
||||||
|
const assessmentsOverviewPlugin: Plugin<{}> = {
|
||||||
|
id: "assessments-overview",
|
||||||
|
name: "Assessments Overview",
|
||||||
|
description:
|
||||||
|
"Adds an overview option to the assessments page that organizes assessments by status",
|
||||||
|
version: "1.0.0",
|
||||||
|
settings: {},
|
||||||
|
disableToggle: false,
|
||||||
|
styles,
|
||||||
|
|
||||||
|
run: async () => {
|
||||||
|
const menu = (await waitForElm(
|
||||||
|
'[data-key="assessments"] > .sub > ul',
|
||||||
|
true,
|
||||||
|
100,
|
||||||
|
60,
|
||||||
|
)) as HTMLElement;
|
||||||
|
const gridItem = document.createElement("li");
|
||||||
|
gridItem.className = "item";
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.textContent = "Overview";
|
||||||
|
gridItem.appendChild(label);
|
||||||
|
menu.insertBefore(gridItem, menu.children[1] || null);
|
||||||
|
|
||||||
|
if (window.location.hash.includes("/assessments/overview")) {
|
||||||
|
loadGridView();
|
||||||
|
}
|
||||||
|
|
||||||
|
const clickHandler = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
loadGridView();
|
||||||
|
};
|
||||||
|
gridItem.addEventListener("click", clickHandler);
|
||||||
|
|
||||||
|
async function loadGridView() {
|
||||||
|
await delay(1);
|
||||||
|
window.history.pushState({}, "", "/#?page=/assessments/overview");
|
||||||
|
document.title = "Overview ― SEQTA Learn";
|
||||||
|
const main = document.getElementById("main");
|
||||||
|
if (!main) return;
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll('[data-key="assessments"] .item')
|
||||||
|
.forEach((item) => {
|
||||||
|
item.classList.remove("active");
|
||||||
|
});
|
||||||
|
gridItem.classList.add("active");
|
||||||
|
document
|
||||||
|
.querySelector('[data-key="assessments"]')
|
||||||
|
?.classList.add("active");
|
||||||
|
|
||||||
|
main.innerHTML = '<div id="grid-view-container"></div>';
|
||||||
|
const container = document.getElementById(
|
||||||
|
"grid-view-container",
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
renderSkeletonLoader(container);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getAssessmentsData();
|
||||||
|
const { renderGrid } = await import("./ui");
|
||||||
|
renderGrid(container, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load assessments:", err);
|
||||||
|
renderErrorState(
|
||||||
|
container,
|
||||||
|
err instanceof Error ? err.message : "Unknown error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
gridItem.removeEventListener("click", clickHandler);
|
||||||
|
gridItem.remove();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default assessmentsOverviewPlugin;
|
||||||
@@ -0,0 +1,798 @@
|
|||||||
|
#grid-view-container {
|
||||||
|
background: transparent;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-title {
|
||||||
|
font-size: 1.875rem !important;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
.dark .grid-view-title {
|
||||||
|
color: #f8fafc;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #d41e3a;
|
||||||
|
box-shadow: 0 0 0 3px rgba(212, 30, 58, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode dropdowns */
|
||||||
|
.dark .filter-select {
|
||||||
|
background: var(--background-primary) !important;
|
||||||
|
border-color: var(--background-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .filter-select:focus {
|
||||||
|
border-color: #d41e3a;
|
||||||
|
box-shadow: 0 0 0 3px rgba(212, 30, 58, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .filter-select:hover {
|
||||||
|
border-color: var(--background-secondary);
|
||||||
|
background: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .filter-select option {
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-grid-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kanban Board Layout */
|
||||||
|
.kanban-board {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column-parent {
|
||||||
|
flex: 0 0 320px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column {
|
||||||
|
max-height: 100%;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 0 0 2px #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode columns */
|
||||||
|
.dark .kanban-column {
|
||||||
|
background: var(--background-primary);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-header {
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
border-bottom: 2px solid #e2e8f0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode column headers */
|
||||||
|
.dark .column-header {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-count {
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #64748b;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-count {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-cards {
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assessment Cards */
|
||||||
|
.assessment-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border-left: 4px solid var(--subject-color, #d41e3a);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode cards */
|
||||||
|
.dark .assessment-card {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .assessment-card:hover {
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-labels {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-subject {
|
||||||
|
background: var(--subject-color, #d41e3a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75rem;
|
||||||
|
right: 0.75rem;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0.25rem !important;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #64748b;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px !important;
|
||||||
|
height: 24px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
position: static !important;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button:hover {
|
||||||
|
background: #f1f5f9 !important;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button svg {
|
||||||
|
width: 16px !important;
|
||||||
|
height: 16px !important;
|
||||||
|
fill: currentColor !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-button {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 140px;
|
||||||
|
z-index: 30;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-dropdown {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-item {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.mark-completed {
|
||||||
|
color: #059669;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-item.mark-completed {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.mark-not-completed {
|
||||||
|
color: #dc2626;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .menu-item.mark-not-completed {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-right: 2rem; /* Make room for menu button */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .assessment-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .assessment-meta {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date.overdue {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date.due-soon {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.due-date.upcoming {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .card-footer {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-display {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-good {
|
||||||
|
background: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #059669;
|
||||||
|
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-average {
|
||||||
|
background: rgba(245, 158, 11, 0.1);
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-bad {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-empty {
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .grade-empty {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-color: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column-specific styling */
|
||||||
|
.column-upcoming .column-header {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-due-soon .column-header {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fffbeb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-overdue .column-header {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fef2f2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-submitted .column-header {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fef3c7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-marked .column-header {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode column headers */
|
||||||
|
.dark .column-upcoming .column-header {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #1e3a8a 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-due-soon .column-header {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #92400e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-overdue .column-header {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #991b1b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-submitted .column-header {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #92400e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-marked .column-header {
|
||||||
|
background: linear-gradient(135deg, var(--background-secondary) 0%, #065f46 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subject filter view */
|
||||||
|
.subject-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-left: 4px solid var(--subject-color, #d41e3a);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .subject-header {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-title {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .subject-title {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subject-code {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .subject-code {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading and error states */
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4rem 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .loading-container {
|
||||||
|
background: var(--background-primary);
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
border: 3px solid #e2e8f0;
|
||||||
|
border-top: 3px solid #d41e3a;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .loading-spinner {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
border-top-color: #d41e3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .loading-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4rem 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #fecaca;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .error-container {
|
||||||
|
background: var(--background-primary);
|
||||||
|
border-color: #991b1b;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-state {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .empty-column {
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -1000px 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 1000px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-element {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#f1f5f9 0%,
|
||||||
|
#e2e8f0 50%,
|
||||||
|
#f1f5f9 100%
|
||||||
|
);
|
||||||
|
background-size: 1000px 100%;
|
||||||
|
animation: shimmer 2s infinite linear;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .skeleton-element {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--background-primary) 0%,
|
||||||
|
var(--background-secondary) 50%,
|
||||||
|
var(--background-primary) 100%
|
||||||
|
);
|
||||||
|
background-size: 1000px 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-label {
|
||||||
|
height: 20px;
|
||||||
|
width: 60px;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-title {
|
||||||
|
height: 16px;
|
||||||
|
width: 80%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-title-line2 {
|
||||||
|
height: 16px;
|
||||||
|
width: 60%;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-meta {
|
||||||
|
height: 12px;
|
||||||
|
width: 40%;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-footer {
|
||||||
|
height: 8px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .skeleton-footer {
|
||||||
|
border-top-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#grid-view-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-filters {
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-board {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kanban-column {
|
||||||
|
flex: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.grid-view-title {
|
||||||
|
font-size: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-card {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling for webkit browsers */
|
||||||
|
.column-cards::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-cards::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-cards::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-cards::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode scrollbars */
|
||||||
|
.dark .column-cards::-webkit-scrollbar-track {
|
||||||
|
background: var(--background-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-cards::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .column-cards::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import renderSvelte from "@/interface/main";
|
||||||
|
import AssessmentsOverview from "./AssessmentsOverview.svelte";
|
||||||
|
import SkeletonLoader from "./SkeletonLoader.svelte";
|
||||||
|
import ErrorState from "./ErrorState.svelte";
|
||||||
|
import { unmount } from "svelte";
|
||||||
|
|
||||||
|
let currentApp: any = null;
|
||||||
|
|
||||||
|
export function renderGrid(container: HTMLElement, data: any) {
|
||||||
|
if (currentApp) {
|
||||||
|
unmount(currentApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.className = "";
|
||||||
|
|
||||||
|
currentApp = renderSvelte(AssessmentsOverview, container, { data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSkeletonLoader(container: HTMLElement) {
|
||||||
|
if (currentApp) {
|
||||||
|
unmount(currentApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.className = "";
|
||||||
|
|
||||||
|
currentApp = renderSvelte(SkeletonLoader, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function renderLoadingState(container: HTMLElement) {
|
||||||
|
renderSkeletonLoader(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderErrorState(container: HTMLElement, error: string) {
|
||||||
|
if (currentApp) {
|
||||||
|
unmount(currentApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.className = "";
|
||||||
|
|
||||||
|
currentApp = renderSvelte(ErrorState, container, { error });
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
export function formatDate(dateStr: string, submitted?: boolean): string {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
const now = new Date();
|
||||||
|
const diffTime = d.getTime() - now.getTime();
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays < 0 && !submitted) {
|
||||||
|
const overdueDays = Math.abs(diffDays);
|
||||||
|
if (overdueDays === 1) return "1 day overdue";
|
||||||
|
return `${overdueDays} days overdue`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffDays === 0) return "Today";
|
||||||
|
|
||||||
|
if (diffDays === 1) return "Tomorrow";
|
||||||
|
|
||||||
|
if (diffDays <= 7) {
|
||||||
|
const weekdayName = d.toLocaleDateString(undefined, { weekday: "long" });
|
||||||
|
|
||||||
|
return diffDays < 0 ? `Last ${weekdayName}` : weekdayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.toLocaleDateString(undefined, {
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function determineStatus(item: any): string {
|
||||||
|
if (
|
||||||
|
item.status === "MARKS_RELEASED" ||
|
||||||
|
item.grade ||
|
||||||
|
(item.percentage !== undefined && item.percentage !== null) ||
|
||||||
|
(item.achieved !== undefined && item.achieved !== null)
|
||||||
|
) {
|
||||||
|
return "MARKS_RELEASED";
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedKey = "betterseqta-completed-assessments";
|
||||||
|
const completed = JSON.parse(localStorage.getItem(completedKey) || "[]");
|
||||||
|
if (completed.includes(item.id)) {
|
||||||
|
return "MARKS_RELEASED";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.submitted) {
|
||||||
|
return "SUBMITTED";
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const due = new Date(item.due);
|
||||||
|
|
||||||
|
const diffTime = due.getTime() - now.getTime();
|
||||||
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffDays < 0) {
|
||||||
|
return "OVERDUE";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diffDays <= 7) {
|
||||||
|
return "DUE_SOON";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "UPCOMING";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGradeValue(assessment: any): number | null {
|
||||||
|
if (
|
||||||
|
assessment.results?.percentage !== undefined &&
|
||||||
|
assessment.results.percentage !== null
|
||||||
|
) {
|
||||||
|
return assessment.results.percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assessment.percentage !== undefined && assessment.percentage !== null) {
|
||||||
|
return assessment.percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
assessment.achieved !== undefined &&
|
||||||
|
assessment.outOf !== undefined &&
|
||||||
|
assessment.achieved !== null &&
|
||||||
|
assessment.outOf !== null &&
|
||||||
|
assessment.outOf > 0
|
||||||
|
) {
|
||||||
|
return (assessment.achieved / assessment.outOf) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
assessment.results?.achieved !== undefined &&
|
||||||
|
assessment.results?.outOf !== undefined &&
|
||||||
|
assessment.results.achieved !== null &&
|
||||||
|
assessment.results.outOf !== null &&
|
||||||
|
assessment.results.outOf > 0
|
||||||
|
) {
|
||||||
|
return (assessment.results.achieved / assessment.results.outOf) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -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 { componentSetting, defineSettings, numberSetting, booleanSetting } 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,91 @@
|
|||||||
|
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 the worker manager to avoid loading heavy dependencies
|
||||||
|
const { VectorWorkerManager } = await import("./src/indexing/worker/vectorWorkerManager");
|
||||||
|
const workerManager = VectorWorkerManager.getInstance();
|
||||||
|
await workerManager.resetWorker();
|
||||||
|
console.log("Vector worker reset successfully");
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to reset vector worker:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete both 'embeddiaDB' and 'betterseqta-index' using native IndexedDB APIs
|
||||||
|
const deleteDb = (dbName: string) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const req = indexedDB.deleteDatabase(dbName);
|
||||||
|
req.onsuccess = () => resolve();
|
||||||
|
req.onerror = () => reject(req.error);
|
||||||
|
req.onblocked = () => {
|
||||||
|
reject(new Error(`One database is open, failed to remove: ${dbName}`));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await deleteDb("embeddiaDB");
|
||||||
|
await deleteDb("betterseqta-index");
|
||||||
|
alert("Search index and storage have been reset.");
|
||||||
|
} catch (e) {
|
||||||
|
alert("Failed to reset one or more databases: " + 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")
|
||||||
|
});
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
searchHotkey: string
|
searchHotkey: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// Make searchHotkey reactive to setting changes
|
|
||||||
let currentSearchHotkey = $state(initialSearchHotkey);
|
let currentSearchHotkey = $state(initialSearchHotkey);
|
||||||
|
|
||||||
let commandsFuse = $state<Fuse<StaticCommandItem>>();
|
let commandsFuse = $state<Fuse<StaticCommandItem>>();
|
||||||
@@ -177,7 +176,7 @@
|
|||||||
isLoading = false;
|
isLoading = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedPerformSearch = debounce(performSearch, 10);
|
const debouncedPerformSearch = debounce(performSearch, 20);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (commandPalleteOpen) {
|
if (commandPalleteOpen) {
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ const globalSearchPlugin: Plugin<typeof settings> = {
|
|||||||
|
|
||||||
initVectorSearch();
|
initVectorSearch();
|
||||||
|
|
||||||
|
// Warm up vector worker in background to improve initial response time
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
VectorWorkerManager.getInstance();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Global Search] Vector worker warm-up failed:", error);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Add debug helpers to window for troubleshooting
|
// Add debug helpers to window for troubleshooting
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.globalSearchDebug = {
|
window.globalSearchDebug = {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import browser from "webextension-polyfill";
|
|||||||
export function mountSearchBar(
|
export function mountSearchBar(
|
||||||
titleElement: Element,
|
titleElement: Element,
|
||||||
api: any,
|
api: any,
|
||||||
appRef: { current: any },
|
appRef: { current: any; storageChangeHandler?: any },
|
||||||
) {
|
) {
|
||||||
if (titleElement.querySelector(".search-trigger")) {
|
if (titleElement.querySelector(".search-trigger")) {
|
||||||
return;
|
return;
|
||||||
@@ -49,6 +49,9 @@ export function mountSearchBar(
|
|||||||
|
|
||||||
browser.storage.onChanged.addListener(handleStorageChange);
|
browser.storage.onChanged.addListener(handleStorageChange);
|
||||||
|
|
||||||
|
// Store reference to cleanup function for proper removal
|
||||||
|
appRef.storageChangeHandler = handleStorageChange;
|
||||||
|
|
||||||
const searchRoot = document.createElement("div");
|
const searchRoot = document.createElement("div");
|
||||||
document.body.appendChild(searchRoot);
|
document.body.appendChild(searchRoot);
|
||||||
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
|
const searchRootShadow = searchRoot.attachShadow({ mode: "open" });
|
||||||
@@ -69,7 +72,7 @@ export function mountSearchBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanupSearchBar(appRef: { current: any }) {
|
export function cleanupSearchBar(appRef: { current: any; storageChangeHandler?: any }) {
|
||||||
if (appRef.current) {
|
if (appRef.current) {
|
||||||
try {
|
try {
|
||||||
unmount(appRef.current);
|
unmount(appRef.current);
|
||||||
@@ -94,6 +97,8 @@ export function cleanupSearchBar(appRef: { current: any }) {
|
|||||||
// Clean up vector worker
|
// Clean up vector worker
|
||||||
VectorWorkerManager.getInstance().terminate();
|
VectorWorkerManager.getInstance().terminate();
|
||||||
|
|
||||||
// Remove storage listener
|
if (appRef.storageChangeHandler) {
|
||||||
browser.storage.onChanged.removeListener(() => {});
|
browser.storage.onChanged.removeListener(appRef.storageChangeHandler);
|
||||||
|
appRef.storageChangeHandler = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,22 @@ const META_STORE = "meta";
|
|||||||
const VERSION_KEY = "betterseqta-index-version";
|
const VERSION_KEY = "betterseqta-index-version";
|
||||||
|
|
||||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||||
|
let cachedDb: IDBDatabase | null = null;
|
||||||
|
|
||||||
// Get the current version from localStorage or start at 1
|
|
||||||
function getCurrentVersion(): number {
|
function getCurrentVersion(): number {
|
||||||
const storedVersion = localStorage.getItem(VERSION_KEY);
|
const storedVersion = localStorage.getItem(VERSION_KEY);
|
||||||
return storedVersion ? parseInt(storedVersion, 10) : 1;
|
return storedVersion ? parseInt(storedVersion, 10) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the version in localStorage
|
|
||||||
function updateVersion(version: number) {
|
function updateVersion(version: number) {
|
||||||
localStorage.setItem(VERSION_KEY, version.toString());
|
localStorage.setItem(VERSION_KEY, version.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDB(): Promise<IDBDatabase> {
|
function openDB(): Promise<IDBDatabase> {
|
||||||
|
if (cachedDb && cachedDb.version >= getCurrentVersion()) {
|
||||||
|
return Promise.resolve(cachedDb);
|
||||||
|
}
|
||||||
|
|
||||||
if (dbPromise) return dbPromise;
|
if (dbPromise) return dbPromise;
|
||||||
|
|
||||||
const currentVersion = getCurrentVersion();
|
const currentVersion = getCurrentVersion();
|
||||||
@@ -26,8 +29,11 @@ function openDB(): Promise<IDBDatabase> {
|
|||||||
try {
|
try {
|
||||||
request = indexedDB.open(DB_NAME, currentVersion);
|
request = indexedDB.open(DB_NAME, currentVersion);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If there's a version error, try to delete the database and start fresh
|
|
||||||
console.warn("Database version conflict, recreating database...");
|
console.warn("Database version conflict, recreating database...");
|
||||||
|
if (cachedDb) {
|
||||||
|
cachedDb.close();
|
||||||
|
cachedDb = null;
|
||||||
|
}
|
||||||
indexedDB.deleteDatabase(DB_NAME);
|
indexedDB.deleteDatabase(DB_NAME);
|
||||||
localStorage.removeItem(VERSION_KEY);
|
localStorage.removeItem(VERSION_KEY);
|
||||||
request = indexedDB.open(DB_NAME, 1);
|
request = indexedDB.open(DB_NAME, 1);
|
||||||
@@ -38,22 +44,37 @@ function openDB(): Promise<IDBDatabase> {
|
|||||||
const db = request.result;
|
const db = request.result;
|
||||||
const existingStores = Array.from(db.objectStoreNames);
|
const existingStores = Array.from(db.objectStoreNames);
|
||||||
|
|
||||||
// Always ensure META_STORE exists
|
|
||||||
if (!existingStores.includes(META_STORE)) {
|
if (!existingStores.includes(META_STORE)) {
|
||||||
db.createObjectStore(META_STORE);
|
db.createObjectStore(META_STORE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update version in localStorage to match the database
|
|
||||||
updateVersion(event.newVersion || 1);
|
updateVersion(event.newVersion || 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = () => resolve(request.result);
|
request.onsuccess = () => {
|
||||||
|
if (cachedDb && cachedDb !== request.result) {
|
||||||
|
cachedDb.close();
|
||||||
|
}
|
||||||
|
cachedDb = request.result;
|
||||||
|
|
||||||
|
cachedDb.onclose = () => {
|
||||||
|
cachedDb = null;
|
||||||
|
dbPromise = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
resolve(request.result);
|
||||||
|
};
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
console.error("Error opening database:", request.error);
|
console.error("Error opening database:", request.error);
|
||||||
// If there's an error, try to recover by deleting and recreating
|
|
||||||
|
if (cachedDb) {
|
||||||
|
cachedDb.close();
|
||||||
|
cachedDb = null;
|
||||||
|
}
|
||||||
indexedDB.deleteDatabase(DB_NAME);
|
indexedDB.deleteDatabase(DB_NAME);
|
||||||
localStorage.removeItem(VERSION_KEY);
|
localStorage.removeItem(VERSION_KEY);
|
||||||
|
dbPromise = null;
|
||||||
reject(request.error);
|
reject(request.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -64,11 +85,12 @@ function openDB(): Promise<IDBDatabase> {
|
|||||||
async function getStore(store: string, mode: IDBTransactionMode = "readonly") {
|
async function getStore(store: string, mode: IDBTransactionMode = "readonly") {
|
||||||
const db = await openDB();
|
const db = await openDB();
|
||||||
|
|
||||||
// Create store dynamically if needed
|
|
||||||
if (!db.objectStoreNames.contains(store)) {
|
if (!db.objectStoreNames.contains(store)) {
|
||||||
db.close();
|
|
||||||
await upgradeDB(store);
|
await upgradeDB(store);
|
||||||
return getStore(store, mode);
|
|
||||||
|
const upgradedDb = await openDB();
|
||||||
|
const tx = upgradedDb.transaction(store, mode);
|
||||||
|
return tx.objectStore(store);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = db.transaction(store, mode);
|
const tx = db.transaction(store, mode);
|
||||||
@@ -80,11 +102,11 @@ function upgradeDB(newStore: string): Promise<void> {
|
|||||||
const currentVersion = getCurrentVersion();
|
const currentVersion = getCurrentVersion();
|
||||||
const newVersion = currentVersion + 1;
|
const newVersion = currentVersion + 1;
|
||||||
|
|
||||||
// Close any existing connections
|
if (cachedDb) {
|
||||||
if (dbPromise) {
|
cachedDb.close();
|
||||||
dbPromise.then((db) => db.close());
|
cachedDb = null;
|
||||||
dbPromise = null;
|
|
||||||
}
|
}
|
||||||
|
dbPromise = null;
|
||||||
|
|
||||||
const request = indexedDB.open(DB_NAME, newVersion);
|
const request = indexedDB.open(DB_NAME, newVersion);
|
||||||
|
|
||||||
@@ -93,11 +115,18 @@ function upgradeDB(newStore: string): Promise<void> {
|
|||||||
if (!db.objectStoreNames.contains(newStore)) {
|
if (!db.objectStoreNames.contains(newStore)) {
|
||||||
db.createObjectStore(newStore);
|
db.createObjectStore(newStore);
|
||||||
}
|
}
|
||||||
// Update version in localStorage
|
|
||||||
updateVersion(event.newVersion || newVersion);
|
updateVersion(event.newVersion || newVersion);
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
|
cachedDb = request.result;
|
||||||
|
|
||||||
|
cachedDb.onclose = () => {
|
||||||
|
cachedDb = null;
|
||||||
|
dbPromise = null;
|
||||||
|
};
|
||||||
|
|
||||||
dbPromise = Promise.resolve(request.result);
|
dbPromise = Promise.resolve(request.result);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
@@ -183,11 +212,17 @@ export async function clear(store: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to reset the database if needed
|
|
||||||
export async function resetDatabase(): Promise<void> {
|
export async function resetDatabase(): Promise<void> {
|
||||||
|
if (cachedDb) {
|
||||||
|
cachedDb.close();
|
||||||
|
cachedDb = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (dbPromise) {
|
if (dbPromise) {
|
||||||
|
try {
|
||||||
const db = await dbPromise;
|
const db = await dbPromise;
|
||||||
db.close();
|
db.close();
|
||||||
|
} catch (e) {}
|
||||||
dbPromise = null;
|
dbPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { renderComponentMap } from "./renderComponents";
|
|||||||
import type { IndexItem, Job, JobContext } from "./types";
|
import type { IndexItem, Job, JobContext } from "./types";
|
||||||
import { VectorWorkerManager } from "./worker/vectorWorkerManager";
|
import { VectorWorkerManager } from "./worker/vectorWorkerManager";
|
||||||
import { loadDynamicItems } from "../utils/dynamicItems";
|
import { loadDynamicItems } from "../utils/dynamicItems";
|
||||||
|
import { getVectorizedItemIds } from "./utils";
|
||||||
|
|
||||||
const META_STORE = "meta";
|
const META_STORE = "meta";
|
||||||
const LOCK_KEY = "bsq-indexer-lock";
|
const LOCK_KEY = "bsq-indexer-lock";
|
||||||
@@ -276,19 +277,28 @@ export async function runIndexing(): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasStreamingJobs) {
|
let allItemsInPrimaryStores = await loadAllStoredItems();
|
||||||
const allItemsInPrimaryStores = await loadAllStoredItems();
|
|
||||||
|
|
||||||
if (allItemsInPrimaryStores.length > 0) {
|
if (allItemsInPrimaryStores.length > 0) {
|
||||||
console.debug(
|
console.debug(
|
||||||
`%c[Indexer] Sending ${allItemsInPrimaryStores.length} items from primary stores to worker for vectorization check...`,
|
`%c[Indexer] Checking ${allItemsInPrimaryStores.length} items for vectorization...`,
|
||||||
"color: #4ea1ff",
|
"color: #4ea1ff",
|
||||||
);
|
);
|
||||||
dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization of stored items");
|
|
||||||
|
// Pre-filter items to avoid initializing worker if nothing new
|
||||||
|
const vectorizedItemIds = await getVectorizedItemIds();
|
||||||
|
const newItemsToVectorize = allItemsInPrimaryStores.filter(item => !vectorizedItemIds.has(item.id));
|
||||||
|
|
||||||
|
if (newItemsToVectorize.length > 0) {
|
||||||
|
console.debug(
|
||||||
|
`%c[Indexer] Sending ${newItemsToVectorize.length} new items to worker for vectorization (${allItemsInPrimaryStores.length - newItemsToVectorize.length} already vectorized)`,
|
||||||
|
"color: #4ea1ff",
|
||||||
|
);
|
||||||
|
dispatchProgress(completedJobs, totalSteps, true, "Starting vectorization of new items");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const workerManager = VectorWorkerManager.getInstance();
|
const workerManager = VectorWorkerManager.getInstance();
|
||||||
await workerManager.processItems(allItemsInPrimaryStores, (progress) => {
|
await workerManager.processItems(newItemsToVectorize, (progress) => {
|
||||||
let detailMessage = progress.message || "";
|
let detailMessage = progress.message || "";
|
||||||
if (
|
if (
|
||||||
progress.status === "processing" &&
|
progress.status === "processing" &&
|
||||||
@@ -356,6 +366,19 @@ export async function runIndexing(): Promise<void> {
|
|||||||
String(error),
|
String(error),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.debug(
|
||||||
|
`%c[Indexer] All ${allItemsInPrimaryStores.length} items are already vectorized, skipping worker initialization.`,
|
||||||
|
"color: gray",
|
||||||
|
);
|
||||||
|
completedJobs++;
|
||||||
|
dispatchProgress(
|
||||||
|
completedJobs,
|
||||||
|
totalSteps,
|
||||||
|
false,
|
||||||
|
"Indexing finished (all items already vectorized)",
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
"%c[Indexer] No items found in primary stores to send for vectorization.",
|
"%c[Indexer] No items found in primary stores to send for vectorization.",
|
||||||
@@ -369,23 +392,10 @@ export async function runIndexing(): Promise<void> {
|
|||||||
"Indexing finished (no items for vectorization)",
|
"Indexing finished (no items for vectorization)",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.debug(
|
|
||||||
"%c[Indexer] Skipping bulk vectorization - streaming jobs will handle vectorization",
|
|
||||||
"color: #4ea1ff",
|
|
||||||
);
|
|
||||||
completedJobs++;
|
|
||||||
dispatchProgress(
|
|
||||||
completedJobs,
|
|
||||||
totalSteps,
|
|
||||||
false,
|
|
||||||
"Indexing finished (streaming vectorization active)",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopHeartbeat();
|
stopHeartbeat();
|
||||||
|
|
||||||
const allItemsInPrimaryStores = await loadAllStoredItems();
|
allItemsInPrimaryStores = await loadAllStoredItems();
|
||||||
allItemsInPrimaryStores.forEach(item => {
|
allItemsInPrimaryStores.forEach(item => {
|
||||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
||||||
if (jobDef) {
|
if (jobDef) {
|
||||||
|
|||||||
@@ -8,18 +8,20 @@ import { renderComponentMap } from "../renderComponents";
|
|||||||
import { jobs } from "../jobs";
|
import { jobs } from "../jobs";
|
||||||
|
|
||||||
const RATE_LIMIT_CONFIG = {
|
const RATE_LIMIT_CONFIG = {
|
||||||
minDelay: 50,
|
minDelay: 30,
|
||||||
maxDelay: 5000,
|
maxDelay: 3000,
|
||||||
baseDelay: 200,
|
baseDelay: 150,
|
||||||
backoffMultiplier: 1.5,
|
backoffMultiplier: 1.3,
|
||||||
maxRetries: 3,
|
maxRetries: 3,
|
||||||
adaptiveBatchSize: true,
|
adaptiveBatchSize: true,
|
||||||
minBatchSize: 10,
|
minBatchSize: 15,
|
||||||
maxBatchSize: 100,
|
maxBatchSize: 150,
|
||||||
baseBatchSize: 50,
|
baseBatchSize: 75,
|
||||||
vectorBatchSize: 5,
|
vectorBatchSize: 10,
|
||||||
parallelRequests: 5,
|
parallelRequests: 8,
|
||||||
parallelDelay: 100,
|
parallelDelay: 50,
|
||||||
|
circuitBreakerThreshold: 5,
|
||||||
|
circuitBreakerResetTime: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MessagesProgress {
|
interface MessagesProgress {
|
||||||
@@ -33,6 +35,9 @@ interface MessagesProgress {
|
|||||||
processedIds: string[];
|
processedIds: string[];
|
||||||
streamingStarted: boolean;
|
streamingStarted: boolean;
|
||||||
totalEstimated: number;
|
totalEstimated: number;
|
||||||
|
circuitBreakerOpen: boolean;
|
||||||
|
circuitBreakerOpenTime: number;
|
||||||
|
consecutiveFailures: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMessages = async (offset = 0, limit = 100) => {
|
const fetchMessages = async (offset = 0, limit = 100) => {
|
||||||
@@ -99,50 +104,38 @@ function calculateAdaptiveDelay(
|
|||||||
progress: MessagesProgress,
|
progress: MessagesProgress,
|
||||||
responseTime: number,
|
responseTime: number,
|
||||||
): number {
|
): number {
|
||||||
const { currentDelay, failedRequests, lastSuccessTime } = progress;
|
const {
|
||||||
|
currentDelay,
|
||||||
|
failedRequests,
|
||||||
|
lastSuccessTime,
|
||||||
|
circuitBreakerOpen,
|
||||||
|
consecutiveFailures,
|
||||||
|
} = progress;
|
||||||
const timeSinceLastSuccess = Date.now() - lastSuccessTime;
|
const timeSinceLastSuccess = Date.now() - lastSuccessTime;
|
||||||
|
|
||||||
if (failedRequests > 0 || responseTime > 2000) {
|
if (circuitBreakerOpen) {
|
||||||
|
return RATE_LIMIT_CONFIG.maxDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consecutiveFailures > 2 || failedRequests > 3 || responseTime > 3000) {
|
||||||
return Math.min(
|
return Math.min(
|
||||||
currentDelay * RATE_LIMIT_CONFIG.backoffMultiplier,
|
currentDelay *
|
||||||
|
(RATE_LIMIT_CONFIG.backoffMultiplier + consecutiveFailures * 0.2),
|
||||||
RATE_LIMIT_CONFIG.maxDelay,
|
RATE_LIMIT_CONFIG.maxDelay,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseTime < 500 && timeSinceLastSuccess > 10000) {
|
if (
|
||||||
return Math.max(currentDelay * 0.8, RATE_LIMIT_CONFIG.minDelay);
|
responseTime < 300 &&
|
||||||
|
timeSinceLastSuccess > 5000 &&
|
||||||
|
consecutiveFailures === 0
|
||||||
|
) {
|
||||||
|
return Math.max(currentDelay * 0.7, RATE_LIMIT_CONFIG.minDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentDelay;
|
return currentDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculateAdaptiveBatchSize(
|
|
||||||
progress: MessagesProgress,
|
|
||||||
responseTime: number,
|
|
||||||
): number {
|
|
||||||
if (!RATE_LIMIT_CONFIG.adaptiveBatchSize) {
|
|
||||||
return progress.currentBatchSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentBatchSize, failedRequests } = progress;
|
|
||||||
|
|
||||||
if (failedRequests > 2 || responseTime > 3000) {
|
|
||||||
return Math.max(
|
|
||||||
Math.floor(currentBatchSize * 0.7),
|
|
||||||
RATE_LIMIT_CONFIG.minBatchSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failedRequests === 0 && responseTime < 1000) {
|
|
||||||
return Math.min(
|
|
||||||
Math.floor(currentBatchSize * 1.2),
|
|
||||||
RATE_LIMIT_CONFIG.maxBatchSize,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentBatchSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function estimateMessageCount(): Promise<number> {
|
async function estimateMessageCount(): Promise<number> {
|
||||||
try {
|
try {
|
||||||
const firstBatch = await fetchMessages(0, 100);
|
const firstBatch = await fetchMessages(0, 100);
|
||||||
@@ -157,6 +150,73 @@ async function estimateMessageCount(): Promise<number> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateAdaptiveBatchSize(
|
||||||
|
progress: MessagesProgress,
|
||||||
|
responseTime: number,
|
||||||
|
): number {
|
||||||
|
if (!RATE_LIMIT_CONFIG.adaptiveBatchSize) {
|
||||||
|
return progress.currentBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentBatchSize,
|
||||||
|
failedRequests,
|
||||||
|
circuitBreakerOpen,
|
||||||
|
consecutiveFailures,
|
||||||
|
} = progress;
|
||||||
|
|
||||||
|
if (circuitBreakerOpen) {
|
||||||
|
return RATE_LIMIT_CONFIG.minBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consecutiveFailures > 1 || failedRequests > 2 || responseTime > 2500) {
|
||||||
|
return Math.max(
|
||||||
|
Math.floor(currentBatchSize * 0.6),
|
||||||
|
RATE_LIMIT_CONFIG.minBatchSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedRequests === 0 && responseTime < 800 && consecutiveFailures === 0) {
|
||||||
|
return Math.min(
|
||||||
|
Math.floor(currentBatchSize * 1.4),
|
||||||
|
RATE_LIMIT_CONFIG.maxBatchSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentBatchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkCircuitBreaker(progress: MessagesProgress): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!progress.circuitBreakerOpen &&
|
||||||
|
progress.consecutiveFailures >= RATE_LIMIT_CONFIG.circuitBreakerThreshold
|
||||||
|
) {
|
||||||
|
progress.circuitBreakerOpen = true;
|
||||||
|
progress.circuitBreakerOpenTime = now;
|
||||||
|
console.warn(
|
||||||
|
`[Messages job] Circuit breaker opened due to ${progress.consecutiveFailures} consecutive failures`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
progress.circuitBreakerOpen &&
|
||||||
|
now - progress.circuitBreakerOpenTime >
|
||||||
|
RATE_LIMIT_CONFIG.circuitBreakerResetTime
|
||||||
|
) {
|
||||||
|
progress.circuitBreakerOpen = false;
|
||||||
|
progress.consecutiveFailures = 0;
|
||||||
|
console.info(
|
||||||
|
`[Messages job] Circuit breaker closed after ${RATE_LIMIT_CONFIG.circuitBreakerResetTime}ms`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress.circuitBreakerOpen;
|
||||||
|
}
|
||||||
|
|
||||||
async function processMessagesInParallel(
|
async function processMessagesInParallel(
|
||||||
messages: any[],
|
messages: any[],
|
||||||
existingIds: Set<string>,
|
existingIds: Set<string>,
|
||||||
@@ -173,7 +233,6 @@ async function processMessagesInParallel(
|
|||||||
let consecutiveExisting = 0;
|
let consecutiveExisting = 0;
|
||||||
const updatedProgress = { ...progress };
|
const updatedProgress = { ...progress };
|
||||||
|
|
||||||
// Filter out messages older than 2 years
|
|
||||||
const twoYearsAgo = Date.now() - 2 * 365 * 24 * 60 * 60 * 1000;
|
const twoYearsAgo = Date.now() - 2 * 365 * 24 * 60 * 60 * 1000;
|
||||||
let shouldStop = false;
|
let shouldStop = false;
|
||||||
|
|
||||||
@@ -181,9 +240,8 @@ async function processMessagesInParallel(
|
|||||||
const id = msg.id.toString();
|
const id = msg.id.toString();
|
||||||
const messageDate = new Date(msg.date).getTime();
|
const messageDate = new Date(msg.date).getTime();
|
||||||
|
|
||||||
// If we encounter a message older than 2 years, we should stop processing
|
|
||||||
// since messages are sorted by date descending
|
|
||||||
if (messageDate < twoYearsAgo) {
|
if (messageDate < twoYearsAgo) {
|
||||||
|
//! older than 2 years ago
|
||||||
shouldStop = true;
|
shouldStop = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -320,6 +378,9 @@ export const messagesJob: Job = {
|
|||||||
processedIds: [],
|
processedIds: [],
|
||||||
streamingStarted: false,
|
streamingStarted: false,
|
||||||
totalEstimated: 0,
|
totalEstimated: 0,
|
||||||
|
circuitBreakerOpen: false,
|
||||||
|
circuitBreakerOpenTime: 0,
|
||||||
|
consecutiveFailures: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
|
const existingIds = new Set((await ctx.getStoredItems()).map((i) => i.id));
|
||||||
@@ -451,6 +512,14 @@ export const messagesJob: Job = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (!progress.done) {
|
while (!progress.done) {
|
||||||
|
if (checkCircuitBreaker(progress)) {
|
||||||
|
console.warn(
|
||||||
|
"[Messages job] Circuit breaker is open, skipping processing",
|
||||||
|
);
|
||||||
|
await delay(RATE_LIMIT_CONFIG.maxDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await delay(progress.currentDelay);
|
await delay(progress.currentDelay);
|
||||||
requestStartTime = Date.now();
|
requestStartTime = Date.now();
|
||||||
|
|
||||||
@@ -459,6 +528,8 @@ export const messagesJob: Job = {
|
|||||||
list = await fetchMessages(progress.offset, progress.currentBatchSize);
|
list = await fetchMessages(progress.offset, progress.currentBatchSize);
|
||||||
const responseTime = Date.now() - requestStartTime;
|
const responseTime = Date.now() - requestStartTime;
|
||||||
|
|
||||||
|
progress.consecutiveFailures = 0;
|
||||||
|
|
||||||
progress.currentDelay = calculateAdaptiveDelay(progress, responseTime);
|
progress.currentDelay = calculateAdaptiveDelay(progress, responseTime);
|
||||||
progress.currentBatchSize = calculateAdaptiveBatchSize(
|
progress.currentBatchSize = calculateAdaptiveBatchSize(
|
||||||
progress,
|
progress,
|
||||||
@@ -467,6 +538,7 @@ export const messagesJob: Job = {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Messages job] list fetch failed:", e);
|
console.error("[Messages job] list fetch failed:", e);
|
||||||
progress.failedRequests++;
|
progress.failedRequests++;
|
||||||
|
progress.consecutiveFailures++;
|
||||||
progress.currentDelay = Math.min(
|
progress.currentDelay = Math.min(
|
||||||
progress.currentDelay * RATE_LIMIT_CONFIG.backoffMultiplier,
|
progress.currentDelay * RATE_LIMIT_CONFIG.backoffMultiplier,
|
||||||
RATE_LIMIT_CONFIG.maxDelay,
|
RATE_LIMIT_CONFIG.maxDelay,
|
||||||
@@ -479,6 +551,7 @@ export const messagesJob: Job = {
|
|||||||
|
|
||||||
if (list.status !== "200") {
|
if (list.status !== "200") {
|
||||||
progress.failedRequests++;
|
progress.failedRequests++;
|
||||||
|
progress.consecutiveFailures++;
|
||||||
|
|
||||||
progress.processedIds = Array.from(processedIdsSet);
|
progress.processedIds = Array.from(processedIdsSet);
|
||||||
await ctx.setProgress(progress);
|
await ctx.setProgress(progress);
|
||||||
@@ -507,7 +580,6 @@ export const messagesJob: Job = {
|
|||||||
|
|
||||||
itemsToStream.push(...processedItems);
|
itemsToStream.push(...processedItems);
|
||||||
|
|
||||||
// Update consecutive existing counter
|
|
||||||
consecutiveExisting = newConsecutiveExisting;
|
consecutiveExisting = newConsecutiveExisting;
|
||||||
if (consecutiveExisting >= 20) {
|
if (consecutiveExisting >= 20) {
|
||||||
progress.done = true;
|
progress.done = true;
|
||||||
@@ -529,14 +601,17 @@ export const messagesJob: Job = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch incremental search update if we processed new items
|
|
||||||
if (processedItems.length > 0) {
|
if (processedItems.length > 0) {
|
||||||
try {
|
try {
|
||||||
const currentItems = await loadAllStoredItems();
|
const currentItems = await loadAllStoredItems();
|
||||||
currentItems.forEach(item => {
|
currentItems.forEach((item) => {
|
||||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
const jobDef =
|
||||||
|
jobs[item.category] ||
|
||||||
|
Object.values(jobs).find((j) => j.id === item.category) ||
|
||||||
|
jobs[item.renderComponentId];
|
||||||
if (jobDef) {
|
if (jobDef) {
|
||||||
const renderComponent = renderComponentMap[jobDef.renderComponentId];
|
const renderComponent =
|
||||||
|
renderComponentMap[jobDef.renderComponentId];
|
||||||
if (renderComponent) {
|
if (renderComponent) {
|
||||||
item.renderComponent = renderComponent;
|
item.renderComponent = renderComponent;
|
||||||
}
|
}
|
||||||
@@ -545,11 +620,21 @@ export const messagesJob: Job = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadDynamicItems(currentItems);
|
loadDynamicItems(currentItems);
|
||||||
window.dispatchEvent(new CustomEvent("dynamic-items-updated", {
|
window.dispatchEvent(
|
||||||
detail: { incremental: true, jobId: "messages", newItemCount: processedItems.length, streaming: true }
|
new CustomEvent("dynamic-items-updated", {
|
||||||
}));
|
detail: {
|
||||||
|
incremental: true,
|
||||||
|
jobId: "messages",
|
||||||
|
newItemCount: processedItems.length,
|
||||||
|
streaming: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Messages job] Failed to dispatch incremental search update:", error);
|
console.warn(
|
||||||
|
"[Messages job] Failed to dispatch incremental search update:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,6 +681,9 @@ export const messagesJob: Job = {
|
|||||||
processedIds: [],
|
processedIds: [],
|
||||||
streamingStarted: false,
|
streamingStarted: false,
|
||||||
totalEstimated: 0,
|
totalEstimated: 0,
|
||||||
|
circuitBreakerOpen: false,
|
||||||
|
circuitBreakerOpenTime: 0,
|
||||||
|
consecutiveFailures: 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
progress.processedIds = Array.from(processedIdsSet);
|
progress.processedIds = Array.from(processedIdsSet);
|
||||||
|
|||||||
@@ -309,10 +309,7 @@ export const notificationsJob: Job = {
|
|||||||
await delay(NOTIFICATIONS_RATE_LIMIT.batchDelay);
|
await delay(NOTIFICATIONS_RATE_LIMIT.batchDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { success, item } = await processNotification(
|
const { success, item } = await processNotification(notif, ctx);
|
||||||
notif,
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
if (progress.retryQueue.length < 10) {
|
if (progress.retryQueue.length < 10) {
|
||||||
progress.retryQueue.push(notif.notificationID);
|
progress.retryQueue.push(notif.notificationID);
|
||||||
@@ -375,23 +372,38 @@ export const notificationsJob: Job = {
|
|||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
try {
|
try {
|
||||||
const currentItems = await loadAllStoredItems();
|
const currentItems = await loadAllStoredItems();
|
||||||
currentItems.forEach(item => {
|
currentItems.forEach((item) => {
|
||||||
const jobDef = jobs[item.category] || Object.values(jobs).find(j => j.id === item.category) || jobs[item.renderComponentId];
|
const jobDef =
|
||||||
|
jobs[item.category] ||
|
||||||
|
Object.values(jobs).find((j) => j.id === item.category) ||
|
||||||
|
jobs[item.renderComponentId];
|
||||||
if (jobDef) {
|
if (jobDef) {
|
||||||
const renderComponent = renderComponentMap[jobDef.renderComponentId];
|
const renderComponent =
|
||||||
|
renderComponentMap[jobDef.renderComponentId];
|
||||||
if (renderComponent) {
|
if (renderComponent) {
|
||||||
item.renderComponent = renderComponent;
|
item.renderComponent = renderComponent;
|
||||||
}
|
}
|
||||||
} else if (renderComponentMap[item.renderComponentId]) {
|
} else if (renderComponentMap[item.renderComponentId]) {
|
||||||
item.renderComponent = renderComponentMap[item.renderComponentId];
|
item.renderComponent =
|
||||||
|
renderComponentMap[item.renderComponentId];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
loadDynamicItems(currentItems);
|
loadDynamicItems(currentItems);
|
||||||
window.dispatchEvent(new CustomEvent("dynamic-items-updated", {
|
window.dispatchEvent(
|
||||||
detail: { incremental: true, jobId: "notifications", newItemCount: items.length, streaming: true }
|
new CustomEvent("dynamic-items-updated", {
|
||||||
}));
|
detail: {
|
||||||
|
incremental: true,
|
||||||
|
jobId: "notifications",
|
||||||
|
newItemCount: items.length,
|
||||||
|
streaming: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Notifications job] Failed to dispatch incremental search update:", error);
|
console.warn(
|
||||||
|
"[Notifications job] Failed to dispatch incremental search update:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Check which items are already vectorized in embeddia's IndexedDB
|
||||||
|
* Returns a Set of item IDs that are already indexed
|
||||||
|
*/
|
||||||
|
export async function getVectorizedItemIds(): Promise<Set<string>> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const request = indexedDB.open("embeddiaDB");
|
||||||
|
|
||||||
|
request.onerror = () => {
|
||||||
|
console.debug("Could not open embeddiaDB, assuming no items are vectorized");
|
||||||
|
resolve(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result;
|
||||||
|
|
||||||
|
if (!db.objectStoreNames.contains("embeddiaObjectStore")) {
|
||||||
|
console.debug("embeddiaObjectStore not found, assuming no items are vectorized");
|
||||||
|
db.close();
|
||||||
|
resolve(new Set());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transaction = db.transaction(["embeddiaObjectStore"], "readonly");
|
||||||
|
const store = transaction.objectStore("embeddiaObjectStore");
|
||||||
|
const getAllRequest = store.getAllKeys();
|
||||||
|
|
||||||
|
getAllRequest.onsuccess = () => {
|
||||||
|
const vectorizedIds = new Set<string>();
|
||||||
|
getAllRequest.result.forEach(key => {
|
||||||
|
if (typeof key === 'string') {
|
||||||
|
vectorizedIds.add(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.debug(`Found ${vectorizedIds.size} already vectorized items in embeddia DB`);
|
||||||
|
db.close();
|
||||||
|
resolve(vectorizedIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
getAllRequest.onerror = () => {
|
||||||
|
console.warn("Error reading vectorized item keys, assuming no items are vectorized");
|
||||||
|
db.close();
|
||||||
|
resolve(new Set());
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Error accessing embeddia store, assuming no items are vectorized:", error);
|
||||||
|
db.close();
|
||||||
|
resolve(new Set());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function htmlToPlainText(rawHtml: string): string {
|
export function htmlToPlainText(rawHtml: string): string {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(rawHtml, "text/html");
|
const doc = parser.parseFromString(rawHtml, "text/html");
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { IndexItem } from "../types";
|
|||||||
let vectorIndex: EmbeddingIndex | null = null;
|
let vectorIndex: EmbeddingIndex | null = null;
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let currentAbortController: AbortController | null = null;
|
let currentAbortController: AbortController | null = null;
|
||||||
let loadedItemIds = new Set<string>(); // Track loaded items to prevent duplicates
|
let loadedItemIds = new Set<string>();
|
||||||
|
|
||||||
let streamingSession: {
|
let streamingSession: {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -26,15 +26,12 @@ async function initWorker() {
|
|||||||
await initializeModel();
|
await initializeModel();
|
||||||
vectorIndex = new EmbeddingIndex([]);
|
vectorIndex = new EmbeddingIndex([]);
|
||||||
|
|
||||||
// Load existing items but track them to prevent duplicates
|
|
||||||
const stored = await vectorIndex.getAllObjectsFromIndexedDB();
|
const stored = await vectorIndex.getAllObjectsFromIndexedDB();
|
||||||
if (stored.length > 0) {
|
if (stored.length > 0) {
|
||||||
console.debug(`Found ${stored.length} existing items in IndexedDB`);
|
console.debug(`Found ${stored.length} existing items in IndexedDB`);
|
||||||
|
|
||||||
// Clear any existing items from memory first
|
|
||||||
loadedItemIds.clear();
|
loadedItemIds.clear();
|
||||||
|
|
||||||
// Add items and track their IDs
|
|
||||||
stored.forEach((item) => {
|
stored.forEach((item) => {
|
||||||
if (item.id && !loadedItemIds.has(item.id)) {
|
if (item.id && !loadedItemIds.has(item.id)) {
|
||||||
vectorIndex!.add(item);
|
vectorIndex!.add(item);
|
||||||
@@ -168,7 +165,6 @@ async function processStreamingItems() {
|
|||||||
streamingSession.batchSize,
|
streamingSession.batchSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use our tracking set for more efficient deduplication
|
|
||||||
const unprocessedItems = batchToProcess.filter((item) => {
|
const unprocessedItems = batchToProcess.filter((item) => {
|
||||||
return item.id && !loadedItemIds.has(item.id);
|
return item.id && !loadedItemIds.has(item.id);
|
||||||
});
|
});
|
||||||
@@ -190,12 +186,12 @@ async function processStreamingItems() {
|
|||||||
try {
|
try {
|
||||||
successfullyVectorized.forEach((item) => {
|
successfullyVectorized.forEach((item) => {
|
||||||
vectorIndex!.add(item);
|
vectorIndex!.add(item);
|
||||||
loadedItemIds.add(item.id); // Track the added item
|
loadedItemIds.add(item.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
streamingSession.totalProcessed % (streamingSession.batchSize * 15) ===
|
streamingSession.totalProcessed % 50 === 0 ||
|
||||||
0
|
loadedItemIds.size % 200 === 0
|
||||||
) {
|
) {
|
||||||
await vectorIndex!.saveIndex("indexedDB");
|
await vectorIndex!.saveIndex("indexedDB");
|
||||||
console.debug(
|
console.debug(
|
||||||
@@ -328,7 +324,6 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use our tracking set for more efficient deduplication
|
|
||||||
const unprocessedItems = items.filter((item) => {
|
const unprocessedItems = items.filter((item) => {
|
||||||
if (signal.aborted) return false;
|
if (signal.aborted) return false;
|
||||||
return item.id && !loadedItemIds.has(item.id);
|
return item.id && !loadedItemIds.has(item.id);
|
||||||
@@ -347,15 +342,22 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (unprocessedItems.length === 0) {
|
if (unprocessedItems.length === 0) {
|
||||||
console.debug(`No new items to process. ${loadedItemIds.size} items already in index.`);
|
console.debug(
|
||||||
|
`No new items to process. ${loadedItemIds.size} items already in index.`,
|
||||||
|
);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: { status: "complete", message: `No new items to process (${loadedItemIds.size} items already indexed)` },
|
data: {
|
||||||
|
status: "complete",
|
||||||
|
message: `No new items to process (${loadedItemIds.size} items already indexed)`,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`Starting processing of ${unprocessedItems.length} items (${items.length - unprocessedItems.length} already processed).`);
|
console.debug(
|
||||||
|
`Starting processing of ${unprocessedItems.length} items (${items.length - unprocessedItems.length} already processed).`,
|
||||||
|
);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: {
|
data: {
|
||||||
@@ -402,7 +404,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
try {
|
try {
|
||||||
successfullyVectorized.forEach((item) => {
|
successfullyVectorized.forEach((item) => {
|
||||||
vectorIndex!.add(item);
|
vectorIndex!.add(item);
|
||||||
loadedItemIds.add(item.id); // Track the added item
|
loadedItemIds.add(item.id);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error adding batch to index:", e);
|
console.error("Error adding batch to index:", e);
|
||||||
@@ -425,9 +427,15 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(i / BATCH_SIZE + 1) % 3 === 0 ||
|
||||||
|
i + BATCH_SIZE >= unprocessedItems.length
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await vectorIndex!.saveIndex("indexedDB");
|
await vectorIndex!.saveIndex("indexedDB");
|
||||||
console.debug(`Saved index after processing batch ${i / BATCH_SIZE + 1} (${loadedItemIds.size} total unique items)`);
|
console.debug(
|
||||||
|
`Saved index after processing batch ${i / BATCH_SIZE + 1} (${loadedItemIds.size} total unique items)`,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving index batch:", e);
|
console.error("Error saving index batch:", e);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
@@ -435,6 +443,7 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
data: { status: "error", message: `Error saving index batch: ${e}` },
|
data: { status: "error", message: `Error saving index batch: ${e}` },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
processedCount += batch.length;
|
processedCount += batch.length;
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
@@ -448,7 +457,9 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug(`Processing complete. Total unique items in index: ${loadedItemIds.size}`);
|
console.debug(
|
||||||
|
`Processing complete. Total unique items in index: ${loadedItemIds.size}`,
|
||||||
|
);
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
data: {
|
data: {
|
||||||
@@ -463,19 +474,15 @@ async function processItems(items: IndexItem[], signal: AbortSignal) {
|
|||||||
async function resetWorker() {
|
async function resetWorker() {
|
||||||
console.debug("Resetting vector worker state...");
|
console.debug("Resetting vector worker state...");
|
||||||
|
|
||||||
// Clear tracking
|
|
||||||
loadedItemIds.clear();
|
loadedItemIds.clear();
|
||||||
|
|
||||||
// Reset streaming session
|
|
||||||
if (streamingSession?.isActive) {
|
if (streamingSession?.isActive) {
|
||||||
streamingSession.isActive = false;
|
streamingSession.isActive = false;
|
||||||
streamingSession = null;
|
streamingSession = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset vector index
|
|
||||||
if (vectorIndex) {
|
if (vectorIndex) {
|
||||||
try {
|
try {
|
||||||
// Save current state before reset
|
|
||||||
await vectorIndex.saveIndex("indexedDB");
|
await vectorIndex.saveIndex("indexedDB");
|
||||||
console.debug("Saved index before reset");
|
console.debug("Saved index before reset");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -483,13 +490,14 @@ async function resetWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinitialize
|
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
vectorIndex = null;
|
vectorIndex = null;
|
||||||
|
|
||||||
await initWorker();
|
await initWorker();
|
||||||
|
|
||||||
console.debug(`Vector worker reset complete. Loaded ${loadedItemIds.size} items.`);
|
console.debug(
|
||||||
|
`Vector worker reset complete. Loaded ${loadedItemIds.size} items.`,
|
||||||
|
);
|
||||||
|
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: "progress",
|
type: "progress",
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export class VectorWorkerManager {
|
|||||||
private isInitialized = false;
|
private isInitialized = false;
|
||||||
private readyPromise: Promise<void> | null = null;
|
private readyPromise: Promise<void> | null = null;
|
||||||
private progressCallback: ProgressCallback | null = null;
|
private progressCallback: ProgressCallback | null = null;
|
||||||
|
private initializationMutex = false;
|
||||||
|
private idleTimer: NodeJS.Timeout | null = null;
|
||||||
|
private unloadTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
private streamingSession: {
|
private streamingSession: {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -23,7 +26,9 @@ export class VectorWorkerManager {
|
|||||||
batchBuffer: IndexItem[];
|
batchBuffer: IndexItem[];
|
||||||
batchSize: number;
|
batchSize: number;
|
||||||
flushTimer: NodeJS.Timeout | null;
|
flushTimer: NodeJS.Timeout | null;
|
||||||
jobId?: string; // Track which job owns the session
|
jobId?: string;
|
||||||
|
inactivityTimer: NodeJS.Timeout | null;
|
||||||
|
lastActivityTime: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
@@ -43,7 +48,6 @@ export class VectorWorkerManager {
|
|||||||
console.debug("Lazy-loading vector worker...");
|
console.debug("Lazy-loading vector worker...");
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
// Terminate any existing worker before creating a new one
|
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
console.debug("Terminating existing worker before creating new one");
|
console.debug("Terminating existing worker before creating new one");
|
||||||
this.worker.terminate();
|
this.worker.terminate();
|
||||||
@@ -62,8 +66,7 @@ export class VectorWorkerManager {
|
|||||||
this.worker = null;
|
this.worker = null;
|
||||||
}
|
}
|
||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
// Don't reset readyPromise here to prevent race conditions
|
|
||||||
// It will be reset when a new initialization is attempted
|
|
||||||
reject(new Error("Worker initialization timed out"));
|
reject(new Error("Worker initialization timed out"));
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@ export class VectorWorkerManager {
|
|||||||
case "ready":
|
case "ready":
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
this.updateActivity(); // Start idle timer after initialization
|
||||||
console.debug("Vector worker initialized and ready.");
|
console.debug("Vector worker initialized and ready.");
|
||||||
resolve();
|
resolve();
|
||||||
break;
|
break;
|
||||||
@@ -90,10 +94,19 @@ export class VectorWorkerManager {
|
|||||||
this.endStreamingSession();
|
this.endStreamingSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch search update when vectorization completes
|
window.dispatchEvent(
|
||||||
window.dispatchEvent(new CustomEvent("dynamic-items-updated", {
|
new CustomEvent("dynamic-items-updated", {
|
||||||
detail: { incremental: true, jobId: "vectorization", vectorUpdate: true }
|
detail: {
|
||||||
}));
|
incremental: true,
|
||||||
|
jobId: "vectorization",
|
||||||
|
vectorUpdate: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === "complete" || data.status === "cancelled" || data.status === "error") {
|
||||||
|
this.scheduleUnload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -128,35 +141,91 @@ export class VectorWorkerManager {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
this.readyPromise = null;
|
this.readyPromise = null;
|
||||||
this.progressCallback = null;
|
this.progressCallback = null;
|
||||||
|
this.initializationMutex = false;
|
||||||
|
this.clearIdleTimer();
|
||||||
|
this.clearUnloadTimer();
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
this.endStreamingSession();
|
this.endStreamingSession();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startIdleTimer() {
|
||||||
|
this.clearIdleTimer();
|
||||||
|
this.idleTimer = setTimeout(() => {
|
||||||
|
if (!this.streamingSession?.isActive && this.isInitialized) {
|
||||||
|
console.debug("[VectorWorker] Auto-shutting down due to 2 minutes of inactivity");
|
||||||
|
this.resetWorkerState();
|
||||||
|
}
|
||||||
|
}, 120000); // 2 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearIdleTimer() {
|
||||||
|
if (this.idleTimer) {
|
||||||
|
clearTimeout(this.idleTimer);
|
||||||
|
this.idleTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearUnloadTimer() {
|
||||||
|
if (this.unloadTimer) {
|
||||||
|
clearTimeout(this.unloadTimer);
|
||||||
|
this.unloadTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleUnload(delay: number = 10000) {
|
||||||
|
this.clearUnloadTimer();
|
||||||
|
this.unloadTimer = setTimeout(() => {
|
||||||
|
if (!this.streamingSession?.isActive && this.isInitialized) {
|
||||||
|
console.debug("[VectorWorker] Auto-unloading after processing complete");
|
||||||
|
this.resetWorkerState();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateActivity() {
|
||||||
|
this.clearUnloadTimer();
|
||||||
|
this.startIdleTimer();
|
||||||
|
}
|
||||||
|
|
||||||
private async ensureReady() {
|
private async ensureReady() {
|
||||||
// If we already have a ready promise, wait for it regardless of outcome
|
if (this.initializationMutex) {
|
||||||
|
while (this.initializationMutex) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInitialized && this.worker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.readyPromise) {
|
if (this.readyPromise) {
|
||||||
try {
|
try {
|
||||||
await this.readyPromise;
|
await this.readyPromise;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If the previous initialization failed, reset state and try again
|
console.warn(
|
||||||
console.warn("Previous worker initialization failed, resetting state and retrying...", error);
|
"Previous worker initialization failed, resetting state and retrying...",
|
||||||
|
error,
|
||||||
|
);
|
||||||
this.resetWorkerState();
|
this.resetWorkerState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double-check if we're actually ready after waiting
|
|
||||||
if (this.isInitialized && this.worker) {
|
if (this.isInitialized && this.worker) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're not ready and there's no active promise, create one
|
if (!this.readyPromise && !this.initializationMutex) {
|
||||||
if (!this.readyPromise) {
|
|
||||||
console.warn("Worker not initialized, attempting init...");
|
console.warn("Worker not initialized, attempting init...");
|
||||||
|
this.initializationMutex = true;
|
||||||
|
try {
|
||||||
this.readyPromise = this.initWorker();
|
this.readyPromise = this.initWorker();
|
||||||
|
await this.readyPromise;
|
||||||
|
} finally {
|
||||||
|
this.initializationMutex = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.readyPromise;
|
|
||||||
if (!this.isInitialized || !this.worker) {
|
if (!this.isInitialized || !this.worker) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Vector Worker is not available after initialization attempt.",
|
"Vector Worker is not available after initialization attempt.",
|
||||||
@@ -165,27 +234,61 @@ export class VectorWorkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async processItems(items: IndexItem[], onProgress?: ProgressCallback) {
|
async processItems(items: IndexItem[], onProgress?: ProgressCallback) {
|
||||||
|
// Only initialize worker if we actually have items to process
|
||||||
|
if (items.length === 0) {
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
status: "complete",
|
||||||
|
message: "No items to process"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueItems = items.filter((item, index, arr) => {
|
||||||
|
return arr.findIndex((i) => i.id === item.id) === index;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uniqueItems.length !== items.length) {
|
||||||
|
console.debug(
|
||||||
|
`Filtered out ${items.length - uniqueItems.length} duplicate items before processing`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If after deduplication we have no items, don't initialize worker
|
||||||
|
if (uniqueItems.length === 0) {
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
status: "complete",
|
||||||
|
message: "No unique items to process after deduplication"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureReady();
|
await this.ensureReady();
|
||||||
|
|
||||||
// Don't allow regular processing if streaming is active
|
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
console.warn("Cannot process items while streaming session is active");
|
console.warn("Cannot process items while streaming session is active");
|
||||||
if (onProgress) {
|
if (onProgress) {
|
||||||
onProgress({
|
onProgress({
|
||||||
status: "error",
|
status: "error",
|
||||||
message: "Cannot process items while streaming session is active"
|
message: "Cannot process items while streaming session is active",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.progressCallback = onProgress || null;
|
this.progressCallback = onProgress || null;
|
||||||
|
this.updateActivity();
|
||||||
|
|
||||||
console.debug(`Sending ${items.length} items to worker for processing.`);
|
console.debug(
|
||||||
|
`Sending ${uniqueItems.length} unique items to worker for processing.`,
|
||||||
|
);
|
||||||
|
|
||||||
this.worker!.postMessage({
|
this.worker!.postMessage({
|
||||||
type: "process",
|
type: "process",
|
||||||
data: { items: items },
|
data: { items: uniqueItems },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,19 +298,22 @@ export class VectorWorkerManager {
|
|||||||
batchSize: number = 10,
|
batchSize: number = 10,
|
||||||
jobId?: string,
|
jobId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
// Only initialize if we expect items to process
|
||||||
|
if (totalExpectedItems === 0) {
|
||||||
|
console.debug("[VectorWorker] No items expected, not starting streaming session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureReady();
|
await this.ensureReady();
|
||||||
|
|
||||||
// Check if another job already has an active streaming session
|
|
||||||
if (this.streamingSession?.isActive) {
|
if (this.streamingSession?.isActive) {
|
||||||
if (this.streamingSession.jobId !== jobId) {
|
if (this.streamingSession.jobId !== jobId) {
|
||||||
console.warn(`Cannot start streaming session for job ${jobId} - job ${this.streamingSession.jobId} already has an active session`);
|
console.warn(
|
||||||
if (onProgress) {
|
`Ending existing streaming session for job ${this.streamingSession.jobId} to start new session for job ${jobId}`,
|
||||||
onProgress({
|
);
|
||||||
status: "error",
|
await this.endStreamingSession();
|
||||||
message: `Another job (${this.streamingSession.jobId}) already has an active streaming session`
|
|
||||||
});
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
console.debug(`Streaming session for job ${jobId} already active`);
|
console.debug(`Streaming session for job ${jobId} already active`);
|
||||||
return;
|
return;
|
||||||
@@ -215,6 +321,7 @@ export class VectorWorkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.progressCallback = onProgress || null;
|
this.progressCallback = onProgress || null;
|
||||||
|
this.updateActivity();
|
||||||
|
|
||||||
this.streamingSession = {
|
this.streamingSession = {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -224,6 +331,8 @@ export class VectorWorkerManager {
|
|||||||
batchSize,
|
batchSize,
|
||||||
flushTimer: null,
|
flushTimer: null,
|
||||||
jobId,
|
jobId,
|
||||||
|
inactivityTimer: null,
|
||||||
|
lastActivityTime: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug(
|
console.debug(
|
||||||
@@ -252,7 +361,34 @@ export class VectorWorkerManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.streamingSession.batchBuffer.push(...items);
|
const uniqueItems = items.filter((item, index, arr) => {
|
||||||
|
return arr.findIndex((i) => i.id === item.id) === index;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (uniqueItems.length !== items.length) {
|
||||||
|
console.debug(
|
||||||
|
`[Streaming] Filtered out ${items.length - uniqueItems.length} duplicate items before streaming`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uniqueItems.length > 0) {
|
||||||
|
this.streamingSession.batchBuffer.push(...uniqueItems);
|
||||||
|
this.streamingSession.lastActivityTime = Date.now();
|
||||||
|
this.updateActivity(); // Update worker activity
|
||||||
|
|
||||||
|
if (this.streamingSession.inactivityTimer) {
|
||||||
|
clearTimeout(this.streamingSession.inactivityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.streamingSession.inactivityTimer = setTimeout(() => {
|
||||||
|
if (this.streamingSession?.isActive) {
|
||||||
|
console.debug(
|
||||||
|
"[VectorWorker] Auto-ending streaming session due to inactivity",
|
||||||
|
);
|
||||||
|
this.endStreamingSession();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.streamingSession.batchBuffer.length >=
|
this.streamingSession.batchBuffer.length >=
|
||||||
@@ -313,6 +449,10 @@ export class VectorWorkerManager {
|
|||||||
clearTimeout(this.streamingSession.flushTimer);
|
clearTimeout(this.streamingSession.flushTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.streamingSession.inactivityTimer) {
|
||||||
|
clearTimeout(this.streamingSession.inactivityTimer);
|
||||||
|
}
|
||||||
|
|
||||||
this.streamingSession.isActive = false;
|
this.streamingSession.isActive = false;
|
||||||
|
|
||||||
this.worker!.postMessage({
|
this.worker!.postMessage({
|
||||||
@@ -331,12 +471,14 @@ export class VectorWorkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.streamingSession = null;
|
this.streamingSession = null;
|
||||||
|
this.scheduleUnload();
|
||||||
}
|
}
|
||||||
|
|
||||||
async streamItem(item: IndexItem): Promise<void> {
|
async streamItem(item: IndexItem): Promise<void> {
|
||||||
return this.streamItems([item]);
|
return this.streamItems([item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
isStreamingActive(): boolean {
|
isStreamingActive(): boolean {
|
||||||
return this.streamingSession?.isActive ?? false;
|
return this.streamingSession?.isActive ?? false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Plugin } from "../../core/types";
|
|||||||
interface NotificationCollectorStorage {
|
interface NotificationCollectorStorage {
|
||||||
lastNotificationCount: number;
|
lastNotificationCount: number;
|
||||||
lastCheckedTime: string;
|
lastCheckedTime: string;
|
||||||
|
consecutiveErrors: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
||||||
@@ -15,19 +16,30 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
|||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
let pollInterval: number | null = null;
|
let pollInterval: number | null = null;
|
||||||
|
let isVisible = !document.hidden;
|
||||||
|
let baseInterval = 30000; // 30 seconds
|
||||||
|
const maxInterval = 300000; // 5 minutes max
|
||||||
|
|
||||||
// Store last notification count in storage
|
// Store last notification count in storage
|
||||||
if (!api.storage.lastNotificationCount) {
|
if (!api.storage.lastNotificationCount) {
|
||||||
api.storage.lastNotificationCount = 0;
|
api.storage.lastNotificationCount = 0;
|
||||||
}
|
}
|
||||||
|
if (!api.storage.consecutiveErrors) {
|
||||||
|
api.storage.consecutiveErrors = 0;
|
||||||
|
}
|
||||||
|
|
||||||
const checkNotifications = async () => {
|
const checkNotifications = async () => {
|
||||||
|
// Skip if tab is not visible to save battery
|
||||||
|
if (!isVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const alertDiv = document.querySelector(
|
const alertDiv = document.querySelector(
|
||||||
"[class*='notifications__bubble___']",
|
"[class*='notifications__bubble___']",
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
|
||||||
if (api.storage.lastNotificationCount !== 0) {
|
if (alertDiv && api.storage.lastNotificationCount !== 0) {
|
||||||
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +64,9 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
|||||||
api.storage.lastNotificationCount = notificationCount;
|
api.storage.lastNotificationCount = notificationCount;
|
||||||
api.storage.lastCheckedTime = new Date().toISOString();
|
api.storage.lastCheckedTime = new Date().toISOString();
|
||||||
|
|
||||||
|
// Reset error count on success
|
||||||
|
api.storage.consecutiveErrors = 0;
|
||||||
|
|
||||||
if (alertDiv) {
|
if (alertDiv) {
|
||||||
alertDiv.textContent = notificationCount.toString();
|
alertDiv.textContent = notificationCount.toString();
|
||||||
} else {
|
} else {
|
||||||
@@ -59,18 +74,42 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[BetterSEQTA+] Error fetching notifications:", error);
|
console.error("[BetterSEQTA+] Error fetching notifications:", error);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
return Math.min(baseInterval * errorMultiplier, maxInterval);
|
||||||
|
};
|
||||||
|
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
if (pollInterval) return; // Already polling
|
if (pollInterval) return; // Already polling
|
||||||
checkNotifications();
|
checkNotifications();
|
||||||
pollInterval = window.setInterval(checkNotifications, 30000);
|
|
||||||
|
const scheduleNext = () => {
|
||||||
|
const interval = getNextInterval();
|
||||||
|
pollInterval = window.setTimeout(() => {
|
||||||
|
checkNotifications().then(() => {
|
||||||
|
if (pollInterval) {
|
||||||
|
// Only continue if not stopped
|
||||||
|
scheduleNext();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, interval);
|
||||||
|
};
|
||||||
|
|
||||||
|
scheduleNext();
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (pollInterval) {
|
if (pollInterval) {
|
||||||
window.clearInterval(pollInterval);
|
window.clearTimeout(pollInterval);
|
||||||
pollInterval = null;
|
pollInterval = null;
|
||||||
const alertDiv = document.querySelector(
|
const alertDiv = document.querySelector(
|
||||||
"[class*='notifications__bubble___']",
|
"[class*='notifications__bubble___']",
|
||||||
@@ -85,12 +124,29 @@ const notificationCollectorPlugin: Plugin<{}, NotificationCollectorStorage> = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Listen for visibility changes to pause/resume polling
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
isVisible = !document.hidden;
|
||||||
|
if (isVisible && !pollInterval) {
|
||||||
|
// Resume polling when tab becomes visible
|
||||||
|
const alertDiv = document.querySelector(
|
||||||
|
"[class*='notifications__bubble___']",
|
||||||
|
);
|
||||||
|
if (alertDiv) {
|
||||||
|
startPolling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
api.seqta.onMount("[class*='notifications__bubble___']", (_) => {
|
api.seqta.onMount("[class*='notifications__bubble___']", (_) => {
|
||||||
startPolling();
|
startPolling();
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import localforage from 'localforage'
|
||||||
|
let value = $state<string | undefined>(undefined)
|
||||||
|
let fileInput = $state<HTMLInputElement | undefined>(undefined)
|
||||||
|
let dragging = $state(false)
|
||||||
|
let blobUrl = $state<string | undefined>(undefined)
|
||||||
|
|
||||||
|
// Setup localforage instance
|
||||||
|
const store = localforage.createInstance({
|
||||||
|
name: 'profile-picture-store',
|
||||||
|
storeName: 'profilePicture',
|
||||||
|
})
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const blob = await store.getItem<Blob>('profile-picture')
|
||||||
|
if (blob && blob instanceof Blob) {
|
||||||
|
// Revoke old blobUrl if any
|
||||||
|
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
||||||
|
blobUrl = URL.createObjectURL(blob)
|
||||||
|
value = blobUrl
|
||||||
|
} else {
|
||||||
|
value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load()
|
||||||
|
|
||||||
|
function triggerSelect() {
|
||||||
|
fileInput?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFiles(files: FileList | null) {
|
||||||
|
const file = files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
// Revoke old blob URL if it exists
|
||||||
|
if (blobUrl) {
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the blob in localforage
|
||||||
|
await store.setItem('profile-picture', file)
|
||||||
|
const newBlobUrl = URL.createObjectURL(file)
|
||||||
|
value = newBlobUrl
|
||||||
|
blobUrl = newBlobUrl
|
||||||
|
window.dispatchEvent(new Event('profile-picture-updated'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileChange() {
|
||||||
|
handleFiles(fileInput?.files || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(event: DragEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
dragging = false
|
||||||
|
handleFiles(event.dataTransfer?.files || null)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeImage() {
|
||||||
|
if (blobUrl) {
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
blobUrl = undefined
|
||||||
|
}
|
||||||
|
value = undefined
|
||||||
|
await store.removeItem('profile-picture')
|
||||||
|
window.dispatchEvent(new Event('profile-picture-updated'))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex relative justify-center items-center rounded-lg cursor-pointer select-none border-zinc-300 dark:border-zinc-600 bg-white/20 dark:bg-zinc-800/30"
|
||||||
|
onclick={() => value ? null : 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"
|
||||||
|
>
|
||||||
|
{#if value}
|
||||||
|
<img src={value} alt="Profile" class="object-cover rounded-full size-10" />
|
||||||
|
<button
|
||||||
|
class="flex justify-center items-center m-1 text-lg dark:text-white size-7"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
removeImage()
|
||||||
|
}}
|
||||||
|
>×</button>
|
||||||
|
{: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">
|
||||||
|
<span class="text-lg font-IconFamily">{'\ued47'}</span>
|
||||||
|
<span>Upload</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<input type="file" accept="image/*" class="hidden" bind:this={fileInput} onchange={onFileChange} />
|
||||||
|
{#if dragging}
|
||||||
|
<div class="absolute inset-0 rounded-full bg-zinc-200/40 dark:bg-zinc-700/40"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { Plugin } from "@/plugins/core/types";
|
||||||
|
import { defineSettings, componentSetting } from "@/plugins/core/settingsHelpers";
|
||||||
|
import ProfilePictureSetting from "./ProfilePictureSetting.svelte";
|
||||||
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
import styles from "./styles.css?inline";
|
||||||
|
import localforage from "localforage";
|
||||||
|
|
||||||
|
const settings = defineSettings({
|
||||||
|
picture: componentSetting({
|
||||||
|
title: "Profile Picture",
|
||||||
|
description: "Upload or remove your custom profile image",
|
||||||
|
component: ProfilePictureSetting,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const profilePicturePlugin: Plugin<typeof settings> = {
|
||||||
|
id: "profile-picture",
|
||||||
|
name: "Custom Profile Picture",
|
||||||
|
description: "Use your own image in place of the profile icon",
|
||||||
|
version: "1.1.0",
|
||||||
|
settings: settings,
|
||||||
|
disableToggle: true,
|
||||||
|
defaultEnabled: false,
|
||||||
|
styles,
|
||||||
|
|
||||||
|
run: async (api) => {
|
||||||
|
await api.storage.loaded;
|
||||||
|
let container: Element;
|
||||||
|
try {
|
||||||
|
container = await waitForElm(".userInfosvgdiv", true, 100, 60);
|
||||||
|
} catch {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = container.querySelector(".userInfosvg") as HTMLElement | null;
|
||||||
|
let img: HTMLImageElement | null = null;
|
||||||
|
let currentBlobUrl: string | undefined;
|
||||||
|
|
||||||
|
// Setup localforage instance
|
||||||
|
const store = localforage.createInstance({
|
||||||
|
name: "profile-picture-store",
|
||||||
|
storeName: "profilePicture",
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateImageFromStore() {
|
||||||
|
// Remove old image if present
|
||||||
|
if (img) {
|
||||||
|
img.remove();
|
||||||
|
img = null;
|
||||||
|
}
|
||||||
|
if (currentBlobUrl) {
|
||||||
|
URL.revokeObjectURL(currentBlobUrl);
|
||||||
|
currentBlobUrl = undefined;
|
||||||
|
}
|
||||||
|
const blob = await store.getItem<Blob>("profile-picture");
|
||||||
|
if (blob && blob instanceof Blob) {
|
||||||
|
currentBlobUrl = URL.createObjectURL(blob);
|
||||||
|
img = document.createElement("img");
|
||||||
|
img.className = "userInfoImg";
|
||||||
|
img.src = currentBlobUrl;
|
||||||
|
if (svg) svg.style.display = "none";
|
||||||
|
container.appendChild(img);
|
||||||
|
} else {
|
||||||
|
if (svg) svg.style.display = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
await updateImageFromStore();
|
||||||
|
|
||||||
|
// Listen for profile picture updates
|
||||||
|
const handler = () => { updateImageFromStore(); };
|
||||||
|
window.addEventListener('profile-picture-updated', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('profile-picture-updated', handler);
|
||||||
|
if (img) img.remove();
|
||||||
|
if (svg) svg.style.display = "";
|
||||||
|
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default profilePicturePlugin;
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.userInfoImg {
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
position: absolute;
|
||||||
|
top: 10%;
|
||||||
|
left: 10%;
|
||||||
|
border-radius: 50%;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,43 +39,14 @@ const zoomHandlers = new WeakMap<
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
function resetTimetableStyles(): void {
|
function resetTimetableStyles(): void {
|
||||||
const firstDayColumn = document.querySelector(
|
// Reset entry opacity (for assessment hide feature)
|
||||||
".dailycal .content .days td",
|
|
||||||
) as HTMLElement;
|
|
||||||
if (!firstDayColumn) return;
|
|
||||||
|
|
||||||
const baseContainerHeight =
|
|
||||||
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
|
|
||||||
|
|
||||||
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
|
|
||||||
dayColumns.forEach((td: Element) => {
|
|
||||||
(td as HTMLElement).style.height = `${baseContainerHeight}px`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const timeColumn = document.querySelector(".times");
|
|
||||||
if (timeColumn) {
|
|
||||||
const times = timeColumn.querySelectorAll(".time");
|
|
||||||
const timeHeight = baseContainerHeight / times.length;
|
|
||||||
times.forEach((time: Element) => {
|
|
||||||
(time as HTMLElement).style.height = `${timeHeight}px`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const lessons = document.querySelectorAll(".dailycal .lesson");
|
|
||||||
lessons.forEach((lesson: Element) => {
|
|
||||||
const lessonEl = lesson as HTMLElement;
|
|
||||||
const originalHeight = lessonEl.getAttribute("data-original-height");
|
|
||||||
if (originalHeight) {
|
|
||||||
lessonEl.style.height = `${originalHeight}px`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const entries = document.querySelectorAll(".entry");
|
const entries = document.querySelectorAll(".entry");
|
||||||
entries.forEach((entry: Element) => {
|
entries.forEach((entry: Element) => {
|
||||||
const entryEl = entry as HTMLElement;
|
const entryEl = entry as HTMLElement;
|
||||||
entryEl.style.opacity = "1";
|
entryEl.style.opacity = "1";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clean up zoom control event handlers
|
||||||
const zoomControls = document.querySelector(".timetable-zoom-controls");
|
const zoomControls = document.querySelector(".timetable-zoom-controls");
|
||||||
if (zoomControls) {
|
if (zoomControls) {
|
||||||
const handlers = zoomHandlers.get(zoomControls);
|
const handlers = zoomHandlers.get(zoomControls);
|
||||||
@@ -94,19 +65,9 @@ function resetTimetableStyles(): void {
|
|||||||
async function handleTimetable(): Promise<void> {
|
async function handleTimetable(): Promise<void> {
|
||||||
await waitForElm(".time", true, 10);
|
await waitForElm(".time", true, 10);
|
||||||
|
|
||||||
// Store original heights when timetable loads
|
// Convert time format if needed
|
||||||
const lessons = document.querySelectorAll(".dailycal .lesson");
|
|
||||||
lessons.forEach((lesson: Element) => {
|
|
||||||
const lessonEl = lesson as HTMLElement;
|
|
||||||
lessonEl.setAttribute(
|
|
||||||
"data-original-height",
|
|
||||||
lessonEl.offsetHeight.toString(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Existing time format code
|
|
||||||
if (settingsState.timeFormat == "12") {
|
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) {
|
for (const time of times) {
|
||||||
if (!time.textContent) continue;
|
if (!time.textContent) continue;
|
||||||
time.textContent = convertTo12HourFormat(time.textContent, true);
|
time.textContent = convertTo12HourFormat(time.textContent, true);
|
||||||
@@ -120,14 +81,6 @@ async function handleTimetable(): Promise<void> {
|
|||||||
function handleTimetableZoom(): void {
|
function handleTimetableZoom(): void {
|
||||||
console.log("Initializing timetable zoom controls");
|
console.log("Initializing timetable zoom controls");
|
||||||
|
|
||||||
// Lazy initialize state variables only when function is first called
|
|
||||||
let timetableZoomLevel = 1;
|
|
||||||
let baseContainerHeight: number | null = null;
|
|
||||||
const originalEntryPositions = new Map<
|
|
||||||
Element,
|
|
||||||
{ topRatio: number; heightRatio: number }
|
|
||||||
>();
|
|
||||||
|
|
||||||
// Create zoom controls
|
// Create zoom controls
|
||||||
const zoomControls = document.createElement("div");
|
const zoomControls = document.createElement("div");
|
||||||
zoomControls.className = "timetable-zoom-controls";
|
zoomControls.className = "timetable-zoom-controls";
|
||||||
@@ -148,16 +101,16 @@ function handleTimetableZoom(): void {
|
|||||||
|
|
||||||
// Store event listener references
|
// Store event listener references
|
||||||
const zoomInHandler = () => {
|
const zoomInHandler = () => {
|
||||||
if (timetableZoomLevel < 2) {
|
const seqtaZoomIn = document.querySelector('.uiButton.zoom.in') as HTMLElement;
|
||||||
timetableZoomLevel += 0.2;
|
if (seqtaZoomIn) {
|
||||||
updateZoom();
|
seqtaZoomIn.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const zoomOutHandler = () => {
|
const zoomOutHandler = () => {
|
||||||
if (timetableZoomLevel > 0.6) {
|
const seqtaZoomOut = document.querySelector('.uiButton.zoom.out') as HTMLElement;
|
||||||
timetableZoomLevel -= 0.2;
|
if (seqtaZoomOut) {
|
||||||
updateZoom();
|
seqtaZoomOut.click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -169,84 +122,6 @@ function handleTimetableZoom(): void {
|
|||||||
zoomIn: zoomInHandler,
|
zoomIn: zoomInHandler,
|
||||||
zoomOut: zoomOutHandler,
|
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 {
|
function handleTimetableAssessmentHide(): void {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
import { eventManager } from "@/seqta/utils/listeners/EventManager";
|
import { eventManager } from "@/seqta/utils/listeners/EventManager";
|
||||||
import ReactFiber from "@/seqta/utils/ReactFiber";
|
import ReactFiber from "@/seqta/utils/ReactFiber";
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
|
||||||
function createSEQTAAPI(): SEQTAAPI {
|
function createSEQTAAPI(): SEQTAAPI {
|
||||||
return {
|
return {
|
||||||
@@ -48,6 +49,40 @@ function createSEQTAAPI(): SEQTAAPI {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reactive and persistent settings store for a given plugin.
|
||||||
|
* This store is a Svelte-like store, providing reactivity, persistence
|
||||||
|
* via `browser.storage.local`, and default value handling.
|
||||||
|
*
|
||||||
|
* @template T - Represents the structure of the plugin's settings, extending `PluginSettings`.
|
||||||
|
* @param {Plugin<T, any>} plugin The plugin instance for which the settings store is being created.
|
||||||
|
* `plugin.id` is used for namespacing the settings in storage,
|
||||||
|
* and `plugin.settings` provides the definitions and default values for each setting.
|
||||||
|
* @returns {SettingsAPI<T> & { loaded: Promise<void> }} An object that functions as a Svelte store,
|
||||||
|
* enhanced with specific methods for settings management.
|
||||||
|
* The object includes:
|
||||||
|
* - Reactivity: Changes to settings can be subscribed to using Svelte's store subscription pattern
|
||||||
|
* (though not explicitly a Svelte store, it behaves similarly for direct property access and updates).
|
||||||
|
* The `onChange` method provides a more direct way to listen for specific key changes.
|
||||||
|
* - Persistence: Settings are automatically loaded from `browser.storage.local` when the store is created
|
||||||
|
* and saved back whenever a setting is changed via the proxy's setter.
|
||||||
|
* - Default Values: Uses default values from the `plugin.settings` definition if no stored value exists for a setting.
|
||||||
|
* - `loaded`: A Promise that resolves when the settings have been successfully loaded from storage,
|
||||||
|
* allowing operations to be deferred until settings are ready.
|
||||||
|
* - Direct property access for getting values (e.g., `settingsStore.mySettingKey`).
|
||||||
|
* - Direct property assignment for setting values (e.g., `settingsStore.mySettingKey = newValue`), which also persists the change.
|
||||||
|
* - `onChange(key, callback)`: Method to listen for changes to a specific setting. (Note: The prompt mentioned `listen`, this is `onChange`).
|
||||||
|
* Returns an object with an `unregister` method.
|
||||||
|
* - `offChange(key, callback)`: Method to stop listening for changes to a specific setting.
|
||||||
|
* The following methods are not explicitly present on the returned proxy from `createSettingsAPI` but are typically
|
||||||
|
* expected in a full "Svelte store" settings manager. The current implementation relies on direct property
|
||||||
|
* manipulation for get/set, and re-initialization for reset-like behavior or would require external implementation
|
||||||
|
* of reset logic if needed:
|
||||||
|
* - `get(key)`: (Achieved by direct property access: `settingsStore.key`)
|
||||||
|
* - `set(key, value)`: (Achieved by direct property assignment: `settingsStore.key = value`)
|
||||||
|
* - `reset(key)`: (Would require manual re-application of `plugin.settings[key].default` and then setting it)
|
||||||
|
* - `resetAll()`: (Would require iterating through all `plugin.settings` and applying defaults, then setting them)
|
||||||
|
*/
|
||||||
function createSettingsAPI<T extends PluginSettings>(
|
function createSettingsAPI<T extends PluginSettings>(
|
||||||
plugin: Plugin<T>,
|
plugin: Plugin<T>,
|
||||||
): SettingsAPI<T> & { loaded: Promise<void> } {
|
): SettingsAPI<T> & { loaded: Promise<void> } {
|
||||||
@@ -81,16 +116,16 @@ function createSettingsAPI<T extends PluginSettings>(
|
|||||||
|
|
||||||
// Fill with defaults first
|
// Fill with defaults first
|
||||||
for (const key in plugin.settings) {
|
for (const key in plugin.settings) {
|
||||||
|
if (plugin.settings[key].type !== 'component' && plugin.settings[key].type !== 'button') {
|
||||||
settingsWithMeta[key] = plugin.settings[key].default;
|
settingsWithMeta[key] = plugin.settings[key].default;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load stored settings and override defaults
|
// Load stored settings and override defaults
|
||||||
const loaded = (async () => {
|
const loaded = (async () => {
|
||||||
try {
|
try {
|
||||||
const stored = await browser.storage.local.get(storageKey);
|
const allSettings = settingsState.getAll() as unknown as Record<string, unknown>;
|
||||||
const storedSettings = stored[storageKey] as Partial<
|
const storedSettings = allSettings[storageKey] as Partial<Record<keyof T, any>>;
|
||||||
Record<keyof T, any>
|
|
||||||
>;
|
|
||||||
if (storedSettings) {
|
if (storedSettings) {
|
||||||
for (const key in storedSettings) {
|
for (const key in storedSettings) {
|
||||||
if (key in settingsWithMeta) {
|
if (key in settingsWithMeta) {
|
||||||
@@ -171,7 +206,7 @@ function createStorageAPI<T = any>(
|
|||||||
// Load all existing storage values for this plugin
|
// Load all existing storage values for this plugin
|
||||||
const loadStoragePromise = (async () => {
|
const loadStoragePromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const allStorage = await browser.storage.local.get(null);
|
const allStorage = settingsState.getAll();
|
||||||
|
|
||||||
// Filter for this plugin's storage keys and populate cache
|
// Filter for this plugin's storage keys and populate cache
|
||||||
Object.entries(allStorage).forEach(([key, value]) => {
|
Object.entries(allStorage).forEach(([key, value]) => {
|
||||||
@@ -293,6 +328,32 @@ function createEventsAPI(pluginId: string): EventsAPI {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns a tailored API object for a specific plugin.
|
||||||
|
* This API object provides the plugin with various functionalities such as
|
||||||
|
* managing settings, accessing namespaced storage, interacting with SEQTA-specific features,
|
||||||
|
* and handling plugin-specific events.
|
||||||
|
*
|
||||||
|
* @template T - The type of the plugin's settings, extending `PluginSettings`.
|
||||||
|
* @template S - The type of the data the plugin will store in its namespaced storage.
|
||||||
|
* @param {Plugin<T, S>} plugin The plugin instance for which the API is being created.
|
||||||
|
* The plugin's `id` and `name` are used internally by the API
|
||||||
|
* for namespacing and identification but are accessed from the `plugin` object directly.
|
||||||
|
* @returns {PluginAPI<T, S>} An API object containing the following key properties:
|
||||||
|
* - `seqta`: An API for interacting with SEQTA-specific functionalities, created by `createSEQTAAPI()`.
|
||||||
|
* This includes methods like `onMount` for DOM element appearance, `getFiber` for React component inspection,
|
||||||
|
* `getCurrentPage` for getting the current SEQTA page, and `onPageChange` for listening to page navigations.
|
||||||
|
* - `settings`: An API for managing plugin-specific settings, created by `createSettingsAPI(plugin)`.
|
||||||
|
* It allows getting, setting, and listening to changes in the plugin's settings,
|
||||||
|
* which are stored persistently and namespaced to the plugin. Includes a `loaded` promise.
|
||||||
|
* - `storage`: An API for providing namespaced storage for the plugin, created by `createStorageAPI<S>(plugin.id)`.
|
||||||
|
* It allows the plugin to store and retrieve arbitrary data, namespaced to prevent conflicts
|
||||||
|
* with other plugins or parts of the extension. Includes a `loaded` promise and `onChange` listeners.
|
||||||
|
* - `events`: An API for allowing the plugin to dispatch and listen for custom events within its own scope,
|
||||||
|
* created by `createEventsAPI(plugin.id)`. It provides `on(event, callback)` to listen for
|
||||||
|
* plugin-specific events and `emit(event, ...args)` to dispatch them. These events are namespaced
|
||||||
|
* to the plugin.
|
||||||
|
*/
|
||||||
export function createPluginAPI<T extends PluginSettings, S = any>(
|
export function createPluginAPI<T extends PluginSettings, S = any>(
|
||||||
plugin: Plugin<T, S>,
|
plugin: Plugin<T, S>,
|
||||||
): PluginAPI<T, S> {
|
): PluginAPI<T, S> {
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
+159
-10
@@ -7,9 +7,11 @@ import type {
|
|||||||
StringSetting,
|
StringSetting,
|
||||||
ButtonSetting,
|
ButtonSetting,
|
||||||
HotkeySetting,
|
HotkeySetting,
|
||||||
|
ComponentSetting,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { createPluginAPI } from "./createAPI";
|
import { createPluginAPI } from "./createAPI";
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
|
||||||
interface PluginSettingsStorage {
|
interface PluginSettingsStorage {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -21,6 +23,12 @@ interface StorageChange<T = any> {
|
|||||||
newValue?: T;
|
newValue?: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton class responsible for the entire lifecycle of plugins.
|
||||||
|
* This includes registration, starting, stopping, event dispatching,
|
||||||
|
* managing plugin-specific styles, and listening for plugin setting changes
|
||||||
|
* to automatically start or stop plugins.
|
||||||
|
*/
|
||||||
export class PluginManager {
|
export class PluginManager {
|
||||||
private static instance: PluginManager;
|
private static instance: PluginManager;
|
||||||
private plugins: Map<string, Plugin<any, any>> = new Map();
|
private plugins: Map<string, Plugin<any, any>> = new Map();
|
||||||
@@ -30,10 +38,18 @@ export class PluginManager {
|
|||||||
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
private listeners: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||||
private styleElements: Map<string, HTMLStyleElement> = new Map();
|
private styleElements: Map<string, HTMLStyleElement> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to enforce singleton pattern.
|
||||||
|
* Initializes the listener for plugin state changes from storage.
|
||||||
|
*/
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.setupPluginStateListener();
|
this.setupPluginStateListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton instance of the PluginManager.
|
||||||
|
* @returns {PluginManager} The singleton instance.
|
||||||
|
*/
|
||||||
public static getInstance(): PluginManager {
|
public static getInstance(): PluginManager {
|
||||||
if (!PluginManager.instance) {
|
if (!PluginManager.instance) {
|
||||||
PluginManager.instance = new PluginManager();
|
PluginManager.instance = new PluginManager();
|
||||||
@@ -41,6 +57,15 @@ export class PluginManager {
|
|||||||
return PluginManager.instance;
|
return PluginManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an event to a specific plugin.
|
||||||
|
* If the plugin is currently running, the event is dispatched immediately via a DOM CustomEvent.
|
||||||
|
* If the plugin is not running, the event is added to a backlog to be processed when the plugin starts.
|
||||||
|
*
|
||||||
|
* @param {string} pluginId The ID of the target plugin.
|
||||||
|
* @param {string} event The name of the event to dispatch (e.g., "update").
|
||||||
|
* @param {any} [args] Optional arguments to pass with the event.
|
||||||
|
*/
|
||||||
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
public dispatchPluginEvent(pluginId: string, event: string, args?: any) {
|
||||||
const fullEventName = `plugin.${pluginId}.${event}`;
|
const fullEventName = `plugin.${pluginId}.${event}`;
|
||||||
|
|
||||||
@@ -56,6 +81,14 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes and dispatches any events that were backlogged for a plugin.
|
||||||
|
* This is typically called after a plugin has successfully started.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} pluginId The ID of the plugin for which to process backlogged events.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
private async processBackloggedEvents(pluginId: string) {
|
private async processBackloggedEvents(pluginId: string) {
|
||||||
for (const [key, argsList] of this.eventBacklog.entries()) {
|
for (const [key, argsList] of this.eventBacklog.entries()) {
|
||||||
const [eventPluginId, event] = key.split(":");
|
const [eventPluginId, event] = key.split(":");
|
||||||
@@ -68,6 +101,15 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a plugin with the manager.
|
||||||
|
* Plugins must have a unique ID.
|
||||||
|
*
|
||||||
|
* @template T - The type of settings the plugin uses.
|
||||||
|
* @template S - The type of storage the plugin uses.
|
||||||
|
* @param {Plugin<T, S>} plugin The plugin object to register.
|
||||||
|
* @throws {Error} If a plugin with the same ID is already registered.
|
||||||
|
*/
|
||||||
public registerPlugin<T extends PluginSettings, S>(
|
public registerPlugin<T extends PluginSettings, S>(
|
||||||
plugin: Plugin<T, S>,
|
plugin: Plugin<T, S>,
|
||||||
): void {
|
): void {
|
||||||
@@ -77,6 +119,22 @@ export class PluginManager {
|
|||||||
this.plugins.set(plugin.id, plugin);
|
this.plugins.set(plugin.id, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a specific plugin by its ID.
|
||||||
|
* This involves:
|
||||||
|
* - Checking if the plugin exists and isn't already running.
|
||||||
|
* - Creating and providing the plugin API (settings, storage, etc.).
|
||||||
|
* - Checking if the plugin is enabled (if `disableToggle` is true), respecting its `defaultEnabled` status.
|
||||||
|
* - Injecting any CSS styles defined by the plugin into the document head.
|
||||||
|
* - Waiting for the plugin's settings and storage to be loaded.
|
||||||
|
* - Executing the plugin's `run` method.
|
||||||
|
* - Storing any cleanup function returned by `run` for later use in `stopPlugin`.
|
||||||
|
* - Marking the plugin as running and processing any backlogged events for it.
|
||||||
|
*
|
||||||
|
* @param {string} pluginId The ID of the plugin to start.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the plugin has started or is determined not to start (e.g., disabled).
|
||||||
|
* @throws {Error} If the plugin is not found, or if an error occurs during plugin initialization or execution.
|
||||||
|
*/
|
||||||
public async startPlugin(pluginId: string): Promise<void> {
|
public async startPlugin(pluginId: string): Promise<void> {
|
||||||
const plugin = this.plugins.get(pluginId);
|
const plugin = this.plugins.get(pluginId);
|
||||||
if (!plugin) {
|
if (!plugin) {
|
||||||
@@ -93,10 +151,8 @@ export class PluginManager {
|
|||||||
|
|
||||||
// Check if plugin is enabled before starting
|
// Check if plugin is enabled before starting
|
||||||
if (plugin.disableToggle) {
|
if (plugin.disableToggle) {
|
||||||
const settings = await browser.storage.local.get(
|
const all = settingsState.getAll() as unknown as Record<string, unknown>;
|
||||||
`plugin.${pluginId}.settings`,
|
const pluginSettings = all[`plugin.${pluginId}.settings`] as
|
||||||
);
|
|
||||||
const pluginSettings = settings[`plugin.${pluginId}.settings`] as
|
|
||||||
| PluginSettingsStorage
|
| PluginSettingsStorage
|
||||||
| undefined;
|
| undefined;
|
||||||
const enabled =
|
const enabled =
|
||||||
@@ -138,17 +194,36 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to start all registered plugins.
|
||||||
|
* Errors during the start of individual plugins are caught and logged,
|
||||||
|
* allowing other plugins to attempt to start.
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} A promise that resolves when all plugins have attempted to start.
|
||||||
|
* It uses `Promise.allSettled` to wait for all start operations.
|
||||||
|
*/
|
||||||
public async startAllPlugins(): Promise<void> {
|
public async startAllPlugins(): Promise<void> {
|
||||||
const startPromises = Array.from(this.plugins.keys()).map((id) =>
|
const startPromises = Array.from(this.plugins.keys()).map((id) =>
|
||||||
this.startPlugin(id).catch((error) => {
|
this.startPlugin(id).catch((error) => {
|
||||||
console.error(`Failed to start plugin "${id}":`, error);
|
console.error(`Failed to start plugin "${id}":`, error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error); // Still reject to indicate failure for this specific plugin if needed by caller
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.allSettled(startPromises);
|
await Promise.allSettled(startPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops a specific plugin by its ID.
|
||||||
|
* This involves:
|
||||||
|
* - Removing any CSS styles injected by the plugin.
|
||||||
|
* - Executing the cleanup function that was returned by the plugin's `run` method (if any).
|
||||||
|
* - Marking the plugin as not running.
|
||||||
|
* - Emitting a "plugin.stopped" event with the pluginId.
|
||||||
|
*
|
||||||
|
* @param {string} pluginId The ID of the plugin to stop.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the plugin has been stopped.
|
||||||
|
*/
|
||||||
public async stopPlugin(pluginId: string): Promise<void> {
|
public async stopPlugin(pluginId: string): Promise<void> {
|
||||||
// Remove plugin styles
|
// Remove plugin styles
|
||||||
const styleElement = this.styleElements.get(pluginId);
|
const styleElement = this.styleElements.get(pluginId);
|
||||||
@@ -167,18 +242,47 @@ export class PluginManager {
|
|||||||
this.emit("plugin.stopped", pluginId);
|
this.emit("plugin.stopped", pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops all currently running plugins.
|
||||||
|
* Iterates through all registered plugins and calls `stopPlugin` for each.
|
||||||
|
*/
|
||||||
public stopAllPlugins(): void {
|
public stopAllPlugins(): void {
|
||||||
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
|
Array.from(this.plugins.keys()).forEach((id) => this.stopPlugin(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a registered plugin by its ID.
|
||||||
|
*
|
||||||
|
* @param {string} pluginId The ID of the plugin to retrieve.
|
||||||
|
* @returns {Plugin | undefined} The plugin object if found, otherwise undefined.
|
||||||
|
*/
|
||||||
public getPlugin(pluginId: string): Plugin | undefined {
|
public getPlugin(pluginId: string): Plugin | undefined {
|
||||||
return this.plugins.get(pluginId);
|
return this.plugins.get(pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves an array of all registered plugin objects.
|
||||||
|
*
|
||||||
|
* @returns {Plugin[]} An array containing all registered plugin objects.
|
||||||
|
*/
|
||||||
public getAllPlugins(): Plugin[] {
|
public getAllPlugins(): Plugin[] {
|
||||||
return Array.from(this.plugins.values());
|
return Array.from(this.plugins.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a structured list of settings for all registered plugins.
|
||||||
|
* This is primarily used for building user interfaces for plugin configuration.
|
||||||
|
* It processes each plugin's defined settings, adding IDs, titles, descriptions,
|
||||||
|
* and default enabled states. For plugins with `disableToggle`, an "enabled"
|
||||||
|
* boolean setting is automatically included.
|
||||||
|
*
|
||||||
|
* @returns {Array<object>} An array of objects, where each object represents a plugin
|
||||||
|
* and contains its ID, name, description, beta status,
|
||||||
|
* and a processed `settings` object. The `settings` object
|
||||||
|
* maps setting keys to their detailed configuration (type, title, etc.).
|
||||||
|
* The specific structure of each setting object within `settings`
|
||||||
|
* depends on its type (boolean, string, number, select, button, hotkey).
|
||||||
|
*/
|
||||||
public getAllPluginSettings(): Array<{
|
public getAllPluginSettings(): Array<{
|
||||||
pluginId: string;
|
pluginId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -195,16 +299,19 @@ export class PluginManager {
|
|||||||
options: Array<{ value: string; label: string }>;
|
options: Array<{ value: string; label: string }>;
|
||||||
})
|
})
|
||||||
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> })
|
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> })
|
||||||
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string });
|
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string })
|
||||||
|
| (Omit<ComponentSetting, "type"> & { type: "component"; id: string; component: any });
|
||||||
};
|
};
|
||||||
|
// Actual type is more complex, see original code, but this gives the gist for the JSDoc.
|
||||||
|
// Array<{ pluginId: string; name: string; description: string; beta?: boolean; settings: Record<string, ProcessedSetting>; disableToggle?: boolean; }>
|
||||||
}> {
|
}> {
|
||||||
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
||||||
const settingsEntries = Object.entries(plugin.settings).map(
|
const settingsEntries = Object.entries(plugin.settings).map(
|
||||||
([key, setting]) => {
|
([key, setting]) => {
|
||||||
const settingObj = setting as any;
|
const settingObj = setting as any;
|
||||||
let result: any;
|
let result: any;
|
||||||
if (settingObj.type === "button") {
|
if (settingObj.type === "button" || settingObj.type === "component") {
|
||||||
// For button, keep the trigger function
|
// For button or component, keep the functions
|
||||||
result = { ...settingObj };
|
result = { ...settingObj };
|
||||||
} else {
|
} else {
|
||||||
// For others, strip functions
|
// For others, strip functions
|
||||||
@@ -245,10 +352,24 @@ export class PluginManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a specific plugin is currently running.
|
||||||
|
*
|
||||||
|
* @param {string} pluginId The ID of the plugin to check.
|
||||||
|
* @returns {boolean} True if the plugin is running, false otherwise.
|
||||||
|
*/
|
||||||
public isPluginRunning(pluginId: string): boolean {
|
public isPluginRunning(pluginId: string): boolean {
|
||||||
return this.runningPlugins.get(pluginId) || false;
|
return this.runningPlugins.get(pluginId) || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event to all registered listeners for that event.
|
||||||
|
* This is an internal event bus for the PluginManager itself.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} event The name of the event to emit.
|
||||||
|
* @param {any[]} args Arguments to pass to the event listeners.
|
||||||
|
*/
|
||||||
private emit(event: string, ...args: any[]): void {
|
private emit(event: string, ...args: any[]): void {
|
||||||
const listeners = this.listeners.get(event);
|
const listeners = this.listeners.get(event);
|
||||||
if (listeners) {
|
if (listeners) {
|
||||||
@@ -256,6 +377,12 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an event listener for PluginManager's internal events.
|
||||||
|
*
|
||||||
|
* @param {string} event The name of the event to listen for (e.g., "plugin.stopped").
|
||||||
|
* @param {(...args: any[]) => void} callback The function to call when the event is emitted.
|
||||||
|
*/
|
||||||
public on(event: string, callback: (...args: any[]) => void): void {
|
public on(event: string, callback: (...args: any[]) => void): void {
|
||||||
if (!this.listeners.has(event)) {
|
if (!this.listeners.has(event)) {
|
||||||
this.listeners.set(event, new Set());
|
this.listeners.set(event, new Set());
|
||||||
@@ -263,6 +390,12 @@ export class PluginManager {
|
|||||||
this.listeners.get(event)!.add(callback);
|
this.listeners.get(event)!.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters an event listener for PluginManager's internal events.
|
||||||
|
*
|
||||||
|
* @param {string} event The name of the event.
|
||||||
|
* @param {(...args: any[]) => void} callback The callback function to remove.
|
||||||
|
*/
|
||||||
public off(event: string, callback: (...args: any[]) => void): void {
|
public off(event: string, callback: (...args: any[]) => void): void {
|
||||||
const listeners = this.listeners.get(event);
|
const listeners = this.listeners.get(event);
|
||||||
if (listeners) {
|
if (listeners) {
|
||||||
@@ -270,7 +403,16 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add handler for plugin enable/disable state changes
|
/**
|
||||||
|
* Handles the change in a plugin's enabled state.
|
||||||
|
* Starts or stops the plugin based on the new `enabled` value.
|
||||||
|
* This is typically called by `setupPluginStateListener` when a relevant storage change is detected.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {string} pluginId The ID of the plugin whose state has changed.
|
||||||
|
* @param {boolean} enabled The new enabled state of the plugin.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
private async handlePluginStateChange(
|
private async handlePluginStateChange(
|
||||||
pluginId: string,
|
pluginId: string,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
@@ -282,7 +424,14 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add listener for plugin settings changes
|
/**
|
||||||
|
* Sets up a listener for browser storage changes.
|
||||||
|
* This listener monitors changes to plugin settings (specifically the `enabled` property
|
||||||
|
* for plugins with `disableToggle: true`) and calls `handlePluginStateChange`
|
||||||
|
* to automatically start or stop plugins accordingly.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private setupPluginStateListener(): void {
|
private setupPluginStateListener(): void {
|
||||||
browser.storage.onChanged.addListener(
|
browser.storage.onChanged.addListener(
|
||||||
(changes: { [key: string]: StorageChange }, area: string) => {
|
(changes: { [key: string]: StorageChange }, area: string) => {
|
||||||
|
|||||||
@@ -5,8 +5,20 @@ import type {
|
|||||||
SelectSetting,
|
SelectSetting,
|
||||||
StringSetting,
|
StringSetting,
|
||||||
HotkeySetting,
|
HotkeySetting,
|
||||||
|
PluginSettings,
|
||||||
|
ComponentSetting,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a complete `NumberSetting` object from its options.
|
||||||
|
* This helper function ensures the `type` property is correctly set to "number".
|
||||||
|
* It's used for defining a numeric setting for a plugin.
|
||||||
|
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||||
|
*
|
||||||
|
* @param {Omit<NumberSetting, "type">} options The configuration options for the number setting,
|
||||||
|
* excluding the `type` property (e.g., `title`, `default`, `min`, `max`).
|
||||||
|
* @returns {NumberSetting} A complete number setting object with `type: "number"`.
|
||||||
|
*/
|
||||||
export function numberSetting(
|
export function numberSetting(
|
||||||
options: Omit<NumberSetting, "type">,
|
options: Omit<NumberSetting, "type">,
|
||||||
): NumberSetting {
|
): NumberSetting {
|
||||||
@@ -16,6 +28,16 @@ export function numberSetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a complete `BooleanSetting` object from its options.
|
||||||
|
* This helper function ensures the `type` property is correctly set to "boolean".
|
||||||
|
* It's used for defining a boolean (true/false) setting for a plugin.
|
||||||
|
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||||
|
*
|
||||||
|
* @param {Omit<BooleanSetting, "type">} options The configuration options for the boolean setting,
|
||||||
|
* excluding the `type` property (e.g., `title`, `default`).
|
||||||
|
* @returns {BooleanSetting} A complete boolean setting object with `type: "boolean"`.
|
||||||
|
*/
|
||||||
export function booleanSetting(
|
export function booleanSetting(
|
||||||
options: Omit<BooleanSetting, "type">,
|
options: Omit<BooleanSetting, "type">,
|
||||||
): BooleanSetting {
|
): BooleanSetting {
|
||||||
@@ -25,6 +47,16 @@ export function booleanSetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a complete `StringSetting` object from its options.
|
||||||
|
* This helper function ensures the `type` property is correctly set to "string".
|
||||||
|
* It's used for defining a text-based setting for a plugin.
|
||||||
|
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||||
|
*
|
||||||
|
* @param {Omit<StringSetting, "type">} options The configuration options for the string setting,
|
||||||
|
* excluding the `type` property (e.g., `title`, `default`, `placeholder`).
|
||||||
|
* @returns {StringSetting} A complete string setting object with `type: "string"`.
|
||||||
|
*/
|
||||||
export function stringSetting(
|
export function stringSetting(
|
||||||
options: Omit<StringSetting, "type">,
|
options: Omit<StringSetting, "type">,
|
||||||
): StringSetting {
|
): StringSetting {
|
||||||
@@ -34,15 +66,36 @@ export function stringSetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectSetting<T extends string>(
|
/**
|
||||||
options: Omit<SelectSetting<T>, "type">,
|
* Creates a complete `SelectSetting` object from its options.
|
||||||
): SelectSetting<T> {
|
* This helper function ensures the `type` property is correctly set to "select".
|
||||||
|
* It's used for defining a setting where the user can choose from a predefined list of options.
|
||||||
|
* This function itself does not handle storage or persistence; it defines the setting's structure.
|
||||||
|
*
|
||||||
|
* @template TValue - The type of the value for each option in the select list (extends string).
|
||||||
|
* @param {Omit<SelectSetting<TValue>, "type">} options The configuration options for the select setting,
|
||||||
|
* excluding the `type` property (e.g., `title`, `default`, `options` array).
|
||||||
|
* @returns {SelectSetting<TValue>} A complete select setting object with `type: "select"`.
|
||||||
|
*/
|
||||||
|
export function selectSetting<TValue extends string>(
|
||||||
|
options: Omit<SelectSetting<TValue>, "type">,
|
||||||
|
): SelectSetting<TValue> {
|
||||||
return {
|
return {
|
||||||
type: "select",
|
type: "select",
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a complete `ButtonSetting` object from its options.
|
||||||
|
* This helper function ensures the `type` property is correctly set to "button".
|
||||||
|
* It's used for defining a button in the plugin's settings UI, which can trigger an action.
|
||||||
|
* This function itself does not handle storage or persistence; it defines the button's structure and action.
|
||||||
|
*
|
||||||
|
* @param {Omit<ButtonSetting, "type">} options The configuration options for the button setting,
|
||||||
|
* excluding the `type` property (e.g., `title`, `label`, `trigger` function).
|
||||||
|
* @returns {ButtonSetting} A complete button setting object with `type: "button"`.
|
||||||
|
*/
|
||||||
export function buttonSetting(
|
export function buttonSetting(
|
||||||
options: Omit<ButtonSetting, "type">,
|
options: Omit<ButtonSetting, "type">,
|
||||||
): ButtonSetting {
|
): ButtonSetting {
|
||||||
@@ -52,6 +105,26 @@ export function buttonSetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a complete `HotkeySetting` object from its options.
|
||||||
|
* This helper function ensures the `type` property is correctly set to "hotkey".
|
||||||
|
* It's used for defining a setting where the user can configure a keyboard shortcut.
|
||||||
|
* This function itself does not handle storage or persistence; it defines the hotkey setting's structure.
|
||||||
|
*
|
||||||
|
* @param {Omit<HotkeySetting, "type">} options The configuration options for the hotkey setting,
|
||||||
|
* excluding the `type` property (e.g., `title`, `default` hotkey string).
|
||||||
|
* @returns {HotkeySetting} A complete hotkey setting object with `type: "hotkey"`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function componentSetting(
|
||||||
|
options: Omit<ComponentSetting, "type">,
|
||||||
|
): ComponentSetting {
|
||||||
|
return {
|
||||||
|
type: "component",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function hotkeySetting(
|
export function hotkeySetting(
|
||||||
options: Omit<HotkeySetting, "type">,
|
options: Omit<HotkeySetting, "type">,
|
||||||
): HotkeySetting {
|
): HotkeySetting {
|
||||||
@@ -61,10 +134,42 @@ export function hotkeySetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineSettings<T extends Record<string, any>>(settings: T): T {
|
/**
|
||||||
|
* Defines a collection of settings for a plugin.
|
||||||
|
* This function currently acts as an identity function, returning the settings object as is.
|
||||||
|
* Its primary purpose is to provide type inference and a structured way to define
|
||||||
|
* the entire settings configuration for a plugin, ensuring it conforms to the expected type.
|
||||||
|
* This function itself does not handle storage or persistence; it's for structural definition.
|
||||||
|
*
|
||||||
|
* @template TSettings - A record type where keys are setting names and values are setting definition objects
|
||||||
|
* (e.g., `NumberSetting`, `BooleanSetting`).
|
||||||
|
* @param {TSettings} settings The complete settings configuration object for the plugin.
|
||||||
|
* @returns {TSettings} The same settings configuration object, primarily for type checking/inference.
|
||||||
|
*/
|
||||||
|
export function defineSettings<TSettings extends Record<string, any>>(settings: TSettings): TSettings {
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A property decorator for declaratively defining a plugin setting on a class property.
|
||||||
|
* When a class property is decorated with `@Setting({...})`, this decorator adds the
|
||||||
|
* provided setting definition (`settingDef`) to a static `settings` object on the
|
||||||
|
* class's prototype. This allows settings to be defined alongside their related class logic.
|
||||||
|
* This decorator itself does not handle runtime storage or persistence of setting *values*;
|
||||||
|
* it is for defining the *structure* and *metadata* of a setting.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```typescript
|
||||||
|
* class MyPlugin extends BasePlugin {
|
||||||
|
* @Setting(numberSetting({ title: "My Number", default: 10 }))
|
||||||
|
* myNumberSetting: number; // Type annotation for the setting's value
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {any} settingDef The setting definition object, typically created by one of the
|
||||||
|
* helper functions like `numberSetting(...)`, `booleanSetting(...)`, etc.
|
||||||
|
* @returns {PropertyDecorator} A property decorator function.
|
||||||
|
*/
|
||||||
export function Setting(settingDef: any): PropertyDecorator {
|
export function Setting(settingDef: any): PropertyDecorator {
|
||||||
return (target, propertyKey) => {
|
return (target, propertyKey) => {
|
||||||
const proto = target.constructor.prototype;
|
const proto = target.constructor.prototype;
|
||||||
|
|||||||
@@ -48,13 +48,21 @@ export interface HotkeySetting {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ComponentSetting {
|
||||||
|
type: "component";
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
component: any;
|
||||||
|
}
|
||||||
|
|
||||||
export type PluginSetting =
|
export type PluginSetting =
|
||||||
| BooleanSetting
|
| BooleanSetting
|
||||||
| StringSetting
|
| StringSetting
|
||||||
| NumberSetting
|
| NumberSetting
|
||||||
| SelectSetting<string>
|
| SelectSetting<string>
|
||||||
| ButtonSetting
|
| ButtonSetting
|
||||||
| HotkeySetting;
|
| HotkeySetting
|
||||||
|
| ComponentSetting;
|
||||||
|
|
||||||
export type PluginSettings = {
|
export type PluginSettings = {
|
||||||
[key: string]: PluginSetting;
|
[key: string]: PluginSetting;
|
||||||
@@ -71,6 +79,8 @@ export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
|
|||||||
? O
|
? O
|
||||||
: T extends HotkeySetting
|
: T extends HotkeySetting
|
||||||
? string
|
? string
|
||||||
|
: T extends ComponentSetting
|
||||||
|
? never
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export type SettingsAPI<T extends PluginSettings> = {
|
export type SettingsAPI<T extends PluginSettings> = {
|
||||||
|
|||||||
+13
-3
@@ -1,14 +1,19 @@
|
|||||||
import { PluginManager } from "./core/manager";
|
import { PluginManager } from "./core/manager";
|
||||||
|
|
||||||
// plugins
|
// Lightweight plugins (load immediately)
|
||||||
import timetablePlugin from "./built-in/timetable";
|
import timetablePlugin from "./built-in/timetable";
|
||||||
import notificationCollectorPlugin from "./built-in/notificationCollector";
|
import notificationCollectorPlugin from "./built-in/notificationCollector";
|
||||||
import themesPlugin from "./built-in/themes";
|
import themesPlugin from "./built-in/themes";
|
||||||
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
||||||
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
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';
|
//import testPlugin from './built-in/test';
|
||||||
|
|
||||||
|
// Heavy plugins (lazy-loaded only when enabled)
|
||||||
|
import globalSearchPluginLazy from "./built-in/globalSearch/lazy";
|
||||||
|
|
||||||
// Initialize plugin manager
|
// Initialize plugin manager
|
||||||
const pluginManager = PluginManager.getInstance();
|
const pluginManager = PluginManager.getInstance();
|
||||||
|
|
||||||
@@ -18,9 +23,14 @@ pluginManager.registerPlugin(animatedBackgroundPlugin);
|
|||||||
pluginManager.registerPlugin(assessmentsAveragePlugin);
|
pluginManager.registerPlugin(assessmentsAveragePlugin);
|
||||||
pluginManager.registerPlugin(notificationCollectorPlugin);
|
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||||
pluginManager.registerPlugin(timetablePlugin);
|
pluginManager.registerPlugin(timetablePlugin);
|
||||||
pluginManager.registerPlugin(globalSearchPlugin);
|
pluginManager.registerPlugin(profilePicturePlugin);
|
||||||
|
pluginManager.registerPlugin(assessmentsOverviewPlugin);
|
||||||
|
pluginManager.registerPlugin(backgroundMusicPlugin);
|
||||||
//pluginManager.registerPlugin(testPlugin);
|
//pluginManager.registerPlugin(testPlugin);
|
||||||
|
|
||||||
|
// Register heavy plugins with lazy loading
|
||||||
|
pluginManager.registerPlugin(globalSearchPluginLazy);
|
||||||
|
|
||||||
export { init as Monofile } from "./monofile";
|
export { init as Monofile } from "./monofile";
|
||||||
|
|
||||||
export async function initializePlugins(): Promise<void> {
|
export async function initializePlugins(): Promise<void> {
|
||||||
|
|||||||
+17
-6
@@ -23,7 +23,10 @@ import { updateAllColors } from "@/seqta/ui/colors/Manager";
|
|||||||
import loading from "@/seqta/ui/Loading";
|
import loading from "@/seqta/ui/Loading";
|
||||||
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
||||||
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
||||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
|
import { OpenWhatsNewPopup } from "@/seqta/utils/Openers/OpenWhatsNewPopup";
|
||||||
|
import { showPrivacyNotification } from "@/seqta/utils/Openers/OpenPrivacyNotification";
|
||||||
|
|
||||||
|
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
|
||||||
|
|
||||||
// JSON content
|
// JSON content
|
||||||
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
||||||
@@ -91,7 +94,12 @@ export async function finishLoad() {
|
|||||||
console.error("Error during loading cleanup:", err);
|
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();
|
OpenWhatsNewPopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,14 +249,16 @@ async function LoadPageElements(): Promise<void> {
|
|||||||
handleReports,
|
handleReports,
|
||||||
);
|
);
|
||||||
|
|
||||||
/* eventManager.register(
|
eventManager.register(
|
||||||
"timetableAdded",
|
"timetableAdded",
|
||||||
{
|
{
|
||||||
elementType: "div",
|
elementType: "div",
|
||||||
className: "timetablepage",
|
className: "timetablepage",
|
||||||
},
|
},
|
||||||
handleTimetable,
|
async () => {
|
||||||
) */
|
await updateTimetableTimes();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
eventManager.register(
|
eventManager.register(
|
||||||
"noticesAdded",
|
"noticesAdded",
|
||||||
@@ -503,7 +513,7 @@ export async function ObserveMenuItemPosition() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!node?.dataset?.checked && !MenuOptionsOpen) {
|
if (!MenuOptionsOpen) {
|
||||||
const key =
|
const key =
|
||||||
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
|
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
|
||||||
if (key) {
|
if (key) {
|
||||||
@@ -626,6 +636,7 @@ export function init() {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
handleDisabled();
|
handleDisabled();
|
||||||
|
InjectCustomIcons();
|
||||||
window.addEventListener("load", handleDisabled);
|
window.addEventListener("load", handleDisabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
+2
-2
@@ -16,9 +16,9 @@ export async function main() {
|
|||||||
if (settingsState.onoff) {
|
if (settingsState.onoff) {
|
||||||
injectPageState();
|
injectPageState();
|
||||||
|
|
||||||
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
|
// 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") {
|
if (import.meta.env.MODE === "development") {
|
||||||
import("../css/injected.scss");
|
import("@/css/injected.scss");
|
||||||
} else {
|
} else {
|
||||||
const injectedStyle = document.createElement("style");
|
const injectedStyle = document.createElement("style");
|
||||||
injectedStyle.textContent = injectedCSS;
|
injectedStyle.textContent = injectedCSS;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { delay } from "@/seqta/utils/delay";
|
|||||||
|
|
||||||
let cachedUserInfo: any = null;
|
let cachedUserInfo: any = null;
|
||||||
|
|
||||||
|
let LightDarkModeSnakeEggButton = 0;
|
||||||
|
|
||||||
async function getUserInfo() {
|
async function getUserInfo() {
|
||||||
if (cachedUserInfo) return cachedUserInfo;
|
if (cachedUserInfo) return cachedUserInfo;
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
|
|||||||
container.append(div);
|
container.append(div);
|
||||||
|
|
||||||
const NewButton = stringToHTML(
|
const NewButton = stringToHTML(
|
||||||
'<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>',
|
/* 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>`
|
||||||
);
|
);
|
||||||
if (NewButton.firstChild) {
|
if (NewButton.firstChild) {
|
||||||
fragment.appendChild(NewButton.firstChild);
|
fragment.appendChild(NewButton.firstChild);
|
||||||
@@ -171,28 +173,35 @@ async function updateStudentInfo(students: any) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
let houseelement1 = document.getElementsByClassName("userInfohouse")[0];
|
const houseelement = document.getElementsByClassName("userInfohouse")[0] as HTMLElement;
|
||||||
const houseelement = houseelement1 as HTMLElement;
|
|
||||||
|
// Fallback to N/A
|
||||||
|
let text = 'N/A';
|
||||||
|
const student = students[index] ?? {};
|
||||||
|
|
||||||
|
// If student has a house, prefer to show year + house. If no year, only show house.
|
||||||
|
if (student.house) {
|
||||||
|
text = `${student.year ?? ""}${student.house}`;
|
||||||
|
|
||||||
|
// If house_colour exists, compute colour
|
||||||
|
if (student.house_colour) {
|
||||||
|
houseelement.style.background = student.house_colour;
|
||||||
|
|
||||||
if (students[index]?.house) {
|
|
||||||
if (students[index]?.house_colour) {
|
|
||||||
houseelement.style.background = students[index].house_colour;
|
|
||||||
try {
|
try {
|
||||||
let colorresult = GetThresholdOfColor(students[index]?.house_colour);
|
const colorresult = GetThresholdOfColor(student.house_colour);
|
||||||
houseelement.style.color =
|
houseelement.style.color =
|
||||||
colorresult && colorresult > 300 ? "black" : "white";
|
colorresult && colorresult > 300 ? "black" : "white";
|
||||||
houseelement.innerText = students[index].year + students[index].house;
|
|
||||||
} catch (error) {
|
|
||||||
houseelement.innerText = students[index].house;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
houseelement.innerText = students[index].year;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
houseelement.innerText = "N/A";
|
// Colour calculation failed, no text colour set
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (student.year) {
|
||||||
|
// No house, only year will be shown
|
||||||
|
text = student.year;
|
||||||
|
}
|
||||||
|
|
||||||
|
houseelement.innerText = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
|
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
|
||||||
@@ -287,6 +296,14 @@ async function addDarkLightToggle() {
|
|||||||
lightDarkModeButtonElement.addEventListener("click", async () => {
|
lightDarkModeButtonElement.addEventListener("click", async () => {
|
||||||
const darklightText = document.getElementById("darklighttooliptext");
|
const darklightText = document.getElementById("darklighttooliptext");
|
||||||
|
|
||||||
|
LightDarkModeSnakeEggButton += 1;
|
||||||
|
|
||||||
|
if (LightDarkModeSnakeEggButton >= 10) {
|
||||||
|
window.open("https://www.youtube.com/watch?v=dQw4w9WgXcQ", "_blank");
|
||||||
|
LightDarkModeSnakeEggButton = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settingsState.originalDarkMode !== undefined &&
|
settingsState.originalDarkMode !== undefined &&
|
||||||
settingsState.selectedTheme
|
settingsState.selectedTheme
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export async function appendBackgroundToUI() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lastLoadedId: string | null = null;
|
||||||
|
|
||||||
export async function loadBackground() {
|
export async function loadBackground() {
|
||||||
if (!isIndexedDBSupported()) {
|
if (!isIndexedDBSupported()) {
|
||||||
console.error("IndexedDB is not supported. Unable to load background.");
|
console.error("IndexedDB is not supported. Unable to load background.");
|
||||||
@@ -33,12 +35,21 @@ export async function loadBackground() {
|
|||||||
if (backgroundContainer) {
|
if (backgroundContainer) {
|
||||||
backgroundContainer.remove();
|
backgroundContainer.remove();
|
||||||
}
|
}
|
||||||
|
lastLoadedId = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const background = await getDataById(selectedBackgroundId);
|
const background = await getDataById(selectedBackgroundId);
|
||||||
if (!background) return;
|
if (!background) return;
|
||||||
|
|
||||||
|
// Skip reload if background hasn't changed and media element already exists
|
||||||
|
if (
|
||||||
|
selectedBackgroundId === lastLoadedId &&
|
||||||
|
document.querySelector("#media-container > .background")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let backgroundContainer = document.querySelector(".imageBackground");
|
let backgroundContainer = document.querySelector(".imageBackground");
|
||||||
if (!backgroundContainer) {
|
if (!backgroundContainer) {
|
||||||
backgroundContainer = document.createElement("div");
|
backgroundContainer = document.createElement("div");
|
||||||
@@ -77,6 +88,7 @@ export async function loadBackground() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mediaContainer.appendChild(mediaElement);
|
mediaContainer.appendChild(mediaElement);
|
||||||
|
lastLoadedId = selectedBackgroundId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading background:", error);
|
console.error("Error loading background:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,21 @@ interface ContentConfig {
|
|||||||
[key: string]: ElementConfig;
|
[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 {
|
function getRandomElement(array: string[]): string {
|
||||||
return array[Math.floor(Math.random() * array.length)];
|
return array[Math.floor(Math.random() * array.length)];
|
||||||
}
|
}
|
||||||
@@ -164,9 +179,32 @@ const contentConfig: ContentConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
forumTopics: {
|
forumTopics: {
|
||||||
selector: "#menu .sub ul li label",
|
selector: "#menu .sub ul li:not([data-colour]):not(.hasChildren) label",
|
||||||
action: (element) => {
|
action: (element) => {
|
||||||
|
// Only redact if not in assessments section
|
||||||
|
const assessmentsSection = element.closest('[data-key="assessments"]');
|
||||||
|
if (!assessmentsSection) {
|
||||||
element.textContent = "Forum Topic Redacted";
|
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: {
|
courseNames: {
|
||||||
@@ -370,13 +408,339 @@ const mockData = {
|
|||||||
"Mrs. Martinez",
|
"Mrs. Martinez",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
noticesData: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Academic Lunch Support",
|
||||||
|
contents: `The following table shows the names of the students who are required to attend at the beginning of lunchtime on the respective days.<br>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<p style="padding-left: 0px; padding-right: 0px;">Monday 16/06<br>
|
||||||
|
Room S201<br>
|
||||||
|
Week A Mrs Thompson<br>
|
||||||
|
Week B Mrs Smith</p></th>
|
||||||
|
<th>
|
||||||
|
<p style="padding-left: 0px; padding-right: 0px;">Wednesday 18/06<br>
|
||||||
|
Room S201<br>
|
||||||
|
Week A Mrs Smith<br>
|
||||||
|
Week B Mrs Smith</p></th>
|
||||||
|
<th>
|
||||||
|
<p style="padding-left: 0px; padding-right: 0px;">Friday 20/06<br>
|
||||||
|
Room M201 <br>
|
||||||
|
Week A Ms Anderson<br>
|
||||||
|
Week B Ms Anderson </p></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>No Academic Support for year 9 and 10 <br>
|
||||||
|
due to exam in P5/6</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
</div>John Smith (Mrs Jones)<br>
|
||||||
|
Wednesday <br>
|
||||||
|
Michael Brown<br>
|
||||||
|
James Wilson (Miss Davis)<br>
|
||||||
|
<br>
|
||||||
|
</td>
|
||||||
|
<td>Friday 20/6<br>
|
||||||
|
Michael Brown<br>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
</div>`,
|
||||||
|
staff: "Mrs Jones",
|
||||||
|
colour: "#9c27b0",
|
||||||
|
label: 1,
|
||||||
|
label_title: "Middle & Senior School (5-12)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Year 12 Study Period Changes",
|
||||||
|
contents: `Please note the following changes to Year 12 study periods for this week:<br><br>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Tuesday:</strong> Study hall relocated to Library - periods 3 & 4</li>
|
||||||
|
<li><strong>Wednesday:</strong> No supervised study - students may use common areas</li>
|
||||||
|
<li><strong>Friday:</strong> Extended study session until 4:30 PM in Room A205</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
Students are expected to bring all necessary materials and maintain academic focus during these sessions.`,
|
||||||
|
staff: "Mr. David Chen",
|
||||||
|
colour: "#2196f3",
|
||||||
|
label: 2,
|
||||||
|
label_title: "Year 12 Students"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Upcoming Science Fair Preparations",
|
||||||
|
contents: `The Annual Science Fair is scheduled for <strong>Friday, June 28th</strong>. All participating students should note:<br><br>
|
||||||
|
<table border="1" style="border-collapse: collapse; width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f0f0f0;">
|
||||||
|
<th style="padding: 8px;">Activity</th>
|
||||||
|
<th style="padding: 8px;">Date</th>
|
||||||
|
<th style="padding: 8px;">Location</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px;">Project Setup</td>
|
||||||
|
<td style="padding: 8px;">Thursday 27/06 - Period 5</td>
|
||||||
|
<td style="padding: 8px;">Main Hall</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px;">Practice Presentations</td>
|
||||||
|
<td style="padding: 8px;">Thursday 27/06 - Period 6</td>
|
||||||
|
<td style="padding: 8px;">Science Labs 1-3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px;">Final Event</td>
|
||||||
|
<td style="padding: 8px;">Friday 28/06 - All Day</td>
|
||||||
|
<td style="padding: 8px;">Main Hall & Courtyard</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<br>Please ensure all safety protocols are followed and display materials are ready by Thursday afternoon.`,
|
||||||
|
staff: "Dr. Sarah Mitchell",
|
||||||
|
colour: "#4caf50",
|
||||||
|
label: 3,
|
||||||
|
label_title: "Science Students"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Library Resource Updates",
|
||||||
|
contents: `Our library has received several important updates this week:<br><br>
|
||||||
|
<strong>New Digital Resources:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Access to Research Database Plus - now available through student portal</li>
|
||||||
|
<li>Updated online textbook collection for all core subjects</li>
|
||||||
|
<li>New citation management tools for senior students</li>
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
<strong>Facility Changes:</strong><br>
|
||||||
|
The quiet study area has been expanded and now includes 8 additional desks with power outlets. Bookings can be made through the student portal under "Library Services".
|
||||||
|
<br><br>
|
||||||
|
For assistance with any digital resources, please contact the library staff during operating hours: 7:30 AM - 4:00 PM.`,
|
||||||
|
staff: "Ms. Rebecca Torres",
|
||||||
|
colour: "#ff9800",
|
||||||
|
label: 4,
|
||||||
|
label_title: "All Students"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "Sports Carnival Team Registrations",
|
||||||
|
contents: `House Sports Carnival is approaching on <strong>August 15th</strong>! Team registrations are now open for all year levels.<br><br>
|
||||||
|
Available Events:
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 10px 0;">
|
||||||
|
<div>
|
||||||
|
<strong>Track Events:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>100m Sprint</li>
|
||||||
|
<li>200m Sprint</li>
|
||||||
|
<li>400m Race</li>
|
||||||
|
<li>800m Distance</li>
|
||||||
|
<li>1500m Distance</li>
|
||||||
|
<li>Relay Races</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Field Events:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Long Jump</li>
|
||||||
|
<li>High Jump</li>
|
||||||
|
<li>Shot Put</li>
|
||||||
|
<li>Discus</li>
|
||||||
|
<li>Javelin</li>
|
||||||
|
<li>Triple Jump</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<strong>Registration Deadline:</strong> July 25th<br>
|
||||||
|
<strong>Training Sessions:</strong> Tuesdays & Thursdays, 3:30-4:30 PM<br>
|
||||||
|
<br>
|
||||||
|
Register through the PE department or see your house captains for more information.`,
|
||||||
|
staff: "Coach Michael Park",
|
||||||
|
colour: "#e91e63",
|
||||||
|
label: 5,
|
||||||
|
label_title: "All Houses"
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function hideSensitiveContent() {
|
export function getMockNotices() {
|
||||||
|
return {
|
||||||
|
payload: mockData.noticesData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }]) => {
|
Object.entries(contentConfig).forEach(([_, { selector, action }]) => {
|
||||||
const elements = document.querySelectorAll(selector);
|
const elements = document.querySelectorAll(selector);
|
||||||
elements.forEach((element: Element) => {
|
elements.forEach((element: Element) => {
|
||||||
|
// Only process elements that haven't been processed before
|
||||||
|
if (!processedElements.has(element)) {
|
||||||
action(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 { SettingsResizer } from "@/seqta/ui/SettingsResizer";
|
||||||
import Settings from "@/interface/pages/settings.svelte";
|
import Settings from "@/interface/pages/settings.svelte";
|
||||||
|
|
||||||
|
let isSettingsRendered = false;
|
||||||
|
|
||||||
export function addExtensionSettings() {
|
export function addExtensionSettings() {
|
||||||
const extensionPopup = document.createElement("div");
|
const extensionPopup = document.createElement("div");
|
||||||
extensionPopup.classList.add("outside-container", "hide");
|
extensionPopup.classList.add("outside-container", "hide");
|
||||||
@@ -17,14 +19,6 @@ export function addExtensionSettings() {
|
|||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
if (extensionContainer) extensionContainer.appendChild(extensionPopup);
|
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");
|
const container = document.getElementById("container");
|
||||||
|
|
||||||
new SettingsResizer();
|
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) {
|
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
||||||
// Creates the stucture and element information for each seperate shortcut
|
// Creates the stucture and element information for each seperate shortcut
|
||||||
|
const container = document.getElementById("shortcuts");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
let shortcut = document.createElement("a");
|
let shortcut = document.createElement("a");
|
||||||
shortcut.setAttribute("href", link);
|
shortcut.setAttribute("href", link);
|
||||||
shortcut.setAttribute("target", "_blank");
|
shortcut.setAttribute("target", "_blank");
|
||||||
@@ -42,5 +45,5 @@ function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
|
|||||||
shortcutdiv.append(text);
|
shortcutdiv.append(text);
|
||||||
shortcut.append(shortcutdiv);
|
shortcut.append(shortcutdiv);
|
||||||
|
|
||||||
document.getElementById("shortcuts")!.appendChild(shortcut);
|
container.appendChild(shortcut);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import stringToHTML from "../stringToHTML";
|
|||||||
|
|
||||||
export function CreateCustomShortcutDiv(element: any) {
|
export function CreateCustomShortcutDiv(element: any) {
|
||||||
// Creates the stucture and element information for each seperate shortcut
|
// Creates the stucture and element information for each seperate shortcut
|
||||||
|
const container = document.getElementById("shortcuts");
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
var shortcut = document.createElement("a");
|
var shortcut = document.createElement("a");
|
||||||
shortcut.setAttribute("href", element.url);
|
shortcut.setAttribute("href", element.url);
|
||||||
shortcut.setAttribute("target", "_blank");
|
shortcut.setAttribute("target", "_blank");
|
||||||
@@ -9,8 +12,19 @@ export function CreateCustomShortcutDiv(element: any) {
|
|||||||
shortcutdiv.classList.add("shortcut");
|
shortcutdiv.classList.add("shortcut");
|
||||||
shortcutdiv.classList.add("customshortcut");
|
shortcutdiv.classList.add("customshortcut");
|
||||||
|
|
||||||
let image = stringToHTML(
|
let image: ChildNode | null = null;
|
||||||
`
|
|
||||||
|
if (typeof element.icon === "string" && element.icon.trim().startsWith("<")) {
|
||||||
|
image = stringToHTML(element.icon).firstChild;
|
||||||
|
} else if (typeof element.icon === "string" && element.icon.startsWith("data:image")) {
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.src = element.icon;
|
||||||
|
img.style.width = "39px";
|
||||||
|
img.style.height = "39px";
|
||||||
|
image = img;
|
||||||
|
} else {
|
||||||
|
image = stringToHTML(
|
||||||
|
/* html */`
|
||||||
<svg style="width:39px;height:39px" viewBox="0 0 40 40" class="shortcuticondiv">
|
<svg style="width:39px;height:39px" viewBox="0 0 40 40" class="shortcuticondiv">
|
||||||
<text
|
<text
|
||||||
text-anchor="middle"
|
text-anchor="middle"
|
||||||
@@ -26,6 +40,7 @@ export function CreateCustomShortcutDiv(element: any) {
|
|||||||
</svg>
|
</svg>
|
||||||
`,
|
`,
|
||||||
).firstChild;
|
).firstChild;
|
||||||
|
}
|
||||||
(image as HTMLElement).classList.add("shortcuticondiv");
|
(image as HTMLElement).classList.add("shortcuticondiv");
|
||||||
var text = document.createElement("p");
|
var text = document.createElement("p");
|
||||||
text.textContent = element.name;
|
text.textContent = element.name;
|
||||||
@@ -33,5 +48,5 @@ export function CreateCustomShortcutDiv(element: any) {
|
|||||||
shortcutdiv.append(text);
|
shortcutdiv.append(text);
|
||||||
shortcut.append(shortcutdiv);
|
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,58 +1,49 @@
|
|||||||
import { delay } from "../delay";
|
|
||||||
import stringToHTML from "../stringToHTML";
|
|
||||||
import { animate, stagger } from "motion";
|
import { animate, stagger } from "motion";
|
||||||
import { settingsState } from "../listeners/SettingsState";
|
|
||||||
|
|
||||||
import { addShortcuts } from "../Adders/AddShortcuts";
|
|
||||||
|
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
|
||||||
import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
|
import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
|
||||||
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
|
|
||||||
|
|
||||||
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
|
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
|
||||||
import coursesicon from "@/seqta/icons/coursesIcon";
|
import coursesicon from "@/seqta/icons/coursesIcon";
|
||||||
|
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
||||||
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
|
||||||
|
|
||||||
import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement";
|
|
||||||
|
|
||||||
import { convertTo12HourFormat } from "../convertTo12HourFormat";
|
import { convertTo12HourFormat } from "../convertTo12HourFormat";
|
||||||
|
import { delay } from "../delay";
|
||||||
|
import { settingsState } from "../listeners/SettingsState";
|
||||||
|
import stringToHTML from "../stringToHTML";
|
||||||
|
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 LessonInterval: any;
|
||||||
let currentSelectedDate = new Date();
|
let currentSelectedDate = new Date();
|
||||||
|
let loadingTimeout: any;
|
||||||
|
|
||||||
export async function loadHomePage() {
|
export async function loadHomePage() {
|
||||||
console.info("[BetterSEQTA+] Started Loading Home Page");
|
console.info("[BetterSEQTA+] Started Loading Home Page");
|
||||||
|
|
||||||
// Wait for the DOM to finish clearing
|
currentSelectedDate = new Date();
|
||||||
|
|
||||||
await delay(10);
|
await delay(10);
|
||||||
|
|
||||||
document.title = "Home ― SEQTA Learn";
|
document.title = "Home ― SEQTA Learn";
|
||||||
const element = document.querySelector("[data-key=home]");
|
const element = document.querySelector("[data-key=home]");
|
||||||
element?.classList.add("active");
|
element?.classList.add("active");
|
||||||
|
|
||||||
// Cache DOM queries
|
|
||||||
const main = document.getElementById("main");
|
const main = document.getElementById("main");
|
||||||
if (!main) {
|
if (!main) {
|
||||||
console.error("[BetterSEQTA+] Main element not found.");
|
console.error("[BetterSEQTA+] Main element not found.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create root container first
|
const homeRoot = stringToHTML(`<div id="home-root" class="home-root"></div>`);
|
||||||
const homeRoot = stringToHTML(
|
|
||||||
/* html */ `<div id="home-root" class="home-root"></div>`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clear main and add home root
|
|
||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
main.appendChild(homeRoot?.firstChild!);
|
main.appendChild(homeRoot?.firstChild!);
|
||||||
|
|
||||||
// Get reference to home container for all subsequent additions
|
|
||||||
const homeContainer = document.getElementById("home-root");
|
const homeContainer = document.getElementById("home-root");
|
||||||
if (!homeContainer) return;
|
if (!homeContainer) return;
|
||||||
|
|
||||||
const skeletonStructure = stringToHTML(/* html */ `
|
const skeletonStructure = stringToHTML(/* html */`
|
||||||
<div class="home-container" id="home-container">
|
<div class="home-container" id="home-container">
|
||||||
<div class="border shortcut-container">
|
<div class="border shortcut-container">
|
||||||
<div class="border shortcuts" id="shortcuts"></div>
|
<div class="border shortcuts" id="shortcuts"></div>
|
||||||
@@ -90,10 +81,8 @@ export async function loadHomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>`);
|
</div>`);
|
||||||
|
|
||||||
// Add skeleton structure
|
|
||||||
homeContainer.appendChild(skeletonStructure.firstChild!);
|
homeContainer.appendChild(skeletonStructure.firstChild!);
|
||||||
|
|
||||||
// Run animations if enabled
|
|
||||||
if (settingsState.animations) {
|
if (settingsState.animations) {
|
||||||
animate(
|
animate(
|
||||||
".home-container > div",
|
".home-container > div",
|
||||||
@@ -108,24 +97,14 @@ export async function loadHomePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup event listeners with cleanup
|
|
||||||
const cleanup = setupTimetableListeners();
|
const cleanup = setupTimetableListeners();
|
||||||
|
|
||||||
// Initialize shortcuts immediately
|
renderShortcuts();
|
||||||
try {
|
|
||||||
addShortcuts(settingsState.shortcuts);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("[BetterSEQTA+] Error adding shortcuts:", err.message || err);
|
|
||||||
}
|
|
||||||
AddCustomShortcutsToPage();
|
|
||||||
|
|
||||||
// Get current date
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const TodayFormatted = formatDate(date);
|
const TodayFormatted = formatDate(date);
|
||||||
|
|
||||||
// Start all data fetching in parallel
|
|
||||||
const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [
|
const [timetablePromise, assessmentsPromise, classesPromise, prefsPromise] = [
|
||||||
// Timetable data
|
|
||||||
fetch(`${location.origin}/seqta/student/load/timetable?`, {
|
fetch(`${location.origin}/seqta/student/load/timetable?`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -136,13 +115,10 @@ export async function loadHomePage() {
|
|||||||
}),
|
}),
|
||||||
}).then((res) => res.json()),
|
}).then((res) => res.json()),
|
||||||
|
|
||||||
// Assessments data
|
|
||||||
GetUpcomingAssessments(),
|
GetUpcomingAssessments(),
|
||||||
|
|
||||||
// Classes data
|
|
||||||
GetActiveClasses(),
|
GetActiveClasses(),
|
||||||
|
|
||||||
// Preferences data
|
|
||||||
fetch(`${location.origin}/seqta/student/load/prefs?`, {
|
fetch(`${location.origin}/seqta/student/load/prefs?`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -150,7 +126,6 @@ export async function loadHomePage() {
|
|||||||
}).then((res) => res.json()),
|
}).then((res) => res.json()),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Process all data in parallel
|
|
||||||
const [timetableData, assessments, classes, prefs] = await Promise.all([
|
const [timetableData, assessments, classes, prefs] = await Promise.all([
|
||||||
timetablePromise,
|
timetablePromise,
|
||||||
assessmentsPromise,
|
assessmentsPromise,
|
||||||
@@ -158,7 +133,6 @@ export async function loadHomePage() {
|
|||||||
prefsPromise,
|
prefsPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Process timetable data
|
|
||||||
const dayContainer = document.getElementById("day-container");
|
const dayContainer = document.getElementById("day-container");
|
||||||
if (dayContainer && timetableData.payload.items.length > 0) {
|
if (dayContainer && timetableData.payload.items.length > 0) {
|
||||||
const lessonArray = timetableData.payload.items.sort((a: any, b: any) =>
|
const lessonArray = timetableData.payload.items.sort((a: any, b: any) =>
|
||||||
@@ -166,7 +140,6 @@ export async function loadHomePage() {
|
|||||||
);
|
);
|
||||||
const colours = await GetLessonColours();
|
const colours = await GetLessonColours();
|
||||||
|
|
||||||
// Process and display lessons
|
|
||||||
dayContainer.innerHTML = "";
|
dayContainer.innerHTML = "";
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
for (let i = 0; i < lessonArray.length; i++) {
|
||||||
const lesson = lessonArray[i];
|
const lesson = lessonArray[i];
|
||||||
@@ -198,7 +171,6 @@ export async function loadHomePage() {
|
|||||||
dayContainer.appendChild(div.firstChild!);
|
dayContainer.appendChild(div.firstChild!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check current lessons
|
|
||||||
if (currentSelectedDate.getDate() === date.getDate()) {
|
if (currentSelectedDate.getDate() === date.getDate()) {
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
for (let i = 0; i < lessonArray.length; i++) {
|
||||||
CheckCurrentLesson(lessonArray[i], i + 1);
|
CheckCurrentLesson(lessonArray[i], i + 1);
|
||||||
@@ -206,7 +178,7 @@ export async function loadHomePage() {
|
|||||||
CheckCurrentLessonAll(lessonArray);
|
CheckCurrentLessonAll(lessonArray);
|
||||||
}
|
}
|
||||||
} else if (dayContainer) {
|
} else if (dayContainer) {
|
||||||
dayContainer.innerHTML = /* html */ `
|
dayContainer.innerHTML = `
|
||||||
<div class="day-empty">
|
<div class="day-empty">
|
||||||
<img src="${browser.runtime.getURL(LogoLight)}" />
|
<img src="${browser.runtime.getURL(LogoLight)}" />
|
||||||
<p>No lessons available.</p>
|
<p>No lessons available.</p>
|
||||||
@@ -214,7 +186,6 @@ export async function loadHomePage() {
|
|||||||
}
|
}
|
||||||
dayContainer?.classList.remove("loading");
|
dayContainer?.classList.remove("loading");
|
||||||
|
|
||||||
// Process assessments data
|
|
||||||
const activeClass = classes.find((c: any) => c.hasOwnProperty("active"));
|
const activeClass = classes.find((c: any) => c.hasOwnProperty("active"));
|
||||||
const activeSubjects = activeClass?.subjects || [];
|
const activeSubjects = activeClass?.subjects || [];
|
||||||
const activeSubjectCodes = activeSubjects.map((s: any) => s.code);
|
const activeSubjectCodes = activeSubjects.map((s: any) => s.code);
|
||||||
@@ -228,7 +199,6 @@ export async function loadHomePage() {
|
|||||||
upcomingItems.classList.remove("loading");
|
upcomingItems.classList.remove("loading");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process notices data
|
|
||||||
const labelArray = prefs.payload
|
const labelArray = prefs.payload
|
||||||
.filter((item: any) => item.name === "notices.filters")
|
.filter((item: any) => item.name === "notices.filters")
|
||||||
.map((item: any) => item.value);
|
.map((item: any) => item.value);
|
||||||
@@ -273,6 +243,18 @@ function setupTimetableListeners() {
|
|||||||
const timetableForward = document.getElementById("home-timetable-forward");
|
const timetableForward = document.getElementById("home-timetable-forward");
|
||||||
|
|
||||||
function changeTimetable(value: number) {
|
function changeTimetable(value: number) {
|
||||||
|
if (loadingTimeout) {
|
||||||
|
clearTimeout(loadingTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingTimeout = setTimeout(() => {
|
||||||
|
const dayContainer = document.getElementById("day-container");
|
||||||
|
if (dayContainer) {
|
||||||
|
dayContainer.classList.add("loading");
|
||||||
|
dayContainer.innerHTML = "";
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
|
||||||
currentSelectedDate.setDate(currentSelectedDate.getDate() + value);
|
currentSelectedDate.setDate(currentSelectedDate.getDate() + value);
|
||||||
const formattedDate = formatDate(currentSelectedDate);
|
const formattedDate = formatDate(currentSelectedDate);
|
||||||
callHomeTimetable(formattedDate, true);
|
callHomeTimetable(formattedDate, true);
|
||||||
@@ -328,6 +310,11 @@ function setupNotices(labelArray: string[], date: string) {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
|
||||||
const fetchNotices = async (date: string) => {
|
const fetchNotices = async (date: string) => {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (settingsState.mockNotices) {
|
||||||
|
data = getMockNotices();
|
||||||
|
} else {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${location.origin}/seqta/student/load/notices?`,
|
`${location.origin}/seqta/student/load/notices?`,
|
||||||
{
|
{
|
||||||
@@ -336,11 +323,12 @@ function setupNotices(labelArray: string[], date: string) {
|
|||||||
body: JSON.stringify({ date }),
|
body: JSON.stringify({ date }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
processNotices(data, labelArray);
|
processNotices(data, labelArray);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Debounce the input handler
|
|
||||||
const debouncedInputChange = debounce((e: Event) => {
|
const debouncedInputChange = debounce((e: Event) => {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
fetchNotices(target.value);
|
fetchNotices(target.value);
|
||||||
@@ -373,21 +361,11 @@ function comparedate(obj1: any, obj2: any) {
|
|||||||
return 0;
|
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[]) {
|
function processNotices(response: any, labelArray: string[]) {
|
||||||
const NoticeContainer = document.getElementById("notice-container");
|
const NoticeContainer = document.getElementById("notice-container");
|
||||||
if (!NoticeContainer) return;
|
if (!NoticeContainer) return;
|
||||||
|
|
||||||
// Clear existing notices
|
|
||||||
NoticeContainer.innerHTML = "";
|
NoticeContainer.innerHTML = "";
|
||||||
|
|
||||||
const notices = response.payload;
|
const notices = response.payload;
|
||||||
@@ -399,19 +377,20 @@ function processNotices(response: any, labelArray: string[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create document fragment for batch DOM updates
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
// Process notices in batch
|
|
||||||
notices.forEach((notice: any) => {
|
notices.forEach((notice: any) => {
|
||||||
if (labelArray.includes(JSON.stringify(notice.label))) {
|
const shouldInclude =
|
||||||
|
settingsState.mockNotices ||
|
||||||
|
labelArray.includes(JSON.stringify(notice.label));
|
||||||
|
|
||||||
|
if (shouldInclude) {
|
||||||
const colour = processNoticeColor(notice.colour);
|
const colour = processNoticeColor(notice.colour);
|
||||||
const noticeElement = createNoticeElement(notice, colour);
|
const noticeElement = createNoticeElement(notice, colour);
|
||||||
fragment.appendChild(noticeElement);
|
fragment.appendChild(noticeElement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Single DOM update
|
|
||||||
NoticeContainer.appendChild(fragment);
|
NoticeContainer.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,36 +405,331 @@ function processNoticeColor(colour: string): string | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createNoticeElement(notice: any, colour: string | undefined): Node {
|
function createNoticeElement(notice: any, colour: string | undefined): Node {
|
||||||
|
const textPreview = notice.contents
|
||||||
|
.replace(/<[^>]*>/g, "")
|
||||||
|
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||||
|
.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 = `
|
const htmlContent = `
|
||||||
<div class="notice" style="--colour: ${colour}">
|
<div class="notice-unified-content notice-card-state" data-notice-id="${noticeId}" style="--colour: ${colour || "#8e8e8e"}; position: relative; background: var(--background-primary); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);">
|
||||||
<h3 style="color:var(--colour)">${notice.title}</h3>
|
<div class="notice-header">
|
||||||
${notice.label_title !== undefined ? `<h5 style="color:var(--colour)">${notice.label_title}</h5>` : ""}
|
<div class="notice-badge-row">
|
||||||
<h6 style="color:var(--colour)">${notice.staff}</h6>
|
<span class="notice-badge" style="background: linear-gradient(135deg, ${colour || "#8e8e8e"}, ${colour || "#8e8e8e"}dd); color: white;">
|
||||||
${notice.contents.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "").replace(/ +/, " ")}
|
${notice.label_title || "General"}
|
||||||
<div class="colourbar" style="background: var(--colour)"></div>
|
</span>
|
||||||
|
<span class="notice-staff">${notice.staff}</span>
|
||||||
|
</div>
|
||||||
|
<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">${textPreview}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const element = stringToHTML(htmlContent).firstChild;
|
const element = stringToHTML(htmlContent).firstChild as HTMLElement;
|
||||||
if (element instanceof HTMLElement) {
|
if (element) {
|
||||||
element.style.setProperty("--colour", colour ?? "");
|
element.addEventListener("click", () =>
|
||||||
|
openNoticeModal(notice, colour, element),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return element!;
|
return element!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openNoticeModal(
|
||||||
|
notice: any,
|
||||||
|
colour: string | undefined,
|
||||||
|
sourceElement: HTMLElement,
|
||||||
|
) {
|
||||||
|
const cleanContent = notice.contents
|
||||||
|
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
||||||
|
.replace(/ +/, " ");
|
||||||
|
|
||||||
|
const existingModal = document.getElementById("notice-modal");
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.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;
|
||||||
|
let sourceHeight = sourceRect.height;
|
||||||
|
|
||||||
|
const modalHtml = `
|
||||||
|
<div id="notice-modal" class="notice-modal-overlay" style="opacity: 0;">
|
||||||
|
<div class="notice-modal-transition" style="
|
||||||
|
position: fixed;
|
||||||
|
left: ${sourceLeft + scrollX}px;
|
||||||
|
top: ${sourceTop + scrollY}px;
|
||||||
|
width: ${sourceWidth}px;
|
||||||
|
height: ${sourceHeight}px;
|
||||||
|
transform-origin: center;
|
||||||
|
z-index: 10001;
|
||||||
|
">
|
||||||
|
<div class="notice-modal-content notice-transitioning">
|
||||||
|
<div class="notice-unified-content notice-card-state">
|
||||||
|
<div class="notice-header">
|
||||||
|
<div class="notice-badge-row">
|
||||||
|
<span class="notice-badge" style="background: linear-gradient(135deg, ${colour || "#8e8e8e"}, ${colour || "#8e8e8e"}dd); color: white;">
|
||||||
|
${notice.label_title || "General"}
|
||||||
|
</span>
|
||||||
|
<span class="notice-staff">${notice.staff}</span>
|
||||||
|
</div>
|
||||||
|
<button class="notice-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<h2 class="notice-content-title">${notice.title}</h2>
|
||||||
|
<div class="notice-content-body">${cleanContent}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const modal = stringToHTML(modalHtml).firstChild as HTMLElement;
|
||||||
|
const transitionContainer = modal.querySelector(
|
||||||
|
".notice-modal-transition",
|
||||||
|
) as HTMLElement;
|
||||||
|
const unifiedContent = modal.querySelector(
|
||||||
|
".notice-unified-content",
|
||||||
|
) as HTMLElement;
|
||||||
|
const closeBtn = modal.querySelector(".notice-close-btn") as HTMLElement;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
sourceElement.setAttribute("data-transitioning", "true");
|
||||||
|
sourceElement.style.opacity = "0";
|
||||||
|
sourceElement.style.transform = "scale(0.95)";
|
||||||
|
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
let targetWidth = Math.round(
|
||||||
|
Math.min(Math.max(sourceWidth, 800), viewportWidth - 40),
|
||||||
|
);
|
||||||
|
|
||||||
|
const tempMeasureDiv = document.createElement("div");
|
||||||
|
tempMeasureDiv.style.position = "absolute";
|
||||||
|
tempMeasureDiv.style.left = "-9999px";
|
||||||
|
tempMeasureDiv.style.width = targetWidth + "px";
|
||||||
|
tempMeasureDiv.style.visibility = "hidden";
|
||||||
|
tempMeasureDiv.innerHTML = `
|
||||||
|
<div class="notice-unified-content notice-modal-state" style="position: relative; width: 100%; padding: 16px; border: 1px solid rgba(255, 255, 255, 0.1);">
|
||||||
|
<div class="notice-header">
|
||||||
|
<div class="notice-badge-row">
|
||||||
|
<span class="notice-badge">${notice.label_title || "General"}</span>
|
||||||
|
<span class="notice-staff">${notice.staff}</span>
|
||||||
|
</div>
|
||||||
|
<button class="notice-close-btn">×</button>
|
||||||
|
</div>
|
||||||
|
<h2 class="notice-content-title">${notice.title}</h2>
|
||||||
|
<div class="notice-content-body">${cleanContent}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(tempMeasureDiv);
|
||||||
|
const measuredHeight =
|
||||||
|
tempMeasureDiv.firstElementChild!.getBoundingClientRect().height;
|
||||||
|
document.body.removeChild(tempMeasureDiv);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
|
||||||
|
if (!settingsState.animations) {
|
||||||
|
modal.remove();
|
||||||
|
sourceElement.style.opacity = "1";
|
||||||
|
sourceElement.style.transform = "";
|
||||||
|
sourceElement.removeAttribute("data-transitioning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
animate(
|
||||||
|
modal,
|
||||||
|
{
|
||||||
|
backgroundColor: ["rgba(0, 0, 0, 0.5)", "rgba(0, 0, 0, 0)"],
|
||||||
|
backdropFilter: ["blur(4px)", "blur(0px)"],
|
||||||
|
},
|
||||||
|
{ duration: 0.2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
animate(
|
||||||
|
transitionContainer,
|
||||||
|
{ opacity: [1, 0] },
|
||||||
|
{ duration: 0.2, delay: 0.3 },
|
||||||
|
);
|
||||||
|
|
||||||
|
sourceElement.style.opacity = "1";
|
||||||
|
sourceElement.style.transform = "";
|
||||||
|
|
||||||
|
modal.style.pointerEvents = "none";
|
||||||
|
|
||||||
|
animate(
|
||||||
|
transitionContainer,
|
||||||
|
{
|
||||||
|
left: [targetLeft + scrollX, sourceLeft + scrollX],
|
||||||
|
top: [targetTop, sourceTop + scrollY],
|
||||||
|
width: [targetWidth, sourceWidth],
|
||||||
|
height: [targetHeight, sourceHeight],
|
||||||
|
scale: [1, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 0.35,
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 400,
|
||||||
|
damping: 35,
|
||||||
|
},
|
||||||
|
).finished.then(async () => {
|
||||||
|
modal.remove();
|
||||||
|
sourceElement.removeAttribute("data-transitioning");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
closeBtn?.addEventListener("click", closeModal);
|
||||||
|
modal?.addEventListener("click", (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
closeModal();
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const newSourceRect = sourceElement.getBoundingClientRect();
|
||||||
|
const newScrollY = Math.round(window.scrollY);
|
||||||
|
const newScrollX = Math.round(window.scrollX);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (transform && transform !== "none") {
|
||||||
|
const matrix = transform.match(/matrix.*\((.+)\)/);
|
||||||
|
if (matrix) {
|
||||||
|
const values = matrix[1].split(", ");
|
||||||
|
scaleX = parseFloat(values[0]);
|
||||||
|
scaleY = parseFloat(values[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply inverse scale to get true original dimensions and positions
|
||||||
|
const newSourceWidth = newSourceRect.width / scaleX;
|
||||||
|
const newSourceHeight = newSourceRect.height / scaleY;
|
||||||
|
|
||||||
|
// Calculate position shift due to center-based scaling
|
||||||
|
const deltaX = (newSourceWidth - newSourceRect.width) / 2;
|
||||||
|
const deltaY = (newSourceHeight - newSourceRect.height) / 2;
|
||||||
|
|
||||||
|
const newSourceLeft = newSourceRect.left - deltaX;
|
||||||
|
const newSourceTop = newSourceRect.top - deltaY;
|
||||||
|
|
||||||
|
const newViewportWidth = window.innerWidth;
|
||||||
|
const newViewportHeight = window.innerHeight;
|
||||||
|
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;
|
||||||
|
|
||||||
|
transitionContainer.style.left =
|
||||||
|
Math.round(newTargetLeft + newScrollX) + "px";
|
||||||
|
transitionContainer.style.top = Math.round(newTargetTop) + "px";
|
||||||
|
transitionContainer.style.width = Math.round(newTargetWidth) + "px";
|
||||||
|
transitionContainer.style.height = Math.round(newTargetHeight) + "px";
|
||||||
|
|
||||||
|
sourceLeft = newSourceLeft;
|
||||||
|
sourceTop = newSourceTop;
|
||||||
|
sourceWidth = newSourceWidth;
|
||||||
|
sourceHeight = newSourceHeight;
|
||||||
|
targetLeft = newTargetLeft;
|
||||||
|
targetTop = newTargetTop;
|
||||||
|
targetWidth = newTargetWidth;
|
||||||
|
targetHeight = newTargetHeight;
|
||||||
|
scrollY = newScrollY;
|
||||||
|
scrollX = newScrollX;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
if (settingsState.animations) {
|
||||||
|
animate(modal, { opacity: [0, 1] }, { duration: 0.2 });
|
||||||
|
|
||||||
|
animate(
|
||||||
|
transitionContainer,
|
||||||
|
{
|
||||||
|
left: [sourceLeft + scrollX, targetLeft + scrollX],
|
||||||
|
top: [sourceTop + scrollY, targetTop],
|
||||||
|
width: [sourceWidth, targetWidth],
|
||||||
|
height: [sourceHeight, targetHeight],
|
||||||
|
scale: [1, 1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration: 0.5,
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 280,
|
||||||
|
damping: 24,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
unifiedContent.classList.remove("notice-card-state");
|
||||||
|
unifiedContent.classList.add("notice-modal-state");
|
||||||
|
} else {
|
||||||
|
modal.style.opacity = "1";
|
||||||
|
transitionContainer.style.left = Math.round(targetLeft + scrollX) + "px";
|
||||||
|
transitionContainer.style.top = Math.round(targetTop) + "px";
|
||||||
|
transitionContainer.style.width = Math.round(targetWidth) + "px";
|
||||||
|
transitionContainer.style.height = Math.round(targetHeight) + "px";
|
||||||
|
unifiedContent.classList.remove("notice-card-state");
|
||||||
|
unifiedContent.classList.add("notice-modal-state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function callHomeTimetable(date: string, change?: any) {
|
function callHomeTimetable(date: string, change?: any) {
|
||||||
// Creates a HTTP Post Request to the SEQTA page for the students timetable
|
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", `${location.origin}/seqta/student/load/timetable?`, true);
|
xhr.open("POST", `${location.origin}/seqta/student/load/timetable?`, true);
|
||||||
// Sets the response type to json
|
|
||||||
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
|
xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8");
|
||||||
|
|
||||||
xhr.onreadystatechange = function () {
|
xhr.onreadystatechange = function () {
|
||||||
// Once the response is ready
|
|
||||||
if (xhr.readyState === 4) {
|
if (xhr.readyState === 4) {
|
||||||
|
if (loadingTimeout) {
|
||||||
|
clearTimeout(loadingTimeout);
|
||||||
|
loadingTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DayContainer = document.getElementById("day-container")!;
|
||||||
|
|
||||||
|
try {
|
||||||
var serverResponse = JSON.parse(xhr.response);
|
var serverResponse = JSON.parse(xhr.response);
|
||||||
let lessonArray: Array<any> = [];
|
let lessonArray: Array<any> = [];
|
||||||
const DayContainer = document.getElementById("day-container")!;
|
|
||||||
// If items in response:
|
|
||||||
if (serverResponse.payload.items.length > 0) {
|
if (serverResponse.payload.items.length > 0) {
|
||||||
if (DayContainer.innerText || change) {
|
if (DayContainer.innerText || change) {
|
||||||
for (let i = 0; i < serverResponse.payload.items.length; i++) {
|
for (let i = 0; i < serverResponse.payload.items.length; i++) {
|
||||||
@@ -464,8 +738,7 @@ function callHomeTimetable(date: string, change?: any) {
|
|||||||
lessonArray.sort(function (a, b) {
|
lessonArray.sort(function (a, b) {
|
||||||
return a.from.localeCompare(b.from);
|
return a.from.localeCompare(b.from);
|
||||||
});
|
});
|
||||||
// If items in the response, set each corresponding value into divs
|
|
||||||
// lessonArray = lessonArray.splice(1)
|
|
||||||
GetLessonColours().then((colours) => {
|
GetLessonColours().then((colours) => {
|
||||||
let subjects = colours;
|
let subjects = colours;
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
for (let i = 0; i < lessonArray.length; i++) {
|
||||||
@@ -484,7 +757,7 @@ function callHomeTimetable(date: string, change?: any) {
|
|||||||
lessonArray[i].invert = true;
|
lessonArray[i].invert = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Removes seconds from the start and end times
|
|
||||||
lessonArray[i].from = lessonArray[i].from.substring(0, 5);
|
lessonArray[i].from = lessonArray[i].from.substring(0, 5);
|
||||||
lessonArray[i].until = lessonArray[i].until.substring(0, 5);
|
lessonArray[i].until = lessonArray[i].until.substring(0, 5);
|
||||||
|
|
||||||
@@ -497,16 +770,15 @@ function callHomeTimetable(date: string, change?: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if attendance is unmarked, and sets the string to " ".
|
|
||||||
lessonArray[i].attendanceTitle = CheckUnmarkedAttendance(
|
lessonArray[i].attendanceTitle = CheckUnmarkedAttendance(
|
||||||
lessonArray[i].attendance,
|
lessonArray[i].attendance,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// If on home page, apply each lesson to HTML with information in each div
|
|
||||||
DayContainer.innerText = "";
|
DayContainer.innerText = "";
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
for (let i = 0; i < lessonArray.length; i++) {
|
||||||
var div = makeLessonDiv(lessonArray[i], i + 1);
|
var div = makeLessonDiv(lessonArray[i], i + 1);
|
||||||
// Append each of the lessons into the day-container
|
|
||||||
if (lessonArray[i].invert) {
|
if (lessonArray[i].invert) {
|
||||||
const div1 = div.firstChild! as HTMLElement;
|
const div1 = div.firstChild! as HTMLElement;
|
||||||
div1.classList.add("day-inverted");
|
div1.classList.add("day-inverted");
|
||||||
@@ -515,12 +787,14 @@ function callHomeTimetable(date: string, change?: any) {
|
|||||||
DayContainer.append(div.firstChild as HTMLElement);
|
DayContainer.append(div.firstChild as HTMLElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DayContainer.classList.remove("loading");
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
if (currentSelectedDate.getDate() == today.getDate()) {
|
if (currentSelectedDate.getDate() == today.getDate()) {
|
||||||
for (let i = 0; i < lessonArray.length; i++) {
|
for (let i = 0; i < lessonArray.length; i++) {
|
||||||
CheckCurrentLesson(lessonArray[i], i + 1);
|
CheckCurrentLesson(lessonArray[i], i + 1);
|
||||||
}
|
}
|
||||||
// For each lesson, check the start and end times
|
|
||||||
CheckCurrentLessonAll(lessonArray);
|
CheckCurrentLessonAll(lessonArray);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -536,22 +810,36 @@ function callHomeTimetable(date: string, change?: any) {
|
|||||||
dummyDay.append(img);
|
dummyDay.append(img);
|
||||||
dummyDay.append(text);
|
dummyDay.append(text);
|
||||||
DayContainer.append(dummyDay);
|
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(
|
xhr.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
// Information sent to SEQTA page as a request with the dates and student number
|
|
||||||
from: date,
|
from: date,
|
||||||
until: date,
|
until: date,
|
||||||
// Funny number
|
|
||||||
student: 69,
|
student: 69,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CheckCurrentLessonAll(lessons: any) {
|
function CheckCurrentLessonAll(lessons: any) {
|
||||||
// Checks each lesson and sets an interval to run every 60 seconds to continue updating
|
|
||||||
LessonInterval = setInterval(
|
LessonInterval = setInterval(
|
||||||
function () {
|
function () {
|
||||||
for (let i = 0; i < lessons.length; i++) {
|
for (let i = 0; i < lessons.length; i++) {
|
||||||
@@ -573,7 +861,6 @@ async function CheckCurrentLesson(lesson: any, num: number) {
|
|||||||
} = lesson;
|
} = lesson;
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
|
|
||||||
// Create Date objects for start and end times
|
|
||||||
const [startHour, startMinute] = startTime.split(":").map(Number);
|
const [startHour, startMinute] = startTime.split(":").map(Number);
|
||||||
const [endHour, endMinute] = endTime.split(":").map(Number);
|
const [endHour, endMinute] = endTime.split(":").map(Number);
|
||||||
|
|
||||||
@@ -583,7 +870,6 @@ async function CheckCurrentLesson(lesson: any, num: number) {
|
|||||||
const endDate = new Date(currentDate);
|
const endDate = new Date(currentDate);
|
||||||
endDate.setHours(endHour, endMinute, 0);
|
endDate.setHours(endHour, endMinute, 0);
|
||||||
|
|
||||||
// Check if the current time is within the lesson time range
|
|
||||||
const isValidTime = startDate < currentDate && endDate > currentDate;
|
const isValidTime = startDate < currentDate && endDate > currentDate;
|
||||||
|
|
||||||
const elementId = `${code}${num}`;
|
const elementId = `${code}${num}`;
|
||||||
@@ -646,8 +932,7 @@ function makeLessonDiv(lesson: any, num: number) {
|
|||||||
assessments,
|
assessments,
|
||||||
} = lesson;
|
} = lesson;
|
||||||
|
|
||||||
// Construct the base lesson string with default values using ternary operators
|
let lessonString = `
|
||||||
let lessonString = /* html */ `
|
|
||||||
<div class="day" id="${code + num}" style="${colour}">
|
<div class="day" id="${code + num}" style="${colour}">
|
||||||
<h2>${description || "Unknown"}</h2>
|
<h2>${description || "Unknown"}</h2>
|
||||||
<h3>${staff || "Unknown"}</h3>
|
<h3>${staff || "Unknown"}</h3>
|
||||||
@@ -656,15 +941,13 @@ function makeLessonDiv(lesson: any, num: number) {
|
|||||||
<h5>${attendanceTitle || "Unknown"}</h5>
|
<h5>${attendanceTitle || "Unknown"}</h5>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add buttons for assessments and courses if applicable
|
|
||||||
if (programmeID !== 0) {
|
if (programmeID !== 0) {
|
||||||
lessonString += /* html */ `
|
lessonString += `
|
||||||
<div class="day-button clickable" style="right: 5px;" onclick="location.href='${buildAssessmentURL(programmeID, metaID)}'">${assessmentsicon}</div>
|
<div class="day-button clickable" style="right: 5px;" onclick="location.href='${buildAssessmentURL(programmeID, metaID)}'">${assessmentsicon}</div>
|
||||||
<div class="day-button clickable" style="right: 35px;" onclick="location.href='../#?page=/courses/${programmeID}:${metaID}'">${coursesicon}</div>
|
<div class="day-button clickable" style="right: 35px;" onclick="location.href='../#?page=/courses/${programmeID}:${metaID}'">${coursesicon}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add assessments if they exist
|
|
||||||
if (assessments && assessments.length > 0) {
|
if (assessments && assessments.length > 0) {
|
||||||
const assessmentString = assessments
|
const assessmentString = assessments
|
||||||
.map(
|
.map(
|
||||||
@@ -673,8 +956,8 @@ function makeLessonDiv(lesson: any, num: number) {
|
|||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
lessonString += /* html */ `
|
lessonString += `
|
||||||
<div class="tooltip assessmenttooltip">
|
<div class="fixed-tooltip assessmenttooltip">
|
||||||
<svg style="width:28px;height:28px;border-radius:0;" viewBox="0 0 24 24">
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -684,8 +967,9 @@ function makeLessonDiv(lesson: any, num: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lessonString += "</div>";
|
lessonString += "</div>";
|
||||||
|
const element = stringToHTML(lessonString);
|
||||||
return stringToHTML(lessonString);
|
setupFixedTooltips(element);
|
||||||
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") {
|
function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") {
|
||||||
@@ -711,7 +995,6 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
|
|
||||||
var Today = new Date();
|
var Today = new Date();
|
||||||
|
|
||||||
// Removes overdue assessments from the upcoming assessments array and pushes to overdue array
|
|
||||||
for (let i = 0; i < assessments.length; i++) {
|
for (let i = 0; i < assessments.length; i++) {
|
||||||
const assessment = assessments[i];
|
const assessment = assessments[i];
|
||||||
let assessmentdue = new Date(assessment.due);
|
let assessmentdue = new Date(assessment.due);
|
||||||
@@ -741,7 +1024,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
assessments[i].colour = "--item-colour: #8e8e8e;";
|
assessments[i].colour = "--item-colour: #8e8e8e;";
|
||||||
} else {
|
} else {
|
||||||
assessments[i].colour = `--item-colour: ${subject.value};`;
|
assessments[i].colour = `--item-colour: ${subject.value};`;
|
||||||
GetThresholdOfColor(subject.value); // result (originally) result = GetThresholdOfColor
|
GetThresholdOfColor(subject.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,9 +1045,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
|
|
||||||
CreateFilters(activeSubjects);
|
CreateFilters(activeSubjects);
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
let type;
|
let type;
|
||||||
// @ts-ignore
|
|
||||||
let class_;
|
let class_;
|
||||||
|
|
||||||
for (let i = 0; i < assessments.length; i++) {
|
for (let i = 0; i < assessments.length; i++) {
|
||||||
@@ -772,10 +1053,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
if (!upcomingDates[element.due as keyof typeof upcomingDates]) {
|
if (!upcomingDates[element.due as keyof typeof upcomingDates]) {
|
||||||
let dateObj: any = new Object();
|
let dateObj: any = new Object();
|
||||||
dateObj.div = CreateElement(
|
dateObj.div = CreateElement(
|
||||||
// TODO: not sure whats going on here?
|
|
||||||
// eslint-disable-next-line
|
|
||||||
(type = "div"),
|
(type = "div"),
|
||||||
// eslint-disable-next-line
|
|
||||||
(class_ = "upcoming-date-container"),
|
(class_ = "upcoming-date-container"),
|
||||||
);
|
);
|
||||||
dateObj.assessments = [];
|
dateObj.assessments = [];
|
||||||
@@ -804,7 +1082,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
assessmentDate = createAssessmentDateDiv(
|
assessmentDate = createAssessmentDateDiv(
|
||||||
date,
|
date,
|
||||||
upcomingDates[date as keyof typeof upcomingDates],
|
upcomingDates[date as keyof typeof upcomingDates],
|
||||||
// eslint-disable-next-line
|
|
||||||
datecase,
|
datecase,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -824,6 +1102,14 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FilterUpcomingAssessments(settingsState.subjectfilters);
|
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) {
|
function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
|
||||||
@@ -909,9 +1195,6 @@ function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
|
|||||||
if (response.payload.length > 0) {
|
if (response.payload.length > 0) {
|
||||||
const assessment = document.querySelector(`#assessment${element.id}`);
|
const assessment = document.querySelector(`#assessment${element.id}`);
|
||||||
|
|
||||||
// ticksvg = stringToHTML(`<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="var(--item-colour)" viewBox="0 0 24 24"><path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"/></svg>`).firstChild
|
|
||||||
// ticksvg.classList.add('upcoming-tick')
|
|
||||||
// assessment.append(ticksvg)
|
|
||||||
let submittedtext = document.createElement("div");
|
let submittedtext = document.createElement("div");
|
||||||
submittedtext.classList.add("upcoming-submittedtext");
|
submittedtext.classList.add("upcoming-submittedtext");
|
||||||
submittedtext.innerText = "Submitted";
|
submittedtext.innerText = "Submitted";
|
||||||
@@ -968,7 +1251,7 @@ function CreateFilters(subjects: any) {
|
|||||||
let filterdiv = document.querySelector("#upcoming-filters");
|
let filterdiv = document.querySelector("#upcoming-filters");
|
||||||
for (let i = 0; i < subjects.length; i++) {
|
for (let i = 0; i < subjects.length; i++) {
|
||||||
const element = subjects[i];
|
const element = subjects[i];
|
||||||
// eslint-disable-next-line
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) {
|
if (!Object.prototype.hasOwnProperty.call(filteroptions, element.code)) {
|
||||||
filteroptions[element.code] = true;
|
filteroptions[element.code] = true;
|
||||||
settingsState.subjectfilters = filteroptions;
|
settingsState.subjectfilters = filteroptions;
|
||||||
|
|||||||
@@ -1,107 +1,68 @@
|
|||||||
import stringToHTML from "../stringToHTML";
|
import stringToHTML from "../stringToHTML";
|
||||||
import browser from "webextension-polyfill";
|
|
||||||
import { settingsState } from "../listeners/SettingsState";
|
import { settingsState } from "../listeners/SettingsState";
|
||||||
import { animate, stagger } from "motion";
|
import { openPopup } from "./PopupManager";
|
||||||
import { DeleteWhatsNew } from "../Whatsnew";
|
|
||||||
|
|
||||||
export function OpenAboutPage() {
|
export function OpenAboutPage() {
|
||||||
const background = document.createElement("div");
|
const header = stringToHTML(
|
||||||
background.id = "whatsnewbk";
|
|
||||||
background.classList.add("whatsnewBackground");
|
|
||||||
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.classList.add("whatsnewContainer");
|
|
||||||
|
|
||||||
var header: any = stringToHTML(
|
|
||||||
/* html */
|
/* html */
|
||||||
`<div class="whatsnewHeader">
|
`<div class="whatsnewHeader">
|
||||||
<h1>About</h1>
|
<h1>About</h1>
|
||||||
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
|
<p>About the extension</p>
|
||||||
</div>`,
|
</div>`,
|
||||||
).firstChild;
|
).firstChild as HTMLElement;
|
||||||
|
|
||||||
let text = stringToHTML(/* html */ `
|
const text = stringToHTML(/* html */ `
|
||||||
<div class="whatsnewTextContainer" style="overflow-y: scroll;">
|
<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" />
|
<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>
|
<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>
|
||||||
<p>We are currently working on fixing bugs and adding useful features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below). We are always looking for more contributors!</p>
|
<p>We are currently working on fixing bugs and adding useful features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below). We are always looking for more contributors!</p>
|
||||||
<h1>Credits:</h1>
|
<h1>Credits:</h1>
|
||||||
<p>Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph, SethBurkart123 and other contributors.</p>
|
<p style="margin: 0;">
|
||||||
|
Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph, SethBurkart123, and other contributors.
|
||||||
|
</p>
|
||||||
|
<h1 style="text-align: left; font-weight: bold; margin: 0;">
|
||||||
|
All Contributors:
|
||||||
|
</h1>
|
||||||
|
<div style="max-width: 600px; margin: auto;">
|
||||||
|
<img
|
||||||
|
src="https://contrib.rocks/image?repo=BetterSEQTA/BetterSEQTA-Plus&columns=14"
|
||||||
|
style="width: 100%; max-width: 500px; height: auto; object-fit: contain; display: block; margin: -110px auto 0;">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild;
|
`).firstChild as HTMLElement;
|
||||||
|
|
||||||
let footer = stringToHTML(/* html */ `
|
const footer = stringToHTML(/* html */ `
|
||||||
<div class="whatsnewFooter">
|
<div class="whatsnewFooter">
|
||||||
<div>
|
<div>
|
||||||
Report bugs and feedback:
|
Resources and Feedback:
|
||||||
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding:0;">
|
<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" xmlns:xlink="http://www.w3.org/1999/xlink" width="25px" height="25px" viewBox="0 0 256 250" version="1.1" preserveAspectRatio="xMidYMid">
|
<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>
|
<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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding:0;">
|
<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" viewBox="0 0 24 24">
|
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
|
||||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0;">
|
|
||||||
<svg style="width: 25px; height: 25px;" viewBox="0 0 16 16">
|
|
||||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
|
<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>
|
</svg>
|
||||||
</a>
|
</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;
|
`).firstChild as HTMLElement;
|
||||||
|
|
||||||
let exitbutton = document.createElement("div");
|
openPopup({
|
||||||
exitbutton.id = "whatsnewclosebutton";
|
header,
|
||||||
|
content: [text, footer],
|
||||||
container.append(header);
|
|
||||||
container.append(text as ChildNode);
|
|
||||||
container.append(footer as ChildNode);
|
|
||||||
container.append(exitbutton);
|
|
||||||
|
|
||||||
background.append(container);
|
|
||||||
|
|
||||||
document.getElementById("container")!.append(background);
|
|
||||||
|
|
||||||
let bkelement = document.getElementById("whatsnewbk");
|
|
||||||
let popup = document.getElementsByClassName("whatsnewContainer")[0];
|
|
||||||
|
|
||||||
if (settingsState.animations) {
|
|
||||||
animate(
|
|
||||||
[popup, bkelement as HTMLElement],
|
|
||||||
{ scale: [0, 1] },
|
|
||||||
{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 220,
|
|
||||||
damping: 18,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
animate(
|
|
||||||
".whatsnewTextContainer *",
|
|
||||||
{ opacity: [0, 1], y: [10, 0] },
|
|
||||||
{
|
|
||||||
delay: stagger(0.05, { startDelay: 0.1 }),
|
|
||||||
duration: 0.5,
|
|
||||||
ease: [0.22, 0.03, 0.26, 1],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete settingsState.justupdated;
|
|
||||||
|
|
||||||
bkelement!.addEventListener("click", function (event) {
|
|
||||||
// Check if the click event originated from the element itself and not any of its children
|
|
||||||
if (event.target === bkelement) {
|
|
||||||
DeleteWhatsNew();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var closeelement = document.getElementById("whatsnewclosebutton");
|
|
||||||
closeelement!.addEventListener("click", function () {
|
|
||||||
DeleteWhatsNew();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,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 stringToHTML from "../stringToHTML";
|
||||||
import { animate, stagger } from "motion";
|
|
||||||
import stringToHTML from "./stringToHTML";
|
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import kofi from "@/resources/kofi.png?base64";
|
import kofi from "@/resources/kofi.png?base64";
|
||||||
|
import { openPopup } from "./PopupManager";
|
||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OpenWhatsNewPopup() {
|
export function OpenWhatsNewPopup() {
|
||||||
const background = document.createElement("div");
|
const header = stringToHTML(
|
||||||
background.id = "whatsnewbk";
|
|
||||||
background.classList.add("whatsnewBackground");
|
|
||||||
|
|
||||||
const container = document.createElement("div");
|
|
||||||
container.classList.add("whatsnewContainer");
|
|
||||||
|
|
||||||
var header: any = stringToHTML(
|
|
||||||
/* html */
|
/* html */
|
||||||
`<div class="whatsnewHeader">
|
`<div class="whatsnewHeader">
|
||||||
<h1>What's New</h1>
|
<h1>What's New</h1>
|
||||||
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
|
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
|
||||||
</div>`,
|
</div>`,
|
||||||
).firstChild;
|
).firstChild as HTMLElement;
|
||||||
|
|
||||||
let imagecont = document.createElement("div");
|
const imageContainer = document.createElement("div");
|
||||||
imagecont.classList.add("whatsnewImgContainer");
|
imageContainer.classList.add("whatsnewImgContainer");
|
||||||
|
|
||||||
let video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
let source = document.createElement("source");
|
const source = document.createElement("source");
|
||||||
|
|
||||||
source.setAttribute(
|
source.setAttribute(
|
||||||
"src",
|
"src",
|
||||||
@@ -53,13 +27,70 @@ export function OpenWhatsNewPopup() {
|
|||||||
video.loop = true;
|
video.loop = true;
|
||||||
video.appendChild(source);
|
video.appendChild(source);
|
||||||
video.classList.add("whatsnewImg");
|
video.classList.add("whatsnewImg");
|
||||||
imagecont.appendChild(video);
|
imageContainer.appendChild(video);
|
||||||
|
|
||||||
let textcontainer = document.createElement("div");
|
const text = stringToHTML(/* html */ `
|
||||||
textcontainer.classList.add("whatsnewTextContainer");
|
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<h1>3.4.7 - Global Search</h1>
|
||||||
|
<li>Added a new global search bar (enable in settings)
|
||||||
|
<span class="beta">beta</span>
|
||||||
|
</li>
|
||||||
|
<li>Fixed news feed not loading</li>
|
||||||
|
<li>Style changes and improvements</li>
|
||||||
|
<li>Other bug fixes</li>
|
||||||
|
|
||||||
let text = stringToHTML(/* html */ `
|
|
||||||
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: scroll;">
|
|
||||||
<h1>3.4.6.1 - Hot patch!</h1>
|
<h1>3.4.6.1 - Hot patch!</h1>
|
||||||
<li>Fixed storage not updating and sometimes being replaced with default values</li>
|
<li>Fixed storage not updating and sometimes being replaced with default values</li>
|
||||||
|
|
||||||
@@ -238,87 +269,45 @@ export function OpenWhatsNewPopup() {
|
|||||||
<h1>Create Custom Shortcuts</h1>
|
<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>
|
<li>Found in the BetterSEQTA+ Settings menu, custom shortcuts can now be created with a name and URL of your choice.</li>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild;
|
`).firstChild as HTMLElement;
|
||||||
|
|
||||||
let footer = stringToHTML(/* html */ `
|
const footer = stringToHTML(/* html */ `
|
||||||
<div class="whatsnewFooter">
|
<div class="whatsnewFooter">
|
||||||
<div>
|
<div>
|
||||||
Report bugs and feedback:
|
Resources and Feedback:
|
||||||
<a class="socials" href="https://github.com/BetterSEQTA/BetterSEQTA-Plus" style="background: none !important; margin: 0 5px; padding:0;">
|
<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" xmlns:xlink="http://www.w3.org/1999/xlink" width="25px" height="25px" viewBox="0 0 256 250" version="1.1" preserveAspectRatio="xMidYMid">
|
<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>
|
<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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a class="socials" href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel" style="background: none !important; margin: 0 5px; padding:0;">
|
<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" viewBox="0 0 24 24">
|
<svg style="width: 25px; height: 25px; vertical-align: middle;" viewBox="0 0 16 16">
|
||||||
<path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
<a class="socials" href="https://discord.gg/YzmbnCDkat" style="background: none !important; margin: 0 5px; padding: 0;">
|
|
||||||
<svg style="width: 25px; height: 25px;" viewBox="0 0 16 16">
|
|
||||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612" fill="currentColor"/>
|
<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>
|
</svg>
|
||||||
</a>
|
</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>
|
||||||
<a href="https://ko-fi.com/sethburkart" target="_blank" style="background: none !important; margin:0;margin-left:6px; padding:0;">
|
<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;">
|
||||||
<img height="25" style="border:0px; height:25px; margin-right: -6px;" src="${kofi}" border="0" alt="Buy Me a Coffee at ko-fi.com" />
|
<img height="25" style="border:0px; height:25px; margin-right: -6px;" src="${kofi}" border="0" alt="Buy Me a Coffee at ko-fi.com" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild;
|
`).firstChild as HTMLElement;
|
||||||
|
|
||||||
let exitbutton = document.createElement("div");
|
openPopup({
|
||||||
exitbutton.id = "whatsnewclosebutton";
|
header,
|
||||||
|
content: [imageContainer, text, footer],
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import { animate, stagger } from "motion";
|
|||||||
export async function SendNewsPage() {
|
export async function SendNewsPage() {
|
||||||
console.info("[BetterSEQTA+] Started Loading News Page");
|
console.info("[BetterSEQTA+] Started Loading News Page");
|
||||||
document.title = "News ― SEQTA Learn";
|
document.title = "News ― SEQTA Learn";
|
||||||
await delay(100);
|
await delay(10);
|
||||||
|
|
||||||
const element = document.querySelector("[data-key=news]");
|
const element = document.querySelector("[data-key=news]");
|
||||||
element!.classList.add("active");
|
element!.classList.add("active");
|
||||||
@@ -18,10 +18,25 @@ export async function SendNewsPage() {
|
|||||||
const main = document.getElementById("main");
|
const main = document.getElementById("main");
|
||||||
main!.innerHTML = "";
|
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 */ `
|
const html = stringToHTML(/* html */ `
|
||||||
<div class="home-root">
|
<div class="home-root">
|
||||||
<div class="home-container" id="news-container">
|
<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>
|
||||||
</div>`);
|
</div>`);
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ export function convertTo12HourFormat(
|
|||||||
noMinutes: boolean = false,
|
noMinutes: boolean = false,
|
||||||
): string {
|
): string {
|
||||||
let [hours, minutes] = time.split(":").map(Number);
|
let [hours, minutes] = time.split(":").map(Number);
|
||||||
let period = "AM";
|
let period = "am";
|
||||||
|
|
||||||
if (hours >= 12) {
|
if (hours >= 12) {
|
||||||
period = "PM";
|
period = "pm";
|
||||||
if (hours > 12) hours -= 12;
|
if (hours > 12) hours -= 12;
|
||||||
} else if (hours === 0) {
|
} else if (hours === 0) {
|
||||||
hours = 12;
|
hours = 12;
|
||||||
@@ -17,5 +17,5 @@ export function convertTo12HourFormat(
|
|||||||
hoursStr = hoursStr.substring(1);
|
hoursStr = hoursStr.substring(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${hoursStr}${noMinutes ? "" : `:${minutes.toString().padStart(2, "0")}`} ${period}`;
|
return `${hoursStr}${noMinutes ? "" : `:${minutes.toString().padStart(2, "0")}`}${period}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user