mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
Merge branch 'BetterSEQTA:main' into main
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
"mathjs": "^14.4.0",
|
"mathjs": "^14.4.0",
|
||||||
"million": "^3.1.11",
|
"million": "^3.1.11",
|
||||||
"motion": "^12.4.12",
|
"motion": "^12.4.12",
|
||||||
|
"pdfjs-dist": "^5.4.530",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"react": "17",
|
"react": "17",
|
||||||
"react-best-gradient-color-picker": "3.0.11",
|
"react-best-gradient-color-picker": "3.0.11",
|
||||||
@@ -265,6 +266,30 @@
|
|||||||
|
|
||||||
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="],
|
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.89", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.89", "@napi-rs/canvas-darwin-arm64": "0.1.89", "@napi-rs/canvas-darwin-x64": "0.1.89", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.89", "@napi-rs/canvas-linux-arm64-gnu": "0.1.89", "@napi-rs/canvas-linux-arm64-musl": "0.1.89", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.89", "@napi-rs/canvas-linux-x64-gnu": "0.1.89", "@napi-rs/canvas-linux-x64-musl": "0.1.89", "@napi-rs/canvas-win32-arm64-msvc": "0.1.89", "@napi-rs/canvas-win32-x64-msvc": "0.1.89" } }, "sha512-7GjmkMirJHejeALCqUnZY3QwID7bbumOiLrqq2LKgxrdjdmxWQBTc6rcASa2u8wuWrH7qo4/4n/VNrOwCoKlKg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.89", "", { "os": "android", "cpu": "arm64" }, "sha512-CXxQTXsjtQqKGENS8Ejv9pZOFJhOPIl2goenS+aU8dY4DygvkyagDhy/I07D1YLqrDtPvLEX5zZHt8qUdnuIpQ=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@0.1.89", "", { "os": "darwin", "cpu": "arm64" }, "sha512-k29cR/Zl20WLYM7M8YePevRu2VQRaKcRedYr1V/8FFHkyIQ8kShEV+MPoPGi+znvmd17Eqjy2Pk2F2kpM2umVg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@0.1.89", "", { "os": "darwin", "cpu": "x64" }, "sha512-iUragqhBrA5FqU13pkhYBDbUD1WEAIlT8R2+fj6xHICY2nemzwMUI8OENDhRh7zuL06YDcRwENbjAVxOmaX9jg=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@0.1.89", "", { "os": "linux", "cpu": "arm" }, "sha512-y3SM9sfDWasY58ftoaI09YBFm35Ig8tosZqgahLJ2WGqawCusGNPV9P0/4PsrLOCZqGg629WxexQMY25n7zcvA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@0.1.89", "", { "os": "linux", "cpu": "arm64" }, "sha512-NEoF9y8xq5fX8HG8aZunBom1ILdTwt7ayBzSBIwrmitk7snj4W6Fz/yN/ZOmlM1iyzHDNX5Xn0n+VgWCF8BEdA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@0.1.89", "", { "os": "linux", "cpu": "arm64" }, "sha512-UQQkIEzV12/l60j1ziMjZ+mtodICNUbrd205uAhbyTw0t60CrC/EsKb5/aJWGq1wM0agvcgZV72JJCKfLS6+4w=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@0.1.89", "", { "os": "linux", "cpu": "none" }, "sha512-1/VmEoFaIO6ONeeEMGoWF17wOYZOl5hxDC1ios2Bkz/oQjbJJ8DY/X22vWTmvuUKWWhBVlo63pxLGZbjJU/heA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@0.1.89", "", { "os": "linux", "cpu": "x64" }, "sha512-ebLuqkCuaPIkKgKH9q4+pqWi1tkPOfiTk5PM1LKR1tB9iO9sFNVSIgwEp+SJreTSbA2DK5rW8lQXiN78SjtcvA=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@0.1.89", "", { "os": "linux", "cpu": "x64" }, "sha512-w+5qxHzplvA4BkHhCaizNMLLXiI+CfP84YhpHm/PqMub4u8J0uOAv+aaGv40rYEYra5hHRWr9LUd6cfW32o9/A=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@0.1.89", "", { "os": "win32", "cpu": "arm64" }, "sha512-DmyXa5lJHcjOsDC78BM3bnEECqbK3xASVMrKfvtT/7S7Z8NGQOugvu+L7b41V6cexCd34mBWgMOsjoEBceeB1Q=="],
|
||||||
|
|
||||||
|
"@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@0.1.89", "", { "os": "win32", "cpu": "x64" }, "sha512-WMej0LZrIqIncQcx0JHaMXlnAG7sncwJh7obs/GBgp0xF9qABjwoRwIooMWCZkSansapKGNUHhamY6qEnFN7gA=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "1.2.0" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
@@ -1133,6 +1158,8 @@
|
|||||||
|
|
||||||
"pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="],
|
"pathval": ["pathval@1.1.1", "", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="],
|
||||||
|
|
||||||
|
"pdfjs-dist": ["pdfjs-dist@5.4.530", "", { "optionalDependencies": { "@napi-rs/canvas": "^0.1.84" } }, "sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|||||||
@@ -0,0 +1,585 @@
|
|||||||
|
# Theme Creation Guide
|
||||||
|
|
||||||
|
This guide covers everything you need to know about creating custom themes for BetterSEQTA+.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Theme Structure](#theme-structure)
|
||||||
|
3. [CSS Variables](#css-variables)
|
||||||
|
4. [CSS Selectors & Classes](#css-selectors--classes)
|
||||||
|
5. [Custom Images](#custom-images)
|
||||||
|
6. [Theme Settings](#theme-settings)
|
||||||
|
7. [Best Practices](#best-practices)
|
||||||
|
8. [Examples](#examples)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Themes in BetterSEQTA+ allow you to completely customize the appearance of SEQTA Learn. A theme consists of:
|
||||||
|
|
||||||
|
- **Custom CSS**: CSS rules that override default styles
|
||||||
|
- **Custom Images**: Images that can be referenced via CSS variables
|
||||||
|
- **Theme Metadata**: Name, description, default color, etc.
|
||||||
|
- **Theme Settings**: Options like forcing dark/light mode
|
||||||
|
|
||||||
|
Themes are applied by injecting CSS into the SEQTA page and setting CSS custom properties (variables) on the document root.
|
||||||
|
|
||||||
|
## CSS Variables
|
||||||
|
|
||||||
|
BetterSEQTA+ provides a comprehensive set of CSS variables that you can use in your themes. These variables automatically adapt to light/dark mode and user preferences.
|
||||||
|
|
||||||
|
### Core Background Variables
|
||||||
|
|
||||||
|
| Variable | Light Mode | Dark Mode | Description |
|
||||||
|
|----------|------------|-----------|-------------|
|
||||||
|
| `--background-primary` | `#ffffff` | `#232323` | Main background color |
|
||||||
|
| `--background-secondary` | `#e5e7eb` | `#1a1a1a` | Secondary background color |
|
||||||
|
| `--theme-primary` | `#ffffff` | `#232323` | Primary theme color (same as background-primary) |
|
||||||
|
| `--theme-secondary` | `#e5e7eb` | `#1a1a1a` | Secondary theme color (same as background-secondary) |
|
||||||
|
| `--text-primary` | `black` | `white` | Primary text color |
|
||||||
|
| `--text-color` | `black` | `white` | Text color (alias for text-primary) |
|
||||||
|
|
||||||
|
### BetterSEQTA+ Specific Variables
|
||||||
|
|
||||||
|
| Variable | Description | Notes |
|
||||||
|
|----------|-------------|-------|
|
||||||
|
| `--better-main` | User's selected accent color | Dynamically set based on color picker |
|
||||||
|
| `--better-sub` | Dark navy color | Always `#161616` |
|
||||||
|
| `--better-pale` | Lightened version of accent color | Only available in light mode |
|
||||||
|
| `--better-light` | Lighter version of accent color | Calculated based on brightness |
|
||||||
|
| `--better-alert-highlight` | Alert/highlight color | `#c61851` |
|
||||||
|
| `--betterseqta-logo` | Logo URL | Changes based on dark/light mode |
|
||||||
|
| `--auto-background` | Auto background color | Falls back to `--better-pale` or `--background-secondary` |
|
||||||
|
| `--navy` | Navy color | `#1a1a1a` |
|
||||||
|
| `--theme-fg-parts` | Theme foreground parts | `white` |
|
||||||
|
|
||||||
|
### Subject/Item Color Variables
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `--item-colour` | Subject/item color | Set dynamically per subject/item |
|
||||||
|
| `--colour` | Generic color variable | Used in various contexts |
|
||||||
|
| `--person-colour` | Person/avatar color | `var(--better-light)` for staff |
|
||||||
|
|
||||||
|
### Transparency Effects
|
||||||
|
|
||||||
|
When transparency effects are enabled, background variables become semi-transparent:
|
||||||
|
|
||||||
|
| Variable | Light Mode (Transparent) | Dark Mode (Transparent) |
|
||||||
|
|----------|--------------------------|-------------------------|
|
||||||
|
| `--background-primary` | `rgba(255, 255, 255, 0.6)` | `rgba(35, 35, 35, 0.6)` |
|
||||||
|
| `--background-secondary` | `rgba(229, 231, 235, 0.6)` | `rgba(26, 26, 26, 0.6)` |
|
||||||
|
|
||||||
|
### Using CSS Variables
|
||||||
|
|
||||||
|
You can use these variables in your custom CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Example: Style a custom element */
|
||||||
|
.my-custom-element {
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--better-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example: Create a gradient */
|
||||||
|
.gradient-box {
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
var(--better-main),
|
||||||
|
var(--background-secondary)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSS Selectors & Classes
|
||||||
|
|
||||||
|
BetterSEQTA+ uses specific CSS selectors and classes that you can target in your themes. Here are the most important ones:
|
||||||
|
|
||||||
|
### Main Layout Elements
|
||||||
|
|
||||||
|
| Selector | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `#container` | Main container element |
|
||||||
|
| `#content` | Content area |
|
||||||
|
| `#main` | Main content wrapper |
|
||||||
|
| `#title` | Top title bar |
|
||||||
|
| `#menu` | Sidebar menu |
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
The `dark` class is added to `html` when dark mode is active:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Target dark mode specifically */
|
||||||
|
html.dark #main {
|
||||||
|
background: var(--background-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Target light mode */
|
||||||
|
html:not(.dark) #main {
|
||||||
|
background: var(--background-primary);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transparency Effects
|
||||||
|
|
||||||
|
When transparency effects are enabled, the `transparencyEffects` class is added to `html`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
html.transparencyEffects .notice {
|
||||||
|
backdrop-filter: blur(80px);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common SEQTA Classes
|
||||||
|
|
||||||
|
| Class/Selector | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `.notice` | Notice cards |
|
||||||
|
| `.day` | Day containers in timetable |
|
||||||
|
| `.dashboard` | Dashboard sections |
|
||||||
|
| `.dashlet` | Dashboard widgets |
|
||||||
|
| `.document` | Document elements |
|
||||||
|
| `.quickbar` | Quick action bar |
|
||||||
|
| `.calendar` | Calendar elements |
|
||||||
|
| `.message` | Message elements |
|
||||||
|
| `.thread` | Forum threads |
|
||||||
|
| `.shortcut` | Shortcut buttons |
|
||||||
|
| `.upcoming-assessment` | Upcoming assessments |
|
||||||
|
| `.entry.class` | Timetable entries |
|
||||||
|
|
||||||
|
### BetterSEQTA+ Specific Classes
|
||||||
|
|
||||||
|
| Class | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `.addedButton` | BetterSEQTA+ added buttons |
|
||||||
|
| `.tooltip` | Tooltip elements |
|
||||||
|
| `.notice-unified-content` | Unified notice content |
|
||||||
|
| `.home-container` | Home page container |
|
||||||
|
| `.timetable-container` | Timetable container |
|
||||||
|
| `.notices-container` | Notices container |
|
||||||
|
|
||||||
|
### Attribute Selectors
|
||||||
|
|
||||||
|
SEQTA uses data attributes that you can target:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Target specific data types */
|
||||||
|
[data-type="student"] .header {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Target specific labels */
|
||||||
|
[data-label="inbox"] {
|
||||||
|
/* Styles */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Modules
|
||||||
|
|
||||||
|
SEQTA uses CSS modules with hashed class names. You can target them using attribute selectors:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Target CSS module classes */
|
||||||
|
[class*="MessageList__MessageList___"] {
|
||||||
|
background: var(--background-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[class*="BasicPanel__BasicPanel___"] {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Images
|
||||||
|
|
||||||
|
Themes can include custom images that are made available as CSS variables.
|
||||||
|
|
||||||
|
### Adding Images
|
||||||
|
|
||||||
|
1. Upload an image in the theme creator
|
||||||
|
2. Set a CSS variable name (e.g., `custom-background`)
|
||||||
|
3. The image will be available as `var(--custom-background)`
|
||||||
|
|
||||||
|
### Using Image Variables
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Use as background */
|
||||||
|
.my-element {
|
||||||
|
background-image: var(--custom-background);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use in content */
|
||||||
|
.my-icon::before {
|
||||||
|
content: '';
|
||||||
|
background-image: var(--custom-icon);
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image Variable Format
|
||||||
|
|
||||||
|
Images are stored as `url()` values:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* The variable contains: url(blob:...) */
|
||||||
|
--custom-background: url(blob:chrome-extension://...);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Theme Settings
|
||||||
|
|
||||||
|
### Force Dark/Light Mode
|
||||||
|
|
||||||
|
You can force a theme to always use dark or light mode:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
forceDark: true // Force dark mode
|
||||||
|
forceDark: false // Force light mode
|
||||||
|
forceDark: undefined // Use user's preference (default)
|
||||||
|
```
|
||||||
|
|
||||||
|
When `forceDark` is set, users cannot toggle dark/light mode while the theme is active.
|
||||||
|
|
||||||
|
### Default Color
|
||||||
|
|
||||||
|
Set a default accent color for your theme:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
defaultColour: "rgba(0, 123, 255, 1)" // Blue
|
||||||
|
defaultColour: "#ff6b6b" // Red (hex format)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Allow Color Changes
|
||||||
|
|
||||||
|
Control whether users can change the accent color:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
CanChangeColour: true // Users can change color
|
||||||
|
CanChangeColour: false // Color is locked
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Use CSS Variables
|
||||||
|
|
||||||
|
Always use CSS variables instead of hardcoded colors:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Good */
|
||||||
|
.my-element {
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bad */
|
||||||
|
.my-element {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Support Both Light and Dark Modes
|
||||||
|
|
||||||
|
Unless your theme forces a specific mode, ensure it works in both:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Use variables that adapt automatically */
|
||||||
|
.my-element {
|
||||||
|
background: var(--background-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Or explicitly handle both modes */
|
||||||
|
html.dark .my-element {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) .my-element {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Use !important Sparingly
|
||||||
|
|
||||||
|
Only use `!important` when necessary to override SEQTA's default styles:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Good - necessary override */
|
||||||
|
#title {
|
||||||
|
background: var(--background-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bad - unnecessary */
|
||||||
|
.my-element {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test Responsive Design
|
||||||
|
|
||||||
|
SEQTA is responsive. Test your theme at different screen sizes:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Example: Mobile-specific styles */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
#menu {
|
||||||
|
transform: translate(-270px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Use Semantic Selectors
|
||||||
|
|
||||||
|
Prefer semantic selectors over fragile ones:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Good - stable selector */
|
||||||
|
#main > .dashboard > section {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Caution - CSS module classes may change */
|
||||||
|
[class*="Dashboard__Dashboard___"] {
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Optimize Images
|
||||||
|
|
||||||
|
Keep image file sizes reasonable:
|
||||||
|
|
||||||
|
- Use appropriate formats (PNG for transparency, JPG for photos)
|
||||||
|
- Compress images before uploading
|
||||||
|
- Consider using CSS for simple graphics instead of images
|
||||||
|
|
||||||
|
### 7. Document Your Theme
|
||||||
|
|
||||||
|
Include comments in your CSS explaining complex styles:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/*
|
||||||
|
* Custom gradient background for dashboard
|
||||||
|
* Uses the user's accent color for a cohesive look
|
||||||
|
*/
|
||||||
|
#main > .dashboard {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--better-main),
|
||||||
|
var(--background-secondary)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Simple Color Theme
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Change accent color throughout */
|
||||||
|
:root {
|
||||||
|
--better-main: #ff6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the menu */
|
||||||
|
#menu {
|
||||||
|
background: var(--background-primary);
|
||||||
|
border-right: 3px solid var(--better-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style buttons */
|
||||||
|
.uiButton {
|
||||||
|
background: var(--better-main);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Custom Background Image
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Use a custom background image */
|
||||||
|
body {
|
||||||
|
background-image: var(--custom-background);
|
||||||
|
background-size: cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add overlay for readability */
|
||||||
|
#main::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Rounded Corners Theme
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Make everything more rounded */
|
||||||
|
#main > .dashboard > section,
|
||||||
|
.dashlet,
|
||||||
|
.notice,
|
||||||
|
.document {
|
||||||
|
border-radius: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Round buttons */
|
||||||
|
.uiButton {
|
||||||
|
border-radius: 25px !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Minimal Theme
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Remove shadows and borders */
|
||||||
|
#main > .dashboard > section,
|
||||||
|
.dashlet,
|
||||||
|
.notice {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border: 1px solid var(--background-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simplify colors */
|
||||||
|
#menu {
|
||||||
|
background: var(--background-primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove gradients */
|
||||||
|
.day {
|
||||||
|
background: var(--background-primary) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: High Contrast Theme
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Increase contrast */
|
||||||
|
:root {
|
||||||
|
--background-primary: #000000;
|
||||||
|
--background-secondary: #1a1a1a;
|
||||||
|
--text-primary: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
html:not(.dark) {
|
||||||
|
--background-primary: #ffffff;
|
||||||
|
--background-secondary: #f0f0f0;
|
||||||
|
--text-primary: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add borders for clarity */
|
||||||
|
.dashlet,
|
||||||
|
.notice,
|
||||||
|
.document {
|
||||||
|
border: 2px solid var(--better-main) !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Techniques
|
||||||
|
|
||||||
|
### CSS Custom Properties Override
|
||||||
|
|
||||||
|
You can override CSS variables in your theme:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Override a variable */
|
||||||
|
:root {
|
||||||
|
--better-main: #your-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override conditionally */
|
||||||
|
html.dark {
|
||||||
|
--background-primary: #your-dark-color;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Animations
|
||||||
|
|
||||||
|
Add smooth transitions:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Smooth color transitions */
|
||||||
|
#menu li {
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover effects */
|
||||||
|
.dashlet:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pseudo-elements
|
||||||
|
|
||||||
|
Use pseudo-elements for decorative elements:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Add decorative border */
|
||||||
|
.notice::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
background: var(--better-main);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Theme Not Applying
|
||||||
|
|
||||||
|
1. Check browser console for CSS errors
|
||||||
|
2. Verify CSS syntax is correct
|
||||||
|
3. Ensure selectors are specific enough
|
||||||
|
4. Check if `!important` is needed
|
||||||
|
|
||||||
|
### Colors Not Changing
|
||||||
|
|
||||||
|
1. Verify you're using CSS variables
|
||||||
|
2. Check if `forceDark` is overriding your styles
|
||||||
|
3. Ensure variables are set on `:root` or `html`
|
||||||
|
|
||||||
|
### Images Not Showing
|
||||||
|
|
||||||
|
1. Verify image variable name matches CSS
|
||||||
|
2. Check image format is supported
|
||||||
|
3. Ensure image size is reasonable
|
||||||
|
4. Verify `url()` wrapper in CSS
|
||||||
|
|
||||||
|
### Dark Mode Issues
|
||||||
|
|
||||||
|
1. Test with `forceDark: true` and `forceDark: false`
|
||||||
|
2. Check if transparency effects are interfering
|
||||||
|
3. Verify `html.dark` selector is correct
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Theme Creator**: Access via BetterSEQTA+ settings
|
||||||
|
- **CSS Variables Reference**: See [CSS Variables](#css-variables) section above
|
||||||
|
- **SEQTA DOM Structure**: Inspect SEQTA pages in browser DevTools
|
||||||
|
- **BetterSEQTA+ Source**: Check `src/css/injected.scss` for default styles
|
||||||
|
|
||||||
|
## Contributing Themes
|
||||||
|
|
||||||
|
If you create a great theme, consider sharing it:
|
||||||
|
|
||||||
|
1. Export your theme (Share button in theme creator)
|
||||||
|
2. Submit to the BetterSEQTA+ theme store
|
||||||
|
3. Or share on GitHub/Discord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This documentation is based on BetterSEQTA+ v3.4.13. Some details may change in future versions.
|
||||||
|
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
"mathjs": "^14.4.0",
|
"mathjs": "^14.4.0",
|
||||||
"million": "^3.1.11",
|
"million": "^3.1.11",
|
||||||
"motion": "^12.4.12",
|
"motion": "^12.4.12",
|
||||||
|
"pdfjs-dist": "^5.4.530",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"react": "17",
|
"react": "17",
|
||||||
"react-best-gradient-color-picker": "3.0.11",
|
"react-best-gradient-color-picker": "3.0.11",
|
||||||
|
|||||||
+34
-22
@@ -25,24 +25,8 @@ if (document.childNodes[1]) {
|
|||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes BetterSEQTA+ on a SEQTA page.
|
|
||||||
*
|
|
||||||
* This function performs the following steps:
|
|
||||||
* 1. Verifies that the current page is a SEQTA page.
|
|
||||||
* 2. Injects CSS styles for document loading.
|
|
||||||
* 3. Changes the page's favicon.
|
|
||||||
* 4. Initializes the extension's settings state.
|
|
||||||
* 5. Sets default storage if settings are not already defined.
|
|
||||||
* 6. Calls the main function to apply core BetterSEQTA+ modifications.
|
|
||||||
* 7. Initializes legacy and new plugins if the extension is enabled.
|
|
||||||
* 8. Logs success or error messages during initialization.
|
|
||||||
*/
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const hasSEQTATitle = document.title.includes("SEQTA Learn");
|
if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) {
|
||||||
|
|
||||||
if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) {
|
|
||||||
// Verify we are on a SEQTA page
|
|
||||||
IsSEQTAPage = true;
|
IsSEQTAPage = true;
|
||||||
console.info("[BetterSEQTA+] Verified SEQTA Page");
|
console.info("[BetterSEQTA+] Verified SEQTA Page");
|
||||||
|
|
||||||
@@ -50,13 +34,31 @@ async function init() {
|
|||||||
documentLoadStyle.textContent = documentLoadCSS;
|
documentLoadStyle.textContent = documentLoadCSS;
|
||||||
document.head.appendChild(documentLoadStyle);
|
document.head.appendChild(documentLoadStyle);
|
||||||
|
|
||||||
const icons =
|
replaceIcons();
|
||||||
document.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]');
|
|
||||||
|
|
||||||
icons.forEach((link) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
link.href = icon48;
|
for (const mutation of mutations) {
|
||||||
|
|
||||||
|
if (
|
||||||
|
mutation.type === "attributes" &&
|
||||||
|
mutation.target instanceof HTMLLinkElement &&
|
||||||
|
mutation.target.rel.includes("icon") &&
|
||||||
|
mutation.attributeName === "href"
|
||||||
|
) {
|
||||||
|
replaceIcons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
observer.observe(document.head, {
|
||||||
|
subtree: true,
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["href"],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initializeSettingsState();
|
await initializeSettingsState();
|
||||||
|
|
||||||
@@ -80,8 +82,18 @@ async function init() {
|
|||||||
console.info(
|
console.info(
|
||||||
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceIcons() {
|
||||||
|
document
|
||||||
|
.querySelectorAll<HTMLLinkElement>('link[rel*="icon"]')
|
||||||
|
.forEach((link) => {
|
||||||
|
if (link.href !== icon48) {
|
||||||
|
link.href = icon48;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,6 +7,20 @@ import {
|
|||||||
import { type Plugin } from "@/plugins/core/types";
|
import { type Plugin } from "@/plugins/core/types";
|
||||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||||
|
import {
|
||||||
|
clearStuck,
|
||||||
|
getClassByPattern,
|
||||||
|
initStorage,
|
||||||
|
letterToNumber,
|
||||||
|
parseAssessments,
|
||||||
|
processAssessments,
|
||||||
|
} from "./utils.ts";
|
||||||
|
|
||||||
|
interface weightingsStorage {
|
||||||
|
weightings: Record<string, string>;
|
||||||
|
assessments: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
const settings = defineSettings({
|
const settings = defineSettings({
|
||||||
lettergrade: booleanSetting({
|
lettergrade: booleanSetting({
|
||||||
@@ -23,7 +37,7 @@ class AssessmentsAveragePluginClass extends BasePlugin<typeof settings> {
|
|||||||
|
|
||||||
const instance = new AssessmentsAveragePluginClass();
|
const instance = new AssessmentsAveragePluginClass();
|
||||||
|
|
||||||
const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
const assessmentsAveragePlugin: Plugin<typeof settings, weightingsStorage> = {
|
||||||
id: "assessments-average",
|
id: "assessments-average",
|
||||||
name: "Assessment Averages",
|
name: "Assessment Averages",
|
||||||
description: "Adds an average grade to the Assessments page",
|
description: "Adds an average grade to the Assessments page",
|
||||||
@@ -32,8 +46,10 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
|||||||
settings: instance.settings,
|
settings: instance.settings,
|
||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
|
await initStorage(api);
|
||||||
|
clearStuck(api);
|
||||||
|
|
||||||
api.seqta.onMount(".assessmentsWrapper", async () => {
|
api.seqta.onMount(".assessmentsWrapper", async () => {
|
||||||
// Wait for any assessment item to load first
|
|
||||||
await waitForElm(
|
await waitForElm(
|
||||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
"#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']",
|
||||||
true,
|
true,
|
||||||
@@ -41,26 +57,13 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
|||||||
1000,
|
1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper function to find actual class names by their base pattern
|
await parseAssessments(api);
|
||||||
const getClassByPattern = (
|
|
||||||
element: Element | Document,
|
|
||||||
basePattern: string,
|
|
||||||
): string => {
|
|
||||||
// Find all classes on the element
|
|
||||||
const classes = Array.from(element.querySelectorAll("*"))
|
|
||||||
.flatMap((el) => Array.from(el.classList))
|
|
||||||
.filter((className) => className.startsWith(basePattern));
|
|
||||||
|
|
||||||
return classes.length ? classes[0] : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find actual class names from the DOM
|
|
||||||
const sampleAssessmentItem = document.querySelector(
|
const sampleAssessmentItem = document.querySelector(
|
||||||
"[class*='AssessmentItem__AssessmentItem___']",
|
"[class*='AssessmentItem__AssessmentItem___']",
|
||||||
);
|
);
|
||||||
if (!sampleAssessmentItem) return;
|
if (!sampleAssessmentItem) return;
|
||||||
|
|
||||||
// Extract all necessary class patterns from a sample assessment item
|
|
||||||
const assessmentItemClass =
|
const assessmentItemClass =
|
||||||
Array.from(sampleAssessmentItem.classList).find((c) =>
|
Array.from(sampleAssessmentItem.classList).find((c) =>
|
||||||
c.startsWith("AssessmentItem__AssessmentItem___"),
|
c.startsWith("AssessmentItem__AssessmentItem___"),
|
||||||
@@ -83,7 +86,6 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
|||||||
"AssessmentItem__title___",
|
"AssessmentItem__title___",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get Thermoscore classes
|
|
||||||
const thermoscoreElement = document.querySelector(
|
const thermoscoreElement = document.querySelector(
|
||||||
"[class*='Thermoscore__Thermoscore___']",
|
"[class*='Thermoscore__Thermoscore___']",
|
||||||
);
|
);
|
||||||
@@ -102,62 +104,34 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
|||||||
"Thermoscore__text___",
|
"Thermoscore__text___",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Find assessment list
|
|
||||||
const assessmentsList = document.querySelector(
|
const assessmentsList = document.querySelector(
|
||||||
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
|
"#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']",
|
||||||
);
|
);
|
||||||
if (!assessmentsList) return;
|
if (!assessmentsList) return;
|
||||||
|
|
||||||
const gradeElements = document.querySelectorAll(
|
const state = await ReactFiber.find(
|
||||||
"[class*='Thermoscore__text___']",
|
"[class*='AssessmentList__items___']",
|
||||||
|
).getState();
|
||||||
|
const marks = state["marks"];
|
||||||
|
if (!marks || !marks.length) return;
|
||||||
|
|
||||||
|
const assessmentItems = Array.from(
|
||||||
|
assessmentsList.querySelectorAll(
|
||||||
|
`[class*='AssessmentItem__AssessmentItem___']`,
|
||||||
|
),
|
||||||
|
).filter(
|
||||||
|
(item) =>
|
||||||
|
!item
|
||||||
|
.querySelector(`[class*='AssessmentItem__title___']`)
|
||||||
|
?.textContent?.includes("Subject Average"),
|
||||||
);
|
);
|
||||||
if (!gradeElements.length) return;
|
|
||||||
|
|
||||||
// Parse and average grades
|
const { weightedTotal, totalWeight, hasInaccurateWeighting, count } =
|
||||||
const letterToNumber: Record<string, number> = {
|
await processAssessments(api, assessmentItems);
|
||||||
"A+": 100,
|
|
||||||
A: 95,
|
|
||||||
"A-": 90,
|
|
||||||
"B+": 85,
|
|
||||||
B: 80,
|
|
||||||
"B-": 75,
|
|
||||||
"C+": 70,
|
|
||||||
C: 65,
|
|
||||||
"C-": 60,
|
|
||||||
"D+": 55,
|
|
||||||
D: 50,
|
|
||||||
"D-": 45,
|
|
||||||
"E+": 40,
|
|
||||||
E: 35,
|
|
||||||
"E-": 30,
|
|
||||||
F: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseGrade(text: string): number {
|
if (!count || totalWeight === 0) return;
|
||||||
const str = text.trim().toUpperCase();
|
|
||||||
if (str.includes("/")) {
|
|
||||||
const [raw, max] = str.split("/").map((n) => parseFloat(n));
|
|
||||||
return (raw / max) * 100;
|
|
||||||
}
|
|
||||||
if (str.includes("%")) {
|
|
||||||
return parseFloat(str.replace("%", "")) || 0;
|
|
||||||
}
|
|
||||||
return letterToNumber[str] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let total = 0;
|
const avg = weightedTotal / totalWeight;
|
||||||
let count = 0;
|
|
||||||
gradeElements.forEach((el) => {
|
|
||||||
const grade = parseGrade(el.textContent || "");
|
|
||||||
if (grade > 0) {
|
|
||||||
total += grade;
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!count) return;
|
|
||||||
|
|
||||||
const avg = total / count;
|
|
||||||
const rounded = Math.ceil(avg / 5) * 5;
|
const rounded = Math.ceil(avg / 5) * 5;
|
||||||
const numberToLetter = Object.entries(letterToNumber).reduce(
|
const numberToLetter = Object.entries(letterToNumber).reduce(
|
||||||
(acc, [k, v]) => {
|
(acc, [k, v]) => {
|
||||||
@@ -172,31 +146,40 @@ const assessmentsAveragePlugin: Plugin<typeof settings> = {
|
|||||||
? letterAvg
|
? letterAvg
|
||||||
: `${avg.toFixed(2)}%`;
|
: `${avg.toFixed(2)}%`;
|
||||||
|
|
||||||
// Prevent duplicate
|
|
||||||
const existing = assessmentsList.querySelector(
|
const existing = assessmentsList.querySelector(
|
||||||
`[class*='AssessmentItem__title___']`,
|
`[class*='AssessmentItem__title___']`,
|
||||||
);
|
);
|
||||||
if (existing?.textContent === "Subject Average") return;
|
if (existing?.textContent === "Subject Average") return;
|
||||||
|
|
||||||
// Use the dynamic class names in the HTML template
|
let warningHTML = "";
|
||||||
const averageElement = stringToHTML(/* html */ `
|
if (hasInaccurateWeighting) {
|
||||||
|
warningHTML = /* html */ `
|
||||||
|
<div style="margin-top: 4px; font-size: 11px; color: rgba(255, 255, 255, 0.6); opacity: 0.8; line-height: 1.3;">
|
||||||
|
⚠ Some weightings unavailable
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
assessmentsList.insertBefore(
|
||||||
|
stringToHTML(/* html */ `
|
||||||
<div class="${assessmentItemClass}">
|
<div class="${assessmentItemClass}">
|
||||||
<div class="${metaContainerClass}">
|
<div class="${metaContainerClass}">
|
||||||
<div class="${metaClass}">
|
<div class="${metaClass}">
|
||||||
<div class="${simpleResultClass}">
|
<div class="${simpleResultClass}">
|
||||||
<div class="${titleClass}">Subject Average</div>
|
<div class="${titleClass}">Subject Average</div>
|
||||||
|
${warningHTML}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="${thermoscoreClass}">
|
<div class="${thermoscoreClass}">
|
||||||
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
|
<div class="${fillClass}" style="width: ${avg.toFixed(2)}%">
|
||||||
<div class="${textClass}" title="${display}">${display}</div>
|
<div class="${textClass}" title="${hasInaccurateWeighting ? display + " (some weightings unavailable)" : display}">${display}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild;
|
`).firstChild!,
|
||||||
|
assessmentsList.firstChild,
|
||||||
assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
|
||||||
|
import ReactFiber from "@/seqta/utils/ReactFiber.ts";
|
||||||
|
import * as pdfjs from "pdfjs-dist";
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||||
|
"https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.worker.min.mjs";
|
||||||
|
|
||||||
|
export async function initStorage(api: any) {
|
||||||
|
await api.storage.loaded;
|
||||||
|
|
||||||
|
if (!api.storage.weightings) {
|
||||||
|
api.storage.weightings = {};
|
||||||
|
}
|
||||||
|
if (!api.storage.assessments) {
|
||||||
|
api.storage.assessments = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStuck(api: any) {
|
||||||
|
let hasStuckProcessing = false;
|
||||||
|
for (const key in api.storage.weightings) {
|
||||||
|
if (api.storage.weightings[key] === "processing") {
|
||||||
|
delete api.storage.weightings[key];
|
||||||
|
hasStuckProcessing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasStuckProcessing) {
|
||||||
|
api.storage.weightings = { ...api.storage.weightings };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to find actual class names by their base pattern
|
||||||
|
export const getClassByPattern = (
|
||||||
|
element: Element | Document,
|
||||||
|
basePattern: string,
|
||||||
|
): string => {
|
||||||
|
const classes = Array.from(element.querySelectorAll("*"))
|
||||||
|
.flatMap((el) => Array.from(el.classList))
|
||||||
|
.filter((className) => className.startsWith(basePattern));
|
||||||
|
|
||||||
|
return classes.length ? classes[0] : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const letterToNumber: Record<string, number> = {
|
||||||
|
"A+": 100,
|
||||||
|
A: 95,
|
||||||
|
"A-": 90,
|
||||||
|
"B+": 85,
|
||||||
|
B: 80,
|
||||||
|
"B-": 75,
|
||||||
|
"C+": 70,
|
||||||
|
C: 65,
|
||||||
|
"C-": 60,
|
||||||
|
"D+": 55,
|
||||||
|
D: 50,
|
||||||
|
"D-": 45,
|
||||||
|
"E+": 40,
|
||||||
|
E: 35,
|
||||||
|
"E-": 30,
|
||||||
|
F: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseGrade(text: string): number {
|
||||||
|
const str = text.trim().toUpperCase();
|
||||||
|
if (str.includes("/")) {
|
||||||
|
const [raw, max] = str.split("/").map((n) => parseFloat(n));
|
||||||
|
return (raw / max) * 100;
|
||||||
|
}
|
||||||
|
if (str.includes("%")) {
|
||||||
|
return parseFloat(str.replace("%", "")) || 0;
|
||||||
|
}
|
||||||
|
return letterToNumber[str] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWeightLabel(
|
||||||
|
assessmentItem: Element,
|
||||||
|
weighting: string | undefined,
|
||||||
|
) {
|
||||||
|
const statsContainer = assessmentItem.querySelector(
|
||||||
|
`[class*='AssessmentItem__stats___']`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!statsContainer ||
|
||||||
|
statsContainer.querySelector(".betterseqta-weight-label")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const label = statsContainer.querySelector(
|
||||||
|
`[class*='Label__Label___']`,
|
||||||
|
) as HTMLElement;
|
||||||
|
|
||||||
|
if (!label) return;
|
||||||
|
|
||||||
|
const weightLabel = label.cloneNode(true) as HTMLElement;
|
||||||
|
weightLabel.classList.add("betterseqta-weight-label");
|
||||||
|
|
||||||
|
const innerTextDiv = weightLabel.querySelector(
|
||||||
|
`[class*='Label__innerText___']`,
|
||||||
|
);
|
||||||
|
if (innerTextDiv) innerTextDiv.textContent = "Weight";
|
||||||
|
|
||||||
|
const textNodes = Array.from(weightLabel.childNodes).filter(
|
||||||
|
(node) => node.nodeType === Node.TEXT_NODE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (textNodes.length) {
|
||||||
|
textNodes[0].textContent =
|
||||||
|
weighting && weighting !== "processing"
|
||||||
|
? `${Number(weighting) % 1 === 0 ? Number(weighting) : weighting}%`
|
||||||
|
: "N/A";
|
||||||
|
}
|
||||||
|
|
||||||
|
statsContainer.style.position = "relative";
|
||||||
|
weightLabel.style.position = "absolute";
|
||||||
|
weightLabel.style.right = "0";
|
||||||
|
weightLabel.style.top = "50%";
|
||||||
|
weightLabel.style.transform = "translateY(-50%)";
|
||||||
|
|
||||||
|
statsContainer.appendChild(weightLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isFirefox =
|
||||||
|
navigator.userAgent.toLowerCase().indexOf("firefox") > -1 &&
|
||||||
|
!navigator.userAgent.toLowerCase().includes("seamonkey") &&
|
||||||
|
!navigator.userAgent.toLowerCase().includes("waterfox");
|
||||||
|
|
||||||
|
async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
|
||||||
|
const isBlobUrl = url.startsWith("blob:");
|
||||||
|
|
||||||
|
if (isBlobUrl || isFirefox) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
const requestId = `pdf-fetch-${Date.now()}-${Math.random()}`;
|
||||||
|
const escapedUrl = url.replace(/'/g, "\\'");
|
||||||
|
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
fetch('${escapedUrl}')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('HTTP ' + response.status + ': ' + response.statusText);
|
||||||
|
}
|
||||||
|
return response.arrayBuffer();
|
||||||
|
})
|
||||||
|
.then(arrayBuffer => {
|
||||||
|
window.postMessage({
|
||||||
|
type: '${requestId}',
|
||||||
|
success: true,
|
||||||
|
data: Array.from(new Uint8Array(arrayBuffer))
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.postMessage({
|
||||||
|
type: '${requestId}',
|
||||||
|
success: false,
|
||||||
|
error: error.message || String(error)
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === requestId) {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.success) {
|
||||||
|
resolve(new Uint8Array(event.data.data).buffer);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error || "Failed to fetch PDF"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", messageHandler);
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
reject(new Error("Timeout fetching PDF"));
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.url && response.url.startsWith("blob:")) {
|
||||||
|
return await fetchPDFAsArrayBuffer(response.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch PDF: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.arrayBuffer();
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error?.message?.includes("blob") ||
|
||||||
|
error?.message?.includes("Security") ||
|
||||||
|
error?.message?.includes("CSP")
|
||||||
|
) {
|
||||||
|
return await fetchPDFAsArrayBuffer(url);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function extractPDFText(url: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
if (isFirefox) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
|
||||||
|
|
||||||
|
const escapedUrl = url
|
||||||
|
.replace(/\\/g, "\\\\")
|
||||||
|
.replace(/'/g, "\\'")
|
||||||
|
.replace(/"/g, '\\"');
|
||||||
|
|
||||||
|
script.textContent = `
|
||||||
|
(function() {
|
||||||
|
const requestId = '${requestId}';
|
||||||
|
const url = '${escapedUrl}';
|
||||||
|
|
||||||
|
if (window.pdfjsLib) {
|
||||||
|
extractPDF();
|
||||||
|
} else {
|
||||||
|
const pdfjsScript = document.createElement('script');
|
||||||
|
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js';
|
||||||
|
pdfjsScript.type = 'text/javascript';
|
||||||
|
|
||||||
|
pdfjsScript.onload = function() {
|
||||||
|
extractPDF();
|
||||||
|
};
|
||||||
|
pdfjsScript.onerror = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load pdfjs library'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.head.appendChild(pdfjsScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPDF() {
|
||||||
|
try {
|
||||||
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = '';
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.responseType = 'arraybuffer';
|
||||||
|
xhr.withCredentials = true;
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'HTTP ' + xhr.status + ': ' + xhr.statusText
|
||||||
|
}, '*');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = xhr.response;
|
||||||
|
if (!arrayBuffer || arrayBuffer.byteLength === 0) {
|
||||||
|
throw new Error('PDF response is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
window.pdfjsLib.getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
useSystemFonts: true,
|
||||||
|
verbosity: 0,
|
||||||
|
useWorkerFetch: false,
|
||||||
|
isEvalSupported: false
|
||||||
|
}).promise
|
||||||
|
.then(pdf => {
|
||||||
|
const pagePromises = [];
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
pagePromises.push(
|
||||||
|
pdf.getPage(i).then(page => {
|
||||||
|
return page.getTextContent().then(content => {
|
||||||
|
return content.items.map(item => item.str).join(' ');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.all(pagePromises);
|
||||||
|
})
|
||||||
|
.then(pages => {
|
||||||
|
const text = pages.join('\\n');
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: true,
|
||||||
|
text: text
|
||||||
|
}, '*');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'PDF parsing error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'ArrayBuffer error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Network error fetching PDF'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.ontimeout = function() {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Timeout fetching PDF'
|
||||||
|
}, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.timeout = 30000;
|
||||||
|
xhr.send();
|
||||||
|
} catch (error) {
|
||||||
|
window.postMessage({
|
||||||
|
type: requestId,
|
||||||
|
success: false,
|
||||||
|
error: 'Setup error: ' + (error.message || String(error))
|
||||||
|
}, '*');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === requestId) {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.success) {
|
||||||
|
resolve(event.data.text);
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(event.data.error || "Failed to extract PDF text"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", messageHandler);
|
||||||
|
(document.head || document.documentElement).appendChild(script);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
window.removeEventListener("message", messageHandler);
|
||||||
|
if (script.parentNode) {
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
|
reject(new Error("Timeout extracting PDF text"));
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await fetchPDFAsArrayBuffer(url);
|
||||||
|
|
||||||
|
if (arrayBuffer.byteLength === 0) {
|
||||||
|
throw new Error("PDF response is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdf = await pdfjs.getDocument({
|
||||||
|
data: arrayBuffer,
|
||||||
|
useSystemFonts: true,
|
||||||
|
}).promise;
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
|
const page = await pdf.getPage(i);
|
||||||
|
const content = await page.getTextContent();
|
||||||
|
text += content.items.map((item: any) => item.str).join(" ") + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[BetterSEQTA+] Failed to extract PDF text:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWeightings(mark: any, api: any) {
|
||||||
|
const assessmentID = mark.id;
|
||||||
|
const metaclassID = mark.metaclassID;
|
||||||
|
const userInfo = await getUserInfo();
|
||||||
|
const userID = userInfo.id;
|
||||||
|
const title = mark.title;
|
||||||
|
|
||||||
|
if (
|
||||||
|
api.storage.weightings[assessmentID] != undefined &&
|
||||||
|
api.storage.weightings[assessmentID] !== "processing"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: "processing",
|
||||||
|
};
|
||||||
|
|
||||||
|
api.storage.assessments = {
|
||||||
|
...api.storage.assessments,
|
||||||
|
[title.trim()]: assessmentID,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filename =
|
||||||
|
"BetterSEQTA-" +
|
||||||
|
String(Math.floor(Math.random() * 1e15)).padStart(15, "0");
|
||||||
|
|
||||||
|
const printResponse = await fetch(
|
||||||
|
`${location.origin}/seqta/student/print/assessment`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
fileName: filename,
|
||||||
|
id: assessmentID,
|
||||||
|
metaclass: metaclassID,
|
||||||
|
student: userID,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!printResponse.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to generate PDF: ${printResponse.status} ${printResponse.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
const pdfUrl = `${location.origin}/seqta/student/report/get?file=${filename}`;
|
||||||
|
|
||||||
|
if (pdfUrl.startsWith("blob:")) {
|
||||||
|
throw new Error(`Cannot fetch blob URL from extension: ${pdfUrl}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text: string;
|
||||||
|
try {
|
||||||
|
text = await extractPDFText(pdfUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
isFirefox &&
|
||||||
|
(error?.message?.includes("blob") ||
|
||||||
|
error?.message?.includes("Security") ||
|
||||||
|
error?.message?.includes("CSP"))
|
||||||
|
) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
text = await extractPDFText(pdfUrl);
|
||||||
|
} else {
|
||||||
|
throw new Error(`PDF extraction failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = text.match(/weight:\s*(\d+\.?\d*)/i);
|
||||||
|
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: match ? match[1] : "N/A",
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
api.storage.weightings = {
|
||||||
|
...api.storage.weightings,
|
||||||
|
[assessmentID]: "N/A",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseAssessments(api: any) {
|
||||||
|
const state = await ReactFiber.find(
|
||||||
|
"[class*='AssessmentList__items___']",
|
||||||
|
).getState();
|
||||||
|
|
||||||
|
const marks = state["marks"];
|
||||||
|
if (!marks) return;
|
||||||
|
|
||||||
|
await Promise.all(marks.map((mark: any) => handleWeightings(mark, api)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processAssessments(api: any, assessmentItems: Element[]) {
|
||||||
|
let weightedTotal = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
let hasInaccurateWeighting = false;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (const assessmentItem of assessmentItems) {
|
||||||
|
const gradeElement = assessmentItem.querySelector(
|
||||||
|
`[class*='Thermoscore__text___']`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!gradeElement) continue;
|
||||||
|
|
||||||
|
const grade = parseGrade(gradeElement.textContent || "");
|
||||||
|
if (grade <= 0) continue;
|
||||||
|
|
||||||
|
const titleEl = assessmentItem.querySelector(
|
||||||
|
`[class*='AssessmentItem__title___']`,
|
||||||
|
);
|
||||||
|
if (!titleEl) continue;
|
||||||
|
|
||||||
|
const title = titleEl.textContent?.trim();
|
||||||
|
if (!title) continue;
|
||||||
|
|
||||||
|
const assessmentID = api.storage.assessments?.[title];
|
||||||
|
const weighting = assessmentID
|
||||||
|
? api.storage.weightings?.[assessmentID]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
createWeightLabel(assessmentItem, weighting);
|
||||||
|
|
||||||
|
if (
|
||||||
|
weighting === null ||
|
||||||
|
weighting === undefined ||
|
||||||
|
weighting === "N/A" ||
|
||||||
|
weighting === "processing"
|
||||||
|
) {
|
||||||
|
hasInaccurateWeighting = true;
|
||||||
|
weightedTotal += grade;
|
||||||
|
totalWeight += 1;
|
||||||
|
} else {
|
||||||
|
const weight = parseFloat(weighting);
|
||||||
|
|
||||||
|
if (!isNaN(weight) && weight >= 0) {
|
||||||
|
weightedTotal += grade * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
} else {
|
||||||
|
weightedTotal += grade;
|
||||||
|
totalWeight += 1;
|
||||||
|
hasInaccurateWeighting = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
weightedTotal,
|
||||||
|
totalWeight,
|
||||||
|
hasInaccurateWeighting,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ let cachedUserInfo: any = null;
|
|||||||
|
|
||||||
let LightDarkModeSnakeEggButton = 0;
|
let LightDarkModeSnakeEggButton = 0;
|
||||||
|
|
||||||
async function getUserInfo() {
|
export async function getUserInfo() {
|
||||||
if (cachedUserInfo) return cachedUserInfo;
|
if (cachedUserInfo) return cachedUserInfo;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -30,11 +30,10 @@ async function getUserInfo() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseData = await response.json();
|
cachedUserInfo = (await response.json()).payload;
|
||||||
cachedUserInfo = responseData.payload;
|
|
||||||
return cachedUserInfo;
|
return cachedUserInfo;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user info:", error);
|
console.error("[BetterSEQTA+] Failed to get user info:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +60,7 @@ export async function AddBetterSEQTAElements() {
|
|||||||
handleStudentData(),
|
handleStudentData(),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error initializing UI elements:", error);
|
console.error("[BetterSEQTA+] Failed to initialize UI elements:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
@@ -80,20 +79,18 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
|
|||||||
div.classList.add("titlebar");
|
div.classList.add("titlebar");
|
||||||
container.append(div);
|
container.append(div);
|
||||||
|
|
||||||
const NewButton = stringToHTML(
|
fragment.appendChild(
|
||||||
/* html */`<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>`
|
stringToHTML(
|
||||||
|
/* html */ `<li class="item" data-key="home" id="homebutton" data-path="/home" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg><span>Home</span></label></li>`,
|
||||||
|
).firstChild!,
|
||||||
);
|
);
|
||||||
if (NewButton.firstChild) {
|
|
||||||
fragment.appendChild(NewButton.firstChild);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUserInfo() {
|
async function handleUserInfo() {
|
||||||
try {
|
try {
|
||||||
const info = await getUserInfo();
|
updateUserInfo(await getUserInfo());
|
||||||
updateUserInfo(info);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching and processing student data:", error);
|
console.error("[BetterSEQTA+] Failed to handle user info:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,15 +114,17 @@ function updateUserInfo(info: {
|
|||||||
}) {
|
}) {
|
||||||
const titlebar = document.getElementsByClassName("titlebar")[0];
|
const titlebar = document.getElementsByClassName("titlebar")[0];
|
||||||
|
|
||||||
const userInfo = stringToHTML(/* html */ `
|
titlebar.append(
|
||||||
|
stringToHTML(/* html */ `
|
||||||
<div class="userInfosvgdiv tooltip">
|
<div class="userInfosvgdiv tooltip">
|
||||||
<svg class="userInfosvg" viewBox="0 0 24 24"><path fill="var(--text-primary)" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path></svg>
|
<svg class="userInfosvg" viewBox="0 0 24 24"><path fill="var(--text-primary)" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path></svg>
|
||||||
<div class="tooltiptext topmenutooltip" id="logouttooltip"></div>
|
<div class="tooltiptext topmenutooltip" id="logouttooltip"></div>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild;
|
`).firstChild!,
|
||||||
titlebar.append(userInfo!);
|
);
|
||||||
|
|
||||||
const userinfo = stringToHTML(/* html */ `
|
titlebar.append(
|
||||||
|
stringToHTML(/* html */ `
|
||||||
<div class="userInfo">
|
<div class="userInfo">
|
||||||
<div class="userInfoText">
|
<div class="userInfoText">
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
@@ -135,12 +134,12 @@ function updateUserInfo(info: {
|
|||||||
<p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p>
|
<p class="userInfoCode">${info.meta.code} // ${info.meta.governmentID}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).firstChild;
|
`).firstChild!,
|
||||||
titlebar.append(userinfo!);
|
);
|
||||||
|
|
||||||
var logoutbutton = document.getElementsByClassName("logout")[0];
|
document
|
||||||
var userInfosvgdiv = document.getElementById("logouttooltip")!;
|
.getElementById("logouttooltip")!
|
||||||
userInfosvgdiv.appendChild(logoutbutton);
|
.appendChild(document.getElementsByClassName("logout")[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStudentData() {
|
async function handleStudentData() {
|
||||||
@@ -156,48 +155,40 @@ async function handleStudentData() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const responseData = await response.json();
|
await updateStudentInfo((await response.json()).payload);
|
||||||
let students = responseData.payload;
|
|
||||||
await updateStudentInfo(students);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching and processing student data:", error);
|
console.error("[BetterSEQTA+] Failed to handle student data:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateStudentInfo(students: any) {
|
async function updateStudentInfo(students: any) {
|
||||||
const info = await getUserInfo();
|
const info = await getUserInfo();
|
||||||
var index = students.findIndex(function (person: any) {
|
const index = students.findIndex(
|
||||||
return (
|
(person: any) =>
|
||||||
person.firstname == info.userDesc.split(" ")[0] &&
|
person.firstname == info.userDesc.split(" ")[0] &&
|
||||||
person.surname == info.userDesc.split(" ")[1]
|
person.surname == info.userDesc.split(" ")[1],
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const houseelement = document.getElementsByClassName("userInfohouse")[0] as HTMLElement;
|
const houseelement = document.getElementsByClassName(
|
||||||
|
"userInfohouse",
|
||||||
// Fallback to N/A
|
)[0] as HTMLElement;
|
||||||
let text = 'N/A';
|
|
||||||
const student = students[index] ?? {};
|
const student = students[index] ?? {};
|
||||||
|
let text = "N/A";
|
||||||
|
|
||||||
// If student has a house, prefer to show year + house. If no year, only show house.
|
|
||||||
if (student.house) {
|
if (student.house) {
|
||||||
text = `${student.year ?? ""}${student.house}`;
|
text = `${student.year ?? ""}${student.house}`;
|
||||||
|
|
||||||
// If house_colour exists, compute colour
|
|
||||||
if (student.house_colour) {
|
if (student.house_colour) {
|
||||||
houseelement.style.background = student.house_colour;
|
houseelement.style.background = student.house_colour;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const colorresult = GetThresholdOfColor(student.house_colour);
|
const colorresult = GetThresholdOfColor(student.house_colour);
|
||||||
houseelement.style.color =
|
houseelement.style.color =
|
||||||
colorresult && colorresult > 300 ? "black" : "white";
|
colorresult && colorresult > 300 ? "black" : "white";
|
||||||
|
} catch {
|
||||||
} catch (err) {
|
// Invalid color format, leave text color as default
|
||||||
// Colour calculation failed, no text colour set
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (student.year) {
|
} else if (student.year) {
|
||||||
// No house, only year will be shown
|
|
||||||
text = student.year;
|
text = student.year;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,15 +196,13 @@ async function updateStudentInfo(students: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
|
function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) {
|
||||||
const NewsButtonStr =
|
fragment.appendChild(
|
||||||
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>';
|
stringToHTML(
|
||||||
const NewsButton = stringToHTML(NewsButtonStr);
|
'<li class="item" data-key="news" id="newsbutton" data-path="/news" data-betterseqta="true"><label><svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 3H4C2.89 3 2 3.89 2 5V19C2 20.11 2.89 21 4 21H20C21.11 21 22 20.11 22 19V5C22 3.89 21.11 3 20 3M5 7H10V13H5V7M19 17H5V15H19V17M19 13H12V11H19V13M19 9H12V7H19V9Z" /></svg><span>News</span></label></li>',
|
||||||
|
).firstChild!,
|
||||||
|
);
|
||||||
|
|
||||||
if (NewsButton.firstChild) {
|
const iconCover = document.createElement("div");
|
||||||
fragment.appendChild(NewsButton.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
let iconCover = document.createElement("div");
|
|
||||||
iconCover.classList.add("icon-cover");
|
iconCover.classList.add("icon-cover");
|
||||||
iconCover.id = "icon-cover";
|
iconCover.id = "icon-cover";
|
||||||
menu.appendChild(iconCover);
|
menu.appendChild(iconCover);
|
||||||
@@ -252,46 +241,42 @@ function setupEventListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createSettingsButton() {
|
async function createSettingsButton() {
|
||||||
let SettingsButton = stringToHTML(/* html */ `
|
document.getElementById("content")!.append(
|
||||||
|
stringToHTML(/* html */ `
|
||||||
<button class="addedButton tooltip" id="AddedSettings">
|
<button class="addedButton tooltip" id="AddedSettings">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||||
<g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g>
|
<g><g><path d="M23.182,6.923c-.29,0-3.662,2.122-4.142,2.4l-2.8-1.555V4.511l4.257-2.456a.518.518,0,0,0,.233-.408.479.479,0,0,0-.233-.407,6.511,6.511,0,1,0-3.327,12.107,6.582,6.582,0,0,0,6.148-4.374,5.228,5.228,0,0,0,.333-1.542A.461.461,0,0,0,23.182,6.923Z"></path><path d="M9.73,10.418,7.376,12.883c-.01.01-.021.016-.03.025L1.158,19.1a2.682,2.682,0,1,0,3.793,3.793l4.583-4.582,0,0,4.1-4.005-.037-.037A9.094,9.094,0,0,1,9.73,10.418ZM3.053,21.888A.894.894,0,1,1,3.946,21,.893.893,0,0,1,3.053,21.888Z"></path></g></g>
|
||||||
</svg>
|
</svg>
|
||||||
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
|
${settingsState.onoff ? '<div class="tooltiptext topmenutooltip">BetterSEQTA+ Settings</div>' : ""}
|
||||||
</button>
|
</button>
|
||||||
`);
|
`).firstChild!,
|
||||||
let ContentDiv = document.getElementById("content");
|
);
|
||||||
ContentDiv!.append(SettingsButton.firstChild!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GetLightDarkModeString() {
|
function GetLightDarkModeString() {
|
||||||
if (settingsState.DarkMode) {
|
return settingsState.DarkMode
|
||||||
return "Switch to light theme";
|
? "Switch to light theme"
|
||||||
} else {
|
: "Switch to dark theme";
|
||||||
return "Switch to dark theme";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addDarkLightToggle() {
|
async function addDarkLightToggle() {
|
||||||
const tooltipString = GetLightDarkModeString();
|
|
||||||
const SUN_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
const SUN_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_80"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_80)"><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -4,-2.2100000381469727 -4,0 C-4,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(1,0,0,1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
||||||
const MOON_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
const MOON_ICON_SVG = /* html */ `<defs><clipPath id="__lottie_element_263"><rect width="24" height="24" x="0" y="0"></rect></clipPath></defs><g clip-path="url(#__lottie_element_263)"><g style="display: block;" transform="matrix(1.5,0,0,1.5,7,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,-4 C-2.2100000381469727,-4 -1.2920000553131104,-2.2100000381469727 -1.2920000553131104,0 C-1.2920000553131104,2.2100000381469727 -2.2100000381469727,4 0,4 C2.2100000381469727,4 4,2.2100000381469727 4,0 C4,-2.2100000381469727 2.2100000381469727,-4 0,-4z"></path></g></g><g style="display: block;" transform="matrix(-1,0,0,-1,12,12)" opacity="1"><g opacity="1" transform="matrix(1,0,0,1,0,0)"><path fill-opacity="1" d=" M0,6 C-3.309999942779541,6 -6,3.309999942779541 -6,0 C-6,-3.309999942779541 -3.309999942779541,-6 0,-6 C3.309999942779541,-6 6,-3.309999942779541 6,0 C6,3.309999942779541 3.309999942779541,6 0,6z M8,-3.309999942779541 C8,-3.309999942779541 8,-8 8,-8 C8,-8 3.309999942779541,-8 3.309999942779541,-8 C3.309999942779541,-8 0,-11.3100004196167 0,-11.3100004196167 C0,-11.3100004196167 -3.309999942779541,-8 -3.309999942779541,-8 C-3.309999942779541,-8 -8,-8 -8,-8 C-8,-8 -8,-3.309999942779541 -8,-3.309999942779541 C-8,-3.309999942779541 -11.3100004196167,0 -11.3100004196167,0 C-11.3100004196167,0 -8,3.309999942779541 -8,3.309999942779541 C-8,3.309999942779541 -8,8 -8,8 C-8,8 -3.309999942779541,8 -3.309999942779541,8 C-3.309999942779541,8 0,11.3100004196167 0,11.3100004196167 C0,11.3100004196167 3.309999942779541,8 3.309999942779541,8 C3.309999942779541,8 8,8 8,8 C8,8 8,3.309999942779541 8,3.309999942779541 C8,3.309999942779541 11.3100004196167,0 11.3100004196167,0 C11.3100004196167,0 8,-3.309999942779541 8,-3.309999942779541z"></path></g></g></g>`;
|
||||||
|
|
||||||
const initialSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
|
document.getElementById("content")!.append(
|
||||||
|
stringToHTML(/* html */ `
|
||||||
const LightDarkModeButton = stringToHTML(/* html */ `
|
|
||||||
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
|
<button class="addedButton DarkLightButton tooltip" id="LightDarkModeButton">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">${initialSvgContent}</svg>
|
<svg xmlns="http://www.w3.org/2000/svg">${settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG}</svg>
|
||||||
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${tooltipString}</div>
|
<div class="tooltiptext topmenutooltip" id="darklighttooliptext">${GetLightDarkModeString()}</div>
|
||||||
</button>
|
</button>
|
||||||
`);
|
`).firstChild!,
|
||||||
|
);
|
||||||
let ContentDiv = document.getElementById("content");
|
|
||||||
ContentDiv!.append(LightDarkModeButton.firstChild!);
|
|
||||||
|
|
||||||
updateAllColors();
|
updateAllColors();
|
||||||
|
|
||||||
const lightDarkModeButtonElement = document.getElementById("LightDarkModeButton")!;
|
const lightDarkModeButtonElement = document.getElementById(
|
||||||
|
"LightDarkModeButton",
|
||||||
|
)!;
|
||||||
|
|
||||||
lightDarkModeButtonElement.addEventListener("click", async () => {
|
lightDarkModeButtonElement.addEventListener("click", async () => {
|
||||||
const darklightText = document.getElementById("darklighttooliptext");
|
const darklightText = document.getElementById("darklighttooliptext");
|
||||||
@@ -303,7 +288,6 @@ async function addDarkLightToggle() {
|
|||||||
LightDarkModeSnakeEggButton = 0;
|
LightDarkModeSnakeEggButton = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
settingsState.originalDarkMode !== undefined &&
|
settingsState.originalDarkMode !== undefined &&
|
||||||
settingsState.selectedTheme
|
settingsState.selectedTheme
|
||||||
@@ -314,38 +298,24 @@ async function addDarkLightToggle() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!document.startViewTransition || !settingsState.animations || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
||||||
settingsState.DarkMode = !settingsState.DarkMode;
|
settingsState.DarkMode = !settingsState.DarkMode;
|
||||||
updateAllColors();
|
updateAllColors();
|
||||||
|
|
||||||
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
|
const svgElement = lightDarkModeButtonElement.querySelector("svg")!;
|
||||||
const svgElement = lightDarkModeButtonElement.querySelector("svg");
|
svgElement.innerHTML = settingsState.DarkMode
|
||||||
if (svgElement) svgElement.innerHTML = newSvgContent;
|
? SUN_ICON_SVG
|
||||||
darklightText!.innerText = GetLightDarkModeString();
|
: MOON_ICON_SVG;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsState.DarkMode = !settingsState.DarkMode;
|
|
||||||
|
|
||||||
updateAllColors();
|
|
||||||
|
|
||||||
const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG;
|
|
||||||
const svgElement = lightDarkModeButtonElement.querySelector("svg");
|
|
||||||
if (svgElement) svgElement.innerHTML = newSvgContent;
|
|
||||||
|
|
||||||
darklightText!.innerText = GetLightDarkModeString();
|
darklightText!.innerText = GetLightDarkModeString();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function customizeMenuToggle() {
|
function customizeMenuToggle() {
|
||||||
const menuToggle = document.getElementById("menuToggle");
|
const menuToggle = document.getElementById("menuToggle")!;
|
||||||
if (menuToggle) {
|
|
||||||
menuToggle.innerHTML = "";
|
menuToggle.innerHTML = "";
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const line = document.createElement("div");
|
const line = document.createElement("div");
|
||||||
line.className = "hamburger-line";
|
line.className = "hamburger-line";
|
||||||
menuToggle!.appendChild(line);
|
menuToggle.appendChild(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user