diff --git a/bun.lock b/bun.lock index 1fd74ab4..fefefbae 100644 --- a/bun.lock +++ b/bun.lock @@ -42,6 +42,7 @@ "mathjs": "^14.4.0", "million": "^3.1.11", "motion": "^12.4.12", + "pdfjs-dist": "^5.4.530", "postcss": "^8.5.3", "react": "17", "react-best-gradient-color-picker": "3.0.11", @@ -265,6 +266,30 @@ "@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.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -1133,6 +1158,8 @@ "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=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], diff --git a/docs/THEME_CREATION.md b/docs/THEME_CREATION.md new file mode 100644 index 00000000..100136b2 --- /dev/null +++ b/docs/THEME_CREATION.md @@ -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. + diff --git a/package.json b/package.json index e9ddc124..9632d079 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "mathjs": "^14.4.0", "million": "^3.1.11", "motion": "^12.4.12", + "pdfjs-dist": "^5.4.530", "postcss": "^8.5.3", "react": "17", "react-best-gradient-color-picker": "3.0.11", diff --git a/src/SEQTA.ts b/src/SEQTA.ts index 9d4dc8c7..d94c7b71 100644 --- a/src/SEQTA.ts +++ b/src/SEQTA.ts @@ -25,24 +25,8 @@ if (document.childNodes[1]) { init(); } -/** - * Initializes BetterSEQTA+ on a SEQTA page. - * - * This function performs the following steps: - * 1. Verifies that the current page is a SEQTA page. - * 2. Injects CSS styles for document loading. - * 3. Changes the page's favicon. - * 4. Initializes the extension's settings state. - * 5. Sets default storage if settings are not already defined. - * 6. Calls the main function to apply core BetterSEQTA+ modifications. - * 7. Initializes legacy and new plugins if the extension is enabled. - * 8. Logs success or error messages during initialization. - */ async function init() { - const hasSEQTATitle = document.title.includes("SEQTA Learn"); - - if (hasSEQTAText && hasSEQTATitle && !IsSEQTAPage) { - // Verify we are on a SEQTA page + if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) { IsSEQTAPage = true; console.info("[BetterSEQTA+] Verified SEQTA Page"); @@ -50,13 +34,31 @@ async function init() { documentLoadStyle.textContent = documentLoadCSS; document.head.appendChild(documentLoadStyle); - const icons = - document.querySelectorAll('link[rel*="icon"]'); + replaceIcons(); - icons.forEach((link) => { - link.href = icon48; + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + + if ( + mutation.type === "attributes" && + mutation.target instanceof HTMLLinkElement && + mutation.target.rel.includes("icon") && + mutation.attributeName === "href" + ) { + replaceIcons(); + return; + } + } }); + observer.observe(document.head, { + subtree: true, + attributes: true, + attributeFilter: ["href"], + }); + + + try { await initializeSettingsState(); @@ -80,8 +82,18 @@ async function init() { console.info( "[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.", ); - } catch (error: any) { + } catch (error) { console.error(error); } } } + +function replaceIcons() { + document + .querySelectorAll('link[rel*="icon"]') + .forEach((link) => { + if (link.href !== icon48) { + link.href = icon48; + } + }); +} \ No newline at end of file diff --git a/src/plugins/built-in/assessmentsAverage/index.ts b/src/plugins/built-in/assessmentsAverage/index.ts index d3ef0051..6db54831 100644 --- a/src/plugins/built-in/assessmentsAverage/index.ts +++ b/src/plugins/built-in/assessmentsAverage/index.ts @@ -7,6 +7,20 @@ import { import { type Plugin } from "@/plugins/core/types"; import stringToHTML from "@/seqta/utils/stringToHTML"; import { waitForElm } from "@/seqta/utils/waitForElm"; +import ReactFiber from "@/seqta/utils/ReactFiber.ts"; +import { + clearStuck, + getClassByPattern, + initStorage, + letterToNumber, + parseAssessments, + processAssessments, +} from "./utils.ts"; + +interface weightingsStorage { + weightings: Record; + assessments: Record; +} const settings = defineSettings({ lettergrade: booleanSetting({ @@ -23,7 +37,7 @@ class AssessmentsAveragePluginClass extends BasePlugin { const instance = new AssessmentsAveragePluginClass(); -const assessmentsAveragePlugin: Plugin = { +const assessmentsAveragePlugin: Plugin = { id: "assessments-average", name: "Assessment Averages", description: "Adds an average grade to the Assessments page", @@ -32,8 +46,10 @@ const assessmentsAveragePlugin: Plugin = { settings: instance.settings, run: async (api) => { + await initStorage(api); + clearStuck(api); + api.seqta.onMount(".assessmentsWrapper", async () => { - // Wait for any assessment item to load first await waitForElm( "#main > .assessmentsWrapper .assessments [class*='AssessmentItem__AssessmentItem___']", true, @@ -41,26 +57,13 @@ const assessmentsAveragePlugin: Plugin = { 1000, ); - // Helper function to find actual class names by their base pattern - const getClassByPattern = ( - element: Element | Document, - basePattern: string, - ): string => { - // Find all classes on the element - const classes = Array.from(element.querySelectorAll("*")) - .flatMap((el) => Array.from(el.classList)) - .filter((className) => className.startsWith(basePattern)); + await parseAssessments(api); - return classes.length ? classes[0] : ""; - }; - - // Find actual class names from the DOM const sampleAssessmentItem = document.querySelector( "[class*='AssessmentItem__AssessmentItem___']", ); if (!sampleAssessmentItem) return; - // Extract all necessary class patterns from a sample assessment item const assessmentItemClass = Array.from(sampleAssessmentItem.classList).find((c) => c.startsWith("AssessmentItem__AssessmentItem___"), @@ -83,7 +86,6 @@ const assessmentsAveragePlugin: Plugin = { "AssessmentItem__title___", ); - // Get Thermoscore classes const thermoscoreElement = document.querySelector( "[class*='Thermoscore__Thermoscore___']", ); @@ -102,62 +104,34 @@ const assessmentsAveragePlugin: Plugin = { "Thermoscore__text___", ); - // Find assessment list const assessmentsList = document.querySelector( "#main > .assessmentsWrapper .assessments [class*='AssessmentList__items___']", ); if (!assessmentsList) return; - const gradeElements = document.querySelectorAll( - "[class*='Thermoscore__text___']", + const state = await ReactFiber.find( + "[class*='AssessmentList__items___']", + ).getState(); + const marks = state["marks"]; + if (!marks || !marks.length) return; + + const assessmentItems = Array.from( + assessmentsList.querySelectorAll( + `[class*='AssessmentItem__AssessmentItem___']`, + ), + ).filter( + (item) => + !item + .querySelector(`[class*='AssessmentItem__title___']`) + ?.textContent?.includes("Subject Average"), ); - if (!gradeElements.length) return; - // Parse and average grades - const letterToNumber: Record = { - "A+": 100, - A: 95, - "A-": 90, - "B+": 85, - B: 80, - "B-": 75, - "C+": 70, - C: 65, - "C-": 60, - "D+": 55, - D: 50, - "D-": 45, - "E+": 40, - E: 35, - "E-": 30, - F: 0, - }; + const { weightedTotal, totalWeight, hasInaccurateWeighting, count } = + await processAssessments(api, assessmentItems); - function parseGrade(text: string): number { - const str = text.trim().toUpperCase(); - if (str.includes("/")) { - const [raw, max] = str.split("/").map((n) => parseFloat(n)); - return (raw / max) * 100; - } - if (str.includes("%")) { - return parseFloat(str.replace("%", "")) || 0; - } - return letterToNumber[str] ?? 0; - } + if (!count || totalWeight === 0) return; - let total = 0; - let count = 0; - gradeElements.forEach((el) => { - const grade = parseGrade(el.textContent || ""); - if (grade > 0) { - total += grade; - count++; - } - }); - - if (!count) return; - - const avg = total / count; + const avg = weightedTotal / totalWeight; const rounded = Math.ceil(avg / 5) * 5; const numberToLetter = Object.entries(letterToNumber).reduce( (acc, [k, v]) => { @@ -172,31 +146,40 @@ const assessmentsAveragePlugin: Plugin = { ? letterAvg : `${avg.toFixed(2)}%`; - // Prevent duplicate const existing = assessmentsList.querySelector( `[class*='AssessmentItem__title___']`, ); if (existing?.textContent === "Subject Average") return; - // Use the dynamic class names in the HTML template - const averageElement = stringToHTML(/* html */ ` + let warningHTML = ""; + if (hasInaccurateWeighting) { + warningHTML = /* html */ ` +
+ ⚠ Some weightings unavailable +
+ `; + } + + assessmentsList.insertBefore( + stringToHTML(/* html */ `
Subject Average
+ ${warningHTML}
-
${display}
+
${display}
- `).firstChild; - - assessmentsList.insertBefore(averageElement!, assessmentsList.firstChild); + `).firstChild!, + assessmentsList.firstChild, + ); }); }, }; diff --git a/src/plugins/built-in/assessmentsAverage/utils.ts b/src/plugins/built-in/assessmentsAverage/utils.ts new file mode 100644 index 00000000..bb93c090 --- /dev/null +++ b/src/plugins/built-in/assessmentsAverage/utils.ts @@ -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 = { + "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 { + 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 { + 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, + }; +} diff --git a/src/seqta/ui/AddBetterSEQTAElements.ts b/src/seqta/ui/AddBetterSEQTAElements.ts index e7b5df62..d7c23d26 100644 --- a/src/seqta/ui/AddBetterSEQTAElements.ts +++ b/src/seqta/ui/AddBetterSEQTAElements.ts @@ -14,7 +14,7 @@ let cachedUserInfo: any = null; let LightDarkModeSnakeEggButton = 0; -async function getUserInfo() { +export async function getUserInfo() { if (cachedUserInfo) return cachedUserInfo; try { @@ -30,11 +30,10 @@ async function getUserInfo() { }), }); - const responseData = await response.json(); - cachedUserInfo = responseData.payload; + cachedUserInfo = (await response.json()).payload; return cachedUserInfo; } catch (error) { - console.error("Error fetching user info:", error); + console.error("[BetterSEQTA+] Failed to get user info:", error); throw error; } } @@ -61,7 +60,7 @@ export async function AddBetterSEQTAElements() { handleStudentData(), ]); } catch (error) { - console.error("Error initializing UI elements:", error); + console.error("[BetterSEQTA+] Failed to initialize UI elements:", error); } setupEventListeners(); @@ -80,20 +79,18 @@ function createHomeButton(fragment: DocumentFragment, _: HTMLElement) { div.classList.add("titlebar"); container.append(div); - const NewButton = stringToHTML( - /* html */`
  • ` + fragment.appendChild( + stringToHTML( + /* html */ `
  • `, + ).firstChild!, ); - if (NewButton.firstChild) { - fragment.appendChild(NewButton.firstChild); - } } async function handleUserInfo() { try { - const info = await getUserInfo(); - updateUserInfo(info); + updateUserInfo(await getUserInfo()); } catch (error) { - console.error("Error fetching and processing student data:", error); + console.error("[BetterSEQTA+] Failed to handle user info:", error); } } @@ -117,30 +114,32 @@ function updateUserInfo(info: { }) { const titlebar = document.getElementsByClassName("titlebar")[0]; - const userInfo = stringToHTML(/* html */ ` -
    - -
    -
    - `).firstChild; - titlebar.append(userInfo!); - - const userinfo = stringToHTML(/* html */ ` -
    -
    -
    -

    -

    ${info.userDesc}

    -
    -

    ${info.meta.code} // ${info.meta.governmentID}

    + titlebar.append( + stringToHTML(/* html */ ` +
    + +
    -
    - `).firstChild; - titlebar.append(userinfo!); + `).firstChild!, + ); - var logoutbutton = document.getElementsByClassName("logout")[0]; - var userInfosvgdiv = document.getElementById("logouttooltip")!; - userInfosvgdiv.appendChild(logoutbutton); + titlebar.append( + stringToHTML(/* html */ ` +
    +
    +
    +

    +

    ${info.userDesc}

    +
    +

    ${info.meta.code} // ${info.meta.governmentID}

    +
    +
    + `).firstChild!, + ); + + document + .getElementById("logouttooltip")! + .appendChild(document.getElementsByClassName("logout")[0]); } async function handleStudentData() { @@ -156,48 +155,40 @@ async function handleStudentData() { }, ); - const responseData = await response.json(); - let students = responseData.payload; - await updateStudentInfo(students); + await updateStudentInfo((await response.json()).payload); } catch (error) { - console.error("Error fetching and processing student data:", error); + console.error("[BetterSEQTA+] Failed to handle student data:", error); } } async function updateStudentInfo(students: any) { const info = await getUserInfo(); - var index = students.findIndex(function (person: any) { - return ( + const index = students.findIndex( + (person: any) => person.firstname == info.userDesc.split(" ")[0] && - person.surname == info.userDesc.split(" ")[1] - ); - }); + person.surname == info.userDesc.split(" ")[1], + ); - const houseelement = document.getElementsByClassName("userInfohouse")[0] as HTMLElement; - - // Fallback to N/A - let text = 'N/A'; + const houseelement = document.getElementsByClassName( + "userInfohouse", + )[0] as HTMLElement; 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) { text = `${student.year ?? ""}${student.house}`; - // If house_colour exists, compute colour if (student.house_colour) { houseelement.style.background = student.house_colour; - try { const colorresult = GetThresholdOfColor(student.house_colour); houseelement.style.color = colorresult && colorresult > 300 ? "black" : "white"; - - } catch (err) { - // Colour calculation failed, no text colour set + } catch { + // Invalid color format, leave text color as default } } } else if (student.year) { - // No house, only year will be shown text = student.year; } @@ -205,15 +196,13 @@ async function updateStudentInfo(students: any) { } function createNewsButton(fragment: DocumentFragment, menu: HTMLElement) { - const NewsButtonStr = - '
  • '; - const NewsButton = stringToHTML(NewsButtonStr); + fragment.appendChild( + stringToHTML( + '
  • ', + ).firstChild!, + ); - if (NewsButton.firstChild) { - fragment.appendChild(NewsButton.firstChild); - } - - let iconCover = document.createElement("div"); + const iconCover = document.createElement("div"); iconCover.classList.add("icon-cover"); iconCover.id = "icon-cover"; menu.appendChild(iconCover); @@ -252,46 +241,42 @@ function setupEventListeners() { } async function createSettingsButton() { - let SettingsButton = stringToHTML(/* html */ ` - - `); - let ContentDiv = document.getElementById("content"); - ContentDiv!.append(SettingsButton.firstChild!); + document.getElementById("content")!.append( + stringToHTML(/* html */ ` + + `).firstChild!, + ); } function GetLightDarkModeString() { - if (settingsState.DarkMode) { - return "Switch to light theme"; - } else { - return "Switch to dark theme"; - } + return settingsState.DarkMode + ? "Switch to light theme" + : "Switch to dark theme"; } async function addDarkLightToggle() { - const tooltipString = GetLightDarkModeString(); const SUN_ICON_SVG = /* html */ ``; const MOON_ICON_SVG = /* html */ ``; - - const initialSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG; - const LightDarkModeButton = stringToHTML(/* html */ ` - - `); - - let ContentDiv = document.getElementById("content"); - ContentDiv!.append(LightDarkModeButton.firstChild!); + document.getElementById("content")!.append( + stringToHTML(/* html */ ` + + `).firstChild!, + ); updateAllColors(); - const lightDarkModeButtonElement = document.getElementById("LightDarkModeButton")!; + const lightDarkModeButtonElement = document.getElementById( + "LightDarkModeButton", + )!; lightDarkModeButtonElement.addEventListener("click", async () => { const darklightText = document.getElementById("darklighttooliptext"); @@ -303,7 +288,6 @@ async function addDarkLightToggle() { LightDarkModeSnakeEggButton = 0; } - if ( settingsState.originalDarkMode !== undefined && settingsState.selectedTheme @@ -314,38 +298,24 @@ async function addDarkLightToggle() { return; } - if (!document.startViewTransition || !settingsState.animations || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - settingsState.DarkMode = !settingsState.DarkMode; - updateAllColors(); - - const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG; - const svgElement = lightDarkModeButtonElement.querySelector("svg"); - if (svgElement) svgElement.innerHTML = newSvgContent; - darklightText!.innerText = GetLightDarkModeString(); - return; - } - settingsState.DarkMode = !settingsState.DarkMode; - - updateAllColors(); - - const newSvgContent = settingsState.DarkMode ? SUN_ICON_SVG : MOON_ICON_SVG; - const svgElement = lightDarkModeButtonElement.querySelector("svg"); - if (svgElement) svgElement.innerHTML = newSvgContent; - + updateAllColors(); + + const svgElement = lightDarkModeButtonElement.querySelector("svg")!; + svgElement.innerHTML = settingsState.DarkMode + ? SUN_ICON_SVG + : MOON_ICON_SVG; darklightText!.innerText = GetLightDarkModeString(); }); } function customizeMenuToggle() { - const menuToggle = document.getElementById("menuToggle"); - if (menuToggle) { - menuToggle.innerHTML = ""; - } + const menuToggle = document.getElementById("menuToggle")!; + menuToggle.innerHTML = ""; for (let i = 0; i < 3; i++) { const line = document.createElement("div"); line.className = "hamburger-line"; - menuToggle!.appendChild(line); + menuToggle.appendChild(line); } }