Compare commits

..

1 Commits

Author SHA1 Message Date
SethBurkart123 2638157d25 feat: custom editor 2025-06-18 16:47:29 +10:00
69 changed files with 2803 additions and 3661 deletions
-114
View File
@@ -1,114 +0,0 @@
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!**
+5 -23
View File
@@ -1,31 +1,13 @@
# Contributing to BetterSEQTA+
# Contributing
Hey there! 👋 Thanks for your interest in contributing to BetterSEQTA+! We're excited to have you join our community of contributors.
## 🚀 New Contributors Start Here!
**Never contributed to an open source project before?** No worries! We've made it super easy to get started:
- **📖 Read our [Getting Started Guide](./docs/GETTING_STARTED_CONTRIBUTING.md)** - This walks you through everything step-by-step, from setting up your development environment to making your first pull request.
- **🏗️ Understand the codebase** with our [Architecture Guide](./docs/ARCHITECTURE.md)
- **🔧 Having issues?** Check our [Troubleshooting Guide](./docs/TROUBLESHOOTING.md)
We have lots of [`good first issue`](https://github.com/BetterSEQTA/BetterSEQTA-plus/labels/good%20first%20issue) labels that are perfect for beginners!
## Discussion Before Contributing
For significant changes, please first discuss what you'd like to change via:
- Opening an issue
- Joining our Discord server
- Emailing the maintainers
This helps ensure your contribution aligns with the project's goals and saves you time!
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
## Community
Join our community channels to discuss the project, get help, and connect with other contributors:
- **Discord Server**: [Join our Discord](https://discord.gg/YzmbnCDkat)
- **Discord Server**: [Join our Discord](https://discord.gg/betterseqta)
- **GitHub Discussions**: For longer-form conversations
- **GitHub Issues**: For bug reports and feature requests
@@ -39,7 +21,7 @@ If you're interested in creating plugins for BetterSEQTA+, check out our plugin
## Pull Request Process
1. It is recommended to start by opening an issue to discuss the change you wish to make. This will allow us to discuss the change and ensure it is a good fit for the project.
2. Fork the repo and create your branch from `main`.
2. Fork the repo and create your branch from `master`.
3. When writing your pull request, make sure to use the pull request template.
### Pull Request Template
+48 -36
View File
@@ -1,3 +1,5 @@
#
<a href="https://chromewebstore.google.com/detail/betterseqta+/afdgaoaclhkhemfkkkonemoapeinchel">
<img src="https://socialify.git.ci/betterseqta/betterseqta-plus/image?description=1&font=Inter&forks=1&issues=1&logo=data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20height%3D%27656pt%27%20fill%3D%27white%27%20preserveAspectRatio%3D%27xMidYMid%20meet%27%20viewBox%3D%270%200%20658%20656%27%20width%3D%27658pt%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%253E%253Cg%20transform%3D%27matrix(.1%200%200%20-.1%200%20656)%27%253E%253Cpath%20d%3D%27m2960%206499c-918-100-1726-561-2278-1299-196-262-374-609-475-925-171-533-203-1109-91-1655%20228-1115%201030-2032%202104-2408%20356-124%20680-177%201080-176%20269%201%20403%2014%20650%2064%20790%20159%201503%20624%201980%201290%20714%20998%20799%202342%20217%203420-488%20902-1361%201515-2382%201671-113%2017-196%2022-430%2024-159%202-328-1-375-6zm566-1443c476-99%20885-385%201134-791%20190-309%20282-696%20250-1045-22-240-73-420-180-635-78-156-159-275-274-401l-77-84h445%20446v-235-236l-1162%204-1163%203-100%2023c-449%20101-812%20337-1071%20697-77%20107-193%20335-233%20459-115%20358-116%20726-1%201078%20209%20644%20766%201101%201446%201187%20128%2016%20405%204%20540-24z%27%2F%253E%253Cpath%20d%3D%27m3065%204604c-250-36-396-89-576-209-280-187-470-478-535-821-25-135-16-395%2019-525%2095-351%20331-644%20651-806%2098-49%20225-93%20331-114%2092-18%20368-18%20460%200%20481%2095%20853%20444%20982%20921%2035%20129%2044%20389%2019%20524-36%20191-121%20387-228%20531-186%20249-476%20428-783%20485-65%2012-291%2021-340%2014z%27%2F%253E%253C%2Fg%253E%253C%2Fsvg%253E&name=1&owner=1&pattern=Signal&stargazers=1&theme=Dark" />
</a>
@@ -8,7 +10,7 @@
<p align="center">
<a target="_blank" href="https://chrome.google.com/webstore/detail/betterseqta%20/afdgaoaclhkhemfkkkonemoapeinchel"><img src="https://user-images.githubusercontent.com/95666457/149519713-159d7ef7-2c21-4034-a616-f037ff46d9a4.png" alt="ChromeDownload" width="250"></a>
<a target="_blank" href="https://discord.gg/YzmbnCDkat"><img src="https://github.com/BetterSEQTA/BetterSEQTA-Plus/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/SethBurkart123/EvenBetterSEQTA/assets/108050083/23055730-b16e-44c0-9bef-221d8545af92" width="240" style="border-radius:10%;" /></a>
</p>
<div>
@@ -54,48 +56,58 @@ 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 :)
## 🚀 Want to Contribute?
## Getting started
**New contributors welcome!** 🎉 We've made it easy to get started:
&nbsp;&nbsp;&nbsp; **1. Clone the repository**
- **👋 New to the project?** Start with our [Getting Started Guide](./docs/GETTING_STARTED_CONTRIBUTING.md)
- **🏗️ Want to understand the code?** Check out our [Architecture Guide](./docs/ARCHITECTURE.md)
- **🧩 Interested in plugins?** Read our [Plugin Development Guide](./docs/plugins/README.md)
- **🐛 Found a bug?** Open an [issue](https://github.com/BetterSEQTA/BetterSEQTA-plus/issues) or fix it yourself!
- **💬 Need help?** Join our [Discord community](https://discord.gg/YzmbnCDkat)
We have lots of https://github.com/BetterSEQTA/BetterSEQTA-Plus/labels/good%20first%20issue labels perfect for beginners!
## Quick Development Setup
&nbsp;&nbsp;&nbsp; **1. Fork & Clone**
```bash
git clone https://github.com/YOUR_USERNAME/BetterSEQTA-Plus
cd BetterSEQTA-Plus
```
git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus
```
&nbsp;&nbsp;&nbsp; **2. Install & Run**
```bash
npm install --legacy-peer-deps
npm run dev
&nbsp;&nbsp;&nbsp; **2. Install dependencies**
You may install the dependencies like below:
```
npm install # or your preferred package manager like pnpm or yarn
```
&nbsp;&nbsp;&nbsp; **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.
But it is recommended to do it like this:
📚 **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
&nbsp;&nbsp;&nbsp; **3. Run the dev script (it updates as you save files)**
```
npm run dev # or use your preferred package manager
```
### Building for production
&nbsp;&nbsp;&nbsp; **4. Run the build script**
```
npm run build # or use your preferred package manager
```
&nbsp;&nbsp;&nbsp; **4.1. Package it up (optional)**
```
npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your preferred package manager
```
&nbsp;&nbsp;&nbsp; **5. Load the extension into chrome**
- Go to `chrome://extensions`
- Enable developer mode
- Click `Load unpacked`
- Select the `dist` folder
Just remember, in order to update changes to the extension if you are running in developer mode, you need to click the refresh button on the extension in `chrome://extensions` whenever anything's changed.
## Folder Structure
@@ -119,7 +131,7 @@ Want to contribute? [Click Here!](https://github.com/BetterSEQTA/BetterSEQTA-Plu
## Credits
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development from lead developers [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) with help from other volunteers.
This extension was initially developed by [Nulkem](https://github.com/Nulkem/betterseqta), was ported to manifest V3 by [MEGA-Dawg68](https://github.com/MEGA-Dawg68) and is currently under active development from lead developers [SethBurkart123](https://github.com/SethBurkart123) and [Crazypersonalph](https://github.com/Crazypersonalph) with help from other volunteers
## Star History
-235
View File
@@ -1,235 +0,0 @@
# 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. 🚀
-285
View File
@@ -1,285 +0,0 @@
# 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!*
+1 -4
View File
@@ -10,10 +10,7 @@ Welcome to the BetterSEQTA+ documentation! This documentation will help you unde
- [Project Overview](./README.md) - This file
- [Installation Guide](./installation.md) - How to install and set up BetterSEQTA+
- [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
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
### Plugin System
-348
View File
@@ -1,348 +0,0 @@
# 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. 💪
-335
View File
@@ -1,335 +0,0 @@
# 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! 🎉
+25 -13
View File
@@ -1,6 +1,6 @@
{
"name": "betterseqtaplus",
"version": "3.4.11",
"version": "3.4.7",
"type": "module",
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
"browserslist": "> 0.5%, last 2 versions, not dead",
@@ -28,26 +28,26 @@
"keywords": [],
"author": {
"name": "SethBurkart123",
"email": "betterseqta.plus@gmail.com",
"email": "betterseqta@betterseqta.com",
"url": "https://github.com/BetterSEQTA/BetterSEQTA-plus"
},
"license": "MIT",
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.26.9",
"@babel/runtime": "^7.26.9",
"@bedframe/cli": "^0.0.95",
"@crxjs/vite-plugin": "^2.2.0",
"@types/mime-types": "^3.0.1",
"@bedframe/cli": "^0.0.91",
"@crxjs/vite-plugin": "2.0.0-beta.32",
"@types/mime-types": "^2.1.4",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"cross-env": "^10.0.0",
"dependency-cruiser": "^17.0.1",
"eslint": "^9.33.0",
"cross-env": "^7.0.3",
"dependency-cruiser": "^16.10.0",
"eslint": "9.22.0",
"glob": "^11.0.1",
"mime-types": "^3.0.1",
"mime-types": "^2.1.35",
"prettier": "^3.5.3",
"process": "^0.11.10",
"publish-browser-extension": "^3.0.1",
"publish-browser-extension": "^3.0.0",
"sass": "^1.85.1",
"sass-loader": "^16.0.5",
"semver": "^7.7.1",
@@ -55,7 +55,6 @@
"url": "^0.11.4"
},
"dependencies": {
"@bedframe/core": "^0.0.46",
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-css": "^6.3.1",
@@ -65,11 +64,22 @@
"@codemirror/view": "^6.36.4",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10",
"@tiptap/core": "^2.14.0",
"@tiptap/extension-bubble-menu": "^2.14.0",
"@tiptap/extension-dropcursor": "^2.14.0",
"@tiptap/extension-image": "^2.14.0",
"@tiptap/extension-link": "^2.14.0",
"@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-task-item": "^2.14.0",
"@tiptap/extension-task-list": "^2.14.0",
"@tiptap/extension-typography": "^2.14.0",
"@tiptap/starter-kit": "^2.14.0",
"@tiptap/suggestion": "^2.14.0",
"@tsconfig/svelte": "^5.0.4",
"@types/chrome": "^0.1.4",
"@types/chrome": "^0.0.308",
"@types/color": "^4.2.0",
"@types/lodash": "^4.17.16",
"@types/node": "^24.3.0",
"@types/node": "^22.13.10",
"@types/sortablejs": "^1.15.8",
"@types/uuid": "^10.0.0",
"@types/webextension-polyfill": "^0.12.3",
@@ -93,6 +103,7 @@
"mathjs": "^14.4.0",
"million": "^3.1.11",
"motion": "^12.4.12",
"motion-start": "^0.1.15",
"postcss": "^8.5.3",
"react": "17",
"react-best-gradient-color-picker": "3.0.11",
@@ -100,6 +111,7 @@
"rss-parser": "^3.13.0",
"sortablejs": "^1.15.6",
"svelte": "^5.22.6",
"svelte-hero-icons": "^5.2.0",
"typescript": "^5.8.2",
"uuid": "^11.1.0",
"vite": "^6.2.1",
+126
View File
@@ -0,0 +1,126 @@
--- a/Users/sethburkart/Documents/Coding/betterseqta-plus/src/plugins/core/settings.ts
+++ b/Users/sethburkart/Documents/Coding/betterseqta-plus/src/plugins/core/settings.ts
@@ -2,7 +2,7 @@
// Base interfaces for our settings
interface BaseSettingOptions {
- title: string;
+ readonly title: string; // Mark as readonly where appropriate
description?: string;
}
@@ -11,21 +11,21 @@
}
interface StringSettingOptions extends BaseSettingOptions {
- default: string;
+ readonly default: string;
maxLength?: number;
pattern?: string;
}
interface NumberSettingOptions extends BaseSettingOptions {
- default: number;
+ readonly default: number;
min?: number;
max?: number;
step?: number;
}
interface SelectSettingOptions<T extends string> extends BaseSettingOptions {
- default: T;
- options: readonly T[];
+ readonly default: T;
+ readonly options: readonly T[];
}
// The actual decorators
@@ -34,14 +34,16 @@
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
- proto.settings = {};
+ // Initialize with a base type that can be extended
+ Object.defineProperty(proto, 'settings', {
+ value: {},
+ writable: true, // Allows adding properties
+ configurable: true,
+ enumerable: true
+ });
}
-
+
// Add the setting to the prototype's settings object with const assertion
proto.settings[propertyKey] = {
type: 'boolean' as const,
...options
};
- };
-}
-
-export function StringSetting(options: StringSettingOptions): PropertyDecorator {
- return (target: Object, propertyKey: string | symbol) => {
- // Ensure the settings property exists on the constructor's prototype
- const proto = target.constructor.prototype;
- if (!proto.hasOwnProperty('settings')) {
- proto.settings = {};
- }
-
- // Add the setting to the prototype's settings object with const assertion
- proto.settings[propertyKey] = {
- type: 'string' as const,
- ...options
- };
};
}
@@ -50,14 +52,16 @@
// Ensure the settings property exists on the constructor's prototype
const proto = target.constructor.prototype;
if (!proto.hasOwnProperty('settings')) {
- proto.settings = {};
+ Object.defineProperty(proto, 'settings', {
+ value: {},
+ writable: true,
+ configurable: true,
+ enumerable: true
+ });
}
-
+
// Add the setting to the prototype's settings object with const assertion
proto.settings[propertyKey] = {
type: 'number' as const,
...options
};
- };
-}
-
-export function SelectSetting<T extends string>(options: SelectSettingOptions<T>): PropertyDecorator {
- return (target: Object, propertyKey: string | symbol) => {
- // Ensure the settings property exists on the constructor's prototype
- const proto = target.constructor.prototype;
- if (!proto.hasOwnProperty('settings')) {
- proto.settings = {};
- }
-
- // Add the setting to the prototype's settings object with const assertion
- proto.settings[propertyKey] = {
- type: 'select' as const,
- ...options
- };
};
}
// Base plugin class that handles settings
export abstract class BasePlugin<T extends PluginSettings = PluginSettings> {
// The settings property will be populated by decorators
- settings!: T;
-
+ // Keep the instance property and constructor logic as is,
+ // as changing it would require changing animated-background/index.ts
+ settings!: T; // Use definite assignment assertion
+
constructor() {
// Copy settings from the prototype to the instance
// This ensures that each instance has its own settings object
-5
View File
@@ -9,7 +9,6 @@ import browser from "webextension-polyfill";
import * as plugins from "@/plugins";
import { main } from "@/seqta/main";
import { delay } from "./seqta/utils/delay";
import { initializeHideSensitiveToggle } from "@/seqta/utils/hideSensitiveToggle";
export let MenuOptionsOpen = false;
@@ -71,10 +70,6 @@ async function init() {
await plugins.initializePlugins();
}
if (settingsState.devMode) {
initializeHideSensitiveToggle();
}
console.info(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
);
+70 -15
View File
@@ -49,7 +49,7 @@ browser.runtime.onMessage.addListener(
break;
case "setDefaultStorage":
SetStorageValue(getDefaultValues());
SetStorageValue(DefaultValues);
break;
case "sendNews":
@@ -64,18 +64,7 @@ browser.runtime.onMessage.addListener(
},
);
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 {
const DefaultValues: SettingsState = {
onoff: true,
animatedbk: true,
bksliderinput: "50",
@@ -107,7 +96,7 @@ function getDefaultValues(): SettingsState {
"linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)",
originalSelectedColor: "",
DarkMode: true,
animations: !isLowEndDevice,
animations: true,
assessmentsAverage: true,
defaultPage: "home",
shortcuts: [
@@ -128,7 +117,6 @@ function getDefaultValues(): SettingsState {
lettergrade: false,
newsSource: "australia",
};
}
function SetStorageValue(object: any) {
for (var i in object) {
@@ -136,11 +124,78 @@ function SetStorageValue(object: any) {
}
}
function convertBksliderToSpeed(bksliderinput: number): number {
const minBase = 50;
const maxBase = 150;
const scaledValue =
2 + ((maxBase - bksliderinput) / (maxBase - minBase)) ** 4;
const baseSpeed = 3;
const speed = baseSpeed / scaledValue;
return speed;
}
async function migrateLegacySettings() {
const storage = (await browser.storage.local.get(
null,
)) as unknown as SettingsState;
// Animated Background Migration
if ("animatedbk" in storage || "bksliderinput" in storage) {
const animatedSettings = {
enabled: storage.animatedbk ?? true,
speed: storage.bksliderinput
? convertBksliderToSpeed(parseFloat(storage.bksliderinput))
: 1,
};
await browser.storage.local.set({
"plugin.animated-background.settings": animatedSettings,
});
}
// Assessments Average Migration
if ("assessmentsAverage" in storage || "lettergrade" in storage) {
const assessmentsSettings = {
enabled: storage.assessmentsAverage ?? true,
lettergrade: storage.lettergrade ?? false,
};
await browser.storage.local.set({
"plugin.assessments-average.settings": assessmentsSettings,
});
}
if ("selectedTheme" in storage) {
const themesSettings = { enabled: true };
await browser.storage.local.set({
"plugin.themes.settings": themesSettings,
});
}
if (storage.notificationCollector !== false) {
await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: true },
});
} else {
await browser.storage.local.set({
"plugin.notificationCollector.settings": { enabled: false },
});
}
const keysToRemove = [
"animatedbk",
"bksliderinput",
"assessmentsAverage",
"lettergrade",
];
await browser.storage.local.remove(keysToRemove);
}
browser.runtime.onInstalled.addListener(function (event) {
browser.storage.local.remove(["justupdated"]);
browser.storage.local.remove(["data"]);
if (event.reason == "install" || event.reason == "update") {
browser.storage.local.set({ justupdated: true });
migrateLegacySettings();
}
});
-54
View File
@@ -94,57 +94,3 @@ body:has(.outside-container:not(.hide))
background: var(--text-primary) !important;
color: var(--theme-primary) !important;
}
.fixed-tooltip {
display: inline-block;
z-index: 5 !important;
width: 28px;
background: none;
box-shadow: none;
padding: 2px;
position: absolute;
}
.fixed-tooltip svg {
fill: var(--theme-primary);
}
.tooltiptext-fixed {
width: 120px;
transform: scale(0);
transition: transform 0.2s;
transform-origin: top;
background: var(--background-primary);
color: var(--text-primary);
text-align: center;
border-radius: 6px;
padding: 2px;
position: fixed;
z-index: 1000;
top: 0;
left: 0;
margin-left: -62px;
}
.tooltiptext-fixed::after {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent var(--text-primary) transparent;
}
.tooltiptext-fixed.show {
transform: scale(1);
transform-origin: top;
transition: transform 0.2s;
}
.tooltiptext-fixed p:hover {
cursor: pointer;
background: rgba(0, 0, 0, 0.3) !important;
transition: 200ms;
}
.tooltiptext-fixed p {
border-radius: 8px !important;
padding-top: 2px;
padding-bottom: 2px;
margin: 2px;
}
+22 -123
View File
@@ -38,21 +38,6 @@ body,
html {
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 {
transition: 200ms;
background: var(--auto-background) !important;
@@ -394,18 +379,6 @@ ul.magicDelete > li.deleting {
padding: 0;
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 {
width: 270px;
z-index: 19;
@@ -478,6 +451,11 @@ ul.magicDelete > li.deleting {
}
}
#menu li > label,
#menu section > label {
text-transform: none;
font-size: 16px;
}
#userActions {
display: none;
}
@@ -823,11 +801,6 @@ div > ol:has(.uiFileHandlerWrapper) {
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
}
html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"],
html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"] [class*="ResourceList__name___ydvDT"] {
border-bottom: none !important;
}
.assessmentsWrapper .message {
display: none;
}
@@ -1138,7 +1111,7 @@ html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"] [class*="
height: 15em;
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(142px, 1fr);
grid-auto-columns: minmax(130px, 1fr);
border-radius: 16px;
overflow-x: auto;
@@ -1283,9 +1256,6 @@ html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"] [class*="
word-wrap: break-word;
line-height: 20px;
}
.customshortcut > svg {
border-radius: 0;
}
.colourbar {
width: 100%;
height: 3px;
@@ -1347,17 +1317,7 @@ html.transparencyEffects [class*="ResourceList__ResourceItem___voTSd"] [class*="
font-size: 20px !important;
font-weight: 500;
min-height: 46px;
/* 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;
height: 36%;
}
.day h3 {
padding: 0px 5px;
@@ -1691,8 +1651,6 @@ iframe.userHTML {
.programmeNavigator {
box-shadow: 0 0 40px 0px rgba(0,0,0,0.05);
overflow-y: scroll;
height: 100%;
.navigator {
padding: 6px !important;
@@ -1705,6 +1663,17 @@ iframe.userHTML {
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 {
padding: 10px;
padding-left: 30px;
@@ -2022,61 +1991,15 @@ div.entry.event {
border-radius: 4px;
}
div.entry.new {
border-radius: 4px;
}
div.liveEntry {
border-radius: 4px;
}
div.dailycalMarker {
border-radius: 4px;
}
.uiFileHandler .uiButton {
border-radius: 32px !important;
color: var(--text-primary) !important;
margin-top: 4px !important;
}
a.uiFile:not(.rows) {
display: flex !important;
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;
}
.uiFile {
border-radius: 8px !important;
transition: all 0.2s ease-in-out;
}
.dark .title a.uiFile {
@@ -2180,33 +2103,10 @@ div.bar.flat {
}
.dashlet-motd {
padding: 7px !important;
.message {
font-size: 24px !important;
border-radius: 12px !important;
border: none !important;
box-shadow: none !important;
color: #fff !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;
}
}
.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 {
@@ -2830,13 +2730,12 @@ body {
.menuShown #menuToggle .hamburger-line:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
div.day-empty {
.day-empty {
display: flex;
align-items: center;
height: 15em;
width: 100%;
border-radius: 16px 0;
padding: 0 !important;
img {
margin: 20px;
-1
View File
@@ -46,7 +46,6 @@ html.transparencyEffects {
}
.filter-select,
.uiShortText.search,
.report {
backdrop-filter: blur(10px) !important;
}
-10
View File
@@ -2,16 +2,6 @@ div:has(> #rbgcp-wrapper) {
background: transparent !important;
}
#rbgcp-inputs-wrap {
padding-top: 4px !important;
margin-bottom: -8px;
#rbgcp-hex-input,
#rbgcp-input {
height: 28px !important;
}
}
.dark {
#rbgcp-wrapper {
div[style="padding-top: 11px; position: relative;"] div {
@@ -108,6 +108,7 @@ export default function Picker({
<ColorPicker
disableDarkMode={true}
presets={presets}
hideInputs={customOnChange ? false : true}
value={customThemeColor ?? ""}
onChange={(color: string) => {
if (customOnChange) {
+2 -18
View File
@@ -8,12 +8,12 @@
let select: HTMLSelectElement;
</script>
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-xl w-full overflow-clip">
<div class="border dark:bg-[#38373D]/50 bg-[#DDDDDD]/50 border-[#DDDDDD]/30 dark:border-[#38373D]/30 shadow-2xl rounded-lg w-full overflow-clip">
<select
bind:this={select}
value={state}
onchange={() => onChange(select.value)}
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"
class="px-4 py-1 text-[0.75rem] dark:text-white w-full border-none bg-transparent focus:ring-0 focus:bg-white/20 dark:focus:bg-black/10"
>
{#each options as option}
<option value={option.value}>
@@ -22,19 +22,3 @@
{/each}
</select>
</div>
<style>
/* Make native dropdown list readable on Windows */
select option {
background-color: #ffffff;
color: #111827; /* zinc-900 */
}
:global(.dark) select option {
background-color: #1f2937; /* zinc-800 */
color: #ffffff;
}
:global(.dark) div::after {
color: rgba(255, 255, 255, 0.6);
}
</style>
+34 -217
View File
@@ -1,44 +1,43 @@
<script lang="ts">
import TabbedContainer from "../components/TabbedContainer.svelte";
import Settings from "./settings/general.svelte";
import Shortcuts from "./settings/shortcuts.svelte";
import Theme from "./settings/theme.svelte";
import browser from "webextension-polyfill";
import TabbedContainer from '../components/TabbedContainer.svelte';
import Settings from './settings/general.svelte';
import Shortcuts from './settings/shortcuts.svelte';
import Theme from './settings/theme.svelte';
import browser from 'webextension-polyfill';
import { standalone as StandaloneStore } from "../utils/standalone.svelte";
import { onMount } from "svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { standalone as StandaloneStore } from '../utils/standalone.svelte';
import { onMount } from 'svelte'
import { settingsState } from '@/seqta/utils/listeners/SettingsState'
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup";
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
import { OpenMinecraftServerPopup } from "@/seqta/utils/AboutMinecraftServer";
import { closeExtensionPopup } from "@/seqta/utils/Closers/closeExtensionPopup"
import { OpenAboutPage } from "@/seqta/utils/Openers/OpenAboutPage"
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew"
import ColourPicker from "../components/ColourPicker.svelte";
import { settingsPopup } from "../hooks/SettingsPopup";
import ColourPicker from '../components/ColourPicker.svelte'
import { settingsPopup } from '../hooks/SettingsPopup'
let devModeSequence = "";
let devModeSequence = '';
const handleDevModeToggle = () => {
const handleKeyDown = (event: KeyboardEvent) => {
devModeSequence += event.key.toLowerCase();
if (devModeSequence.includes("dev")) {
document.removeEventListener("keydown", handleKeyDown);
if (devModeSequence.includes('dev')) {
document.removeEventListener('keydown', handleKeyDown);
settingsState.devMode = true;
alert("Dev mode is now enabled");
alert('Dev mode is now enabled');
}
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener('keydown', handleKeyDown);
setTimeout(() => {
document.removeEventListener("keydown", handleKeyDown);
devModeSequence = "";
document.removeEventListener('keydown', handleKeyDown);
devModeSequence = '';
}, 10000);
};
const openColourPicker = () => {
showColourPicker = true;
};
}
const openChangelog = () => {
OpenWhatsNewPopup();
@@ -50,11 +49,6 @@
closeExtensionPopup();
};
const openMinecraftServer = () => {
OpenMinecraftServerPopup();
closeExtensionPopup();
};
let { standalone } = $props<{ standalone?: boolean }>();
let showColourPicker = $state<boolean>(false);
@@ -68,207 +62,30 @@
});
</script>
<div
class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode
? 'dark'
: ''} {standalone ? 'h-[600px]' : 'h-full rounded-xl'} overflow-clip"
>
<div
class="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white"
>
<div
class="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40"
>
<div class="w-[384px] no-scrollbar shadow-2xl {$settingsState.DarkMode ? 'dark' : ''} { standalone ? 'h-[600px]' : 'h-full rounded-xl' } overflow-clip">
<div class="flex relative flex-col gap-2 h-full overflow-clip bg-white dark:bg-zinc-800 dark:text-white">
<div class="grid place-items-center border-b border-b-zinc-200/40 dark:border-b-zinc-700/40">
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img
src={browser.runtime.getURL(
"resources/icons/betterseqta-dark-full.png",
)}
class="w-4/5 dark:hidden"
alt="Light logo"
onclick={handleDevModeToggle}
/>
<img src={browser.runtime.getURL('resources/icons/betterseqta-dark-full.png')} class="w-4/5 dark:hidden" alt="Light logo" onclick={handleDevModeToggle} />
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<img
src={browser.runtime.getURL(
"resources/icons/betterseqta-light-full.png",
)}
class="hidden w-4/5 dark:block"
alt="Dark logo"
onclick={handleDevModeToggle}
/>
<img src={browser.runtime.getURL('resources/icons/betterseqta-light-full.png')} class="hidden w-4/5 dark:block" alt="Dark logo" onclick={handleDevModeToggle} />
{#if !standalone}
<button
onclick={openAbout}
class="absolute top-1 right-[62px] w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
>
{"\ueb73"}
</button>
<button
onclick={openChangelog}
class="absolute top-1 right-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700"
>
{"\ue929"}
</button>
<button
onclick={openMinecraftServer}
class="absolute top-1 right-1 w-8 h-8 bg-zinc-100 dark:bg-zinc-700 rounded-xl p-1"
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>
<button onclick={openChangelog} class="absolute top-1 right-1 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ue929'}</button>
<button onclick={openAbout} class="absolute top-1 right-10 w-8 h-8 text-lg rounded-xl font-IconFamily bg-zinc-100 dark:bg-zinc-700">{'\ueb73'}</button>
{/if}
</div>
<TabbedContainer
tabs={[
{
title: "Settings",
Content: Settings,
props: { showColourPicker: openColourPicker },
},
{ title: "Shortcuts", Content: Shortcuts },
{ title: "Themes", Content: Theme },
]}
/>
<TabbedContainer tabs={[
{ title: 'Settings', Content: Settings, props: { showColourPicker: openColourPicker } },
{ title: 'Shortcuts', Content: Shortcuts },
{ title: 'Themes', Content: Theme },
]} />
</div>
{#if showColourPicker}
<ColourPicker
hidePicker={() => {
showColourPicker = false;
}}
/>
<ColourPicker hidePicker={() => { showColourPicker = false }} />
{/if}
</div>
+5 -5
View File
@@ -10,6 +10,7 @@
import type { SettingsList } from "@/interface/types/SettingsProps"
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
import { getAllPluginSettings } from "@/plugins"
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
@@ -186,16 +187,15 @@
options: [
{ value: "australia", label: "Australia" },
{ value: "usa", label: "USA" },
{ value: "uk", label: "UK" },
{ value: "taiwan", label: "Taiwan" },
{ value: "hong_kong", label: "Hong Kong" },
{ value: "panama", label: "Panama" },
{ value: "canada", label: "Canada" },
{ value: "singapore", label: "Singapore" },
{ value: "uk", label: "UK" },
{ value: "japan", label: "Japan" },
{ value: "netherlands", label: "Netherlands" }
]
}
}
] as option}
@@ -322,9 +322,9 @@
<p class="text-xs">Replace sensitive content with mock data</p>
</div>
<div>
<Switch
state={$settingsState.hideSensitiveContent ?? false}
onChange={(isOn: boolean) => settingsState.hideSensitiveContent = isOn}
<Button
onClick={() => hideSensitiveContent()}
text="Hide"
/>
</div>
</div>
+15 -21
View File
@@ -24,18 +24,12 @@
});
const switchChange = (shortcut: any) => {
const idx = $settingsState.shortcuts.findIndex(s => s.name === shortcut);
if (idx !== -1) {
// Create a new array with the toggled value to ensure reactivity
const updated = settingsState.shortcuts.map(s =>
s.name === shortcut ? { ...s, enabled: !s.enabled } : s
);
settingsState.shortcuts = updated;
const value = $settingsState.shortcuts.find(s => s.name === shortcut);
if (value) {
value.enabled = !value.enabled;
settingsState.shortcuts = settingsState.shortcuts;
} else {
settingsState.shortcuts = [
...settingsState.shortcuts,
{ name: shortcut, enabled: true }
];
settingsState.shortcuts = [...settingsState.shortcuts, { name: shortcut, enabled: true }];
}
}
@@ -202,6 +196,16 @@
</MotionDiv>
</div>
{#each Object.entries(Shortcuts) as shortcut}
<div class="flex justify-between items-center px-4 py-3">
<div class="pr-4">
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
<h2 class="text-sm">{shortcut[1].DisplayName || shortcut[0]}</h2>
</div>
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
</div>
{/each}
<!-- Custom Shortcuts Section -->
{#each $settingsState.customshortcuts as shortcut, index}
<div class="flex justify-between items-center px-4 py-3">
@@ -213,16 +217,6 @@
</button>
</div>
{/each}
{#each Object.entries(Shortcuts) as shortcut}
<div class="flex justify-between items-center px-4 py-3">
<div class="pr-4">
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
<h2 class="text-sm">{shortcut[1].DisplayName || shortcut[0]}</h2>
</div>
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
</div>
{/each}
{:else}
<div class="p-4 text-center">
Loading shortcuts...
+2 -5
View File
@@ -21,16 +21,13 @@
<div class="relative w-full">
<button
onclick={() => editMode = !editMode}
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>
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>
<BackgroundSelector isEditMode={editMode} bind:selectedBackground={selectedBackground} bind:selectNoBackground={selectNoBackground} />
<ThemeSelector isEditMode={editMode} />
</div>
{:else}
<div class="flex justify-center items-center w-full h-full">
<div class="flex items-center justify-center w-full h-full">
<div class="text-lg">
Open SEQTA and use the embedded settings to access theme settings. 🫠
</div>
+39 -399
View File
@@ -10,20 +10,6 @@ class ReactFiber {
console.log("Selected Nodes:", this.nodes);
console.log("🔍 Found Fibers:", this.fibers);
console.log("🛠 Found Components:", this.components);
// Debug fiber info
this.fibers.forEach((fiber, index) => {
if (fiber) {
console.log(`Fiber ${index}:`, {
tag: fiber.tag,
type: fiber.type?.name || fiber.type,
elementType: fiber.elementType,
stateNode: fiber.stateNode,
hasState: !!fiber.stateNode?.state,
hasMemoizedState: !!fiber.memoizedState
});
}
});
}
}
@@ -33,27 +19,10 @@ class ReactFiber {
getFiberNode(node) {
if (!node) return null;
// Try multiple property name patterns for different React versions
const possibleKeys = [
'__reactFiber$', // React 16+
'__reactInternalFiber$', // React 15
'__reactInternalInstance$', // Older versions
'__reactFiber',
'__reactInternalInstance'
];
// Check for exact matches first
for (const key of possibleKeys) {
if (node[key]) return node[key];
}
// Fall back to pattern matching
const fiberKey = Object.getOwnPropertyNames(node).find(
(name) =>
name.startsWith("__reactFiber") ||
name.startsWith("__reactInternalInstance") ||
name.startsWith("__reactInternalFiber")
name.startsWith("__reactInternalInstance"),
);
return fiberKey ? node[fiberKey] : null;
}
@@ -61,71 +30,20 @@ class ReactFiber {
getOwnerComponent(fiberNode) {
let current = fiberNode;
while (current) {
// Use React's internal tag system to identify component types
// Based on React's WorkTags: ClassComponent = 1, FunctionComponent = 0
if (current.tag === 1) { // ClassComponent
return current.stateNode; // For class components, stateNode is the component instance
}
// For function components, look for hooks in memoizedState
if (current.tag === 0 || current.tag === 15) { // FunctionComponent or MemoComponent
// Function components don't have setState, but we can still track them
if (current.memoizedState && current.type) {
return {
type: 'function',
hooks: current.memoizedState,
fiber: current,
forceUpdate: () => {
// Trigger re-render by updating fiber
if (current.alternate) {
current.alternate.expirationTime = 1;
}
current.expirationTime = 1;
}
};
}
}
// Legacy fallback: check if stateNode has React component methods
if (
current.stateNode &&
current.stateNode !== null &&
typeof current.stateNode === 'object' &&
(current.stateNode.setState || current.stateNode.forceUpdate)
) {
return current.stateNode;
}
current = current.return;
}
return null;
}
getState(key) {
if (!this.components.length && !this.fibers.length) return null;
const component = this.components[0];
const fiber = this.fibers[0];
let state = null;
// Handle class components
if (component?.state) {
state = component.state;
}
// Handle function components with hooks - look directly at fiber
else if (fiber?.memoizedState) {
if (this.debug) {
console.log("🔍 Raw fiber.memoizedState:", fiber.memoizedState);
}
// Extract useState values from the hook chain
const states = this.extractStateFromHooks(fiber.memoizedState);
state = states.length === 1 ? states[0] : states;
}
// Fallback: try component hooks if available
else if (component?.type === 'function' && component?.hooks) {
const states = this.extractStateFromHooks(component.hooks);
state = states.length === 1 ? states[0] : states;
}
if (!this.components.length) return null;
const state = this.components[0]?.state || null;
if (key === undefined) {
return state;
@@ -143,137 +61,8 @@ class ReactFiber {
return null;
}
extractStateFromHooks(hookChain) {
const states = [];
let mainStateFound = false;
let currentHook = hookChain;
let hookIndex = 0;
if (this.debug) {
console.log("🔍 Hook chain analysis:");
}
while (currentHook) {
if (this.debug) {
console.log(`Hook ${hookIndex}:`, {
type: currentHook.tag || 'unknown',
memoizedState: currentHook.memoizedState,
queue: currentHook.queue,
next: !!currentHook.next
});
}
// Try different approaches to extract state
if (currentHook.memoizedState !== undefined && currentHook.memoizedState !== null) {
const state = currentHook.memoizedState;
// Priority 1: Check for useRef hooks with complex state in .current
if (!currentHook.queue &&
typeof state === 'object' &&
state !== null &&
state.current !== undefined &&
typeof state.current === 'object' &&
state.current !== null) {
// Check if this looks like a substantial state object (has multiple properties)
const currentKeys = Object.keys(state.current);
if (currentKeys.length > 2) {
states.push(state.current);
mainStateFound = true;
if (this.debug) console.log(` 🎯 Found main state in useRef:`, state.current);
}
}
// Priority 2: useState hooks with queue
else if (currentHook.queue && typeof state !== 'function') {
states.push(state);
if (this.debug) console.log(` ✅ Found useState state:`, state);
}
// Priority 3: Other potential state objects (only if we haven't found main state)
else if (!mainStateFound && !currentHook.queue && typeof state === 'object' && state !== null) {
// Skip useEffect hooks (they have tag 36)
if (!(state.tag === 36 && state.create)) {
states.push(state);
if (this.debug) console.log(` 📦 Found potential state object:`, state);
}
}
// Priority 4: Simple primitive state
else if (typeof state !== 'function' && typeof state !== 'object') {
states.push(state);
if (this.debug) console.log(` 🔹 Found primitive state:`, state);
}
}
currentHook = currentHook.next;
hookIndex++;
}
if (this.debug) {
console.log(`🎯 Extracted ${states.length} state values:`, states);
}
// If we found main state objects, prioritize and deduplicate them
if (mainStateFound && states.length > 1) {
const mainStates = states.filter(state =>
typeof state === 'object' &&
state !== null &&
Object.keys(state).length > 2
);
if (mainStates.length > 1) {
// If we have multiple main state objects, find the most comprehensive one
// or merge them if they seem complementary
const largestState = mainStates.reduce((largest, current) => {
const largestKeys = Object.keys(largest).length;
const currentKeys = Object.keys(current).length;
// Prefer the one with more properties
if (currentKeys > largestKeys) return current;
// If same number of properties, prefer the one with more complex data
if (currentKeys === largestKeys) {
const largestComplexity = this.calculateStateComplexity(largest);
const currentComplexity = this.calculateStateComplexity(current);
return currentComplexity > largestComplexity ? current : largest;
}
return largest;
});
if (this.debug) {
console.log(`🎯 Selected most comprehensive state from ${mainStates.length} candidates:`, largestState);
}
return [largestState];
}
return mainStates;
}
return states;
}
calculateStateComplexity(state) {
if (!state || typeof state !== 'object') return 0;
let complexity = 0;
for (const [key, value] of Object.entries(state)) {
complexity += 1; // Base point for each property
if (Array.isArray(value)) {
complexity += value.length * 0.1; // Arrays get points based on length
} else if (typeof value === 'object' && value !== null) {
complexity += Object.keys(value).length * 0.5; // Nested objects get points
} else if (typeof value === 'function') {
complexity += 2; // Functions are valuable
}
}
return complexity;
}
setState(update) {
this.components.forEach((component) => {
// Handle class components
if (component?.setState) {
if (typeof update === "function") {
// Functional update
@@ -296,13 +85,6 @@ 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;
}
@@ -317,7 +99,7 @@ class ReactFiber {
return this.fibers[0]?.memoizedProps?.[propName];
}
setProp(propName, value) {
setProp(propName) {
this.fibers.forEach((fiber) => {
if (fiber?.memoizedProps) {
fiber.memoizedProps[propName] = value;
@@ -337,176 +119,38 @@ class ReactFiber {
}
}
function makeSerializable(obj, visited = new WeakSet(), depth = 0, maxDepth = 10) {
// Handle primitives first
if (obj === null || obj === undefined) {
function makeSerializable(obj) {
if (typeof obj !== "object" || obj === null) {
return obj;
}
// Catch ALL functions early
if (typeof obj === "function") {
return `[Function: ${obj.name || 'anonymous'}]`;
}
if (typeof obj !== "object") {
// Handle other primitives
if (typeof obj === "symbol") return obj.toString();
if (typeof obj === "bigint") return obj.toString() + "n";
return obj;
}
// Prevent infinite recursion - depth limit
if (depth > maxDepth) {
return "[Max Depth Reached]";
}
// Prevent circular references
if (visited.has(obj)) {
return "[Circular Reference]";
}
visited.add(obj);
try {
// Handle special objects first
if (obj instanceof HTMLElement) {
return {
type: "HTMLElement",
tagName: obj.tagName,
id: obj.id || null,
className: obj.className || null,
attributes: obj.attributes ? Array.from(obj.attributes).map(attr => ({ name: attr.name, value: attr.value })) : []
};
}
if (obj instanceof Event) {
return {
type: "Event",
eventType: obj.type,
target: obj.target?.tagName || null
};
}
if (obj instanceof Date) {
return { type: "Date", value: obj.toISOString() };
}
if (obj instanceof RegExp) {
return { type: "RegExp", source: obj.source, flags: obj.flags };
}
if (obj instanceof Error) {
return { type: "Error", message: obj.message, name: obj.name };
}
// Handle React Fiber nodes - these are super circular
if (obj.tag !== undefined && obj.elementType !== undefined) {
return {
type: "ReactFiber",
tag: obj.tag,
elementType: typeof obj.elementType === 'function' ? obj.elementType.name || 'AnonymousComponent' : String(obj.elementType),
key: obj.key,
hasState: !!obj.stateNode?.state,
hasMemoizedState: !!obj.memoizedState,
hasProps: !!obj.memoizedProps
};
}
// Handle arrays
if (Array.isArray(obj)) {
return obj.slice(0, 50).map((item, index) => {
if (index >= 25) return "[...truncated]"; // Smaller limit
return makeSerializable(item, visited, depth + 1, maxDepth);
});
return obj.map((item) => makeSerializable(item));
}
// Handle regular objects
const serializableObj = {};
// 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;
}
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
let value = obj[key];
// Handle symbols specifically (React context symbols)
if (typeof value === "symbol") {
value = `[Symbol: ${value.description || 'anonymous'}]`;
}
// Extra function check
else if (typeof value === "function") {
value = `[Function: ${value.name || 'anonymous'}]`;
} else if (value && typeof value === "object") {
value = makeSerializable(value, visited, depth + 1, maxDepth);
if (typeof value === "function") {
value = "[Function]";
} else if (value instanceof HTMLElement) {
value = {
type: "HTMLElement",
id: value.id,
tagName: value.tagName,
}; // Replace DOM node with ID/tag info
} else if (typeof value === "symbol") {
value = value.toString();
} else if (typeof value === "object" && value !== null) {
value = makeSerializable(value);
}
serializableObj[key] = value;
} catch (error) {
serializableObj[key] = `[Error: ${error.message}]`;
}
}
if (ownKeys.length > maxKeys) {
serializableObj['...'] = `[${ownKeys.length - maxKeys} more properties]`;
}
return serializableObj;
} catch (error) {
return `[Serialization Error: ${error.message}]`;
} finally {
visited.delete(obj); // Clean up for potential reuse
}
}
// Final safety check - recursively scan for any remaining functions
function deepFunctionCheck(obj, path = "") {
if (typeof obj === "function") {
throw new Error(`Found function at path: ${path}`);
}
if (obj && typeof obj === "object") {
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
deepFunctionCheck(item, `${path}[${index}]`);
});
} else {
Object.keys(obj).forEach(key => {
deepFunctionCheck(obj[key], path ? `${path}.${key}` : key);
});
}
}
}
window.addEventListener("message", (event) => {
@@ -552,28 +196,6 @@ window.addEventListener("message", (event) => {
response = makeSerializable(response);
}
// Final safety check before postMessage
try {
deepFunctionCheck(response);
} catch (functionError) {
console.warn("[pageState] Function detected in response, cleaning:", functionError.message);
response = `[Cleaned Response - Function found at: ${functionError.message}]`;
}
// Additional structured clone test
try {
// Test if the object can be cloned (same algorithm as postMessage)
if (typeof structuredClone === 'function') {
structuredClone(response);
} else {
// Fallback for older browsers - try JSON round-trip
JSON.parse(JSON.stringify(response));
}
} catch (cloneError) {
console.warn("[pageState] Response not cloneable, fallback:", cloneError.message);
response = `[Uncloneable Response: ${cloneError.message}]`;
}
window.postMessage(
{
type: "reactFiberResponse",
@@ -600,5 +222,23 @@ window.addEventListener("message", (event) => {
});
document.dispatchEvent(keyboardEvent);
} else if (event.data.type === "ckeditorSetData") {
// Handle CKEditor data setting
const { editorId, content } = event.data;
if (window.CKEDITOR && window.CKEDITOR.instances && window.CKEDITOR.instances[editorId]) {
window.CKEDITOR.instances[editorId].setData(content);
} else {
console.warn(`[pageState] CKEditor instance '${editorId}' not found`);
}
} else if (event.data.type === "ckeditorGetData") {
const { editorId } = event.data;
if (window.CKEDITOR && window.CKEDITOR.instances && window.CKEDITOR.instances[editorId]) {
const data = window.CKEDITOR.instances[editorId].getData();
window.postMessage({
type: "ckeditorGetDataResponse",
data,
}, "*");
}
}
});
@@ -9,9 +9,6 @@ interface PrefItem {
value: string;
}
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import { getMockAssessmentsData } from "@/seqta/ui/dev/hideSensitiveContent";
let cache: { time: number; data: any } | null = null;
const CACHE_MS = 10 * 60 * 1000;
const student = 69;
@@ -105,10 +102,6 @@ async function loadSubmissions(student: number, assessments: any[]) {
}
export async function getAssessmentsData() {
if (settingsState.mockNotices) {
return getMockAssessmentsData();
}
if (cache && Date.now() - cache.time < CACHE_MS) return cache.data;
const [subjects, colors, upcoming] = await Promise.all([
loadSubjects(),
@@ -1,120 +0,0 @@
<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"
>&#215;</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>
@@ -1,187 +0,0 @@
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;
@@ -1,2 +0,0 @@
.background-music-hidden{display:none}
@@ -0,0 +1,65 @@
<script lang="ts">
import Editor from './Editor/Editor.svelte';
import EditorStyles from './Editor/EditorStyles.css?raw';
import EditorOverrideStyles from './Editor/EditorOverrideStyles.css?raw';
import TiptapStyles from './Editor/TiptapStyles.css?raw';
import { onMount } from 'svelte';
import { settingsState } from '@/seqta/utils/listeners/SettingsState';
interface Props {
onchange: (value: string) => void;
initialContent?: string;
scale?: number; // Scale factor for the editor (1.0 = normal, 1.2 = 120%, etc.)
}
let { onchange, initialContent = '', scale = 1.3 }: Props = $props();
let content = $state('');
let betterEditor = $state<HTMLElement | null>(null);
// Watch for content changes and call the callback
$effect(() => {
if (onchange) {
onchange(content);
}
});
onMount(async () => {
if (betterEditor) {
const styles = EditorStyles + EditorOverrideStyles + TiptapStyles;
const scalingCSS = `
.better-editor {
--scale-factor: ${scale};
}
.better-editor .editor-prose {
transform-origin: top left;
zoom: ${scale};
-moz-transform: scale(${scale});
-moz-transform-origin: 0 0;
}
/* For Firefox which doesn't support zoom */
@-moz-document url-prefix() {
.better-editor .editor-prose {
transform: scale(${scale});
width: ${100 / scale}%;
}
}
`;
const styleElement = document.createElement('style');
styleElement.textContent = styles + scalingCSS;
betterEditor.appendChild(styleElement);
}
});
</script>
<div
class="h-full better-editor {settingsState.DarkMode ? 'dark' : ''}"
bind:this={betterEditor}
style="font-size: {scale * 16}px; --editor-scale: {scale};"
>
<Editor bind:content {initialContent} />
</div>
@@ -0,0 +1,154 @@
<script lang="ts">
import Placeholder from '@tiptap/extension-placeholder';
import Commands from './Plugins/Commands/command';
import { Dropcursor } from '@tiptap/extension-dropcursor';
import Image from '@tiptap/extension-image'
import BubbleMenu from '@tiptap/extension-bubble-menu';
import Typography from '@tiptap/extension-typography';
import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item';
import StarterKit from '@tiptap/starter-kit';
import Link from '@tiptap/extension-link';
import { Editor } from '@tiptap/core';
import CommandList from './Plugins/Commands/CommandList.svelte';
import suggestion from './Plugins/Commands/suggestion';
import { slashVisible } from './Plugins/Commands/stores';
import { get } from 'svelte/store';
import BubbleMenuComponent from './Plugins/BubbleMenu.svelte';
import { onMount, onDestroy } from 'svelte';
import EditorStyles from './EditorOverrideStyles.css?raw';
// Make htmlContent bindable from parent components
let { content = $bindable(''), initialContent = '' } = $props<{ content: string; initialContent?: string }>();
let commandListInstance = $state<any>(null);
let element = $state<HTMLElement | null>(null);
let editor = $state<Editor | null>(null);
onMount(() => {
editor = new Editor({
element: element!,
content: initialContent || '',
editorProps: {
attributes: {
class: 'focus:outline-none px-3 md:px-0',
},
handleKeyDown: (_, event) => {
// Handle keyboard events when slash menu is visible
if (get(slashVisible) && commandListInstance) {
if (event.key === 'Enter' || event.key === 'ArrowUp' || event.key === 'ArrowDown') {
const handled = commandListInstance.handleKeydown(event, editor);
if (handled) {
return true; // Prevent TipTap from handling this event
}
}
}
return false; // Let TipTap handle other events
},
},
extensions: [
StarterKit,
Placeholder.configure({
placeholder: ({ node }: { node: any }) => {
if (node.type.name === 'heading') {
return 'Heading';
} else if (node.type.name === 'paragraph') {
return "Type '/' for commands";
}
return 'Type something...';
},
}),
TaskList,
TaskItem,
Link,
Typography,
Commands.configure({
suggestion,
}),
BubbleMenu.configure({
element: document.querySelector('.menu') as HTMLElement,
}),
Dropcursor.configure({ width: 5, color: '#ddeeff' }),
Image.configure({
allowBase64: true,
}),
],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor;
},
onUpdate: ({ editor }: { editor: Editor }) => {
// Update the htmlContent with the editor's HTML plus CSS
const editorHTML = editor.getHTML();
content = `<div class="editor-prose">${editorHTML}<${''}style>${EditorStyles}</${''}style></div>`;
},
});
});
onMount(() => {
if (initialContent) {
content = initialContent;
}
});
onDestroy(() => {
if (editor) {
editor.destroy();
}
});
function handleKeydownCapture(event: KeyboardEvent) {
if (commandListInstance && editor && get(slashVisible)) {
if (event.key === 'Escape') {
if (commandListInstance.handleKeydown(event, editor)) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
function handleClick(event: MouseEvent) {
if (!editor) return;
// Check if the click happened in empty space below content
const editorElement = element;
if (!editorElement) return;
const clickY = event.clientY;
// Get the last node in the editor
const lastNode = editorElement.lastElementChild;
if (lastNode) {
const lastNodeRect = lastNode.getBoundingClientRect();
// If click is below the last content node, move cursor to end
if (clickY > lastNodeRect.bottom) {
const docSize = editor.state.doc.content.size;
editor.commands.setTextSelection(docSize);
editor.commands.focus();
event.preventDefault();
}
}
}
</script>
<div class="relative h-full">
<div
class="w-full min-h-full editor-prose"
bind:this={element}
onkeydown={handleKeydownCapture}
onclick={handleClick}
role="textbox"
tabindex="-1">
</div>
<CommandList bind:this={commandListInstance} />
</div>
<BubbleMenuComponent bind:editor />
@@ -0,0 +1,398 @@
.editor-prose {
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
'Noto Sans',
sans-serif !important;
line-height: 1.6 !important;
color: #374151 !important;
font-size: 14px !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box !important;
.dark & * {
color: #d1d5db !important;
}
* {
color: #374151 !important;
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
ol {
width: 100% !important;
min-width: 2px !important;
box-sizing: border-box !important;
}
h1 {
font-size: 1.5rem !important;
font-weight: 700 !important;
margin: 0.75rem 0 0.5rem 0 !important;
line-height: 1.3 !important;
color: #111827 !important;
padding: 0 !important;
border: none !important;
background: none !important;
text-shadow: none !important;
.dark & {
color: #f9fafb !important;
}
}
h2 {
font-size: 1.25rem !important;
font-weight: 600 !important;
margin: 0.6rem 0 0.4rem 0 !important;
line-height: 1.4 !important;
color: #1f2937 !important;
padding: 0 !important;
border: none !important;
background: none !important;
text-shadow: none !important;
.dark & {
color: #e5e7eb !important;
}
}
h3 {
font-size: 1.125rem !important;
font-weight: 600 !important;
margin: 0.5rem 0 0.3rem 0 !important;
line-height: 1.4 !important;
color: #374151 !important;
padding: 0 !important;
border: none !important;
background: none !important;
text-shadow: none !important;
.dark & {
color: #d1d5db !important;
}
}
p {
margin: 0.4rem 0 !important;
line-height: 1.6 !important;
font-size: 0.875rem !important;
padding: 0 !important;
border: none !important;
background: none !important;
color: inherit !important;
text-shadow: none !important;
}
ul {
margin: 0.5rem 0 !important;
padding-left: 1.25rem !important;
list-style-type: disc !important;
border: none !important;
background: none !important;
ul {
list-style-type: circle !important;
ul {
list-style-type: square !important;
}
}
&[data-type='taskList'] {
list-style: none !important;
padding: 0 !important;
margin: 0.5rem 0 !important;
border: none !important;
background: none !important;
p {
margin: 0 !important;
font-size: 0.875rem !important;
line-height: 1.5 !important;
padding: 0 !important;
border: none !important;
background: none !important;
color: inherit !important;
text-shadow: none !important;
}
li {
display: flex !important;
align-items: flex-start !important;
margin: 0.25rem 0 !important;
padding: 0 !important;
border: none !important;
background: none !important;
color: inherit !important;
text-shadow: none !important;
list-style: none !important;
> label {
flex: 0 0 auto !important;
margin-right: 0.5rem !important;
margin-top: 0.125rem !important;
user-select: none !important;
padding: 0 !important;
border: none !important;
background: none !important;
input[type='checkbox'] {
width: 1rem !important;
height: 1rem !important;
border-radius: 0.25rem !important;
border: 2px solid #d1d5db !important;
background-color: #fff !important;
cursor: pointer !important;
appearance: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
position: relative !important;
margin: 0 !important;
padding: 0 !important;
box-shadow: none !important;
&:hover {
border-color: #3b82f6 !important;
}
&:checked {
background-color: #3b82f6 !important;
border-color: #3b82f6 !important;
&::after {
content: '' !important;
position: absolute !important;
left: 0.125rem !important;
top: 0.0625rem !important;
width: 0.375rem !important;
height: 0.625rem !important;
border: 2px solid white !important;
border-top: 0 !important;
border-left: 0 !important;
transform: rotate(45deg) !important;
}
}
.dark & {
border-color: #4b5563 !important;
background-color: #374151 !important;
&:hover {
border-color: #60a5fa !important;
}
&:checked {
background-color: #60a5fa !important;
border-color: #60a5fa !important;
}
}
}
}
> div {
flex: 1 1 auto !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
background: none !important;
color: inherit !important;
}
}
}
li {
margin: 0.25rem 0 !important;
line-height: 1.5 !important;
font-size: 0.875rem !important;
display: list-item !important;
list-style-type: disc !important;
padding: 0 !important;
border: none !important;
background: none !important;
color: inherit !important;
text-shadow: none !important;
}
}
ol {
margin: 0.5rem 0 !important;
padding-left: 1.25rem !important;
list-style-type: decimal !important;
border: none !important;
background: none !important;
li {
margin: 0.25rem 0 !important;
line-height: 1.5 !important;
font-size: 0.875rem !important;
display: list-item !important;
list-style-type: decimal !important;
padding: 0 !important;
border: none !important;
background: none !important;
color: inherit !important;
text-shadow: none !important;
}
}
strong {
font-weight: 600 !important;
color: #111827 !important;
text-shadow: none !important;
.dark & {
color: #f9fafb !important;
}
}
em {
font-style: italic !important;
text-shadow: none !important;
}
a {
color: #3b82f6 !important;
text-decoration: underline !important;
text-decoration-color: rgba(59, 130, 246, 0.3) !important;
text-shadow: none !important;
background: none !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
&:hover {
text-decoration-color: #3b82f6 !important;
background: none !important;
}
.dark & {
color: #60a5fa !important;
&:hover {
text-decoration-color: #60a5fa !important;
}
}
}
blockquote {
padding: 0.2rem 1rem !important;
margin: 1rem 0 !important;
font-style: italic !important;
color: #6b7280 !important;
text-align: left !important;
border-right: none !important;
border-top: none !important;
border-bottom: none !important;
box-shadow: none !important;
text-shadow: none !important;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background-color: #d1d5db;
z-index: 1;
border-radius: 0.5rem;
}
.dark &::before {
background-color: #4b5563;
}
.dark & {
color: #9ca3af !important;
}
}
pre {
background-color: #f3f4f6 !important;
color: #1f2937 !important;
padding: 1rem !important;
border-radius: 0.5rem !important;
margin: 1rem 0 !important;
overflow-x: auto !important;
font-family:
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace !important;
font-size: 0.875rem !important;
line-height: 1.5 !important;
text-align: left !important;
white-space: pre !important;
border: none !important;
box-shadow: none !important;
text-shadow: none !important;
.dark & {
background-color: rgba(35, 36, 41, 0.5) !important;
border: 1px solid rgba(35, 36, 41, 0.5) !important;
color: #e5e7eb !important;
}
code {
background-color: transparent !important;
color: inherit !important;
padding: 0 !important;
border-radius: 0 !important;
font-size: inherit !important;
font-family: inherit !important;
border: none !important;
margin: 0 !important;
.dark & {
background-color: transparent !important;
color: inherit !important;
}
}
}
hr {
border: none !important;
border-top: 1px solid #e5e7eb !important;
margin: 1rem 0 !important;
width: 100% !important;
background: none !important;
height: 0 !important;
padding: 0 !important;
.dark & {
border-top-color: #3f4854 !important;
}
}
code {
background-color: #f3f4f6 !important;
color: #d97706 !important;
padding: 0.125rem 0.25rem !important;
border-radius: 0.25rem !important;
font-size: 0.8125rem !important;
font-family:
ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace !important;
border: none !important;
margin: 0 !important;
text-shadow: none !important;
.dark & {
background-color: #374151 !important;
color: #fbbf24 !important;
}
}
}
@@ -0,0 +1,256 @@
/* Editor-specific styles (animations, transitions, editor-only features) - !these are not applied to sent messages! */
/* Nested content styling with animated borders */
.editor-prose li > *:not(:first-child) {
position: relative;
margin-left: -0.5rem;
}
.editor-prose li:not(:has(> label)) > *:not(:first-child)::before {
content: '';
position: absolute;
left: -0.75rem;
top: 0;
bottom: 0;
width: 1.5px;
background-color: #e5e7eb7e;
transform-origin: top;
animation: expandDown 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.dark .editor-prose li > *:not(:first-child)::before {
background-color: #4b55637b;
}
/* Special handling for nested lists to extend the line properly */
.editor-prose li > ul,
.editor-prose li > ol {
margin-left: -0.5rem;
}
.editor-prose li > ul::before,
.editor-prose li > ol::before {
bottom: -0.25rem; /* Extend slightly below for better visual connection */
}
@keyframes expandDown {
0% {
transform: scaleY(0);
opacity: 0;
}
100% {
transform: scaleY(1);
opacity: 1;
}
}
/* Placeholders for editor-only */
.editor-prose p::before,
.editor-prose h1::before,
.editor-prose h2::before,
.editor-prose h3::before,
.editor-prose h4::before,
.editor-prose h5::before,
.editor-prose h6::before {
content: attr(data-placeholder);
color: #9ca3af;
float: left;
height: 0;
}
.dark .editor-prose p::before,
.dark .editor-prose h1::before,
.dark .editor-prose h2::before,
.dark .editor-prose h3::before,
.dark .editor-prose h4::before,
.dark .editor-prose h5::before,
.dark .editor-prose h6::before {
color: #6b7280;
}
.bnEditor {
outline: none;
padding-inline: 50px;
border-radius: 8px;
/* Define a set of colors to be used throughout the app for consistency
see https://atlassian.design/foundations/color for more info */
--N800: #172b4d; /* Dark neutral used for tooltips and text on light background */
--N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */
}
/*
bnRoot should be applied to all top-level elements
This includes the Prosemirror editor, but also <div> element such as
Tippy popups that are appended to document.body directly
*/
.bnRoot {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.bnRoot *,
.bnRoot *::before,
.bnRoot *::after {
-webkit-box-sizing: inherit;
-moz-box-sizing: inherit;
box-sizing: inherit;
}
/* reset styles, they will be set on blockContent */
.defaultStyles p,
.defaultStyles h1,
.defaultStyles h2,
.defaultStyles h3,
.defaultStyles li {
all: unset !important;
margin: 0;
padding: 0;
font-size: inherit;
/* min width to make sure cursor is always visible */
min-width: 2px !important;
}
.defaultStyles {
font-size: 16px;
font-weight: normal;
font-family:
'Inter',
'SF Pro Display',
-apple-system,
BlinkMacSystemFont,
'Open Sans',
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.dragPreview {
position: absolute;
top: -1000px;
}
@keyframes fadeInScale {
0% {
opacity: 0;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* Animate headers only */
.editor-prose h1,
.editor-prose h2,
.editor-prose h3,
.editor-prose h4,
.editor-prose h5,
.editor-prose h6 {
animation: fadeInScale 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: left center;
}
/* Smooth transitions for all interactive elements */
.editor-prose {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Bold and italic transitions */
.editor-prose strong,
.editor-prose em {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Selected node styling (Notion-like) */
.ProseMirror-selectednode {
box-shadow: 0 0 0 4px #3b82f6;
border-radius: 4px;
background-color: rgba(59, 130, 246, 0.05);
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.dark .ProseMirror-selectednode {
box-shadow: 0 0 0 2px #3a3e44;
background-color: rgba(96, 165, 250, 0.08);
}
/* Ensure selected nodes have proper spacing */
.ProseMirror-selectednode {
margin: 2px;
}
/* Drag and drop containment */
.editor-prose {
position: relative;
overflow: hidden;
contain: layout style;
}
/* Image drag styling */
.editor-prose img.tiptap-image {
cursor: grab;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
max-width: 100%;
height: auto;
}
.editor-prose img.tiptap-image:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: scale(1.02);
}
.editor-prose img.tiptap-image:active {
cursor: grabbing;
transform: scale(0.98);
}
/* Dropcursor styling */
.tiptap-dropcursor {
pointer-events: none;
border-radius: 2px;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Prevent drag operations outside editor */
.editor-prose * {
-webkit-user-drag: auto;
-moz-user-drag: auto;
user-drag: auto;
}
/* Ensure only images within editor are draggable */
.editor-prose img {
-webkit-user-drag: element;
-moz-user-drag: element;
user-drag: element;
}
/* Prevent text selection during drag */
.editor-prose.ProseMirror-dragover * {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
@@ -0,0 +1,196 @@
<script lang="ts">
import { Icon, Bold, Italic, Strikethrough, CodeBracket, ChevronDown } from 'svelte-hero-icons';
import { M } from 'motion-start';
import type { Editor } from '@tiptap/core';
let { editor = $bindable() } = $props<{ editor: Editor | null }>();
// Turn into dropdown state
let showTurnInto = $state(false);
// Turn into options
const turnIntoOptions = [
{ id: 'paragraph', label: 'Text', icon: 'T', iconClass: 'font-mono' },
{ id: 'heading1', label: 'Heading 1', icon: 'H1', iconClass: 'font-bold' },
{ id: 'heading2', label: 'Heading 2', icon: 'H2', iconClass: 'font-bold' },
{ id: 'heading3', label: 'Heading 3', icon: 'H3', iconClass: 'font-bold' },
{ id: 'separator' },
{ id: 'bulletList', label: 'Bulleted list', icon: '•' },
{ id: 'orderedList', label: 'Numbered list', icon: '1.' },
{ id: 'taskList', label: 'To-do list', icon: '☐' },
{ id: 'separator' },
{ id: 'codeBlock', label: 'Code', icon: '</>' },
{ id: 'blockquote', label: 'Quote', icon: '"' }
];
function getCurrentBlockType(): string {
if (!editor) return 'Text';
if (editor.isActive('heading', { level: 1 })) return 'Heading 1';
if (editor.isActive('heading', { level: 2 })) return 'Heading 2';
if (editor.isActive('heading', { level: 3 })) return 'Heading 3';
if (editor.isActive('bulletList')) return 'Bulleted list';
if (editor.isActive('orderedList')) return 'Numbered list';
if (editor.isActive('taskList')) return 'To-do list';
if (editor.isActive('codeBlock')) return 'Code';
if (editor.isActive('blockquote')) return 'Quote';
return 'Text';
}
function turnInto(type: string) {
if (!editor) return;
switch (type) {
case 'paragraph':
editor.chain().focus().setParagraph().run();
break;
case 'heading1':
editor.chain().focus().toggleHeading({ level: 1 }).run();
break;
case 'heading2':
editor.chain().focus().toggleHeading({ level: 2 }).run();
break;
case 'heading3':
editor.chain().focus().toggleHeading({ level: 3 }).run();
break;
case 'bulletList':
editor.chain().focus().toggleBulletList().run();
break;
case 'orderedList':
editor.chain().focus().toggleOrderedList().run();
break;
case 'taskList':
editor.chain().focus().toggleTaskList().run();
break;
case 'codeBlock':
editor.chain().focus().toggleCodeBlock().run();
break;
case 'blockquote':
editor.chain().focus().toggleBlockquote().run();
break;
}
showTurnInto = false;
}
function handleKeydown(event: KeyboardEvent) {
// Close modals/dropdowns on Escape
if (event.key === 'Escape') {
if (showTurnInto) {
showTurnInto = false;
event.preventDefault();
}
}
}
function handleClick(event: MouseEvent) {
// Close turn into dropdown if clicking outside
if (showTurnInto && !(event.target as Element).closest('.turn-into-dropdown')) {
showTurnInto = false;
}
}
</script>
<svelte:window onkeydown={handleKeydown} onclick={handleClick} />
<!-- Main Bubble Menu -->
<M.div
class="flex gap-1 items-center p-1 rounded-lg border shadow-xl backdrop-blur-lg menu dark:bg-zinc-900/90 bg-white/90 dark:border-zinc-700/30 border-zinc-200/50"
layout
transition={{ duration: 0.3, ease: "easeInOut" }}
>
{#if editor}
<M.div
class="flex gap-1 items-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
>
<!-- Turn Into Dropdown -->
<div class="relative turn-into-dropdown">
<M.button
onclick={() => showTurnInto = !showTurnInto}
class="flex gap-1 items-center px-3 py-2 text-sm rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
title="Turn into"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{getCurrentBlockType()}
<M.div
animate={{ rotate: showTurnInto ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<Icon src={ChevronDown} size="14" />
</M.div>
</M.button>
{#if showTurnInto}
<M.div
class="absolute left-0 top-full z-50 mt-1 w-48 bg-white rounded-lg border shadow-xl dark:bg-zinc-800 border-zinc-200/40 dark:border-zinc-700/40"
initial={{ opacity: 0, y: -10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.95 }}
transition={{ duration: 0.15 }}
>
{#each turnIntoOptions as option}
{#if option.id === 'separator'}
<div class="my-1 h-px bg-zinc-200/60 dark:bg-zinc-600/60"></div>
{:else}
<button
onclick={() => turnInto(option.id)}
class="flex gap-2 items-center px-3 py-2 w-full text-sm text-left transition-colors hover:bg-zinc-100/60 dark:hover:bg-zinc-700/40"
>
<span class="{option.iconClass || ''}">{option.icon}</span>
{option.label}
</button>
{/if}
{/each}
</M.div>
{/if}
</div>
<div class="mx-1 w-px h-6 bg-zinc-300 dark:bg-zinc-600"></div>
<M.button
onclick={() => editor.chain().focus().toggleBold().run()}
class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('bold') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }"
title="Bold"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Icon src={Bold} size="16" />
</M.button>
<M.button
onclick={() => editor.chain().focus().toggleItalic().run()}
class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('italic') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }"
title="Italic"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Icon src={Italic} size="16" />
</M.button>
<M.button
onclick={() => editor.chain().focus().toggleStrike().run()}
class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('strike') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }"
title="Strikethrough"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Icon src={Strikethrough} size="16" />
</M.button>
<M.button
onclick={() => editor.chain().focus().toggleCode().run()}
class="p-2 rounded-md transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800 {editor.isActive('code') ? 'bg-zinc-200 dark:bg-zinc-700' : '' }"
title="Code"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<Icon src={CodeBracket} size="16" />
</M.button>
</M.div>
{/if}
</M.div>
@@ -0,0 +1,172 @@
<script lang="ts">
import { slashVisible, slashItems, slashLocation, slashProps, selectedIndex } from './stores';
import { fly } from 'svelte/transition';
import { get } from 'svelte/store';
let height = $state(0);
let elements = $state<any[]>([]);
export function handleKeydown(event: any, editor: any) {
if (!get(slashVisible)) return;
if (event.key === 'ArrowUp') {
event.preventDefault();
upHandler();
return true;
}
if (event.key === 'ArrowDown') {
event.preventDefault();
downHandler();
return true;
}
if (event.key === 'Enter') {
event.preventDefault();
selectItem(editor);
return true;
}
return false;
}
function upHandler() {
const currentIndex = get(selectedIndex);
const itemsLength = get(slashItems).length;
const newIndex = currentIndex === 0 ? itemsLength - 1 : currentIndex - 1;
selectedIndex.set(newIndex);
}
function downHandler() {
const currentIndex = get(selectedIndex);
const itemsLength = get(slashItems).length;
const newIndex = currentIndex === itemsLength - 1 ? 0 : currentIndex + 1;
selectedIndex.set(newIndex);
}
$effect(() => {
const element = elements[$selectedIndex];
if (!element) return;
const container = element.closest('.overflow-auto');
if (!container) return;
const elementRect = element.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const elementTop = elementRect.top - containerRect.top + container.scrollTop;
const elementBottom = elementTop + elementRect.height;
const containerHeight = containerRect.height;
// Check if element is outside visible area
if (elementTop < container.scrollTop) {
// Element is above visible area
container.scrollTop = elementTop - 8;
} else if (elementBottom > container.scrollTop + containerHeight) {
// Element is below visible area
container.scrollTop = elementBottom - containerHeight + 8;
}
});
function selectItem(editor: any) {
const item = get(slashItems)[get(selectedIndex)];
if (item) {
let range = get(slashProps).range;
item.command({ editor, range });
slashVisible.set(false);
}
}
function closeSlashMenu() {
slashVisible.set(false);
selectedIndex.set(0);
}
function handleItemClick(item: any) {
const editor = get(slashProps).editor;
const range = get(slashProps).range;
slashVisible.set(false);
selectedIndex.set(0);
item.command({ editor, range });
}
function getCommandIcon(title: string): string {
const icons: Record<string, string> = {
'To Dos':
'<svg class="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"></path></svg>',
'Heading 1':
'<svg class="w-5 h-5 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path><text x="2" y="18" font-size="12" font-weight="bold" fill="currentColor">H1</text></svg>',
'Heading 2':
'<svg class="w-5 h-5 text-purple-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path><text x="2" y="18" font-size="12" font-weight="bold" fill="currentColor">H2</text></svg>',
'Heading 3':
'<svg class="w-5 h-5 text-purple-200" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path><text x="2" y="18" font-size="12" font-weight="bold" fill="currentColor">H3</text></svg>',
'Bullet List':
'<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>',
'Numbered List':
'<svg class="w-5 h-5 text-orange-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>',
Text: '<svg class="w-5 h-5 text-zinc-300" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 0v12h8V4H6z" clip-rule="evenodd"></path><path d="M8 6h4M8 8h4M8 10h2"></path></svg>',
Quote:
'<svg class="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0-3a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0-3a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0-3a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm8-3a3 3 0 11-6 0 3 3 0 016 0z" clip-rule="evenodd"></path></svg>',
'Code Block':
'<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.316 3.051a1 1 0 01.633 1.265l-4 12a1 1 0 11-1.898-.632l4-12a1 1 0 011.265-.633zM5.707 6.293a1 1 0 010 1.414L3.414 10l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm8.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L16.586 10l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>',
Divider:
'<svg class="w-5 h-5 text-zinc-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg>',
'Bold Text':
'<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20"><path d="M6 4v12h3.5c2.5 0 4.5-2 4.5-4.5S12 7 9.5 7H9V4H6zm3 5.5h.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H9V9.5z"></path></svg>',
'Italic Text':
'<svg class="w-5 h-5 text-pink-400" fill="currentColor" viewBox="0 0 20 20"><path d="M8 4h4l-2 12H6l2-12z"></path></svg>',
Link: '<svg class="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd"></path></svg>',
'Inline Code':
'<svg class="w-5 h-5 text-cyan-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7l2.293 2.293a1 1 0 11-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0zm4.586 0a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 11-1.414-1.414L14.586 7l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>',
};
return (
icons[title] ||
'<svg class="w-5 h-5 text-zinc-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>'
);
}
</script>
<svelte:window bind:innerHeight={height} />
{#if $slashVisible}
<div
class="fixed top-0 w-full h-screen"
onkeydown={() => {}}
onclick={closeSlashMenu}
role="menu"
tabindex="-1">
</div>
<div
transition:fly={{ y: 10, duration: 300 }}
class="overflow-auto absolute pb-2 w-80 max-w-full max-h-80 rounded-xl border shadow-xl backdrop-blur-lg origin-top-left scale-125 dark:bg-zinc-900/70 bg-zinc-100/70 dark:border-zinc-700/20 border-zinc-200"
style="left: {$slashLocation.x}px; top: {$slashLocation.y + $slashLocation.height + 320 > height
? $slashLocation.y - $slashLocation.height - 320
: $slashLocation.y + $slashLocation.height}px;">
<div class="p-2 text-sm text-zinc-500">Basic Blocks</div>
{#each $slashItems as { title, subtitle, command }, i}
<div
class="p-2 flex gap-3 cursor-pointer {i == $selectedIndex &&
'dark:bg-zinc-950/50 bg-zinc-300/50'} dark:hover:bg-zinc-950/30 hover:bg-zinc-300/20 rounded-lg mx-2"
onclick={() => handleItemClick({ command })}
onkeydown={() => {}}
role="menuitem"
tabindex="-1"
bind:this={elements[i]}>
<div class="flex justify-center items-center w-8 h-8 rounded-lg bg-zinc-800">
{@html getCommandIcon(title)}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium truncate dark:text-white">
{title}
</div>
<p class="text-xs truncate text-zinc-400">
{subtitle ? subtitle : ''}
</p>
</div>
</div>
{/each}
</div>
{/if}
@@ -0,0 +1,26 @@
import { Extension } from '@tiptap/core';
import Suggestion from '@tiptap/suggestion';
export default Extension.create({
name: 'slash',
addOptions() {
return {
suggestion: {
char: '/',
command: ({ editor, range, props }: any) => {
props.command({ editor, range });
},
},
};
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
];
},
});
@@ -0,0 +1,46 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
type SlashItems = SlashItem[];
type SlashItem = {
title: string;
subtitle: string;
command: ({ editor, range }: EditorProps) => void;
};
type Component = {
name: string;
description: string;
code: string;
};
type Components = Component[];
type EditorProps = {
editor: any;
range: number | null;
};
type Location = {
x: number;
y: number;
height: number;
};
// For now we'll keep using stores until we can fully convert to runes in all components
export const slashItems: Writable<SlashItems> = writable([]);
export const slashVisible: Writable<boolean> = writable(false);
export const slashLocation: Writable<Location> = writable({
x: 0,
y: 0,
height: 0,
});
export const slashProps: Writable<EditorProps> = writable({
editor: null,
range: null,
});
export const desktopMenu: Writable<boolean> = writable(true);
export const components: Writable<Components> = writable([]);
export const editorWidth: Writable<number> = writable(0);
export const selectedIndex: Writable<number> = writable(0);
@@ -0,0 +1,159 @@
import { slashVisible, slashItems, slashLocation, slashProps, selectedIndex } from './stores';
export default {
items: ({ query }: any) => {
return [
{
title: 'To Dos',
subtitle: 'Create a to do list with checkboxes',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
{
title: 'Heading 1',
subtitle: 'BIG heading',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run();
},
},
{
title: 'Heading 2',
subtitle: 'Less Big heading',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run();
},
},
{
title: 'Heading 3',
subtitle: 'Medium big heading',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run();
},
},
{
title: 'Bullet List',
subtitle: 'Pew pew pew',
command: ({ editor, range }: any) => {
editor.commands.deleteRange(range);
editor.commands.toggleBulletList();
},
},
{
title: 'Numbered List',
subtitle: '1, 2, 3, 4...',
command: ({ editor, range }: any) => {
editor.commands.deleteRange(range);
editor.commands.toggleOrderedList();
},
},
{
title: 'Text',
subtitle: 'Just plain text paragraph',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).setNode('paragraph').run();
},
},
{
title: 'Quote',
subtitle: 'Capture important quotes',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
},
},
{
title: 'Code Block',
subtitle: 'Formatted code snippet',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
},
},
{
title: 'Divider',
subtitle: 'Add a horizontal line',
command: ({ editor, range }: any) => {
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
},
},
{
title: 'Bold Text',
subtitle: 'Make text bold',
command: ({ editor, range }: any) => {
editor.commands.deleteRange(range);
editor.commands.toggleBold();
},
},
{
title: 'Italic Text',
subtitle: 'Make text italic',
command: ({ editor, range }: any) => {
editor.commands.deleteRange(range);
editor.commands.toggleItalic();
},
},
{
title: 'Link',
subtitle: 'Add a web link',
command: ({ editor, range }: any) => {
const url = prompt('Enter the URL:');
if (url) {
editor
.chain()
.focus()
.deleteRange(range)
.setLink({ href: url })
.insertContent('Link text')
.run();
}
},
},
{
title: 'Inline Code',
subtitle: 'Inline code snippet',
command: ({ editor, range }: any) => {
editor.commands.deleteRange(range);
editor.commands.toggleCode();
},
},
]
.filter((item) => item.title.toLowerCase().startsWith(query.toLowerCase()))
.slice(0, 10);
},
render: () => {
return {
onStart: (props: any) => {
let editor = props.editor;
let range = props.range;
let location = props.clientRect();
const editorRect = editor.view.dom.getBoundingClientRect();
slashProps.set({ editor, range });
slashVisible.set(true);
slashLocation.set({
x: location.x - editorRect.left,
y: location.y - editorRect.top + location.height / 2 + 4,
height: location.height,
});
slashItems.set(props.items);
selectedIndex.set(0);
},
onUpdate(props: any) {
slashItems.set(props.items);
selectedIndex.set(0);
},
onKeyDown(props: any) {
if (props.event.key === 'Escape') {
slashVisible.set(false);
return true;
}
},
onExit() {
slashVisible.set(false);
selectedIndex.set(0);
},
};
},
};
@@ -0,0 +1,75 @@
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
white-space: break-spaces;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
}
.ProseMirror [contenteditable="false"] {
white-space: normal;
}
.ProseMirror [contenteditable="false"] [contenteditable="true"] {
white-space: pre-wrap;
}
.ProseMirror pre {
white-space: pre-wrap;
}
img.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
width: 0 !important;
height: 0 !important;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
margin: 0;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-hideselection *::selection {
background: transparent;
}
.ProseMirror-hideselection *::-moz-selection {
background: transparent;
}
.ProseMirror-hideselection * {
caret-color: transparent;
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
.tippy-box[data-animation=fade][data-state=hidden] {
opacity: 0
}
@@ -0,0 +1,238 @@
/* SEQTA Applied styles on DMs (applied to ensure consistency) */
.editor-prose {
font-family: 'Roboto', sans-serif;
border: 0;
padding: 0 8px;
margin: 0;
line-height: 1.2;
/* Removed font size because drag and drop text content within the editor insert html span with font sizing */
font-size: 10pt;
/* Macro: Image */
/* Macro: Image gallery (display) */
/* Fake macro element from plugin "seqta-macro" */
img[data-macro],
a[data-macro] {
border: 2px dashed #ccc;
padding: 8px;
border-radius: 4px;
position: relative;
box-sizing: border-box;
}
img[data-macro].selected,
a[data-macro].selected {
border: 2px solid #204a87;
box-shadow: inset 0 0 4px #204a87;
}
img[data-macro='Resource'] {
background-image: repeating-linear-gradient(
-45deg,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0) 12px,
rgba(0, 0, 0, 0.05) 12px,
rgba(0, 0, 0, 0.05) 24px
);
}
img[data-macro='Embed'] {
background-image: repeating-linear-gradient(
45deg,
rgba(0, 0, 0, 0),
rgba(0, 0, 0, 0) 12px,
rgba(0, 0, 0, 0.05) 12px,
rgba(0, 0, 0, 0.05) 24px
);
}
img[data-macro='Embed'][data-full] {
width: 100%;
}
img[data-macro='Gallery'] {
display: block;
margin: 0 auto;
max-width: 100%;
padding: 0;
}
/* Direqt message-specific styling */
blockquote.forward {
margin: 0;
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
}
blockquote.forward > .preamble {
background: rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 8px;
}
blockquote.forward > .preamble > .date > .label,
blockquote.forward > .preamble > .sender > .label {
color: rgba(0, 0, 0, 0.7);
}
blockquote.forward > .preamble > .date > .value,
blockquote.forward > .preamble > .sender > .value {
color: rgba(0, 0, 0, 0.9);
}
blockquote.forward > .body {
padding: 8px;
}
/** Assessment display **/
.assessmentWrapper {
position: relative;
display: inline-block;
bottom: -15px;
}
.macro-assessment {
padding: 8px 16px;
margin: 0 8px;
border: 4px solid rgba(0, 0, 0, 0.25);
display: inline-block;
width: 256px;
background-color: #fff;
overflow: hidden;
text-shadow: none;
position: relative;
word-wrap: break-word;
min-height: 30px;
}
.macro-assessment > .title {
font-size: 150%;
max-width: 230px;
}
.macro-assessment > .due > span.weight {
padding-left: 24px;
}
.macro-assessment > .due > span.marked {
padding-left: 24px;
font-weight: bold;
}
.macro-assessment > .hidden,
.macro-assessment > .deleted {
font-style: italic;
max-width: 230px;
}
/** Syllabus display **/
.macro-syllabus {
padding: 8px;
margin: 0 8px;
border: 4px solid #eee;
display: inline-block;
max-width: 200px;
background-color: #fff;
overflow: hidden;
color: #444;
text-shadow: none;
position: relative;
bottom: -15px;
}
.macro-syllabus > .label {
font-weight: bold;
}
.macro-syllabus > .extra {
font-style: italic;
}
.macro-syllabus > .meta {
text-transform: uppercase;
font-size: var(--small-text);
color: #999;
}
/* Drop-down menu for plugins like "seqta-macro" */
.cke_panel_block > h1 {
display: none;
}
.cke_panel_block > .cke_panel_list {
list-style: none;
padding: 0;
margin: 0;
}
.cke_panel_block > .cke_panel_list > li {
color: #888;
margin: 0;
cursor: pointer;
}
.cke_panel_block > .cke_panel_list > li:hover {
color: white;
background: #1b315e;
}
.cke_panel_block > .cke_panel_list > li > a {
display: block;
color: inherit;
text-decoration: inherit;
text-transform: uppercase;
font-size: 90%;
padding: 8px; /* Padding on the <a>, not the <li>, so our click target is full size. */
text-shadow: none;
}
.cke_panel_block > .cke_panel_list > li > a > p,
.cke_panel_block > .cke_panel_list > li > a > h1,
.cke_panel_block > .cke_panel_list > li > a > h2,
.cke_panel_block > .cke_panel_list > li > a > h3,
.cke_panel_block > .cke_panel_list > li > a > pre {
margin: 0;
color: inherit;
padding: 0;
}
.moodleFrame > .userHTML {
width: 100%;
height: 600px;
margin: 16px 0 16px 0;
}
.application.restricted {
display: block;
max-width: 320px;
margin: 32px auto;
border: 1px dashed #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: 24px;
background: #f8f8f8;
background-image: -webkit-linear-gradient(315deg, #fff, #f8f8f8);
background-image: linear-gradient(135deg, #fff, #f8f8f8);
border-radius: 8px;
box-sizing: border-box;
}
.application.restricted > .title {
margin: 0;
padding: 0;
font-size: 100%;
font-weight: bold;
color: #666;
}
.application.restricted > .message {
margin: 0;
padding: 0;
font-size: var(--small-text);
}
}
@@ -0,0 +1,236 @@
import type { Plugin } from "@/plugins/core/types";
import { BasePlugin } from "@/plugins/core/settings";
import { defineSettings } from "@/plugins/core/settingsHelpers";
import { waitForElm } from "@/seqta/utils/waitForElm";
import renderSvelte from "@/interface/main";
import BetterEditor from "./BetterEditor.svelte";
import { unmount } from "svelte";
const settings = defineSettings({});
class CustomMessageEditorPlugin extends BasePlugin<typeof settings> {}
const settingsInstance = new CustomMessageEditorPlugin();
const customMessageEditorPlugin: Plugin<typeof settings> = {
id: "custom-message-editor",
name: "Custom Message Editor",
description: "Enhanced message editor with better editing capabilities",
version: "1.0.0",
settings: settingsInstance.settings,
defaultEnabled: true,
run: async (api) => {
let currentShadowContainer: HTMLElement | null = null;
let currentSvelteApp: any = null;
let currentEditorId: string | null = null;
let lastCKEditorContent: string = "";
const cleanup = (resetEditorId = true) => {
if (currentSvelteApp) {
unmount(currentSvelteApp);
currentSvelteApp = null;
}
if (currentShadowContainer) {
currentShadowContainer.remove();
currentShadowContainer = null;
}
if (resetEditorId) {
currentEditorId = null;
}
};
const handleEditorChange = (value: string) => {
if (currentEditorId) {
window.postMessage(
{
type: "ckeditorSetData",
editorId: currentEditorId,
content: value,
},
"*",
);
}
};
const getCKEditorContent = () => {
if (currentEditorId) {
window.postMessage(
{
type: "ckeditorGetData",
editorId: currentEditorId,
},
"*",
);
}
};
const messageListener = (event: MessageEvent) => {
if (event.data.type === "ckeditorGetDataResponse") {
lastCKEditorContent = event.data.data;
console.log("Retrieved CKEditor content:", lastCKEditorContent);
}
};
window.addEventListener("message", messageListener);
const injectBetterEditorButton = async (composer: Element) => {
try {
const pillbox = await waitForElm(
".coneqtMessage.composer .footer .pillbox",
true,
100,
50,
);
if (!pillbox) {
console.error("Could not find pillbox element");
return;
}
if (pillbox.querySelector(".better-editor-btn")) {
return;
}
const betterEditorBtn = document.createElement("button");
betterEditorBtn.type = "button";
betterEditorBtn.className = "notLast editorMode better-editor-btn";
betterEditorBtn.textContent = "Better Editor";
betterEditorBtn.setAttribute("data-key", "better");
const htmlEditorBtn = pillbox.querySelector(
'button[data-key="html"]',
) as HTMLButtonElement;
if (!htmlEditorBtn) {
console.error("Could not find HTML editor button");
return;
}
pillbox.insertBefore(betterEditorBtn, htmlEditorBtn);
betterEditorBtn.addEventListener("click", async () => {
const simpleEditorBtn = pillbox.querySelector(
'button[data-key="content"]',
) as HTMLButtonElement;
if (simpleEditorBtn) {
simpleEditorBtn.click();
}
pillbox.querySelectorAll(".editorMode").forEach((btn) => {
btn.classList.remove("depressed");
});
if (simpleEditorBtn) {
simpleEditorBtn.classList.add("depressed");
}
const wrapper = composer.querySelector(
".prime .body .formattedText .wrapper",
);
const ckeElement = wrapper?.querySelector(".cke");
if (!wrapper || !ckeElement) {
console.error("Could not find wrapper or CKE elements");
return;
}
if (ckeElement.id) {
const ckeMatch = ckeElement.id.match(/^cke_(.+)$/);
if (ckeMatch) {
currentEditorId = ckeMatch[1];
console.log("Found CKEditor ID:", currentEditorId);
}
}
let initialContent = "";
if (currentEditorId) {
window.postMessage(
{
type: "ckeditorGetData",
editorId: currentEditorId,
},
"*",
);
initialContent = await new Promise<string>((resolve) => {
const timeout = setTimeout(() => resolve(""), 1000);
const responseListener = (event: MessageEvent) => {
if (event.data.type === "ckeditorGetDataResponse") {
clearTimeout(timeout);
window.removeEventListener("message", responseListener);
resolve(event.data.data || "");
}
};
window.addEventListener("message", responseListener);
});
}
(ckeElement as HTMLElement).style.display = "none";
cleanup(false);
const shadowContainer = document.createElement("div");
shadowContainer.className = "better-editor-container";
shadowContainer.style.cssText =
"width: 100%; height: 100%; min-height: 200px; overflow-y: scroll; background: var(--background-primary); border-radius: 16px; padding: 4px;";
const shadowRoot = shadowContainer.attachShadow({ mode: "open" });
currentSvelteApp = renderSvelte(BetterEditor, shadowRoot, {
initialContent,
onchange: handleEditorChange,
});
wrapper.appendChild(shadowContainer);
currentShadowContainer = shadowContainer;
pillbox.querySelectorAll(".editorMode").forEach((btn) => {
btn.classList.remove("depressed");
});
betterEditorBtn.classList.add("depressed");
});
pillbox
.querySelectorAll(".editorMode:not(.better-editor-btn)")
.forEach((btn) => {
btn.addEventListener("click", () => {
getCKEditorContent();
cleanup(false);
const wrapper = composer.querySelector(
".prime .body .formattedText .wrapper",
);
const ckeElement = wrapper?.querySelector(".cke");
if (ckeElement) {
(ckeElement as HTMLElement).style.display = "";
}
});
});
} catch (error) {
console.error("Error injecting Better Editor button:", error);
}
};
const { unregister } = api.seqta.onMount(".uiSlidePane", (slidePane) => {
console.log("Found slide pane, checking for message composer");
const messageComposer = slidePane.querySelector(
".coneqtMessage.composer",
);
if (messageComposer) {
console.log("Found message composer, injecting Better Editor button");
injectBetterEditorButton(messageComposer);
}
});
return () => {
cleanup();
unregister();
window.removeEventListener("message", messageListener);
};
},
};
export default customMessageEditorPlugin;
-91
View File
@@ -1,91 +0,0 @@
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")
});
@@ -13,7 +13,3 @@
.dark .userInfoImg {
box-shadow: 0 0 0 3px #ffffff;
}
.userInfosvgdiv {
filter: invert(0) !important;
}
+135 -10
View File
@@ -39,14 +39,43 @@ const zoomHandlers = new WeakMap<
>();
function resetTimetableStyles(): void {
// Reset entry opacity (for assessment hide feature)
const firstDayColumn = document.querySelector(
".dailycal .content .days td",
) as HTMLElement;
if (!firstDayColumn) return;
const baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${baseContainerHeight}px`;
});
const timeColumn = document.querySelector(".times");
if (timeColumn) {
const times = timeColumn.querySelectorAll(".time");
const timeHeight = baseContainerHeight / times.length;
times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px`;
});
}
const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement;
const originalHeight = lessonEl.getAttribute("data-original-height");
if (originalHeight) {
lessonEl.style.height = `${originalHeight}px`;
}
});
const entries = document.querySelectorAll(".entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement;
entryEl.style.opacity = "1";
});
// Clean up zoom control event handlers
const zoomControls = document.querySelector(".timetable-zoom-controls");
if (zoomControls) {
const handlers = zoomHandlers.get(zoomControls);
@@ -65,9 +94,19 @@ function resetTimetableStyles(): void {
async function handleTimetable(): Promise<void> {
await waitForElm(".time", true, 10);
// Convert time format if needed
// Store original heights when timetable loads
const lessons = document.querySelectorAll(".dailycal .lesson");
lessons.forEach((lesson: Element) => {
const lessonEl = lesson as HTMLElement;
lessonEl.setAttribute(
"data-original-height",
lessonEl.offsetHeight.toString(),
);
});
// Existing time format code
if (settingsState.timeFormat == "12") {
const times = document.querySelectorAll(".timetablepage .times .time, .timetablepage .entry.new");
const times = document.querySelectorAll(".timetablepage .times .time");
for (const time of times) {
if (!time.textContent) continue;
time.textContent = convertTo12HourFormat(time.textContent, true);
@@ -81,6 +120,14 @@ async function handleTimetable(): Promise<void> {
function handleTimetableZoom(): void {
console.log("Initializing timetable zoom controls");
// Lazy initialize state variables only when function is first called
let timetableZoomLevel = 1;
let baseContainerHeight: number | null = null;
const originalEntryPositions = new Map<
Element,
{ topRatio: number; heightRatio: number }
>();
// Create zoom controls
const zoomControls = document.createElement("div");
zoomControls.className = "timetable-zoom-controls";
@@ -101,16 +148,16 @@ function handleTimetableZoom(): void {
// Store event listener references
const zoomInHandler = () => {
const seqtaZoomIn = document.querySelector('.uiButton.zoom.in') as HTMLElement;
if (seqtaZoomIn) {
seqtaZoomIn.click();
if (timetableZoomLevel < 2) {
timetableZoomLevel += 0.2;
updateZoom();
}
};
const zoomOutHandler = () => {
const seqtaZoomOut = document.querySelector('.uiButton.zoom.out') as HTMLElement;
if (seqtaZoomOut) {
seqtaZoomOut.click();
if (timetableZoomLevel > 0.6) {
timetableZoomLevel -= 0.2;
updateZoom();
}
};
@@ -122,6 +169,84 @@ function handleTimetableZoom(): void {
zoomIn: zoomInHandler,
zoomOut: zoomOutHandler,
});
const initializePositions = () => {
// Get the base container height from the first TD
const firstDayColumn = document.querySelector(
".dailycal .content .days td",
) as HTMLElement;
if (!firstDayColumn) return false;
baseContainerHeight =
parseInt(firstDayColumn.style.height) || firstDayColumn.offsetHeight;
// Store original ratios
const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement;
// Calculate ratios relative to detected base height
if (baseContainerHeight === null) return;
const topRatio = parseInt(entryEl.style.top) / baseContainerHeight;
const heightRatio = parseInt(entryEl.style.height) / baseContainerHeight;
originalEntryPositions.set(entry, { topRatio, heightRatio });
});
return true;
};
const updateZoom = () => {
// Initialize positions if not already done
if (baseContainerHeight === null && !initializePositions()) {
console.error("Failed to initialize positions");
return;
}
console.debug(`Updating zoom level to: ${timetableZoomLevel}`);
// Calculate new container height
if (baseContainerHeight === null) return;
const newContainerHeight = baseContainerHeight * timetableZoomLevel;
// Update all day columns (TDs)
const dayColumns = document.querySelectorAll(".dailycal .content .days td");
dayColumns.forEach((td: Element) => {
(td as HTMLElement).style.height = `${newContainerHeight}px`;
});
// Update all entries using stored ratios
const entries = document.querySelectorAll(".entriesWrapper .entry");
entries.forEach((entry: Element) => {
const entryEl = entry as HTMLElement;
const originalRatios = originalEntryPositions.get(entry);
if (originalRatios) {
// Calculate new positions from original ratios
const newTop = originalRatios.topRatio * newContainerHeight;
const newHeight = originalRatios.heightRatio * newContainerHeight;
// Apply new values
entryEl.style.top = `${Math.round(newTop)}px`;
entryEl.style.height = `${Math.round(newHeight)}px`;
}
});
// Update time column to match
const timeColumn = document.querySelector(".times");
if (timeColumn) {
const times = timeColumn.querySelectorAll(".time");
const timeHeight = newContainerHeight / times.length;
times.forEach((time: Element) => {
(time as HTMLElement).style.height = `${timeHeight}px`;
});
}
entries[Math.round((entries.length - 1) / 2)].scrollIntoView({
behavior: "instant",
block: "center",
});
};
}
function handleTimetableAssessmentHide(): void {
-66
View File
@@ -1,66 +0,0 @@
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);
}
+5 -9
View File
@@ -1,19 +1,17 @@
import { PluginManager } from "./core/manager";
// Lightweight plugins (load immediately)
// plugins
import timetablePlugin from "./built-in/timetable";
import notificationCollectorPlugin from "./built-in/notificationCollector";
import themesPlugin from "./built-in/themes";
import animatedBackgroundPlugin from "./built-in/animatedBackground";
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
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 customMessageEditorPlugin from "./built-in/customMessageEditor";
//import testPlugin from './built-in/test';
// Heavy plugins (lazy-loaded only when enabled)
import globalSearchPluginLazy from "./built-in/globalSearch/lazy";
// Initialize plugin manager
const pluginManager = PluginManager.getInstance();
@@ -23,14 +21,12 @@ pluginManager.registerPlugin(animatedBackgroundPlugin);
pluginManager.registerPlugin(assessmentsAveragePlugin);
pluginManager.registerPlugin(notificationCollectorPlugin);
pluginManager.registerPlugin(timetablePlugin);
pluginManager.registerPlugin(globalSearchPlugin);
pluginManager.registerPlugin(profilePicturePlugin);
pluginManager.registerPlugin(assessmentsOverviewPlugin);
pluginManager.registerPlugin(backgroundMusicPlugin);
pluginManager.registerPlugin(customMessageEditorPlugin);
//pluginManager.registerPlugin(testPlugin);
// Register heavy plugins with lazy loading
pluginManager.registerPlugin(globalSearchPluginLazy);
export { init as Monofile } from "./monofile";
export async function initializePlugins(): Promise<void> {
-2
View File
@@ -24,8 +24,6 @@ import loading from "@/seqta/ui/Loading";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
//import { OpenMinecraftServerPopup } from "@/seqta/utils/AboutMinecraftServer";
import {
updateTimetableTimes,
} from "@/seqta/utils/updateTimetableTimes";
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -16,9 +16,9 @@ export async function main() {
if (settingsState.onoff) {
injectPageState();
// 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
// TEMP FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
if (import.meta.env.MODE === "development") {
import("@/css/injected.scss");
import("../css/injected.scss");
} else {
const injectedStyle = document.createElement("style");
injectedStyle.textContent = injectedCSS;
+2 -197
View File
@@ -7,21 +7,6 @@ interface ContentConfig {
[key: string]: ElementConfig;
}
// Track processed elements to avoid re-randomizing
const processedElements = new WeakSet<Element>();
function debounce(func: Function, wait: number): Function {
let timeout: NodeJS.Timeout;
return function executedFunction(...args: any[]) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function getRandomElement(array: string[]): string {
return array[Math.floor(Math.random() * array.length)];
}
@@ -179,32 +164,9 @@ const contentConfig: ContentConfig = {
},
},
forumTopics: {
selector: "#menu .sub ul li:not([data-colour]):not(.hasChildren) label",
selector: "#menu .sub ul li label",
action: (element) => {
// Only redact if not in assessments section
const assessmentsSection = element.closest('[data-key="assessments"]');
if (!assessmentsSection) {
element.textContent = "Forum Topic Redacted";
}
},
},
assessmentSubjects: {
selector: '[data-key="assessments"] .sub ul li[data-colour] label',
action: (element) => {
element.textContent = getRandomElement(mockData.subjects);
},
},
assessmentYearGroups: {
selector: '[data-key="assessments"] .sub ul li.hasChildren:not([data-colour]) label',
action: (element) => {
const yearGroup = Math.floor(Math.random() * 5) + 8; // Years 8-12
element.textContent = `Year ${yearGroup}`;
},
},
assessmentSubYearGroups: {
selector: '[data-key="assessments"] .sub .sub ul li[data-colour] label',
action: (element) => {
element.textContent = getRandomElement(mockData.subjects);
},
},
courseNames: {
@@ -579,168 +541,11 @@ export function getMockNotices() {
};
}
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() {
export default function hideSensitiveContent() {
Object.entries(contentConfig).forEach(([_, { selector, action }]) => {
const elements = document.querySelectorAll(selector);
elements.forEach((element: Element) => {
// Only process elements that haven't been processed before
if (!processedElements.has(element)) {
action(element);
processedElements.add(element);
}
});
});
}
let observer: MutationObserver | null = null;
let intervalId: NodeJS.Timeout | null = null;
export default function hideSensitiveContent() {
// Initial processing of existing elements
processNewElements();
// Set up MutationObserver if not already created
if (!observer) {
observer = new MutationObserver((mutations) => {
let shouldProcess = false;
mutations.forEach((mutation) => {
// Check for both childList and subtree changes
if (mutation.type === 'childList') {
// Check added nodes
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
// Check if the added element or its children match any of our selectors
for (const config of Object.values(contentConfig)) {
if (element.matches?.(config.selector) || element.querySelector?.(config.selector)) {
shouldProcess = true;
break;
}
}
}
});
}
// Also trigger on large DOM replacements (like page navigation)
if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) {
shouldProcess = true;
}
}
// Check for attribute changes that might affect our selectors
if (mutation.type === 'attributes') {
const target = mutation.target as Element;
for (const config of Object.values(contentConfig)) {
if (target.matches?.(config.selector)) {
shouldProcess = true;
break;
}
}
}
});
if (shouldProcess) {
debouncedProcessElements();
}
});
// Start observing with more comprehensive options
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'id'] // Watch for class/id changes that might affect our selectors
});
}
// Fallback: periodic check for new elements (especially useful for SPA navigation)
if (!intervalId) {
intervalId = setInterval(() => {
debouncedProcessElements();
}, 500); // Check every 500ms as a fallback
}
}
// Function to stop observing (useful for cleanup)
export function stopHidingSensitiveContent() {
if (observer) {
observer.disconnect();
observer = null;
}
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
-202
View File
@@ -1,202 +0,0 @@
import { settingsState } from "./listeners/SettingsState";
import { animate, stagger } from "motion";
import stringToHTML from "./stringToHTML";
export async function DeleteWhatsNew() {
const bkelement = document.getElementById("whatsnewbk");
const popup = document.querySelector(".whatsnewContainer") as HTMLElement;
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 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 background = document.createElement("div");
background.id = "whatsnewbk";
background.classList.add("whatsnewBackground");
const container = document.createElement("div");
container.classList.add("whatsnewContainer");
var header: any = stringToHTML(
/* html */
`<div class="whatsnewHeader">
<h1>Minecraft Server</h1>
<p>The official BetterSEQTA+ Minecraft Server</p>
</div>`,
).firstChild;
let imagecont = document.createElement("div");
imagecont.classList.add("whatsnewImgContainer");
let video = document.createElement("video");
video.style.aspectRatio = "16/9";
video.style.background = "black";
let 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");
imagecont.appendChild(video);
let textcontainer = document.createElement("div");
textcontainer.classList.add("whatsnewTextContainer");
let text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%; overflow-y: scroll;">
<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;
let 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;
let exitbutton = document.createElement("div");
exitbutton.id = "whatsnewclosebutton";
container.append(
header,
imagecont,
text as HTMLElement,
footer as HTMLElement,
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();
});
}
+8 -21
View File
@@ -7,8 +7,6 @@ import renderSvelte from "@/interface/main";
import { SettingsResizer } from "@/seqta/ui/SettingsResizer";
import Settings from "@/interface/pages/settings.svelte";
let isSettingsRendered = false;
export function addExtensionSettings() {
const extensionPopup = document.createElement("div");
extensionPopup.classList.add("outside-container", "hide");
@@ -19,6 +17,14 @@ export function addExtensionSettings() {
) as HTMLDivElement;
if (extensionContainer) extensionContainer.appendChild(extensionPopup);
// create shadow dom and render svelte app
try {
const shadow = extensionPopup.attachShadow({ mode: "open" });
requestIdleCallback(() => renderSvelte(Settings, shadow));
} catch (err) {
console.error(err);
}
const container = document.getElementById("container");
new SettingsResizer();
@@ -32,22 +38,3 @@ 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);
}
}
+1 -4
View File
@@ -26,9 +26,6 @@ export function addShortcuts(shortcuts: any) {
function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
// Creates the stucture and element information for each seperate shortcut
const container = document.getElementById("shortcuts");
if (!container) return;
let shortcut = document.createElement("a");
shortcut.setAttribute("href", link);
shortcut.setAttribute("target", "_blank");
@@ -45,5 +42,5 @@ function createNewShortcut(link: any, icon: any, viewBox: any, title: any) {
shortcutdiv.append(text);
shortcut.append(shortcutdiv);
container.appendChild(shortcut);
document.getElementById("shortcuts")!.appendChild(shortcut);
}
@@ -2,9 +2,6 @@ import stringToHTML from "../stringToHTML";
export function CreateCustomShortcutDiv(element: any) {
// Creates the stucture and element information for each seperate shortcut
const container = document.getElementById("shortcuts");
if (!container) return;
var shortcut = document.createElement("a");
shortcut.setAttribute("href", element.url);
shortcut.setAttribute("target", "_blank");
@@ -48,5 +45,5 @@ export function CreateCustomShortcutDiv(element: any) {
shortcutdiv.append(text);
shortcut.append(shortcutdiv);
container.append(shortcut);
document.getElementById("shortcuts")!.append(shortcut);
}
@@ -0,0 +1,28 @@
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();
}
});
});
}
+27 -28
View File
@@ -4,15 +4,15 @@ import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
import coursesicon from "@/seqta/icons/coursesIcon";
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
import { addShortcuts } from "../Adders/AddShortcuts";
import { convertTo12HourFormat } from "../convertTo12HourFormat";
import { delay } from "../delay";
import { settingsState } from "../listeners/SettingsState";
import stringToHTML from "../stringToHTML";
import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement";
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent";
import { setupFixedTooltips } from "@/seqta/utils/fixedTooltip";
let LessonInterval: any;
let currentSelectedDate = new Date();
@@ -43,7 +43,7 @@ export async function loadHomePage() {
const homeContainer = document.getElementById("home-root");
if (!homeContainer) return;
const skeletonStructure = stringToHTML(/* html */`
const skeletonStructure = stringToHTML(`
<div class="home-container" id="home-container">
<div class="border shortcut-container">
<div class="border shortcuts" id="shortcuts"></div>
@@ -99,7 +99,12 @@ export async function loadHomePage() {
const cleanup = setupTimetableListeners();
renderShortcuts();
try {
addShortcuts(settingsState.shortcuts);
} catch (err: any) {
console.error("[BetterSEQTA+] Error adding shortcuts:", err.message || err);
}
AddCustomShortcutsToPage();
const date = new Date();
const TodayFormatted = formatDate(date);
@@ -361,6 +366,15 @@ function comparedate(obj1: any, obj2: any) {
return 0;
}
async function AddCustomShortcutsToPage() {
let customshortcuts: any = settingsState.customshortcuts;
if (customshortcuts.length > 0) {
for (let i = 0; i < customshortcuts.length; i++) {
const element = customshortcuts[i];
CreateCustomShortcutDiv(element);
}
}
}
function processNotices(response: any, labelArray: string[]) {
const NoticeContainer = document.getElementById("notice-container");
@@ -405,14 +419,9 @@ function processNoticeColor(colour: string): string | undefined {
}
function createNoticeElement(notice: any, colour: string | undefined): Node {
const textPreview = notice.contents
.replace(/<[^>]*>/g, "")
const cleanContent = notice.contents
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/\s+/g, " ")
.trim()
.substring(0, 150)
+ (notice.contents.length > 150 ? "..." : "");
.replace(/ +/, " ");
const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const htmlContent = `
@@ -427,7 +436,7 @@ function createNoticeElement(notice: any, colour: string | undefined): Node {
<button class="notice-close-btn" style="opacity: 0; pointer-events: none;">&times;</button>
</div>
<h2 class="notice-content-title">${notice.title}</h2>
<div class="notice-content-body">${textPreview}</div>
<div class="notice-content-body">${cleanContent}</div>
</div>`;
const element = stringToHTML(htmlContent).firstChild as HTMLElement;
@@ -619,13 +628,12 @@ function openNoticeModal(
// Get the current scale applied to the source element and compensate for it
const computedStyle = getComputedStyle(sourceElement);
const transform = computedStyle.transform;
let scaleX = 1,
scaleY = 1;
let scaleX = 1, scaleY = 1;
if (transform && transform !== "none") {
if (transform && transform !== 'none') {
const matrix = transform.match(/matrix.*\((.+)\)/);
if (matrix) {
const values = matrix[1].split(", ");
const values = matrix[1].split(', ');
scaleX = parseFloat(values[0]);
scaleY = parseFloat(values[3]);
}
@@ -957,7 +965,7 @@ function makeLessonDiv(lesson: any, num: number) {
.join("");
lessonString += `
<div class="fixed-tooltip assessmenttooltip">
<div class="tooltip assessmenttooltip">
<svg style="width:28px;height:28px;border-radius:0;" viewBox="0 0 24 24">
<path fill="#ed3939" d="M16 2H4C2.9 2 2 2.9 2 4V20C2 21.11 2.9 22 4 22H16C17.11 22 18 21.11 18 20V4C18 2.9 17.11 2 16 2M16 20H4V4H6V12L8.5 9.75L11 12V4H16V20M20 15H22V17H20V15M22 7V13H20V7H22Z" />
</svg>
@@ -967,9 +975,8 @@ function makeLessonDiv(lesson: any, num: number) {
}
lessonString += "</div>";
const element = stringToHTML(lessonString);
setupFixedTooltips(element);
return element;
return stringToHTML(lessonString);
}
function buildAssessmentURL(programmeID: any, metaID: any, itemID = "") {
@@ -1102,14 +1109,6 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
}
}
FilterUpcomingAssessments(settingsState.subjectfilters);
if (assessments.length === 0) {
upcomingitemcontainer!.innerHTML = `
<div class="day-empty">
<img src="${browser.runtime.getURL(LogoLight)}" />
<p>No assessments available.</p>
</div>`;
}
}
function createAssessmentDateDiv(date: string, value: any, datecase?: any) {
+9 -15
View File
@@ -1,4 +1,5 @@
import stringToHTML from "../stringToHTML";
import browser from "webextension-polyfill";
import { settingsState } from "../listeners/SettingsState";
import { animate, stagger } from "motion";
import { DeleteWhatsNew } from "../Whatsnew";
@@ -15,7 +16,7 @@ export function OpenAboutPage() {
/* html */
`<div class="whatsnewHeader">
<h1>About</h1>
<p>About the extension</p>
<p>BetterSEQTA+ V${browser.runtime.getManifest().version}</p>
</div>`,
).firstChild;
@@ -33,7 +34,7 @@ export function OpenAboutPage() {
</h1>
<div style="max-width: 600px; margin: auto;">
<img
src="https://contrib.rocks/image?repo=BetterSEQTA/BetterSEQTA-Plus&columns=14"
src="https://contrib.rocks/image?repo=BetterSEQTA/BetterSEQTA-Plus&columns=13"
style="width: 100%; max-width: 500px; height: auto; object-fit: contain; display: block; margin: -110px auto 0;">
</div>
</div>
@@ -42,29 +43,22 @@ export function OpenAboutPage() {
let 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>
Report bugs and feedback:
<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>
<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>
</div>
</div>
`).firstChild;
-26
View File
@@ -1,26 +0,0 @@
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);
}
}
}
+1 -16
View File
@@ -18,25 +18,10 @@ export async function SendNewsPage() {
const main = document.getElementById("main");
main!.innerHTML = "";
const displayCountry = (() => {
switch (settingsState.newsSource?.toLowerCase()) {
case "usa": return "the USA";
case "uk": return "the UK";
case "netherlands": return "the Netherlands";
default:
return settingsState.newsSource
? settingsState.newsSource
.split("_")
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
: "Australia";
}
})();
const html = stringToHTML(/* html */ `
<div class="home-root">
<div class="home-container" id="news-container">
<h1 class="border">Latest Headlines in ${displayCountry}</h1>
<h1 class="border">Latest Headlines in ${settingsState.newsSource ? settingsState.newsSource.charAt(0).toUpperCase() + settingsState.newsSource.slice(1) : "Australia"}</h1>
</div>
</div>`);
+10 -56
View File
@@ -41,7 +41,7 @@ export function OpenWhatsNewPopup() {
let imagecont = document.createElement("div");
imagecont.classList.add("whatsnewImgContainer");
let video = document.createElement("video");
/* let video = document.createElement("video");
let source = document.createElement("source");
source.setAttribute(
@@ -53,58 +53,19 @@ export function OpenWhatsNewPopup() {
video.loop = true;
video.appendChild(source);
video.classList.add("whatsnewImg");
imagecont.appendChild(video);
imagecont.appendChild(video); */
/* let whatsnewimg = document.createElement("img");
let whatsnewimg = document.createElement("img");
//whatsnewimg.src = "https://raw.githubusercontent.com/BetterSEQTA/BetterSEQTA-Plus/main/src/resources/update-image.webp";
whatsnewimg.src = browser.runtime.getURL('../../resources/update-image.webp');
whatsnewimg.classList.add("whatsnewImg");
imagecont.appendChild(whatsnewimg); */
imagecont.appendChild(whatsnewimg);
let textcontainer = document.createElement("div");
textcontainer.classList.add("whatsnewTextContainer");
let text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: scroll;">
<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>
@@ -296,29 +257,22 @@ export function OpenWhatsNewPopup() {
let 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>
Report bugs and feedback:
<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>
<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>
</div>
<div>
<a href="https://ko-fi.com/sethburkart" target="_blank" style="background: none !important; margin:0;margin-left:6px; padding:0; display: flex; align-items: center;">
-59
View File
@@ -1,59 +0,0 @@
export function setupFixedTooltips(root: Document | HTMLElement = document) {
const elements = root.querySelectorAll<HTMLElement>(".fixed-tooltip");
elements.forEach((tooltip) => {
const text = tooltip.querySelector<HTMLElement>(".tooltiptext");
if (!text) return;
tooltip.removeChild(text);
text.classList.add("tooltiptext-fixed");
let hideTimeout: number | undefined;
const show = () => {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = undefined;
}
document.body.appendChild(text);
const rect = tooltip.getBoundingClientRect();
text.style.left = `${rect.left + rect.width / 2}px`;
text.style.top = `${rect.bottom + 5}px`;
requestAnimationFrame(() => text.classList.add("show"));
};
const scheduleHide = () => {
hideTimeout = window.setTimeout(() => {
text.classList.remove("show");
setTimeout(() => {
if (text.parentElement === document.body) {
document.body.removeChild(text);
}
}, 200);
}, 300);
};
tooltip.addEventListener("mouseenter", show);
tooltip.addEventListener("mouseleave", scheduleHide);
tooltip.addEventListener("blur", scheduleHide);
tooltip.addEventListener("click", scheduleHide);
text.addEventListener("mouseenter", () => {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = undefined;
}
});
text.addEventListener("mouseleave", scheduleHide);
text.addEventListener("click", () => {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = undefined;
}
text.classList.remove("show");
setTimeout(() => {
if (text.parentElement === document.body) {
document.body.removeChild(text);
}
}, 200);
});
});
}
-18
View File
@@ -1,18 +0,0 @@
import { settingsState } from "./listeners/SettingsState";
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent";
function maybeHide() {
if (settingsState.hideSensitiveContent) {
hideSensitiveContent();
}
}
export function initializeHideSensitiveToggle() {
maybeHide();
window.addEventListener("hashchange", maybeHide);
settingsState.register("hideSensitiveContent", (val) => {
if (val) {
maybeHide();
}
});
}
+1 -20
View File
@@ -1,6 +1,5 @@
import { waitForElm } from "@/seqta/utils/waitForElm";
import ReactFiber from "../ReactFiber";
import { delay } from "../delay";
const handleNotificationClick = async (target: HTMLElement) => {
const notificationItem = target.closest(
@@ -18,7 +17,7 @@ const handleNotificationClick = async (target: HTMLElement) => {
if (!buttonId) return;
const matchingNotification =
notificationList.items.find(
notificationList.storeState.notifications.items.find(
(item: any) => item.notificationID === parseInt(buttonId),
);
@@ -34,24 +33,6 @@ const handleNotificationClick = async (target: HTMLElement) => {
'[class*="notifications__notifications___"] > button',
) as HTMLButtonElement | null;
notificationButton?.click();
await delay(10);
const button = document.querySelector('[class*="MessageList__selected___"]');
if (button) {
(button as HTMLElement).click();
}
// send a network request to mark as read
fetch('/seqta/student/save/message', {
method: "POST",
credentials: "include",
body: JSON.stringify({
items: [matchingNotification.message.messageID],
mode: 'x-read',
read: true,
}),
});
};
const clickListeners = [
+1 -25
View File
@@ -57,37 +57,13 @@ class EventManager {
return { unregister };
}
private buildSelector(options: EventListenerOptions): string | null {
if (options.textContent || options.customCheck) return null;
let selector = options.elementType || "";
if (options.id) {
selector += `#${CSS.escape(options.id)}`;
}
if (options.className) {
selector += `.${CSS.escape(options.className)}`;
}
return selector.trim() || null;
}
private async scanExistingElements(
options: EventListenerOptions,
callback: (element: Element) => void,
): Promise<void> {
const root = options.parentElement || document.documentElement;
const selector = this.buildSelector(options);
let elements: Element[] = [];
if (selector) {
elements = Array.from(root.querySelectorAll(selector));
if (selector && root.matches && root.matches(selector)) {
const elements = Array.from(root.getElementsByTagName("*"));
elements.unshift(root);
}
} else {
elements = Array.from(root.getElementsByTagName("*"));
elements.unshift(root);
}
for (let i = 0; i < elements.length; i += this.chunkSize) {
const chunk = elements.slice(i, i + this.chunkSize);
+15 -75
View File
@@ -8,17 +8,15 @@ type GlobalChangeListener = (newValue: any, oldValue: any, key: string) => void;
class StorageManager {
private static instance: StorageManager;
private data: SettingsState;
private listeners: Map<string, Set<ChangeListener>>;
private globalListeners: Set<GlobalChangeListener>;
private listeners: { [key: string]: ChangeListener[] };
private globalListeners: GlobalChangeListener[];
private subscribers: Set<Subscriber<SettingsState>> = new Set();
private saveTimeout: NodeJS.Timeout | null = null;
private initialized = false;
private constructor() {
this.data = {} as SettingsState;
this.listeners = new Map();
this.globalListeners = new Set();
// Don't call async loadFromStorage in constructor
this.listeners = {};
this.globalListeners = [];
this.loadFromStorage();
const handler: ProxyHandler<StorageManager> = {
get: (target, prop: keyof SettingsState | "register" | "initialize") => {
@@ -28,13 +26,8 @@ class StorageManager {
return Reflect.get(target.data, prop);
},
set: (target, prop: keyof SettingsState, value) => {
const oldValue = target.data[prop];
// Only save if the reference actually changed
if (oldValue !== value) {
Reflect.set(target.data, prop, value);
target.saveToStorage();
}
return true;
},
deleteProperty: (target, prop: keyof SettingsState) => {
@@ -42,9 +35,8 @@ class StorageManager {
if (oldValue !== undefined) {
delete target.data[prop];
target.removeFromStorage(prop);
const listeners = target.listeners.get(prop as string);
if (listeners) {
for (const listener of listeners) {
if (target.listeners[prop]) {
for (const listener of target.listeners[prop]) {
listener(undefined, oldValue);
}
}
@@ -67,10 +59,7 @@ class StorageManager {
public static async initialize(): Promise<StorageManager & SettingsState> {
const instance = StorageManager.getInstance();
if (!instance.initialized) {
await instance.loadFromStorage();
instance.initialized = true;
}
return instance;
}
@@ -78,19 +67,8 @@ class StorageManager {
key: K,
value: SettingsState[K],
): void {
const oldValue = this.data[key];
if (oldValue !== value) {
this.data[key] = value;
this.saveToStorage();
// Notify listeners
const listeners = this.listeners.get(key as string);
if (listeners) {
for (const listener of listeners) {
listener(value, oldValue);
}
}
}
}
private async loadFromStorage(): Promise<void> {
@@ -100,11 +78,7 @@ class StorageManager {
});
}
public async saveToStorage(): Promise<void> {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
this.saveTimeout = null;
}
private async saveToStorage(): Promise<void> {
// @ts-expect-error
await browser.storage.local.set(this.data);
this.notifySubscribers();
@@ -117,38 +91,22 @@ class StorageManager {
private initStorageListener(): void {
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName === "local") {
const actualChanges: string[] = [];
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
// Only process if value actually changed
if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
if (newValue !== undefined) {
(this.data as any)[key] = newValue;
} else {
delete (this.data as any)[key];
}
actualChanges.push(key);
// Notify specific listeners
const listeners = this.listeners.get(key);
if (listeners) {
for (const listener of listeners) {
if (this.listeners[key]) {
for (const listener of this.listeners[key]) {
listener(newValue, oldValue);
}
}
}
}
// Only notify global listeners if there were actual changes
if (actualChanges.length > 0 && this.globalListeners.size > 0) {
for (const listener of this.globalListeners) {
for (const key of actualChanges) {
const { oldValue, newValue } = changes[key];
listener(newValue, oldValue, key);
}
}
}
}
});
}
@@ -158,36 +116,18 @@ class StorageManager {
* @param listener The listener to call when the setting changes -> takes two arguments, (newValue, oldValue)
*/
public register(prop: keyof SettingsState, listener: ChangeListener): void {
const key = prop as string;
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
if (!this.listeners[prop]) {
this.listeners[prop] = [];
}
this.listeners.get(key)!.add(listener);
}
/**
* Unregister a listener for a setting.
* @param prop The setting to stop listening to.
* @param listener The listener to remove.
*/
public unregister(prop: keyof SettingsState, listener: ChangeListener): void {
this.listeners.get(prop as string)?.delete(listener);
this.listeners[prop].push(listener);
}
/**
* Register a listener for any setting.
* @param listener The listener to call when any setting changes -> takes three arguments, (newValue, oldValue, key)
* @param listener The listener to call when any setting changes -> takes two arguments, (newValue, oldValue)
*/
public registerGlobal(listener: GlobalChangeListener): void {
this.globalListeners.add(listener);
}
/**
* Unregister a global listener.
* @param listener The listener to remove.
*/
public unregisterGlobal(listener: GlobalChangeListener): void {
this.globalListeners.delete(listener);
this.globalListeners.push(listener);
}
/**
+43 -6
View File
@@ -1,9 +1,10 @@
import { settingsState } from "./SettingsState";
import { updateAllColors } from "@/seqta/ui/colors/Manager";
// Shortcuts rendering
import { renderShortcuts } from "@/seqta/utils/Render/renderShortcuts";
import { addShortcuts } from "@/seqta/utils/Adders/AddShortcuts";
import { CreateCustomShortcutDiv } from "@/seqta/utils/CreateEnable/CreateCustomShortcutDiv";
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
import { RemoveShortcutDiv } from "@/seqta/utils/DisableRemove/RemoveShortcutDiv";
import browser from "webextension-polyfill";
import type { CustomShortcut } from "@/types/storage";
@@ -45,16 +46,52 @@ export class StorageChangeHandler {
newValue: CustomShortcut[],
oldValue: CustomShortcut[],
) {
if (!newValue || !oldValue) return;
renderShortcuts();
if (newValue) {
if (newValue.length > oldValue.length) {
CreateCustomShortcutDiv(newValue[oldValue.length]);
} else if (newValue.length < oldValue.length) {
const removedElement = oldValue.find(
(oldItem: any) =>
!newValue.some(
(newItem: any) =>
JSON.stringify(oldItem) === JSON.stringify(newItem),
),
);
if (removedElement) {
RemoveShortcutDiv([removedElement]);
}
}
}
}
private handleShortcutsChange(
newValue: { enabled: boolean; name: string }[],
oldValue: { enabled: boolean; name: string }[],
) {
if (!newValue || !oldValue) return;
renderShortcuts();
const addedShortcuts = newValue.filter((newItem: any) => {
const wasDisabledAndNowEnabled = oldValue.some((oldItem: any) => {
return oldItem.name === newItem.name && !oldItem.enabled && newItem.enabled;
});
const isNewShortcut = !oldValue.some((oldItem: any) => oldItem.name === newItem.name);
return (wasDisabledAndNowEnabled || isNewShortcut) && newItem.enabled;
});
const removedShortcuts = newValue.filter((newItem: any) => {
const isRemoved = oldValue.some((oldItem: any) => {
const match = oldItem.name === newItem.name;
const wasEnabled = oldItem.enabled;
const isDisabled = !newItem.enabled;
return match && wasEnabled && isDisabled;
});
return isRemoved;
});
addShortcuts(addedShortcuts);
RemoveShortcutDiv(removedShortcuts);
}
private handleTransparencyEffectsChange(newValue: boolean) {
-6
View File
@@ -5,8 +5,6 @@ import {
} from "./Closers/closeExtensionPopup";
import { animate } from "motion";
import { settingsState } from "./listeners/SettingsState";
import { renderSettingsIfNeeded } from "./Adders/AddExtensionSettings";
import { delay } from "./delay";
export function setupSettingsButton() {
var AddedSettings = document.getElementById("AddedSettings");
@@ -16,10 +14,6 @@ export function setupSettingsButton() {
if (SettingsClicked) {
closeExtensionPopup(extensionPopup as HTMLElement);
} else {
renderSettingsIfNeeded();
await delay(30);
if (settingsState.animations) {
animate(0, 1, {
onUpdate: (progress) => {
-14
View File
@@ -37,20 +37,6 @@ function updateTimeElements(): void {
const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", "");
el.textContent = `${start12}${end12}`;
});
const quickbarTimes = document.querySelectorAll<HTMLElement>(".quickbar .meta .times");
quickbarTimes.forEach((el) => {
if (!el.dataset.original) el.dataset.original = el.textContent || "";
const original = el.dataset.original || "";
if (!original.includes("") && !original.includes("-")) return;
const [start, end] = original.split(/[-]/).map((p) => p.trim());
if (!start || !end) return;
const start12 = convertTo12HourFormat(start).toLowerCase().replace(" ", "");
const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", "");
el.textContent = `${start12}${end12}`;
});
}
function checkIfOnTimetablePage(): boolean {
-1
View File
@@ -37,7 +37,6 @@ export interface SettingsState {
originalDarkMode?: boolean;
newsSource?: string;
mockNotices?: boolean;
hideSensitiveContent?: boolean;
// depreciated keys
animatedbk: boolean;
-4
View File
@@ -84,10 +84,6 @@ export default defineConfig(({ command }) => ({
settings: join(__dirname, "src", "interface", "index.html"),
pageState: join(__dirname, "src", "pageState.js"),
},
onwarn(warning, warn) {
if (warning.code === "FILE_NAME_CONFLICT") return;
warn(warning);
},
},
},
}));