├── .gitignore
├── media
├── icons
│ ├── smpp
│ │ ├── 128.png
│ │ ├── 16.png
│ │ └── 48.png
│ ├── sm-icon.svg
│ ├── weather-overlay
│ │ ├── snowflake.svg
│ │ ├── raindrop.svg
│ │ ├── blossom.svg
│ │ └── raindropfancy.svg
│ └── top-nav
│ │ └── gc-icon.svg
├── settings-icons
│ ├── other.webp
│ ├── pasta.webp
│ ├── topNav.webp
│ ├── features.webp
│ ├── widgets.webp
│ ├── appearance.webp
│ └── appearance_eye.webp
└── theme-backgrounds
│ ├── birb.jpg
│ ├── fall.jpg
│ ├── ldev.jpg
│ ├── pink.jpg
│ ├── sand.jpg
│ ├── tech.jpg
│ ├── vax.jpg
│ ├── custom.jpg
│ ├── galaxy.jpg
│ ├── japan.jpg
│ ├── matcha.jpg
│ ├── ocean.jpg
│ ├── pizza.jpg
│ ├── purple.jpg
│ ├── summer.jpg
│ ├── white.jpg
│ ├── winter.jpg
│ ├── chocolate.jpg
│ ├── default.jpg
│ ├── mountain.jpg
│ ├── smkerst.jpg
│ ├── stalker.jpg
│ ├── compressJPG's.sh
│ └── compressJPG's.bat
├── styles
├── smpp-styles
│ ├── profile.css
│ ├── assignments.css
│ ├── ui.css
│ ├── tutorial-widget.css
│ ├── games.css
│ ├── clock.css
│ ├── smpp.css
│ ├── weather.css
│ ├── dmenu.css
│ ├── delijn.css
│ └── plant.css
└── fixes
│ ├── startpage.css
│ ├── smartschool-widgets.css
│ ├── agenda.css
│ ├── notification.css
│ └── login.css
├── background-scripts
├── data
│ ├── default-plant-data.json
│ ├── default-settings.json
│ ├── settings-options.json
│ └── delijn-kleuren.json
├── utils.js
├── api-background-script.js
├── json-loader.js
├── data-background-script.js
└── background-script.js
├── main-features
├── keybinds.js
├── appearance
│ ├── background-image.js
│ ├── ui.js
│ ├── themes.js
│ └── weather-effects.js
├── globalchat.js
├── profile.js
├── modules
│ ├── windows.js
│ └── images.js
└── quick-menu
│ ├── config.js
│ ├── quick.js
│ └── dmenu.js
├── fixes-utils
├── scraper.js
├── results.js
├── titlefix.js
├── migration.js
├── login.js
└── utils.js
├── README.md
├── widgets
├── photo-widget.js
└── clock.js
├── manifest.json
└── games
├── breakout.js
├── flappy.js
├── pong.js
├── snake.js
└── games.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs/
2 | web-ext-artifacts/
3 | build/smpp-lite-build/
4 | build/smpp-build
--------------------------------------------------------------------------------
/media/icons/smpp/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/icons/smpp/128.png
--------------------------------------------------------------------------------
/media/icons/smpp/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/icons/smpp/16.png
--------------------------------------------------------------------------------
/media/icons/smpp/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/icons/smpp/48.png
--------------------------------------------------------------------------------
/media/settings-icons/other.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/other.webp
--------------------------------------------------------------------------------
/media/settings-icons/pasta.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/pasta.webp
--------------------------------------------------------------------------------
/media/settings-icons/topNav.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/topNav.webp
--------------------------------------------------------------------------------
/media/theme-backgrounds/birb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/birb.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/fall.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/fall.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/ldev.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/ldev.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/pink.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/pink.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/sand.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/sand.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/tech.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/tech.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/vax.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/vax.jpg
--------------------------------------------------------------------------------
/media/settings-icons/features.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/features.webp
--------------------------------------------------------------------------------
/media/settings-icons/widgets.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/widgets.webp
--------------------------------------------------------------------------------
/media/theme-backgrounds/custom.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/custom.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/galaxy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/galaxy.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/japan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/japan.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/matcha.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/matcha.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/ocean.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/ocean.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/pizza.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/pizza.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/purple.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/purple.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/summer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/summer.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/white.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/white.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/winter.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/winter.jpg
--------------------------------------------------------------------------------
/media/settings-icons/appearance.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/appearance.webp
--------------------------------------------------------------------------------
/media/theme-backgrounds/chocolate.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/chocolate.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/default.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/default.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/mountain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/mountain.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/smkerst.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/smkerst.jpg
--------------------------------------------------------------------------------
/media/theme-backgrounds/stalker.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/theme-backgrounds/stalker.jpg
--------------------------------------------------------------------------------
/media/settings-icons/appearance_eye.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sprksoft/smpp/HEAD/media/settings-icons/appearance_eye.webp
--------------------------------------------------------------------------------
/styles/smpp-styles/profile.css:
--------------------------------------------------------------------------------
1 | .personal-profile-picture {
2 | background-size: cover !important;
3 | object-fit: cover !important;
4 | }
5 |
--------------------------------------------------------------------------------
/background-scripts/data/default-plant-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "age": 0,
3 | "lastWaterTime": null,
4 | "lastGrowTime": null,
5 | "uniqueColor": "#fff",
6 | "plantVersion": 2,
7 | "birthday": null,
8 | "daysSinceBirthday": 0,
9 | "isAlive": true
10 | }
11 |
--------------------------------------------------------------------------------
/media/theme-backgrounds/compressJPG's.sh:
--------------------------------------------------------------------------------
1 | OUTDIR="optimized"
2 | # === Create output folder ===
3 | mkdir -p $OUTDIR
4 |
5 | # === Loop through all JPG files ===
6 | for file in *.{jpg,jpeg} ; do
7 | echo Procesing $file
8 | # === Convert and compress ===
9 | magick "$file" -resize "2560x1440>" -strip -interlace Plane -quality 85 "$outdir/$file.jpg"
10 | done
11 |
12 | echo "Done! Optimized images saved in $OUTDIR"
13 |
--------------------------------------------------------------------------------
/background-scripts/utils.js:
--------------------------------------------------------------------------------
1 |
2 | /// Fills missing fields in an object with values from a default object.
3 | export function fillObjectWithDefaults(object, defaults) {
4 | if (!object) {
5 | object = {};
6 | }
7 |
8 | for (const key of Object.keys(defaults)) {
9 | if (typeof defaults[key] === "object" && defaults[key] !== null) {
10 | object[key] = fillObjectWithDefaults(object[key], defaults[key]);
11 | }
12 |
13 | if (object[key] === undefined) {
14 | object[key] = defaults[key];
15 | }
16 | }
17 |
18 | return object;
19 | }
20 |
--------------------------------------------------------------------------------
/media/theme-backgrounds/compressJPG's.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal enabledelayedexpansion
3 |
4 | REM === Create output folder ===
5 | set "outdir=optimized"
6 | if not exist "%outdir%" mkdir "%outdir%"
7 |
8 | REM === Loop through all JPG files ===
9 | for %%f in (*.jpg *.jpeg) do (
10 | echo Processing: %%f
11 |
12 | REM === Convert and compress ===
13 | magick "%%f" ^
14 | -resize "2560x1440>" ^
15 | -strip ^
16 | -interlace Plane ^
17 | -quality 85 ^
18 | "%outdir%\%%~nf.jpg"
19 | )
20 |
21 | echo.
22 | echo Done! Optimized images saved in "%outdir%".
23 | pause
24 |
--------------------------------------------------------------------------------
/styles/fixes/startpage.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: var(--base-00) !important;
3 | color: var(--color-text);
4 | }
5 |
6 | #homepage__block--google {
7 | border-bottom: none !important;
8 | }
9 |
10 | .enableAnimations #homepage__block--google {
11 | transition: none;
12 | }
13 |
14 | .homepage {
15 | background-image: var(--loginpage-image) !important;
16 | background-position: center !important;
17 | background-size: cover !important;
18 | background-attachment: fixed !important;
19 | display: block !important;
20 | height: 100% !important;
21 | }
22 |
23 | body.modern-ui .homepage__center {
24 | min-width: 0px !important;
25 | }
26 |
27 | #leftcontainer,
28 | .homepage__left {
29 | padding: 16px 24px !important;
30 | }
31 |
32 | .toast__content {
33 | background-color: var(--color-base01) !important;
34 | }
35 |
36 | .modern-ui .float-label__input + label > span {
37 | top: -67px !important;
38 | }
39 |
--------------------------------------------------------------------------------
/background-scripts/api-background-script.js:
--------------------------------------------------------------------------------
1 | export async function fetchWeatherData(location) {
2 | const apiKey = "2b6f9b6dbe5064dd770f29d4b229a22c";
3 | try {
4 | console.log("Fetching Weather data...");
5 | const response = await fetch(
6 | `https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${apiKey}&units=metric`,
7 | );
8 | const data = await response.json();
9 | return data;
10 | } catch (error) {
11 | console.error("Error fetching Weather data:", error);
12 | return {
13 | cod: 69,
14 | message: "error code for internal use",
15 | };
16 | }
17 | }
18 | export async function fetchDelijnData(apiUrl) {
19 | const apiKey = "ddb68605719d4bb8b6444b6871cefc7a";
20 | try {
21 | console.log("Fetching Delijn data...");
22 | const response = await fetch(apiUrl, {
23 | headers: { "Ocp-Apim-Subscription-Key": apiKey },
24 | });
25 | const data = await response.json();
26 | return data;
27 | } catch (error) {
28 | console.error("Error fetching Delijn data:", error);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/styles/smpp-styles/assignments.css:
--------------------------------------------------------------------------------
1 | /* Whaatt Jorne gebruikt classes in assignments??? - you rn */
2 |
3 | .assignment__item {
4 | transition-property: background-color, margin-left, padding-left;
5 | transition: 0.3s ease;
6 | }
7 | .assignment__item {
8 | position: relative;
9 | border-radius: 0.75rem;
10 | overflow: visible;
11 | display: flex;
12 | align-items: stretch;
13 | padding-top: 0.25rem;
14 | padding-bottom: 0.25rem;
15 | }
16 |
17 | .assignment__item:hover {
18 | margin-left: 0.25rem;
19 | padding-left: 0.25rem;
20 | background-color: var(--color-base00);
21 | cursor: pointer;
22 | }
23 |
24 | .abvr__div {
25 | max-width: fit-content;
26 | display: flex;
27 | align-items: center;
28 | padding-right: 7.5px;
29 | }
30 |
31 | .wrapperdiv {
32 | display: flex;
33 | justify-content: center;
34 | align-items: center;
35 | width: 2.667rem;
36 | height: 2.667rem;
37 | border-radius: 0.5rem;
38 | }
39 |
40 | .as_all_done {
41 | text-align: center;
42 | margin-top: 1rem;
43 | font-size: 1.2em;
44 | color: var(--color-text);
45 | }
46 |
--------------------------------------------------------------------------------
/background-scripts/data/default-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profile": {
3 | "username": null,
4 | "useSMpfp": false
5 | },
6 | "appearance": {
7 | "theme": "default",
8 | "background": {
9 | "blur": 0
10 | },
11 | "weatherOverlay": {
12 | "type": "realtime",
13 | "amount": 0,
14 | "opacity": 1
15 | },
16 | "tabLogo": "sm",
17 | "news": true
18 | },
19 | "topNav": {
20 | "buttons": {
21 | "GO": false,
22 | "GC": true,
23 | "search": false,
24 | "quickMenu": false
25 | },
26 | "switchCoursesAndLinks": true,
27 | "icons": {
28 | "home": true,
29 | "mail": true,
30 | "notifications": true,
31 | "settings": true
32 | }
33 | },
34 | "other": {
35 | "quicks": [],
36 | "performanceMode": false,
37 | "splashText": true,
38 | "discordButton": true,
39 | "dmenu": {
40 | "centered": true,
41 | "itemScore": false,
42 | "toplevelConfig": false
43 | },
44 | "keybinds": {
45 | "dmenu": ":",
46 | "widgetEditMode": "E",
47 | "widgetBag": "Space",
48 | "settings": ",",
49 | "gc": "G"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/main-features/keybinds.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("keyup", async (e) => {
2 | if (e.target?.tagName === "INPUT") return;
3 | if (e.target?.tagName === "TEXTAREA") return;
4 | if (document.getElementById("tinymce")) return;
5 |
6 | const key =
7 | e.key === " " ? "Space" : e.key.length === 1 ? e.key.toUpperCase() : e.key; // this is so readable i love it
8 |
9 | if (
10 | (typeof keybinds === "undefined" ||
11 | !keybinds ||
12 | Object.keys(keybinds).length === 0) &&
13 | key === ":"
14 | ) {
15 | do_qm("dmenu");
16 | return;
17 | }
18 |
19 | // General
20 | switch (key) {
21 | case keybinds.dmenu:
22 | do_qm(keybinds.dmenu);
23 | break;
24 | case keybinds.settings:
25 | openSettingsWindow(e);
26 | break;
27 | case keybinds.gc:
28 | openGlobalChat(e);
29 | break;
30 | }
31 |
32 | // Widget
33 | if (!widgetEditModeInit) return;
34 |
35 | if (key === "Escape" && widgetEditMode) {
36 | await setEditMode(false);
37 | return;
38 | }
39 |
40 | switch (key) {
41 | case keybinds.widgetEditMode:
42 | await setEditMode(true);
43 | break;
44 | case keybinds.widgetBag:
45 | if (widgetEditMode) toggleBag();
46 | break;
47 | }
48 | });
49 |
--------------------------------------------------------------------------------
/styles/fixes/smartschool-widgets.css:
--------------------------------------------------------------------------------
1 | #homeAgenda
2 | .datepicker__cell.datepicker--current-day
3 | .datepicker__cell__content {
4 | border: 2px solid var(--color-accent) !important;
5 | z-index: 10 !important;
6 | }
7 |
8 | #homeAgenda .datepicker__cell__content:not(.datepicker--bold):hover {
9 | background-color: var(--color-base02) !important;
10 | }
11 |
12 | #homeAgenda .datepicker__cell__content {
13 | aspect-ratio: 1 !important;
14 | }
15 |
16 | input#googleQuery {
17 | background-color: var(--color-base02) !important;
18 | border-radius: 10px !important;
19 | margin-right: 10px !important;
20 | }
21 |
22 | .date-header-assignments {
23 | padding-top: 1rem;
24 | padding-bottom: 0.3rem;
25 | border-bottom: 3px solid var(--color-base02);
26 | font-size: 1.5rem;
27 | text-align: center;
28 | font-weight: 600;
29 | }
30 |
31 | h2.assignments-title {
32 | margin-bottom: 0.5rem !important;
33 | margin-top: 0 !important;
34 | text-align: center;
35 | font-size: 2rem;
36 | font-weight: 800;
37 | }
38 |
39 | .date-header-assignments::first-letter {
40 | text-transform: capitalize !important;
41 | }
42 |
43 | .task-description {
44 | width: fit-content;
45 | font-weight: 400;
46 | padding-left: 0.3rem;
47 | display: block;
48 | }
49 |
--------------------------------------------------------------------------------
/background-scripts/json-loader.js:
--------------------------------------------------------------------------------
1 | async function loadJSON(path) {
2 | const url = chrome.runtime.getURL(path);
3 | const response = await fetch(url);
4 | if (!response.ok) {
5 | throw new Error(`Failed to load JSON: ${path}`);
6 | }
7 | return response.json();
8 | }
9 |
10 | export async function initGlobals() {
11 | const [
12 | themes,
13 | settingsOptions,
14 | defaultSettings,
15 | defaultPlantData,
16 | fallbackColorData,
17 | ] = await Promise.all([
18 | loadJSON("background-scripts/data/themes.json"),
19 | loadJSON("background-scripts/data/settings-options.json"),
20 | loadJSON("background-scripts/data/default-settings.json"),
21 | loadJSON("background-scripts/data/default-plant-data.json"),
22 | loadJSON("background-scripts/data/delijn-kleuren.json"),
23 | ]);
24 |
25 | // Expand themes in settings
26 | settingsOptions.appearance.theme = Object.keys(themes);
27 |
28 | // Attach to globalThis so they're everywhere
29 | Object.assign(globalThis, {
30 | themes,
31 | settingsOptions,
32 | defaultSettings,
33 | defaultPlantData,
34 | fallbackColorData,
35 | });
36 | let globalsInitialized = true;
37 | Object.assign(globalThis, { globalsInitialized });
38 |
39 | console.log("JSON data loaded");
40 | }
41 |
--------------------------------------------------------------------------------
/background-scripts/data/settings-options.json:
--------------------------------------------------------------------------------
1 | {
2 | "profile": {
3 | "username": "string",
4 | "useSMpfp": "boolean"
5 | },
6 | "appearance": {
7 | "theme": null,
8 | "background": {
9 | "blur": "number"
10 | },
11 | "weatherOverlay": {
12 | "type": ["realtime", "rain", "snow"],
13 | "amount": "number",
14 | "opacity": "number"
15 | },
16 | "tabLogo": ["sm", "smpp"],
17 | "news": "boolean"
18 | },
19 | "topNav": {
20 | "buttons": {
21 | "GO": "boolean",
22 | "GC": "boolean",
23 | "search": "boolean",
24 | "quickMenu": "boolean"
25 | },
26 | "switchCoursesAndLinks": "boolean",
27 | "icons": {
28 | "home": "boolean",
29 | "mail": "boolean",
30 | "notifications": "boolean",
31 | "settings": "boolean"
32 | }
33 | },
34 | "other": {
35 | "quicks": "array",
36 | "performanceMode": "boolean",
37 | "splashText": "boolean",
38 | "discordButton": "boolean",
39 | "dmenu": {
40 | "centered": "boolean",
41 | "itemScore": "boolean",
42 | "toplevelConfig": "boolean"
43 | },
44 | "keybinds": {
45 | "dmenu": "keybind",
46 | "widgetEditMode": "keybind",
47 | "widgetBag": "keybind",
48 | "settings": "keybind",
49 | "gc": "keybind"
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/media/icons/sm-icon.svg:
--------------------------------------------------------------------------------
1 |
13 |
--------------------------------------------------------------------------------
/main-features/appearance/background-image.js:
--------------------------------------------------------------------------------
1 | async function setBackground(appearance) {
2 | function displayBackgroundImage(image) {
3 | document.documentElement.style.setProperty(
4 | "--background-color",
5 | `transparent`
6 | );
7 | let img =
8 | document.getElementById("background_image") ||
9 | document.createElement("img");
10 | img.id = "background_image";
11 | img.style.backgroundColor = "var(--color-base00)";
12 | img.style.position = "absolute";
13 | img.style.top = "0";
14 | img.style.left = "0";
15 | img.style.width = "100%";
16 | img.style.height = "100%";
17 | img.style.objectFit = "cover";
18 | img.style.zIndex = -1;
19 | img.style.display = "block";
20 | if (image) img.src = image;
21 | if (
22 | !document.getElementById("background_image") &&
23 | !document.getElementById("tinymce") // check for message writing box
24 | ) {
25 | document.body.appendChild(img);
26 | }
27 | }
28 | let result = await browser.runtime.sendMessage({
29 | action: "getImage",
30 | id: "backgroundImage",
31 | });
32 |
33 | if (result.type == "default") {
34 | result.imageData = await getExtensionImage(
35 | "theme-backgrounds/" + appearance.theme + ".jpg"
36 | );
37 | }
38 | displayBackgroundImage(result.imageData);
39 | }
40 |
--------------------------------------------------------------------------------
/styles/smpp-styles/ui.css:
--------------------------------------------------------------------------------
1 | /* For UI Elements created by ui.js */
2 |
3 | .smpp-input-with-label {
4 | border-radius: 999999999px;
5 |
6 | min-width: 20rem;
7 |
8 | background-color: var(--color-base01);
9 | border: 2px solid var(--color-base02);
10 | padding: 0.75rem 1rem;
11 | display: flex;
12 | flex-direction: row;
13 | align-items: center;
14 | justify-content: start;
15 | font-size: 1.6rem !important;
16 | gap: 1rem;
17 | }
18 |
19 | .smpp-input-with-label > span:first-child {
20 | }
21 |
22 | .smpp-input-with-label > .switch {
23 | margin-left: auto !important;
24 | }
25 |
26 | .smpp-input-with-label > input {
27 | margin-left: auto !important;
28 | }
29 |
30 | .smpp-input-with-label:focus-visible {
31 | border-color: var(--color-base03) !important;
32 | }
33 |
34 | .full-file-input-container {
35 | position: relative;
36 | width: fit-content;
37 | display: flex;
38 | flex-direction: row;
39 | justify-content: left;
40 | gap: 0.5rem;
41 | }
42 |
43 | .full-file-input-container .smpp-text-input {
44 | background-color: var(--color-base01) !important;
45 | border-color: var(--color-base02) !important;
46 | border-radius: 1rem !important;
47 | padding: 0.6rem 0.8rem !important;
48 | border: 2px solid var(--color-base02) !important;
49 | font-size: 1.2rem !important;
50 | width: 100% !important;
51 | }
52 |
--------------------------------------------------------------------------------
/fixes-utils/scraper.js:
--------------------------------------------------------------------------------
1 | function scrape_from_html(query, func) {
2 | data = [];
3 | let scrape_els = document.querySelectorAll(query);
4 | if (scrape_els.length == 0) {
5 | return data;
6 | }
7 | for (let i = 0; i < scrape_els.length; i++) {
8 | func(scrape_els[i], data);
9 | }
10 |
11 | return data;
12 | }
13 |
14 | function is_valid_data(data) {
15 | return !(data == undefined || data.length == 0 || data.length == undefined);
16 | }
17 |
18 | function scrape_data_if_needed(data, query, func, done_func) {
19 | if (is_valid_data(data)) {
20 | done_func(data);
21 | window.localStorage.setItem(name, JSON.stringify(data));
22 | return true;
23 | }
24 |
25 | data = scrape_from_html(query, func);
26 | if (is_valid_data(data)) {
27 | done_func(data);
28 | window.localStorage.setItem(name, JSON.stringify(data));
29 | return true;
30 | }
31 |
32 | return false;
33 | }
34 |
35 | function scrape(name, query, func, done_func, interval_time = 0) {
36 | let data = JSON.parse(window.localStorage.getItem(name));
37 | if (scrape_data_if_needed(data, query, func, done_func)) {
38 | return;
39 | }
40 | if (interval_time == 0) {
41 | return;
42 | }
43 |
44 | let interval = setInterval(() => {
45 | if (scrape_data_if_needed(data, query, func, done_func)) {
46 | clearInterval(interval);
47 | }
48 | }, interval_time);
49 | }
50 |
--------------------------------------------------------------------------------
/media/icons/weather-overlay/snowflake.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/main-features/appearance/ui.js:
--------------------------------------------------------------------------------
1 | function createButton(id = "") {
2 | let outerSwitch = document.createElement("label");
3 | outerSwitch.classList.add("switch");
4 | let innerButton = document.createElement("input");
5 | innerButton.classList.add("popupinput");
6 | innerButton.tabIndex = "-1";
7 | innerButton.type = "checkbox";
8 | innerButton.id = id;
9 | let innerSwitch = document.createElement("span");
10 | innerSwitch.classList.add("slider", "round");
11 | outerSwitch.appendChild(innerButton);
12 | outerSwitch.appendChild(innerSwitch);
13 | return outerSwitch;
14 | }
15 |
16 | function createButtonWithLabel(id = "", text) {
17 | let container = document.createElement("label");
18 | container.classList.add("smpp-input-with-label");
19 | container.htmlFor = id;
20 | container.dataset.for = id;
21 |
22 | let label = document.createElement("span");
23 | label.innerText = text;
24 | let button = createButton(id);
25 |
26 | // Add keyboard support
27 | container.addEventListener("keydown", (e) => {
28 | if (e.key === " " || e.key === "Enter") {
29 | e.preventDefault(); // Prevent page scroll on Space
30 | button.click(); // Trigger the checkbox
31 | container.focus();
32 | }
33 | });
34 |
35 | container.appendChild(label);
36 | container.appendChild(button);
37 | return container;
38 | }
39 |
40 | function createTextInput(id = "", placeholder = "") {
41 | let textInput = document.createElement("input");
42 | textInput.id = id;
43 | textInput.type = "text";
44 | textInput.placeholder = placeholder;
45 | textInput.spellcheck = false;
46 | textInput.classList.add("smpp-text-input");
47 | return textInput;
48 | }
49 |
--------------------------------------------------------------------------------
/fixes-utils/results.js:
--------------------------------------------------------------------------------
1 | function buisStats() {
2 | setTimeout(function () {
3 | const url = `https://${getSchoolName()}.smartschool.be/results/api/v1/evaluations/?itemsOnPage=1000`;
4 |
5 | fetch(url)
6 | .then((response) => response.json())
7 | .then((data) => {
8 | const categories = {
9 | buis: 0,
10 | voldoende: 0,
11 | };
12 |
13 | data.forEach((evaluation) => {
14 | if (evaluation.graphic && evaluation.graphic.value !== undefined) {
15 | const value = evaluation.graphic.value;
16 | if (value < 50) {
17 | categories.buis++;
18 | } else {
19 | categories.voldoende++;
20 | }
21 | }
22 | });
23 | newElement = document.createElement("div");
24 | newElement.id = "buis-stats";
25 | document
26 | .getElementsByClassName("results-evaluations__filters")[0]
27 | .appendChild(newElement);
28 | newElement.innerHTML = `
`;
29 | document.getElementById(
30 | "buis_amount"
31 | ).innerHTML = `Onvoldoendes:${categories.buis}
`;
32 | document.getElementById(
33 | "voldoende_amount"
34 | ).innerHTML = `Voldoendes:${categories.voldoende}
`;
35 | })
36 | .catch((error) => console.error("Error fetching:", error));
37 | }, 1000);
38 | }
39 |
--------------------------------------------------------------------------------
/media/icons/weather-overlay/raindrop.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🌟 Smartschool++, Geef Smartschool een upgrade
2 |
3 | Verbeter en personaliseer je Smartschool-ervaring met thema's, widgets, games en meer.
4 |
5 | # 📌 Overzicht
6 |
7 | Smartschool++ is een Chrome-extensie die het Belgische leerplatform Smartschool uitbreidt met diverse visuele en functionele verbeteringen.
8 | Ontwikkeld door [drie middelbare scholieren met hulp van vrijwilligers](https://github.com/sprksoft/smpp/graphs/contributors), biedt deze extensie een reeks aanpassingsmogelijkheden om de gebruikerservaring te verrijken.
9 |
10 | # 🎯 Belangrijkste functies
11 |
12 | ✅ Thema’s – Kies uit 15 unieke thema’s om de interface van Smartschool aan te passen.
13 |
14 | ❄️ Weereffecten – Voeg sneeuw- of regenanimaties toe voor een dynamische achtergrond.
15 |
16 | 🌱 Virtuele plant – Zorg voor een digitale plant op je dashboard die dagelijks aandacht nodig heeft.
17 |
18 | 🗓️ Planner-widget – Bekijk je agenda direct op de startpagina.
19 |
20 | 🚌 De Lijn-integratie – Ontdek buslijnen en haltes rechtstreeks op je dashboard.
21 |
22 | 🎮 Ingebouwde games – Speel klassieke spellen zoals Flappy Bird++ en Snake++.
23 |
24 | ⚡ Snelmenu – Navigeer efficiënt door Smartschool met het : toets snelmenu.
25 |
26 | 🖼️ Achtergrondaanpassing – Stel een eigen afbeelding of link in als achtergrond.
27 |
28 | 💬 Global Chat – Communiceer met andere gebruikers via de ingebouwde chatfunctie.
29 |
30 | # 🚀 Installatie
31 |
32 | Installeer de extentie op de [Chrome web store](https://chromewebstore.google.com/detail/bdhficnphioomdjhdfbhdepjgggekodf)
33 |
34 | Klik op "Toevoegen aan Chrome".
35 |
36 | Open Smartschool en klik op "Settings" rechtsboven om de extensie aan te passen.
37 |
38 | # 👥 Community & Ondersteuning
39 |
40 | Voor vragen of feedback, sluit je aan bij onze [Discord server](https://discord.gg/A77xPC9qdW
41 | ).
42 |
43 | # 👨💻 Voor Developers
44 |
45 | Wil je een pull request maken of helpen?
46 | Gebruik de "Prettier" code-formatter anders zal je pull request niet aanvaard worden.
47 |
--------------------------------------------------------------------------------
/widgets/photo-widget.js:
--------------------------------------------------------------------------------
1 | class PhotoWidget extends WidgetBase {
2 | constructor() {
3 | super();
4 | }
5 | displayImage() {
6 | browser.runtime
7 | .sendMessage({ action: "getPhotoWidgetImage" })
8 | .then((result) => {
9 | if (result && result.photoWidgetImage) {
10 | this.img =
11 | document.getElementById("photo_widget_background_image") ||
12 | document.createElement("img");
13 | if (this.img) {
14 | this.img.src = result.photoWidgetImage;
15 | } else {
16 | this.img = document.createElement("img");
17 | this.img.id = "photo_widget_background_image";
18 | this.img.style.backgroundColor = "var(--color-base00)";
19 | this.img.style.position = "relative";
20 | this.img.style.top = "0";
21 | this.img.style.left = "0";
22 | this.img.style.width = "100%";
23 | this.img.style.height = "100%";
24 | this.img.style.objectFit = "cover";
25 | this.img.style.zIndex = -1;
26 | this.img.style.display = "block";
27 | this.img.src = result.photoWidgetImage;
28 | }
29 | this.content.appendChild(this.img);
30 | } else {
31 | this.noImageDisplay = document.createElement("div");
32 | this.noImageDisplay.className = "no-image-display";
33 | this.noImageDisplay.textContent = "No image available";
34 | this.content.appendChild(this.noImageDisplay);
35 | }
36 | });
37 | }
38 |
39 | createContent() {
40 | this.content = document.createElement("div");
41 | this.content.className = "photo-widget";
42 | this.content.id = "photo_widget";
43 |
44 | this.displayImage();
45 | return this.content;
46 | }
47 |
48 | createPreview() {
49 | this.previewContent = document.createElement("div");
50 | this.previewContent.className = "photo-widget-preview";
51 | this.previewContent.id = "photo_widget_preview";
52 |
53 | this.previewContent.textContent = "Photo Widget Preview";
54 | return this.previewContent;
55 | }
56 | }
57 |
58 | registerWidget(new PhotoWidget());
59 |
--------------------------------------------------------------------------------
/styles/smpp-styles/tutorial-widget.css:
--------------------------------------------------------------------------------
1 | .lang-buttons {
2 | flex-direction: row;
3 | align-items: center;
4 | justify-content: center;
5 | gap: 0.5rem;
6 | display: flex;
7 | position: relative;
8 | }
9 |
10 | #english-tutorial-lang-icon,
11 | #dutch-tutorial-lang-icon {
12 | border-radius: 10000px;
13 | width: 2.4rem;
14 | background-position: center;
15 | background-size: cover;
16 | }
17 |
18 | #english-tutorial-lang-icon {
19 | background-image: url("https://upload.wikimedia.org/wikipedia/en/thumb/a/ae/Flag_of_the_United_Kingdom.svg/500px-Flag_of_the_United_Kingdom.svg.png");
20 | }
21 | #dutch-tutorial-lang-icon {
22 | background-image: url("https://upload.wikimedia.org/wikipedia/commons/thumb/6/65/Flag_of_Belgium.svg/250px-Flag_of_Belgium.svg.png");
23 | }
24 |
25 | #english-tutorial-lang,
26 | #dutch-tutorial-lang {
27 | border-radius: 1000px !important;
28 | border: 2px solid var(--color-base03) !important;
29 | background-color: var(--color-base02) !important;
30 | font-size: 1.2rem !important;
31 | font-weight: 600 !important;
32 | padding: 0.5rem !important;
33 | padding-right: 1rem !important;
34 | display: flex;
35 | flex-direction: row;
36 | gap: 0.5rem;
37 | }
38 |
39 | .enableAnimations #english-tutorial-lang,
40 | .enableAnimations #dutch-tutorial-lang {
41 | transition-property: border-color, background-color;
42 | transition: 0.2s ease-in-out !important;
43 | }
44 |
45 | #english-tutorial-lang:hover,
46 | #dutch-tutorial-lang:hover {
47 | background-color: var(--color-base03) !important;
48 | border-color: var(--color-accent) !important;
49 | }
50 |
51 | .tutorial-widget-title {
52 | font-size: 2rem !important;
53 | margin-bottom: 0.5rem !important;
54 | color: var(--color-accent);
55 | text-align: left;
56 | }
57 |
58 | .tutorial-lang-title {
59 | font-size: 1.2rem;
60 | font-weight: 600;
61 | text-align: center;
62 | }
63 |
64 | .tutorial-widget-description {
65 | font-size: 1.1rem;
66 | font-weight: 500;
67 | text-align: left;
68 | margin-bottom: 0 !important;
69 | }
70 |
71 | .smpp-widget:has(#tutorial-widget) {
72 | background: linear-gradient(
73 | 60deg,
74 | var(--color-base01),
75 | var(--color-base02)
76 | ) !important;
77 | border-color: var(--color-accent) !important;
78 | }
79 |
--------------------------------------------------------------------------------
/main-features/appearance/themes.js:
--------------------------------------------------------------------------------
1 | let currentThemeName;
2 | let currentTheme;
3 | let customTheme;
4 |
5 | async function setTheme(themeName) {
6 | let style = document.documentElement.style;
7 | currentThemeName = themeName;
8 | currentTheme = await browser.runtime.sendMessage({
9 | action: "getTheme",
10 | theme: themeName,
11 | });
12 | if (themeName != "custom") {
13 | Object.keys(currentTheme).forEach((key) => {
14 | style.setProperty(key, currentTheme[key]);
15 | });
16 | } else {
17 | const themeData = await browser.runtime.sendMessage({
18 | action: "getCustomThemeData",
19 | });
20 | customTheme = themeData;
21 | const settingsData = await browser.runtime.sendMessage({
22 | action: "getSettingsData",
23 | });
24 | Object.keys(themeData).forEach((key) => {
25 | style.setProperty("--" + key.replace("_", "-"), themeData[key]);
26 | });
27 | if (settingsData.selection == 0) {
28 | style.setProperty("--loginpage-image", "url(https://about:blank)");
29 | }
30 | }
31 | await widgetSystemNotifyThemeChange();
32 | }
33 | function getThemeVar(varName) {
34 | if (currentThemeName != "custom") {
35 | return currentTheme[varName];
36 | } else {
37 | return customTheme[varName.replace("--", "").replace("-", "_")];
38 | }
39 | }
40 |
41 | function getThemeQueryString(queryVars = []) {
42 | let queryString = "";
43 | if (currentThemeName != "custom") {
44 | queryVars.forEach((queryVar) => {
45 | themeVar = currentTheme["--" + queryVar];
46 | queryString += `&${queryVar}=${
47 | themeVar.startsWith("#") ? themeVar.substring(1) : themeVar
48 | }`;
49 | });
50 | } else {
51 | queryVars.forEach((queryVar) => {
52 | themeVar = customTheme[queryVar.replace("-", "_")];
53 | queryString += `&${queryVar}=${
54 | themeVar.startsWith("#") ? themeVar.substring(1) : themeVar
55 | }`;
56 | });
57 | }
58 | queryString = queryString.substring(1);
59 | return queryString;
60 | }
61 |
62 | function getHiddenThemes() {
63 | let hiddenThemes = {};
64 | settingsOptions.appearance.theme.forEach((key) => {
65 | if (themes[key]["display-name"].startsWith("__")) {
66 | hiddenThemes[key] = themes[key];
67 | }
68 | });
69 | return hiddenThemes;
70 | }
71 |
--------------------------------------------------------------------------------
/background-scripts/data-background-script.js:
--------------------------------------------------------------------------------
1 | if (typeof browser === "undefined") {
2 | var browser = chrome;
3 | }
4 |
5 | import { fetchDelijnData } from "./api-background-script.js";
6 | import { fillObjectWithDefaults } from "./utils.js";
7 |
8 | function getDefaultCustomThemeData() {
9 | return {
10 | color_accent: "#a3a2ec",
11 | color_base00: "#38313a",
12 | color_base01: "#826882",
13 | color_base02: "#ac85b7",
14 | color_base03: "#c78af0",
15 | color_text: "#ede3e3",
16 | };
17 | }
18 |
19 | export function getDefaultSettings() {
20 | return defaultSettings;
21 | }
22 |
23 | export async function getSettingsData() {
24 | let data = (await browser.storage.local.get("settingsData")).settingsData;
25 | console.log("set before filled: ");
26 | console.log(data);
27 | data = fillObjectWithDefaults(data, getDefaultSettings());
28 | console.log("set after filled: ");
29 | console.log(data);
30 | return data;
31 | }
32 |
33 | export async function getPlantAppData() {
34 | let data = await browser.storage.local.get("plantAppData");
35 | let plantAppData = data.plantAppData || defaultPlantData;
36 | return plantAppData;
37 | }
38 |
39 | export async function getCustomThemeData() {
40 | let data = await browser.storage.local.get("customThemeData");
41 | return data.customThemeData || getDefaultCustomThemeData();
42 | }
43 |
44 | export function getSettingsOptions() {
45 | return settingsOptions;
46 | }
47 |
48 | export function getAllThemes() {
49 | return themes;
50 | }
51 |
52 | export function getTheme(theme) {
53 | console.log(themes[theme]);
54 | if (themes[theme] != undefined) {
55 | return themes[theme];
56 | } else {
57 | throw new Error("Theme not found");
58 | }
59 | }
60 |
61 | export async function getDelijnColorData() {
62 | try {
63 | let data = await browser.storage.local.get("delijnColorData");
64 | let delijnColorData;
65 | if (data.delijnColorData?.kleuren != undefined) {
66 | delijnColorData = data.delijnColorData;
67 | } else {
68 | delijnColorData = await fetchDelijnData(
69 | "https://api.delijn.be/DLKernOpenData/api/v1/kleuren"
70 | );
71 | }
72 |
73 | await browser.storage.local.set({
74 | delijnColorData: delijnColorData,
75 | });
76 | console.log("Retrieved Delijn Color Data.");
77 | return delijnColorData;
78 | } catch (error) {
79 | console.error("Error retrieving Delijn Color Data:", error);
80 | return fallbackColorData;
81 | }
82 | }
83 |
84 | export async function setImage(id, data) {
85 | const images = (await browser.storage.local.get("images")).images || {};
86 | images[id] = data;
87 | await browser.storage.local.set({ images });
88 | }
89 |
90 | export async function getImage(id) {
91 | const images = (await browser.storage.local.get("images")).images || {};
92 | if (!images[id]) return { type: "default", link: null, imageData: null };
93 | return images[id];
94 | }
95 |
--------------------------------------------------------------------------------
/fixes-utils/titlefix.js:
--------------------------------------------------------------------------------
1 | if (browser == undefined) {
2 | var browser = chrome;
3 | }
4 |
5 | function vak_prefix(page) {
6 | switch (page) {
7 | case "news":
8 | return "Vaknieuws";
9 |
10 | case "course":
11 | return "Online les";
12 |
13 | case "documents":
14 | return "Documenten";
15 |
16 | case "uploadzone":
17 | return "Uploadzone";
18 |
19 | case "exercises":
20 | return "Oefeningen";
21 |
22 | case "lpaths":
23 | return "Leerpaden";
24 |
25 | case "weblinks":
26 | return "Weblinks";
27 |
28 | case "tasks":
29 | return "Taken";
30 |
31 | case "cooperate":
32 | return "Samenwerken";
33 |
34 | case "classmates":
35 | return "Studiegenoten";
36 |
37 | case "forum":
38 | return "Forum";
39 |
40 | case "survey":
41 | return "Enquêtes";
42 |
43 | case "wiki":
44 | return "Wiki";
45 |
46 | default:
47 | break;
48 | }
49 | }
50 |
51 | function title_prefix() {
52 | let subdomain =
53 | location.host.split(".")[0].charAt(0).toUpperCase() +
54 | location.host.split(".")[0].slice(1);
55 | let url = location.pathname;
56 | let qstr = new URLSearchParams(location.search);
57 | let module = qstr.get("module");
58 | if (!url.split("/")[1]) return;
59 | let page = url.split("/")[1].toLowerCase();
60 | if (module !== null) {
61 | page = module.toLowerCase();
62 | }
63 |
64 | switch (page) {
65 | case "planner":
66 | return "Planner";
67 | case "photos":
68 | return "Photos";
69 | case "agenda":
70 | return "Agenda";
71 | case "results":
72 | return "Resultaten";
73 | case "messages":
74 | return "Berichten";
75 | case "mydoc":
76 | return "Mijn documenten";
77 | case "forms":
78 | return "Formulieren";
79 | case "studentcard":
80 | return "Mijn leerlingfiche";
81 | case "manual":
82 | return "Handleiding";
83 | case "timetable":
84 | return "Lesrooster";
85 | case "intradesk":
86 | return "Intradesk";
87 | case "online-session":
88 | return "Online sessies";
89 | case "lvs":
90 | return "Leerlingvolgsysteem";
91 | case "":
92 | return "Start - " + subdomain;
93 | default:
94 | break;
95 | }
96 |
97 | let topnav_title = document.querySelector(".topnav__title");
98 | if (topnav_title) {
99 | topnav_title = topnav_title.innerText;
100 | }
101 | let prefix = vak_prefix(page);
102 | if (prefix != undefined) {
103 | if (topnav_title) {
104 | return prefix + " - " + topnav_title;
105 | } else {
106 | return prefix;
107 | }
108 | }
109 | }
110 |
111 | function titleFix() {
112 | let prepend = title_prefix();
113 | if (prepend != undefined) {
114 | let title = document.querySelector("head > title");
115 | title.innerText = prepend + " - Smartschool";
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/styles/smpp-styles/games.css:
--------------------------------------------------------------------------------
1 | .game-container {
2 | padding: 0px !important;
3 | height: 260px;
4 | overflow: hidden;
5 | width: 100%;
6 | }
7 | .smpp-widget-preview .game-container {
8 | height: 100% !important;
9 | aspect-ratio: 1;
10 | }
11 | .game-menu {
12 | display: flex;
13 | flex-direction: column;
14 | text-align: center;
15 | justify-content: space-between;
16 | height: 100%;
17 | }
18 | .game-menu-top,
19 | .game-menu-bottom {
20 | display: flex;
21 | flex-direction: column;
22 | text-align: center;
23 | align-items: center;
24 | justify-content: center;
25 | }
26 | .game-title {
27 | font-size: 3rem;
28 | font-weight: 800;
29 | margin-bottom: 0.1rem !important;
30 | }
31 |
32 | .smpp-widget-preview .game-title {
33 | font-size: 2.5rem;
34 | }
35 |
36 | .game-score {
37 | font-size: 1.75rem !important;
38 | font-weight: 500;
39 | }
40 |
41 | .game-canvas {
42 | width: 260px;
43 | height: 260px;
44 | }
45 |
46 | .game-button {
47 | font-size: 1.7rem !important;
48 | font-weight: 500;
49 | width: 80% !important;
50 |
51 | margin-top: 1rem;
52 | padding: 0.5rem 0rem !important;
53 | align-self: center !important;
54 | background: linear-gradient(
55 | 100deg,
56 | color-mix(in srgb, var(--color-base01) 30%, var(--color-base02) 70%),
57 | var(--color-base02),
58 | var(--color-base02),
59 | color-mix(in srgb, var(--color-base01) 30%, var(--color-base02) 70%)
60 | );
61 | border: 3px solid var(--color-base03);
62 | border-radius: 1000px !important;
63 | margin-bottom: 1.8rem;
64 | }
65 |
66 | .game-button:hover {
67 | border-color: var(--color-accent) !important;
68 | font-weight: 600;
69 | }
70 |
71 | .enableAnimations .game-button {
72 | transition-property: border, font-weight !important;
73 | transition: 0.2s ease-in-out;
74 | }
75 |
76 | .game-slide-container {
77 | display: flex;
78 | flex-direction: row;
79 | justify-content: space-between;
80 | align-items: center;
81 | width: 70%;
82 | }
83 |
84 | .game-slider-label {
85 | margin: 0 !important;
86 | margin-bottom: 1rem !important;
87 | font-size: 1.6rem;
88 | font-weight: 400;
89 | cursor: text !important;
90 | }
91 | .game-option-value {
92 | font-size: 1.2rem;
93 | }
94 | .game-slider {
95 | -webkit-appearance: none;
96 | appearance: none;
97 | height: 15px;
98 | border-radius: 10px;
99 | background: var(--color-base02);
100 | outline: none;
101 | opacity: 1;
102 | width: 80%;
103 | }
104 |
105 | .enableAnimations .game-slider {
106 | transition: opacity 0.2s;
107 | }
108 |
109 | .game-slider::-webkit-slider-thumb {
110 | width: 1.5rem;
111 | height: 1.5rem;
112 | border-radius: 50%;
113 | background: var(--color-accent) !important;
114 | border-color: transparent;
115 | cursor: pointer;
116 | transition: opacity 0.2s ease-in-out;
117 | -webkit-appearance: none;
118 | appearance: none;
119 | }
120 | .game-slider::-moz-range-thumb {
121 | width: 1.5rem;
122 | height: 1.5rem;
123 | border-radius: 50%;
124 | background: var(--color-accent) !important;
125 | border-color: transparent;
126 | cursor: pointer;
127 | transition: opacity 0.2s ease-in-out;
128 | appearance: none;
129 | }
130 |
--------------------------------------------------------------------------------
/media/icons/weather-overlay/blossom.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/styles/fixes/agenda.css:
--------------------------------------------------------------------------------
1 | .border {
2 | background-image: none !important;
3 | background-color: var(--color-base00) !important;
4 | }
5 |
6 | body {
7 | width: 100%;
8 | height: 100%;
9 | margin: 0px;
10 | padding: 0px;
11 | overflow-x: hidden;
12 | overflow-y: hidden;
13 | display: flex !important;
14 | flex-direction: column !important;
15 | }
16 |
17 | .agenda_grid_main {
18 | background-color: var(--color-base01) !important;
19 | border: 3px solid var(--color-base03) !important;
20 | border-radius: 10px !important;
21 | }
22 |
23 | #menuContainerContent {
24 | background-color: var(--color-base01) !important;
25 | border: 3px solid var(--color-base03) !important;
26 | border-radius: 10px !important;
27 | width: 34px !important;
28 | }
29 |
30 | .color_snow_white {
31 | background-color: var(--color-base01) !important;
32 | border: 2px solid var(--color-base03) !important;
33 | border-radius: 5px !important;
34 | }
35 |
36 | .navNextInput,
37 | .navPrevInput,
38 | .ui-datepicker-month,
39 | .ui-datepicker-year {
40 | color: var(--color-accent) !important;
41 | background-color: var(--color-base01) !important;
42 | border: 2px solid var(--color-base03) !important;
43 | border-radius: 5px !important;
44 | }
45 |
46 | .agenda_grid_header_day,
47 | .navTitle {
48 | color: var(--color-accent) !important;
49 | }
50 |
51 | .ui-datepicker-header {
52 | background-color: var(--color-base01) !important;
53 | border: 2px solid var(--color-base03) !important;
54 | border-radius: 5px !important;
55 | }
56 |
57 | .ui-state-default {
58 | background-color: var(--color-base02) !important;
59 | border: 1px solid var(--color-base03) !important;
60 | }
61 |
62 | .border.centery.bottomy,
63 | .smsclayerloading {
64 | display: none !important;
65 | }
66 |
67 | .cf .margin-top--milli {
68 | border: 0px !important;
69 | }
70 |
71 | .border.centerxy {
72 | border-right: 1px solid var(--color-base02) !important;
73 | }
74 |
75 | .tasksAndMaterials__label:hover {
76 | color: var(--color-accent) !important;
77 | }
78 |
79 | .titleWithLine.grey:after {
80 | background: linear-gradient(var(--color-base02), var(--color-base02)) repeat-x !important;
81 | background-size: 1px 1px !important;
82 | background-position: left center !important;
83 | }
84 |
85 | #infoContainerStudent,
86 | #infoFormWindow_movableDiv,
87 | #infoFormWindow_titlebar,
88 | #infoFormWindow_contentDiv,
89 | .wborder.wleftxy,
90 | .wborder.wrightxy,
91 | .wborder.wcentery.wbottomy,
92 | .infoContainer {
93 | color: var(--color-accent) !important;
94 | background: var(--color-base01) !important;
95 | }
96 |
97 | #infoContainerNote .infoContainerNoteContent,
98 | .infoContainerBlock {
99 | background-color: var(--color-base02) !important;
100 | }
101 |
102 | .wborder.wbright.wtopr {
103 | background: var(--color-base01) !important;
104 | }
105 |
106 | .wborder.wbright.wbottomr {
107 | border-bottom-right-radius: 4px !important;
108 | background: var(--color-base01) !important;
109 | }
110 |
111 | .wborder.wbleft.wtopl {
112 | border-top-left-radius: 4px !important;
113 | background: var(--color-base01) !important;
114 | }
115 |
116 | .wborder.wbleft.wbottoml {
117 | border-bottom-left-radius: 8px !important;
118 | background: var(--color-base01) !important;
119 | }
120 |
--------------------------------------------------------------------------------
/styles/smpp-styles/clock.css:
--------------------------------------------------------------------------------
1 | .clock-widget,
2 | .clock-widget-preview {
3 | display: flex;
4 | flex-direction: column;
5 | align-items: center;
6 | background: transparent;
7 | padding: 0;
8 | }
9 |
10 | .clock-center {
11 | position: absolute;
12 | left: calc(50% - 0.6rem);
13 | top: calc(50% - 0.6rem);
14 | width: 1.2rem;
15 | height: 1.2rem;
16 | background-color: var(--color-accent);
17 | border-radius: 100000px;
18 | }
19 |
20 | .clock-preview-title {
21 | font-size: 2.5rem;
22 | font-weight: 700;
23 | }
24 |
25 | .clock-container,
26 | .clock-widget-preview .clock-container {
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | width: 100%;
31 | }
32 |
33 | .clock-face,
34 | .clock-widget-preview .clock-face {
35 | border-radius: 50%;
36 | aspect-ratio: 1;
37 | background-color: transparent;
38 | position: relative;
39 | margin: 0 auto;
40 | }
41 |
42 | .clock-widget-preview .clock-hand,
43 | .clock-hand {
44 | background-color: var(--color-text);
45 | /* No transition because hands are updated every frame and transitions cause hands to spin when tab is unloaded*/
46 | transition: none;
47 | }
48 |
49 | .clock-widget-preview .second-hand,
50 | .second-hand {
51 | background-color: var(--color-accent);
52 | }
53 |
54 | .clock-bottom,
55 | .clock-widget-preview .clock-bottom {
56 | display: flex;
57 | flex-direction: column;
58 | text-align: center;
59 | width: 100%;
60 | margin-top: 5px;
61 | }
62 |
63 | .digital-time,
64 | .clock-widget-preview .digital-time {
65 | font-weight: 800;
66 | text-align: center;
67 | }
68 |
69 | /* Widget-specific differences */
70 |
71 | .clock-widget-preview .clock-face,
72 | .clock-face {
73 | width: 50%;
74 | margin: 0px 25%;
75 | background-color: var(--darken-background);
76 | aspect-ratio: 1;
77 | position: relative;
78 | }
79 |
80 | /* put pivot at the center, then move the element so its bottom is exactly there */
81 | .clock-hand {
82 | position: absolute;
83 | left: 50%;
84 | top: 50%;
85 | transform-origin: 50% 100%; /* bottom center */
86 | transform: translate(-50%, -100%) rotate(0deg);
87 | width: 1rem;
88 | border-radius: 0.5rem;
89 | box-shadow: 0 0 30px 4px var(--color-shadow);
90 |
91 | /* No transition because hands are updated every frame and transitions cause hands to spin when tab is unloaded*/
92 | transition: none;
93 | }
94 |
95 | /* lengths expressed as percentage of the face's size */
96 | .hour-hand {
97 | height: 30%;
98 | /*opacity: 0.8;*/
99 | z-index: -3;
100 | } /* shorter */
101 |
102 | .minute-hand {
103 | height: 42.5%;
104 | z-index: -2;
105 | }
106 |
107 | .second-hand {
108 | height: 45%;
109 | width: 0.5rem; /* thinner */
110 | transform-origin: 50% 100%; /* still bottom center */
111 | z-index: -1;
112 | }
113 |
114 | .smpp-widget-preview .hour-hand {
115 | width: 0.9rem;
116 | }
117 | .smpp-widget-preview .minute-hand {
118 | width: 0.7rem;
119 | }
120 | .smpp-widget-preview .second-hand {
121 | width: 0.5rem;
122 | }
123 |
124 | .clock-widget-preview .clock-face {
125 | width: 70%;
126 | }
127 |
128 | /* rotate in JS by setting style.transform = `translate(-50%, -100%) rotate(${deg}deg)` */
129 |
130 | .digital-time {
131 | font-size: 1.6rem;
132 | font-weight: 800;
133 | letter-spacing: 0.5rem;
134 | }
135 |
--------------------------------------------------------------------------------
/main-features/globalchat.js:
--------------------------------------------------------------------------------
1 | const GC_DOMAINS = {
2 | main: "https://gc.smartschoolplusplus.com",
3 | beta: "https://gcbeta.smartschoolplusplus.com",
4 | };
5 |
6 | class GlobalChatWindow extends BaseWindow {
7 | beta;
8 | iframe;
9 | gcContent;
10 | constructor(hidden = true) {
11 | super("global_chat_window", hidden);
12 | }
13 |
14 | async renderContent() {
15 | this.gcContent = document.createElement("div");
16 | return this.gcContent;
17 | }
18 |
19 | onOpened() {
20 | const queryString = getThemeQueryString([
21 | "color-base00",
22 | "color-base01",
23 | "color-base02",
24 | "color-base03",
25 | "color-accent",
26 | "color-text",
27 | ]);
28 | this.iframe = document.createElement("iframe");
29 | this.iframe.style = "width:100%; height:100%; border:none";
30 | this.iframe.src = GC_DOMAINS[this.beta ? "beta" : "main"] + "/v1?" + queryString;
31 | this.gcContent.appendChild(this.iframe);
32 | }
33 | }
34 |
35 | let gcWindow;
36 |
37 | async function openGlobalChat(event, beta = false) {
38 | if (!gcWindow || !gcWindow.element?.isConnected) {
39 | gcWindow = new GlobalChatWindow();
40 | await gcWindow.create();
41 | }
42 | gcWindow.beta = beta;
43 | gcWindow.show(event);
44 | }
45 |
46 | function createGC() {
47 | const GlobalChatOpenButton = document.createElement("button");
48 | GlobalChatOpenButton.title =
49 | "Global chat (chat met iedereen die de extensie gebruikt)";
50 | GlobalChatOpenButton.id = "global_chat_button";
51 | GlobalChatOpenButton.className = "topnav__btn";
52 | GlobalChatOpenButton.innerHTML = gcIconSvg;
53 | GlobalChatOpenButton.addEventListener("click", (e) => openGlobalChat(e));
54 | return GlobalChatOpenButton;
55 | }
56 |
57 |
58 | // public smpp api
59 | // Versions will change when a breaking change is required (Adding fields is not a breaking change)
60 | window.addEventListener("message", async (e) => {
61 | if (!Object.values(GC_DOMAINS).includes(e.origin)) {
62 | console.warn("Got a message but it was not from one of the global chat domains.")
63 | return;
64 | }
65 | let response = { error: "not found" };
66 | switch (e.data.action) {
67 | // Get the current plant.
68 | case "plantapi.v1.get_current":
69 | response = await getPlantV1();
70 | break;
71 | case "plantapi.v1.get_stage_svg":
72 | response = {
73 | svg: getPlantSvg(stageDataToInternalPlantData(e.data.stageData)),
74 | };
75 | break;
76 | }
77 | response.callId = e.data.callId;
78 | e.source.postMessage(response, e.origin);
79 | });
80 |
81 | function stageDataToInternalPlantData(stageData) {
82 | return {
83 | uniqueColor: stageData.color,
84 | isAlive: stageData.isAlive,
85 | age: stageData.stage,
86 | };
87 | }
88 |
89 | async function getPlantV1() {
90 | let data = await browser.runtime.sendMessage({
91 | action: "getPlantAppData",
92 | });
93 | if (!data) {
94 | return {
95 | stageData: {
96 | color: "#fff",
97 | isAlive: true,
98 | stage: 0,
99 | },
100 | age: 0,
101 | }
102 | }
103 | return {
104 | stageData: {
105 | color: data.uniqueColor,
106 | isAlive: data.isAlive,
107 | stage: data.age,
108 | },
109 | age: data.daysSinceBirthday,
110 | };
111 | }
112 |
--------------------------------------------------------------------------------
/main-features/profile.js:
--------------------------------------------------------------------------------
1 | async function displayUsernameTopNav(name) {
2 | let originalNameElement = document.querySelector(
3 | ".js-btn-profile .hlp-vert-box span"
4 | );
5 | if (originalNameElement) originalNameElement.innerHTML = name;
6 | }
7 |
8 | async function attachProfilePictureObserver(url) {
9 | // helper to process a single
element
10 | function processImg(img) {
11 | try {
12 | // ignore if:
13 | // not an img or no src
14 | if (!(img instanceof HTMLImageElement) || !img.src) return;
15 | // img src was already changed
16 | if (img.src == url) return;
17 | // not the original pfp and doesn't already have the class (I know it's confusing... and I made it, so)
18 | if (
19 | !isOriginalPfpUrl(img.src) &&
20 | !img.classList.contains("personal-profile-picture")
21 | )
22 | return;
23 |
24 | img.src = url;
25 | img.classList.add("personal-profile-picture");
26 | } catch (e) {
27 | console.error("Error processing img:", e, img);
28 | }
29 | }
30 |
31 | const existingImgs = Array.from(document.getElementsByTagName("img"));
32 | existingImgs.forEach(processImg);
33 |
34 | const observer = new MutationObserver((mutations) => {
35 | for (const mutation of mutations) {
36 | for (const node of mutation.addedNodes) {
37 | if (node.nodeType !== Node.ELEMENT_NODE) continue;
38 |
39 | if (node instanceof HTMLImageElement) {
40 | processImg(node);
41 | continue;
42 | }
43 |
44 | try {
45 | const imgs = node.querySelectorAll?.("img");
46 | if (imgs && imgs.length) {
47 | for (const img of imgs) processImg(img);
48 | }
49 | } catch (e) {
50 | console.error("Error querying imgs inside node:", e, node);
51 | }
52 | }
53 |
54 | if (
55 | mutation.type === "attributes" &&
56 | mutation.target instanceof HTMLImageElement
57 | ) {
58 | processImg(mutation.target);
59 | }
60 | }
61 | });
62 |
63 | observer.observe(document.body, {
64 | childList: true,
65 | subtree: true,
66 | attributes: true,
67 | attributeFilter: ["src"],
68 | });
69 |
70 | return observer;
71 | }
72 |
73 | async function applyUsername(customName) {
74 | if (customName) {
75 | displayUsernameTopNav(customName);
76 | } else {
77 | displayUsernameTopNav(originalUsername);
78 | }
79 | }
80 |
81 | async function applyProfilePicture(profile) {
82 | let style = document.documentElement.style;
83 | const setPFPstyle = (url) => {
84 | style.setProperty("--profile-picture", "url(" + url + ")");
85 | };
86 | const fixObserver = async (url) => {
87 | if (profilePictureObserver != null) {
88 | profilePictureObserver.disconnect();
89 | }
90 |
91 | profilePictureObserver = await attachProfilePictureObserver(url);
92 | };
93 |
94 | let profileImageURL;
95 | switch (profile.useSMpfp) {
96 | case true:
97 | profileImageURL = originalPfpUrl;
98 | break;
99 | case false:
100 | const onDefault = () => {
101 | return getPfpLink(profile.username || originalUsername);
102 | };
103 | let result = await getImageURL("profilePicture", onDefault);
104 |
105 | profileImageURL = result.url;
106 | break;
107 | }
108 |
109 | setPFPstyle(profileImageURL);
110 | fixObserver(profileImageURL);
111 | }
112 |
113 | function isOriginalPfpUrl(url) {
114 | function createSimpleUrl(url) {
115 | const parts = url.split("/");
116 | parts.pop();
117 | return parts.join("/");
118 | }
119 | return createSimpleUrl(originalPfpUrl) == createSimpleUrl(url);
120 | }
121 |
--------------------------------------------------------------------------------
/styles/smpp-styles/smpp.css:
--------------------------------------------------------------------------------
1 | /* CSS for things that are smpp specific */
2 |
3 | #quickSettings .full-file-input-container {
4 | color: var(--color-text) !important;
5 | position: relative;
6 | overflow: visible !important;
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: stretch;
10 | gap: 0.5rem;
11 | transition: none !important;
12 | flex-grow: 1;
13 | text-align: center !important;
14 | }
15 |
16 | .smpp-file-input-button,
17 | #quickSettings .full-file-input-container .smpp-text-input {
18 | border: 2px solid var(--color-base03) !important;
19 | background-color: var(--color-base01) !important;
20 | border-radius: 1rem;
21 | box-shadow: 0px 2px 10px 0px var(--color-shadow);
22 | }
23 |
24 | #quickSettings .full-file-input-container .smpp-text-input:hover,
25 | #quickSettings .full-file-input-container .smpp-text-input:focus-visible {
26 | border-color: var(--color-accent) !important;
27 | }
28 |
29 | .enableAnimations #quickSettings .full-file-input-container {
30 | transition-property: opacity, filter;
31 | transition: 0.3s !important;
32 | }
33 |
34 | .smpp-file-input-button {
35 | margin-right: 0px !important;
36 | background: none !important;
37 | opacity: 0.5;
38 | display: flex;
39 | }
40 |
41 | .active.smpp-file-input-button {
42 | opacity: 1 !important;
43 | }
44 |
45 | .enableAnimations .smpp-file-input-button {
46 | transition: opacity 0.3s, filter 0.3s, border-color ease-in-out 0.3s !important;
47 | }
48 |
49 | .smpp-file-input-button:hover,
50 | .smpp-file-input-button:focus-visible {
51 | border-color: var(--color-accent) !important;
52 | }
53 |
54 | .smpp-link-clear-button:active {
55 | scale: 0.9 !important;
56 | }
57 |
58 | .smpp-link-clear-button {
59 | opacity: 0;
60 | border-radius: 50%;
61 | background-color: var(--color-base02);
62 | border: 2px solid var(--color-base03);
63 | height: 2rem;
64 | aspect-ratio: 1;
65 | padding: 0 !important;
66 | z-index: -10;
67 | pointer-events: none;
68 | stroke: var(--color-text);
69 | }
70 |
71 | .active.smpp-link-clear-button {
72 | opacity: 1;
73 | pointer-events: all;
74 | z-index: auto;
75 | }
76 | .enableAnimations .smpp-link-clear-button {
77 | transition: filter 0.2s ease-in-out, stroke 0.2s ease-in-out,
78 | scale 0.2s ease-in-out;
79 | }
80 |
81 | .smpp-link-clear-button:hover {
82 | filter: brightness(125%);
83 | stroke: var(--color-red);
84 | }
85 |
86 | .full-file-input-container {
87 | display: flex;
88 | gap: 0.75rem;
89 | align-items: center;
90 | width: 100%;
91 | box-sizing: border-box;
92 | }
93 |
94 | .link-input-container {
95 | display: flex;
96 | gap: 0.5rem;
97 | align-items: center;
98 | flex: 1 1 auto;
99 | position: relative;
100 | min-width: 0;
101 | }
102 |
103 | .smp-text-input {
104 | flex: 1 1 auto;
105 | min-width: 0;
106 | width: 100%;
107 | box-sizing: border-box;
108 | }
109 |
110 | /* clear button stays its intrinsic size */
111 | .smpp-link-clear-button {
112 | position: absolute;
113 | flex: 0 0 auto;
114 | right: -0.25rem;
115 | top: -0.25rem;
116 | white-space: nowrap;
117 | }
118 |
119 | /* right side: hidden file input + visible button */
120 | .file-input-container {
121 | display: flex;
122 | align-items: center;
123 | flex: 0 0 auto; /* keep it only as wide as it needs to be */
124 | }
125 |
126 | /* visible "choose file" button */
127 | .smpp-file-input-button {
128 | white-space: nowrap; /* avoid wrapping and weird width jumps */
129 | flex: 0 0 auto;
130 | }
131 |
132 | /* if you want the slider below and matching width of this container */
133 | .slider-container {
134 | width: 100%;
135 | max-width: 420px; /* match the same max-width as above */
136 | box-sizing: border-box;
137 | }
138 |
--------------------------------------------------------------------------------
/main-features/appearance/weather-effects.js:
--------------------------------------------------------------------------------
1 | function setSnowLevel(amount, opacity) {
2 | document.getElementById("snowflakes")?.remove();
3 | amount = amount > 3000 ? 3000 : amount;
4 | let snowDiv = document.createElement("div");
5 | snowDiv.id = "snowflakes";
6 |
7 | for (let i = 0; i < amount; i++) {
8 | let flake = document.createElement("img");
9 | flake.classList = "snowflake";
10 | flake.src =
11 | currentThemeName == "pink"
12 | ? getExtensionImage("icons/weather-overlay/blossom.svg")
13 | : getExtensionImage("icons/weather-overlay/snowflake.svg");
14 |
15 | flake.style.left = `${Math.floor(Math.random() * 100)}%`;
16 | flake.style.animation = `snowflake_fall_${Math.floor(Math.random() * 3)} ${
17 | Math.floor(Math.random() * 7) + 10
18 | }s ease-in-out infinite`;
19 | flake.style.animationDelay = `${Math.floor(Math.random() * 40) - 40}s`;
20 | flake.style.width = `${Math.floor(Math.random() * 20) + 10}px`;
21 | flake.style.opacity = opacity;
22 | snowDiv.appendChild(flake);
23 | }
24 | document.documentElement.appendChild(snowDiv);
25 | }
26 |
27 | function setRainLevel(amount, opacity) {
28 | document.getElementById("raindrops")?.remove();
29 | amount = amount > 3000 ? 3000 : amount;
30 | let rainDiv = document.createElement("div");
31 | rainDiv.id = "raindrops";
32 |
33 | for (let i = 0; i < amount; i++) {
34 | let raindrop = document.createElement("img");
35 | raindrop.classList.add("raindrop");
36 | raindrop.src = getExtensionImage("icons/weather-overlay/raindrop.svg");
37 | raindrop.style.left = `${Math.random() * 100}%`;
38 | raindrop.style.animation = `raindrop_fall ${
39 | Math.random() * 2 + 2
40 | }s linear infinite`;
41 | raindrop.style.animationDelay = `${Math.random() * 5 - 5}s`;
42 | raindrop.style.width = `${Math.random() * 7.5 + 7.5}px`;
43 | raindrop.style.opacity = opacity;
44 | rainDiv.appendChild(raindrop);
45 | }
46 | document.documentElement.appendChild(rainDiv);
47 | }
48 |
49 | async function setOverlayBasedOnConditions(amount, opacity) {
50 | async function getWeatherDescription(widget) {
51 | const weatherData = await getWidgetSetting(widget + ".cache.weatherData");
52 | if (weatherData == null) return null;
53 | if (weatherData.cod != 200) return null;
54 | return weatherData.weather[0].main;
55 | }
56 |
57 | let weatherWidgets = widgets.filter(
58 | (item) => item.name.toLowerCase().includes("weather") && item.isActive
59 | );
60 | let weathers = await Promise.all(
61 | weatherWidgets.map(async (widget) => {
62 | return await getWeatherDescription(widget.name);
63 | })
64 | );
65 | weathers = weathers.filter((description) => description != null);
66 |
67 | if (weathers.includes("Rain") || weathers.includes("Drizzle")) {
68 | setRainLevel(amount, opacity);
69 | }
70 | if (weathers.includes("Snow")) {
71 | setSnowLevel(amount, opacity);
72 | }
73 | }
74 |
75 | function applyWeatherEffects(weatherOverlay) {
76 | let rainDiv = document.getElementById("raindrops");
77 | let snowDiv = document.getElementById("snowflakes");
78 | switch (weatherOverlay.type) {
79 | case "snow":
80 | if (rainDiv) rainDiv.remove();
81 | setSnowLevel(weatherOverlay.amount, weatherOverlay.opacity);
82 | break;
83 | case "realtime":
84 | if (rainDiv) rainDiv.remove();
85 | if (snowDiv) snowDiv.remove();
86 | setOverlayBasedOnConditions(
87 | weatherOverlay.amount,
88 | weatherOverlay.opacity
89 | );
90 | break;
91 | case "rain":
92 | if (snowDiv) snowDiv.remove();
93 | setRainLevel(weatherOverlay.amount, weatherOverlay.opacity);
94 | break;
95 | default:
96 | console.error("No weather selector");
97 | break;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/main-features/modules/windows.js:
--------------------------------------------------------------------------------
1 | class BaseWindow {
2 | #keydownHandler;
3 | constructor(id, hidden = true) {
4 | this.id = id;
5 | this.hidden = hidden;
6 | this.element = null;
7 | }
8 |
9 | async create() {
10 | this.element = await this.renderContent();
11 | this.element.id = this.id;
12 | this.element.classList.add("base-window");
13 | if (this.hidden) this.element.classList.add("hidden");
14 |
15 | // Create controls container
16 | const controls = document.createElement("div");
17 | controls.classList.add("window-controls");
18 |
19 | const fullscreenBtn = document.createElement("button");
20 | fullscreenBtn.classList.add("window-button", "window-fullscreen-btn");
21 | fullscreenBtn.title = "Volledig scherm";
22 | fullscreenBtn.innerHTML = contractIconSVG + expandIconSVG;
23 |
24 | const closeBtn = document.createElement("button");
25 | closeBtn.classList.add("window-button", "window-close");
26 | closeBtn.title = "Sluiten";
27 | closeBtn.innerHTML = closeIconSVG;
28 |
29 | controls.appendChild(fullscreenBtn);
30 | controls.appendChild(closeBtn);
31 | this.element.appendChild(controls);
32 |
33 | document.body.appendChild(this.element);
34 |
35 | fullscreenBtn.addEventListener("click", () => {
36 | this.element.classList.toggle("fullscreen-window");
37 | void this.element.offsetWidth;
38 | });
39 |
40 | closeBtn.addEventListener("click", () => this.hide());
41 | }
42 |
43 | async renderContent() {
44 | // Override this in subclass
45 | return document.createElement("div");
46 | }
47 |
48 | // Called every time the window is opened
49 | // Override this in subclass
50 | onOpened() {}
51 |
52 | show(triggerEvent = null) {
53 | if (!this.hidden) return;
54 | this.hidden = false;
55 |
56 | this.element.classList.remove("hidden");
57 | const isKeyboardEvent =
58 | triggerEvent &&
59 | (typeof KeyboardEvent !== "undefined"
60 | ? triggerEvent instanceof KeyboardEvent
61 | : String(triggerEvent.type).startsWith("key"));
62 | const openEventTarget = isKeyboardEvent
63 | ? null
64 | : triggerEvent?.target ?? null;
65 | this._outsideClickHandler = (e) => {
66 | if (
67 | openEventTarget &&
68 | (e.target === openEventTarget || openEventTarget.contains(e.target))
69 | ) {
70 | return;
71 | }
72 | if (!this.element.contains(e.target)) {
73 | this.hide();
74 | }
75 | };
76 | document.addEventListener("click", this._outsideClickHandler, {
77 | capture: true,
78 | });
79 | if (!this.#keydownHandler) {
80 | this.#keydownHandler = (e) => {
81 | if (e.key === "Escape") {
82 | this.hide();
83 | }
84 | };
85 | document.addEventListener("keydown", this.#keydownHandler);
86 | }
87 |
88 | // Focus the first focusable element inside the window
89 | if (isKeyboardEvent) {
90 | requestAnimationFrame(() => {
91 | const focusableElements = this.element.querySelectorAll("label");
92 | if (focusableElements.length > 0) {
93 | focusableElements[0].focus();
94 | }
95 | });
96 | }
97 |
98 | this.onOpened();
99 | }
100 |
101 | hide() {
102 | if (this.hidden) return;
103 |
104 | this.hidden = true;
105 | this.element.classList.add("hidden");
106 | if (this._outsideClickHandler) {
107 | document.removeEventListener("click", this._outsideClickHandler, {
108 | capture: true,
109 | });
110 | this._outsideClickHandler = null;
111 | }
112 | if (this.#keydownHandler) {
113 | document.removeEventListener("keydown", this.#keydownHandler);
114 | this.#keydownHandler = null;
115 | }
116 |
117 | this.onClosed?.();
118 | }
119 |
120 | remove() {
121 | this.element?.remove();
122 | this.isOpen = false;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/styles/fixes/notification.css:
--------------------------------------------------------------------------------
1 | .notifs__hdr {
2 | color: var(--color-text) !important;
3 | border-bottom: 1px solid var(--color-base02) !important;
4 | }
5 |
6 | .notification__title {
7 | color: var(--color-text) !important;
8 | }
9 |
10 | .smsc-topnav [data-notifs] .notifs__hdr [role="switch"] {
11 | background-color: var(--color-base01) !important;
12 | border: 2px solid var(--color-base02) !important;
13 | border-radius: 1rem !important;
14 | }
15 |
16 | .smsc-topnav [data-notifs] .notifs__hdr [role="switch"] .switch-values:after {
17 | background-color: var(--color-base03) !important;
18 | }
19 |
20 | .smsc-topnav [data-notifs] .notifs__hdr button:focus:not(.focus-ring) {
21 | box-shadow: none !important;
22 | }
23 |
24 | .notification__btn--unread .notification__title {
25 | color: var(--color-accent) !important;
26 | font-weight: 800 !important;
27 | }
28 |
29 | .notification__content {
30 | color: var(--color-text) !important;
31 | }
32 |
33 | .notification {
34 | border: 2px solid var(--color-base02) !important;
35 | border-radius: 15px !important;
36 | background: var(--color-base01) !important;
37 | padding: 2px 10px 2px 10px !important;
38 | height: auto !important;
39 | line-height: initial !important;
40 | color: var(--color-accent) !important;
41 | font-weight: bold !important;
42 | text-decoration-color: var(--color-accent) !important;
43 | text-decoration-thickness: 2px !important;
44 | width: auto !important;
45 | margin: 3px !important;
46 | }
47 |
48 | .div {
49 | color: var(--color-text) !important;
50 | }
51 |
52 | .notification:hover,
53 | .modern-message:hover {
54 | background-color: var(--color-base02) !important;
55 | border: var(--border-size) solid var(--color-accent) !important;
56 | text-decoration: none !important;
57 | }
58 |
59 | a.js-toast-btn.toast__btn.smscButton.blue {
60 | border: 2px solid var(--color-base02) !important;
61 | border-radius: 50px !important;
62 | background: var(--color-base01) !important;
63 | padding: 2px 10px 2px 10px !important;
64 | height: auto !important;
65 | line-height: initial !important;
66 | color: var(--color-accent) !important;
67 | font-weight: bold !important;
68 | text-decoration-color: var(--color-accent) !important;
69 | text-decoration-thickness: 2px !important;
70 | width: auto !important;
71 | box-shadow: none !important;
72 | }
73 |
74 | li.notifs-toaster__toast.modern-ui {
75 | background-color: var(--color-base01) !important;
76 | border-radius: 10px !important;
77 | border-color: var(--color-accent) !important;
78 | }
79 |
80 | button.js-notifs-clearbtn.smscButton.smscButton--fit-text {
81 | color: var(--color-red) !important;
82 | }
83 |
84 | button.js-notifs-clearbtn.smscButton.smscButton--fit-text:hover {
85 | border-color: var(--color-red) !important;
86 | }
87 |
88 | button.tox-tbtn.tox-tbtn--enabled {
89 | background-color: var(--color-base03) !important;
90 | border: 1px solid var(--color-accent) !important;
91 | }
92 |
93 | .js-notifs-list::after {
94 | color: var(--color-text) !important;
95 | opacity: 0.7 !important;
96 | }
97 |
98 | .notification__info {
99 | font-weight: 400 !important;
100 | }
101 |
102 | .smsc-topnav [data-notifs] .notification__btn {
103 | padding: 1rem !important;
104 | }
105 |
106 | .smsc-topnav [data-notifs] .notifs__hdr [role="switch"] .switch-values span {
107 | color: var(--color-accent) !important;
108 | }
109 |
110 | .smsc-topnav
111 | [data-notifs]
112 | .notifs__hdr
113 | [role="switch"][aria-checked="false"]
114 | :last-child,
115 | .smsc-topnav
116 | [data-notifs]
117 | .notifs__hdr
118 | [role="switch"][aria-checked="true"]
119 | :first-child {
120 | color: var(--color-text) !important;
121 | }
122 |
123 | .notifs__content {
124 | width: 100% !important;
125 | padding: 5px !important;
126 | }
127 |
128 | ul.js-notifs-list.notifs__list {
129 | display: flex !important;
130 | flex-direction: column !important;
131 | gap: 3px !important;
132 | }
133 |
--------------------------------------------------------------------------------
/media/icons/weather-overlay/raindropfancy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
103 |
--------------------------------------------------------------------------------
/main-features/quick-menu/config.js:
--------------------------------------------------------------------------------
1 | function gatherOptions(template, name) {
2 | let options = [];
3 | for (let keyName of Object.keys(template)) {
4 | const key = template[keyName];
5 | const newName = (name ? name + "." : "") + keyName;
6 | if (typeof key == "object" && !Array.isArray(key)) {
7 | options = options.concat(gatherOptions(template[keyName], newName));
8 | } else if (key == "array") {
9 | // we don't support arrays yet so just hide array options from the menu
10 | //TODO: maybe add array support
11 | } else {
12 | options.push({ meta: "config", value: newName });
13 | }
14 | }
15 | return options;
16 | }
17 |
18 | /// Returns an array with settings for dmenu
19 | async function getDMenuOptionsForSettings(toplevel) {
20 | const template = await browser.runtime.sendMessage({
21 | action: "getSettingsTemplate",
22 | });
23 | let options = [];
24 | for (let cat of Object.keys(template)) {
25 | options = options.concat(
26 | gatherOptions(template[cat], toplevel ? "config" : null),
27 | );
28 | }
29 | return options;
30 | }
31 |
32 | /// Get the full option path from a beautified option path.
33 | // dmenu.itemScore => other.dmenu.itemScore
34 | // config.dmenu.itemScore => other.dmenu.itemScore
35 | function getFullOptPath(template, optName) {
36 | if (optName.startsWith("config.")) {
37 | optName = optName.substring("config.".length);
38 | }
39 | const first = optName.split(".")[0];
40 | for (let key of Object.keys(template)) {
41 | if (template[key][first] !== undefined) {
42 | return key + "." + optName;
43 | }
44 | }
45 | console.error(
46 | `Was not able to convert the optName '${optName}' to full because it was not found in the template json (returning an option with no category)`,
47 | );
48 | return optName;
49 | }
50 |
51 | async function setSettingByPath(path, value) {
52 | await browser.runtime.sendMessage({
53 | action: "setSettings",
54 | name: path,
55 | data: value,
56 | });
57 | await apply();
58 | }
59 |
60 | async function dmenuEditConfig(path) {
61 | const templates = await browser.runtime.sendMessage({
62 | action: "getSettingsTemplate",
63 | });
64 | configPath = getFullOptPath(templates, path);
65 | const template = getByPath(templates, configPath);
66 |
67 | let optionValue = await browser.runtime.sendMessage({
68 | action: "getSetting",
69 | name: configPath,
70 | });
71 | if (optionValue.error) {
72 | console.error("error from service worker: " + optionValue.error);
73 | optionValue = "worker error";
74 | }
75 |
76 | const label = path + " (" + optionValue + "): ";
77 | if (template == "boolean") {
78 | dmenu(
79 | ["true", "false"],
80 | function(cmd, shift) {
81 | if (cmd == "true") {
82 | setSettingByPath(configPath, true);
83 | } else if (cmd == "false") {
84 | setSettingByPath(configPath, false);
85 | }
86 | },
87 | label,
88 | );
89 | } else if (template == "number") {
90 | dmenu(
91 | [],
92 | function(cmd, shift) {
93 | if (configPath == "appearance.background.blur" && cmd == "song 2") {
94 | openURL("https://www.youtube.com/watch?v=Bz4l9_bzfZM", true);
95 | return;
96 | }
97 | setSettingByPath(configPath, new Number(cmd));
98 | },
99 | label,
100 | );
101 | } else if (Array.isArray(template)) {
102 | dmenu(
103 | template,
104 | function(cmd, shift) {
105 | setSettingByPath(configPath, cmd);
106 | },
107 | label,
108 | );
109 | } else {
110 | if (template !== "string") {
111 | console.error(
112 | "Invalid template type: '" +
113 | template +
114 | "' Falling back to 'string' template type",
115 | );
116 | }
117 | dmenu(
118 | [],
119 | function(cmd, shift) {
120 | setSettingByPath(configPath, cmd);
121 | },
122 | label,
123 | );
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Smartschool++",
4 | "version": "4.1.0",
5 | "description": "Enhance your Smartschool experience with themes and more",
6 | "homepage_url": "https://smartschoolplusplus.com",
7 | "content_scripts": [
8 | {
9 | "matches": ["*://*.smartschool.be/*"],
10 | "exclude_matches": [
11 | "*://www.smartschool.be/*",
12 | "*://status.smartschool.be/*",
13 | "*://smartschool.be/bb/*",
14 | "*://*.smartschool.be/online-session/meeting/redirect/*",
15 | "*://*.smartschool.be/html5client/*"
16 | ],
17 | "all_frames": true,
18 | "match_about_blank": true,
19 | "css": [
20 | "styles/smpp-styles/dmenu.css",
21 | "styles/smpp-styles/smpp.css",
22 | "styles/smpp-styles/ui.css",
23 | "styles/smpp-styles/games.css",
24 | "styles/smpp-styles/weather.css",
25 | "styles/smpp-styles/delijn.css",
26 | "styles/smpp-styles/plant.css",
27 | "styles/smpp-styles/clock.css",
28 | "styles/smpp-styles/widgets.css",
29 | "styles/smpp-styles/tutorial-widget.css",
30 | "styles/smpp-styles/settings.css",
31 | "styles/smpp-styles/assignments.css",
32 | "styles/smpp-styles/profile.css",
33 | "styles/fixes/general.css",
34 | "styles/fixes/login.css",
35 | "styles/fixes/messages.css",
36 | "styles/fixes/navigation.css",
37 | "styles/fixes/notification.css",
38 | "styles/fixes/planner.css",
39 | "styles/fixes/results.css",
40 | "styles/fixes/startpage.css",
41 | "styles/fixes/agenda.css",
42 | "styles/fixes/root.css",
43 | "styles/fixes/smartschool-widgets.css"
44 | ],
45 | "js": [
46 | "fixes-utils/utils.js",
47 | "widgets/widgets.js",
48 | "main-features/modules/windows.js",
49 | "main-features/globalchat.js",
50 | "fixes-utils/json.js",
51 | "fixes-utils/login.js",
52 | "fixes-utils/migration.js",
53 | "fixes-utils/scraper.js",
54 | "fixes-utils/titlefix.js",
55 | "fixes-utils/results.js",
56 | "main-features/profile.js",
57 | "widgets/tutorial-widget.js",
58 | "widgets/assignments.js",
59 | "widgets/delijn.js",
60 | "widgets/plant.js",
61 | "widgets/planner.js",
62 | "widgets/weather.js",
63 | "widgets/clock.js",
64 | "games/games.js",
65 | "games/breakout.js",
66 | "games/flappy.js",
67 | "games/snake.js",
68 | "games/pong.js",
69 | "games/tetris.js",
70 | "main-features/keybinds.js",
71 | "main-features/appearance/background-image.js",
72 | "main-features/appearance/themes.js",
73 | "main-features/appearance/ui.js",
74 | "main-features/appearance/weather-effects.js",
75 | "main-features/modules/images.js",
76 | "main-features/settings/quick-settings.js",
77 | "main-features/settings/main-settings.js",
78 | "main-features/quick-menu/dmenu.js",
79 | "main-features/quick-menu/config.js",
80 | "main-features/quick-menu/quick.js",
81 | "main-features/main.js"
82 | ]
83 | }
84 | ],
85 | "web_accessible_resources": [
86 | {
87 | "resources": ["media/*", "icons/*"],
88 | "matches": ["*://*.smartschool.be/*"]
89 | }
90 | ],
91 | "icons": {
92 | "16": "media/icons/smpp/16.png",
93 | "48": "media/icons/smpp/48.png",
94 | "128": "media/icons/smpp/128.png"
95 | },
96 | "browser_specific_settings": {
97 | "gecko": {
98 | "id": "smartschoolplusplus@smpp.be",
99 | "data_collection_permissions": {
100 | "required": ["none"],
101 | "optional": []
102 | }
103 | }
104 | },
105 | "background": {
106 | "service_worker": "background-scripts/background-script.js",
107 | "scripts": ["background-scripts/background-script.js"],
108 | "type": "module"
109 | },
110 | "permissions": ["storage", "unlimitedStorage"]
111 | }
112 |
--------------------------------------------------------------------------------
/games/breakout.js:
--------------------------------------------------------------------------------
1 | const BALL_RADIUS = 4;
2 | const PADDLE_WIDTH = 50;
3 | const PADDLE_HEIGHT = 5;
4 | const BRICK_ROWS = 4;
5 | const BRICK_COLS = 8;
6 | const BRICK_WIDTH = 30;
7 | const BRICK_HEIGHT = 10;
8 | const BRICK_PADDING = 4;
9 | const PADDLE_SPEED = 0.2;
10 | const BALL_SPEED = 0.1;
11 |
12 | class BreakoutWidget extends GameBase {
13 | ball;
14 | paddleX;
15 | leftPressed = false;
16 | rightPressed = false;
17 | bricks;
18 | score = 0;
19 |
20 | get title() {
21 | return "Breakout++";
22 | }
23 |
24 | get options() {
25 | return [GameOption.slider("speed", "Speed:", 10, 300, 100)];
26 | }
27 |
28 | async onGameStart() {
29 | const w = this.canvas.width;
30 | const h = this.canvas.height;
31 |
32 | this.ball = {
33 | x: w / 2,
34 | y: h / 2,
35 | dx: BALL_SPEED * this.getOpt("speed") * 0.01,
36 | dy: -BALL_SPEED * this.getOpt("speed") * 0.01,
37 | };
38 | this.paddleX = (w - PADDLE_WIDTH) / 2;
39 | this.leftPressed = false;
40 | this.rightPressed = false;
41 | this.score = 0;
42 |
43 | this.bricks = [];
44 | for (let c = 0; c < BRICK_COLS; c++) {
45 | for (let r = 0; r < BRICK_ROWS; r++) {
46 | this.bricks.push({
47 | x: c * (BRICK_WIDTH + BRICK_PADDING) + 10,
48 | y: r * (BRICK_HEIGHT + BRICK_PADDING) + 10,
49 | status: 1,
50 | });
51 | }
52 | }
53 | }
54 |
55 | drawRoundedRect(ctx, x, y, width, height, radius) {
56 | ctx.beginPath();
57 | ctx.moveTo(x + radius, y);
58 | ctx.lineTo(x + width - radius, y);
59 | ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
60 | ctx.lineTo(x + width, y + height - radius);
61 | ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
62 | ctx.lineTo(x + radius, y + height);
63 | ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
64 | ctx.lineTo(x, y + radius);
65 | ctx.quadraticCurveTo(x, y, x + radius, y);
66 | ctx.closePath();
67 | ctx.fill();
68 | }
69 |
70 | onGameDraw(ctx, dt) {
71 | const w = this.canvas.width;
72 | const h = this.canvas.height;
73 |
74 | ctx.fillStyle = getThemeVar("--color-base01");
75 | ctx.fillRect(0, 0, w, h);
76 |
77 | const speed = PADDLE_SPEED * this.getOpt("speed") * 0.01;
78 | if (this.leftPressed) this.paddleX -= speed * dt;
79 | if (this.rightPressed) this.paddleX += speed * dt;
80 | this.paddleX = Math.max(0, Math.min(w - PADDLE_WIDTH, this.paddleX));
81 |
82 | let ball = this.ball;
83 | ball.x += ball.dx * dt;
84 | ball.y += ball.dy * dt;
85 |
86 | if (ball.x < BALL_RADIUS) {
87 | ball.x = BALL_RADIUS;
88 | ball.dx *= -1;
89 | }
90 | if (ball.x > w - BALL_RADIUS) {
91 | ball.x = w - BALL_RADIUS;
92 | ball.dx *= -1;
93 | }
94 | if (ball.y < BALL_RADIUS) {
95 | ball.y = BALL_RADIUS;
96 | ball.dy *= -1;
97 | }
98 |
99 | if (
100 | ball.y > h - PADDLE_HEIGHT - BALL_RADIUS &&
101 | ball.x > this.paddleX &&
102 | ball.x < this.paddleX + PADDLE_WIDTH
103 | ) {
104 | ball.dy *= -1;
105 | ball.y = h - PADDLE_HEIGHT - BALL_RADIUS;
106 | }
107 |
108 | if (ball.y > h + BALL_RADIUS) {
109 | this.stopGame();
110 | }
111 |
112 | for (let b of this.bricks) {
113 | if (b.status === 1) {
114 | if (
115 | ball.x > b.x &&
116 | ball.x < b.x + BRICK_WIDTH &&
117 | ball.y > b.y &&
118 | ball.y < b.y + BRICK_HEIGHT
119 | ) {
120 | b.status = 0;
121 | ball.dy *= -1;
122 | this.score++;
123 | break;
124 | }
125 | }
126 | }
127 |
128 | ctx.fillStyle = getThemeVar("--color-accent");
129 | this.drawRoundedRect(
130 | ctx,
131 | this.paddleX,
132 | h - PADDLE_HEIGHT,
133 | PADDLE_WIDTH,
134 | PADDLE_HEIGHT,
135 | 5,
136 | );
137 |
138 | ctx.fillStyle = getThemeVar("--color-text");
139 | ctx.beginPath();
140 | ctx.arc(ball.x, ball.y, BALL_RADIUS, 0, 2 * Math.PI);
141 | ctx.fill();
142 |
143 | ctx.fillStyle = getThemeVar("--color-accent");
144 | for (let b of this.bricks) {
145 | if (b.status === 1) {
146 | this.drawRoundedRect(ctx, b.x, b.y, BRICK_WIDTH, BRICK_HEIGHT, 5); // 5 = corner radius
147 | }
148 | }
149 | }
150 |
151 | onKeyDown(e) {
152 | if (e.code === "ArrowLeft") this.leftPressed = true;
153 | else if (e.code === "ArrowRight") this.rightPressed = true;
154 | }
155 |
156 | onKeyUp(e) {
157 | if (e.code === "ArrowLeft") this.leftPressed = false;
158 | else if (e.code === "ArrowRight") this.rightPressed = false;
159 | }
160 | }
161 |
162 | registerWidget(new BreakoutWidget());
163 |
--------------------------------------------------------------------------------
/games/flappy.js:
--------------------------------------------------------------------------------
1 | const BIRD_RADIUS = 5;
2 | const BIRD_X = 50;
3 | const FLOOR_H = 15;
4 | const PIPE_GAP = 50;
5 | const PIPE_SPEED = 0.2;
6 | const TERMVEL = 0.3;
7 | const PIPE_W = 10;
8 | const GRAVITY = 0.0005;
9 | class FlappyWidget extends GameBase {
10 | bgX;
11 | birdY;
12 | birdVel;
13 | jump;
14 | pipe;
15 | color;
16 |
17 | get title() {
18 | return "Flappy++";
19 | }
20 | get options() {
21 | return [GameOption.slider("speed", "Speed:", 10, 300, 100)];
22 | }
23 |
24 | #drawGround(ctx) {
25 | let w = this.canvas.width;
26 | let h = this.canvas.height;
27 | ctx.beginPath();
28 | ctx.lineWidth = 1;
29 | ctx.moveTo(0, h - FLOOR_H);
30 | ctx.lineTo(w, h - FLOOR_H);
31 | ctx.stroke();
32 |
33 | ctx.fillStyle = getThemeVar("--color-accent");
34 | ctx.strokeStyle = getThemeVar("--color-base01");
35 | ctx.fillRect(0, h - FLOOR_H, w, FLOOR_H);
36 | ctx.strokeRect(0, h - FLOOR_H, w, FLOOR_H);
37 |
38 | for (let i = 0; i < (w / 20) * 2; i++) {
39 | ctx.beginPath();
40 | ctx.moveTo(i * 20 + this.bgX, h - FLOOR_H);
41 | ctx.lineTo(i * 20 + this.bgX + FLOOR_H, h);
42 | ctx.stroke();
43 | }
44 | }
45 |
46 | #calcGap() {
47 | return Math.max(
48 | PIPE_GAP * this.getOpt("speed") * 0.01,
49 | BIRD_RADIUS * 2 + 5
50 | );
51 | }
52 |
53 | #drawPipe(ctx, pipe) {
54 | const gap_size = this.#calcGap();
55 | ctx.fillStyle = getThemeVar("--color-accent");
56 | ctx.lineCap = "round";
57 | ctx.beginPath();
58 | ctx.roundRect(
59 | pipe.x,
60 | -PIPE_W,
61 | PIPE_W,
62 | pipe.y + PIPE_W - gap_size / 2,
63 | PIPE_W
64 | );
65 | ctx.fill();
66 | ctx.beginPath();
67 | const botStartY = pipe.y + gap_size * 0.5;
68 | ctx.roundRect(
69 | pipe.x,
70 | botStartY,
71 | PIPE_W,
72 | this.canvas.height - botStartY + PIPE_W,
73 | PIPE_W
74 | );
75 | ctx.fill();
76 | }
77 | #updatePipe(pipe, dt) {
78 | pipe.x -= this.getOpt("speed") * 0.01 * dt * PIPE_SPEED;
79 | if (pipe.x < -PIPE_W) {
80 | this.#resetPipe(pipe);
81 | }
82 |
83 | if (
84 | pipe.x <= BIRD_X + BIRD_RADIUS &&
85 | pipe.x + PIPE_W > BIRD_X - BIRD_RADIUS
86 | ) {
87 | const gap_size = this.#calcGap();
88 | const gapTop = pipe.y - gap_size * 0.5;
89 | const gapBot = pipe.y + gap_size * 0.5;
90 | if (this.birdY >= gapTop && this.birdY < gapBot) {
91 | if (!pipe.checked) {
92 | pipe.checked = true;
93 | this.score++;
94 | }
95 | } else {
96 | if (pipe.checked) {
97 | this.score--; // undo the point.
98 | this.checked = false;
99 | }
100 | this.stopGame();
101 | }
102 | }
103 | }
104 | #resetPipe(pipe) {
105 | const gap_size = this.#calcGap();
106 | pipe.x = this.canvas.width + PIPE_W;
107 | const MARGIN = gap_size * 0.5 + FLOOR_H + 10;
108 | pipe.y = Math.random() * (this.canvas.height - MARGIN * 2) + MARGIN;
109 | pipe.checked = false;
110 | }
111 |
112 | #drawBird(ctx) {
113 | ctx.fillStyle = getThemeVar("--color-accent");
114 | ctx.beginPath();
115 | ctx.arc(BIRD_X, this.birdY, BIRD_RADIUS, 0, 2 * Math.PI);
116 | ctx.fill();
117 | }
118 |
119 | async onGameStart() {
120 | this.bgX = 0;
121 | this.jump = false;
122 | this.birdY = this.canvas.height / 2.0;
123 | this.birdVel = 0;
124 | this.pipe = { x: 0, y: 0 };
125 | this.#resetPipe(this.pipe);
126 | }
127 |
128 | onGameDraw(ctx, dt) {
129 | ctx.fillStyle = getThemeVar("--color-base01");
130 | ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
131 | this.birdY += this.birdVel * dt;
132 | this.birdVel = Math.min(
133 | this.birdVel + GRAVITY * this.getOpt("speed") * 0.01 * dt,
134 | TERMVEL * this.getOpt("speed") * 0.01
135 | );
136 | ctx;
137 | if (this.jump) {
138 | this.jump = false;
139 | this.birdVel = -0.4 * this.getOpt("speed") * 0.01 * 0.4; //Math.min(this.birdVel-0.2, -0.2);
140 | }
141 | if (this.birdY > this.canvas.height - FLOOR_H - BIRD_RADIUS) {
142 | this.birdY = this.canvas.height - FLOOR_H - BIRD_RADIUS;
143 | this.stopGame();
144 | }
145 |
146 | this.bgX =
147 | (this.bgX - this.getOpt("speed") * 0.01 * PIPE_SPEED * dt) %
148 | this.canvas.width;
149 | this.#updatePipe(this.pipe, dt);
150 |
151 | this.#drawBird(ctx);
152 | this.#drawPipe(ctx, this.pipe);
153 | this.#drawGround(ctx);
154 | }
155 |
156 | async onMouse(e) {
157 | this.jump = true;
158 | }
159 |
160 | async onKeyDown(e) {
161 | if (e.code === "Space" || e.code === "ArrowUp") {
162 | this.jump = true;
163 | e.preventDefault();
164 | }
165 | }
166 | }
167 | registerWidget(new FlappyWidget());
168 |
--------------------------------------------------------------------------------
/fixes-utils/migration.js:
--------------------------------------------------------------------------------
1 | async function migrateV5() {
2 | const settingsData = await browser.runtime.sendMessage({
3 | action: "getSettingsData",
4 | });
5 | console.log("MIG V:\n Started migration with", settingsData);
6 | await migrateSettingsV5(settingsData);
7 | await migrateImageV5(settingsData);
8 | await migrateWidgetSettingsData();
9 | }
10 |
11 | async function migrateWidgetSettingsData() {
12 | let delijnAppData = await browser.runtime.sendMessage({
13 | action: "getDelijnAppData",
14 | });
15 | let weatherAppData = await browser.runtime.sendMessage({
16 | action: "getWeatherAppData",
17 | });
18 |
19 | if (Object.keys(delijnAppData).length !== 0) {
20 | await setWidgetSetting("DelijnWidget.halte", {
21 | entiteit: delijnAppData.delijnAppData.entiteitnummer,
22 | nummer: delijnAppData.delijnAppData.haltenummer,
23 | });
24 | }
25 | if (Object.keys(weatherAppData).length !== 0) {
26 | let weatherWidgets = widgets.filter((item) =>
27 | item.name.toLowerCase().includes("weather")
28 | );
29 | weatherWidgets.forEach(async (widget) => {
30 | await widget.setSetting(
31 | "currentLocation",
32 | weatherAppData.weatherAppData.lastLocation
33 | );
34 | });
35 | }
36 | }
37 |
38 | async function migrateImageV5(oldData) {
39 | let data;
40 | switch (oldData.backgroundSelection) {
41 | case 0: //default
42 | data = {
43 | imageData: null,
44 | link: "",
45 | type: "default",
46 | };
47 | break;
48 | case 1: //link
49 | data = {
50 | imageData: oldData.backgroundLink,
51 | link: oldData.backgroundLink,
52 | type: "link",
53 | };
54 | break;
55 | case 2: //file
56 | let imageData = await browser.runtime.sendMessage({
57 | action: "getBackgroundImage",
58 | });
59 | data = {
60 | imageData: imageData.backgroundImage,
61 | link: oldData.backgroundLink,
62 | type: "file",
63 | };
64 | break;
65 | default:
66 | break;
67 | }
68 |
69 | await browser.runtime.sendMessage({
70 | action: "setImage",
71 | id: "backgroundImage",
72 | data: data,
73 | });
74 |
75 | console.log(
76 | "MIG V: \n Successfully migrated background image with data:",
77 | data
78 | );
79 | }
80 |
81 | async function migrateSettingsV5(oldData) {
82 | let newWeatherOverlayType;
83 | switch (oldData.weatherOverlaySelection) {
84 | case 0:
85 | newWeatherOverlayType = "snow";
86 | break;
87 | case 1:
88 | newWeatherOverlayType = "realtime";
89 | break;
90 | case 2:
91 | newWeatherOverlayType = "snow";
92 | break;
93 | }
94 | let newSettingsData = {
95 | profile: {
96 | username: oldData.customUserName,
97 | useSMpfp: false,
98 | },
99 | appearance: {
100 | theme: oldData.theme,
101 | background: {
102 | blur: oldData.backgroundBlurAmount,
103 | },
104 | weatherOverlay: {
105 | type: newWeatherOverlayType,
106 | amount: oldData.weatherOverlayAmount,
107 | opacity: 1,
108 | },
109 | tabLogo: oldData.enableSMPPLogo ? "smpp" : "sm",
110 | news: oldData.showNews,
111 | },
112 | topNav: {
113 | buttons: {
114 | GO: false,
115 | GC: true,
116 | search: false,
117 | quickMenu: false,
118 | },
119 | switchCoursesAndLinks: true,
120 | icons: {
121 | home: true,
122 | mail: true,
123 | notifications: true,
124 | settings: false,
125 | },
126 | },
127 | other: {
128 | quicks: oldData.quicks,
129 | performanceMode: oldData.enablePerfomanceMode,
130 | splashText: true,
131 | discordButton: true,
132 | dmenu: {
133 | centered: true,
134 | itemScore: false,
135 | toplevelConfig: false,
136 | },
137 | keybinds: {
138 | dmenu: ":",
139 | widgetEditMode: "E",
140 | widgetBag: "Space",
141 | settings: ",",
142 | gc: "G",
143 | },
144 | },
145 | };
146 |
147 | await browser.runtime.sendMessage({
148 | action: "setSettingsData",
149 | data: newSettingsData,
150 | });
151 | console.log(
152 | "MIG V: \n Succesfully migrated settings data to:",
153 | newSettingsData
154 | );
155 | }
156 |
157 | async function migrate() {
158 | await removeLegacyData(); // will reload the page if legacy data is present
159 |
160 | let settingsData = await browser.runtime.sendMessage({
161 | action: "getSettingsData",
162 | });
163 |
164 | if (settingsData.theme != null) {
165 | await migrateV5();
166 | }
167 | }
168 |
169 | async function removeLegacyData() {
170 | if (window.localStorage.getItem("settingsdata")) {
171 | await clearAllData();
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/styles/smpp-styles/weather.css:
--------------------------------------------------------------------------------
1 | .weather-div {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | text-align: center;
6 | }
7 | .weather-location-input:hover {
8 | background-color: var(--color-base02) !important;
9 | border-color: var(--color-base03) !important;
10 | }
11 |
12 | .weather-location-input::placeholder {
13 | opacity: 0.5 !important;
14 | }
15 |
16 | input.weather-location-input:focus {
17 | color: var(--color-text) !important;
18 | border-color: var(--color-accent) !important;
19 | }
20 | .weather-location-input {
21 | text-align: center;
22 | color: var(--color-accent) !important;
23 | font-size: 2.5rem;
24 | font-weight: 600;
25 | width: 100%;
26 | padding: 0 !important;
27 | text-wrap: unset;
28 | border: 2px solid transparent !important;
29 | border-radius: 1rem;
30 | }
31 |
32 | .weather-div.smpp-widget-content .not-initialized.weather-location-input {
33 | border-color: var(--color-accent) !important;
34 | color: var(--color-text) !important;
35 | background-color: var(--color-base02) !important;
36 | }
37 |
38 | .enableAnimations .weather-location-input {
39 | transition-property: border-color, background-color, color;
40 | transition: 0.2s ease-in-out !important;
41 | }
42 | .weather-icon-container {
43 | width: 100%;
44 | }
45 |
46 | .weather-widget-content {
47 | width: 100%;
48 | display: flex;
49 | flex-direction: column;
50 | align-items: center;
51 | }
52 |
53 | .top-weather-content-container,
54 | .bottom-weather-content-container {
55 | width: 100%;
56 | display: flex;
57 | flex-direction: column;
58 | align-items: center;
59 | }
60 |
61 | .conditions-container {
62 | display: flex;
63 | flex-direction: row;
64 | align-items: center;
65 | justify-content: space-between;
66 | width: 100%;
67 | }
68 |
69 | .condition-container {
70 | width: 25%;
71 | display: flex;
72 | flex-direction: column;
73 | justify-content: center;
74 | align-items: center;
75 | }
76 |
77 | .wind-container,
78 | .humidity-container {
79 | width: 35%;
80 | }
81 |
82 | .wind-icon,
83 | .humidity-icon,
84 | .feels-like-icon,
85 | .temperature-icon {
86 | height: 6rem;
87 | width: 100%;
88 | display: flex;
89 | align-items: center;
90 | justify-content: center;
91 | }
92 |
93 | .feels-like,
94 | .wind,
95 | .humidity {
96 | font-size: 1.4rem;
97 | font-weight: 600;
98 | }
99 | .weather-description {
100 | font-size: 2rem;
101 | font-weight: 500;
102 | }
103 |
104 | .temperature {
105 | font-size: 4rem;
106 | font-weight: 200;
107 | margin-bottom: 1rem;
108 | }
109 | .no-location-icon {
110 | height: 15rem;
111 | }
112 | .not-found-text {
113 | font-size: 1.8rem;
114 | font-weight: 700;
115 | }
116 | .compact .conditions-container {
117 | flex-direction: column;
118 | gap: 1rem;
119 | }
120 |
121 | .compact .wind-container,
122 | .compact .humidity-container {
123 | height: 15%;
124 | width: 100%;
125 | }
126 | .compact .temperature-container {
127 | height: 25%;
128 | width: 100%;
129 | }
130 | .compact .wind-icon,
131 | .compact .humidity-icon {
132 | width: 6rem;
133 | height: 100%;
134 | }
135 | .compact .temperature-icon {
136 | width: 2.75rem;
137 | height: 100%;
138 | }
139 |
140 | .compact.weather-div:not(:has(.not-found-text)):not(:has(.not-initialized)) {
141 | flex-direction: row;
142 | gap: 0.5rem;
143 | }
144 |
145 | .compact.weather-div:has(.not-initialized) .weather-icon-container {
146 | width: 50%;
147 | }
148 |
149 | .preview.compact.weather-div:not(:has(.not-initialized)) {
150 | flex-direction: column;
151 | }
152 |
153 | .preview.compact .weather-widget-content {
154 | flex-direction: row;
155 | }
156 |
157 | .compact .bottom-weather-content-container {
158 | width: 37%;
159 | }
160 |
161 | .compact .temperature,
162 | .compact .humidity,
163 | .compact .wind {
164 | font-weight: 600;
165 | font-size: 1.3rem;
166 | margin-bottom: 0;
167 | }
168 |
169 | .compact .weather-location-input {
170 | font-size: 2rem;
171 | }
172 | .compact .not-found-text {
173 | font-size: 1.3rem;
174 | }
175 | .compact .no-location-icon {
176 | height: 10rem;
177 | }
178 |
179 | .preview.compact .conditions-container {
180 | width: 20%;
181 | }
182 | .preview.compact .weather-widget-content {
183 | gap: 0 !important;
184 | }
185 | .weather-preview-title {
186 | font-size: 2.5rem;
187 | font-weight: 700;
188 | }
189 |
190 | .compact .weather-preview-title {
191 | font-size: 2rem;
192 | margin-bottom: 0.5rem;
193 | }
194 |
195 | .preview.compact .wind-icon,
196 | .preview.compact .humidity-icon {
197 | width: 4rem;
198 | height: 100%;
199 | }
200 | .preview.compact .temperature-icon {
201 | width: 1.75rem;
202 | height: 100%;
203 | }
204 |
205 | .light-blue-weather-fill {
206 | fill: #72b8d4;
207 | }
208 |
209 | .orange-weather-fill {
210 | fill: #f4a71d;
211 | }
212 |
213 | .blue-weather-fill {
214 | fill: #2885c7;
215 | }
216 |
--------------------------------------------------------------------------------
/fixes-utils/login.js:
--------------------------------------------------------------------------------
1 | /* vim:set shiftwidth=4: */
2 |
3 | function updateLoginPanel() {
4 | let login_app_left = document.querySelector(".login-app__left");
5 | login_app_left.innerHTML = " ";
6 |
7 | document.getElementsByClassName(
8 | "login-app__platform-indicator"
9 | )[0].innerHTML = 'Smartschool ++
';
10 |
11 | document.getElementsByClassName("login-app__title--separator")[0].innerHTML =
12 | '';
13 |
14 | document.getElementById("showmore").addEventListener("click", showmore);
15 |
16 | function showmore() {
17 | document.documentElement.style.setProperty("--show-options", "flex");
18 | document.getElementById("showmore").style.display = "none";
19 | }
20 | }
21 |
22 | function addSplashText() {
23 | var loginApp = document.querySelector(".login-app");
24 | var splashTextContainer = document.createElement("div");
25 | loginApp.prepend(splashTextContainer);
26 | splashTextContainer.classList.add("splashtextcontainer");
27 | splashTextContainer.innerHTML = `${getSplashText()}
`;
28 | }
29 |
30 | function removeSplashText() {
31 | document.querySelector("splashtextcontainer")?.remove();
32 | }
33 |
34 | function updateSplashText(splashTextEnabled) {
35 | if (splashTextEnabled) {
36 | addSplashText();
37 | } else {
38 | removeSplashText();
39 | }
40 | }
41 |
42 | let splashtexts = [
43 | `It even works under water!`,
44 | `Hoelang is een chinees.`,
45 | `404 Splashtext not found`,
46 | `Don't Smartschool \n and drive kids!`,
47 | `Home-made!`,
48 | `Pythagoras was used in this`, // where????
49 | `Like that smash button!`,
50 | `What DOES the fox say?`,
51 | `Supercalifragilisticexpialidocious!`,
52 | `Hottentotententententoonstelling`,
53 | `Not on steam!`,
54 | `Made in Belgium!`,
55 | `De L in frans \n staat voor leuk!`,
56 | `Doesn't contain nuts!`,
57 | `Glutenfree!!!`,
58 | `Colorblind approved!`,
59 | `Not vegan!`, // :O
60 | `Never gonna give you up ;)`,
61 | `The cake is a lie!`,
62 | `I know what you did...`,
63 | `Join de discord!`,
64 | `2 + 2 = 5`,
65 | `2 * 3 = 4`,
66 | `Listen to the \n Arctic Monkeys!`,
67 | `Global chat before GTA VI???`,
68 | `Water your plant(s)!`,
69 | `Check out Snake++!`,
70 | `Je suis une baguette`,
71 | `Dikke BMW!`,
72 | `Mand!`,
73 | `Net pindakaas gegeten dus...`,
74 | `BINGO!`,
75 | `GEKOLONISEERD!`,
76 | `pssst, its free real estate`,
77 | `Europapa!`,
78 | `Made by Sprksoft!`,
79 | `Made by Lukas`,
80 | `Made by Bjarne`,
81 | `Made by Sibe`,
82 | `Not made in China!`,
83 | `How did we get here?`,
84 | `Aboo?!`,
85 | `Check out Insym!`,
86 | `Do your homework!`,
87 | `Look mom I'm in \n the splashtext!`,
88 | `[Insert funny splashtext]`,
89 | `Totally not ripping off Minecraft!`,
90 | `Yippieeee`,
91 | `Chop Suey!`,
92 | `AAAAAAAAA!!!`,
93 | `Is tHaT a sUpRA?!?!`,
94 | `English or Spanish?`,
95 | `Jennifer eet matrassen! WTF?`,
96 | `Listen to Amélie Farren!`,
97 | `"How to exit VIM???"`,
98 | `Are ya vimming son?`,
99 | `It's either Spanish or vanish`,
100 | `Are you hacking??`,
101 | `:3`,
102 | `Ma alleee ik word \n gekilled door ne kerstboom!`,
103 | `SQL, Squil ✓`,
104 | `undefined is not a function`,
105 | `:p`,
106 | `I'm blue dabadie dabadaa`,
107 | `Chet jipitie`,
108 | `Bloat`,
109 | `MERGE CONFLICT!`,
110 | `In case of fire: git add . \n ; git commit ; git push`,
111 | `Always remove French language \npack: sudo rm -fr /`,
112 | undefined, // never change this losers!!!! // rude
113 | `Is tHaT A JOjO ReFEreNce?`,
114 | `https://ldev.eu.org`,
115 | `https://smartschoolplusplus.com`,
116 | `weak fingers`,
117 | `Beep beep I'm a sheep`,
118 | `I love yavascript !`,
119 | `Always take your maths kids`,
120 | `Gele auto!`,
121 | `LET ME OUTTT!!!`,
122 | `HELP`,
123 | `It's that me espresso!`,
124 | `You lied about your age, \ndidn't you?`,
125 | `Check out Undertale`,
126 | `Nobody expects the \n Spanish Inquisition!`,
127 | `Beans Beans Beans!`,
128 | `Baby Shark, doo-doo, \n doo-doo, doo-doo`,
129 | `Brood int frans`,
130 | `CORS >:(`,
131 | `Le poisson Steve!`,
132 | `Il est oraaaange! \n Il a des bras.. et des jambes`,
133 | `Since Nov 14, 2023`,
134 | `Over 1000+ commits!`,
135 | `Took at least \n 5 hours to make!`,
136 | `Sometimes works!`,
137 | `Survive, Adapt, Overcome!`,
138 | `Rode auto!`,
139 | `Cookie clicker`,
140 | `Are you serious?`,
141 | `Breakout++`,
142 | `Ruby chan, haii!!`,
143 | `It's so sweet`,
144 | `Feeling diskinserted?`,
145 | `Vamipre in the corner.\nIs it scaring you off?`,
146 | `Whopper Whopper`,
147 | `nginx, Enginks`,
148 | `I love ECMAScript`,
149 | `bAcK iN My DaY!`,
150 | `I forgot 💀`,
151 | `Error 418 \n I'm a tea pot...`,
152 | `Met 100+ splash texts!`,
153 | `Do it`,
154 | ];
155 |
156 | function getSplashText() {
157 | return splashtexts[Math.floor(Math.random() * splashtexts.length)];
158 | }
159 |
--------------------------------------------------------------------------------
/styles/smpp-styles/dmenu.css:
--------------------------------------------------------------------------------
1 | html {
2 | --dmenu-margin: 1px;
3 | --dmenu-padding: 3px;
4 | --dmenu-font-size: 1.5rem;
5 | }
6 |
7 | .dmenu-centered {
8 | left: 10%;
9 | right: 10%;
10 | top: 10%;
11 | bottom: 10%;
12 | }
13 |
14 | .dmenu-hidden {
15 | display: none !important;
16 | }
17 |
18 | .dmenu {
19 | overflow: clip;
20 | background: var(--color-base00);
21 | border: var(--border-size) solid var(--color-accent);
22 | position: absolute;
23 | display: flex;
24 | flex-direction: column;
25 | font-size: var(--dmenu-font-size) !important;
26 | z-index: 20;
27 | box-shadow: 0px 0px 150px black;
28 | border-radius: 10px !important;
29 | }
30 |
31 | .enableAnimations .dmenu {
32 | animation: dmenu 0.2s linear 0ms 1 !important;
33 | }
34 |
35 | .enableAnimations .jokeDmenu .dmenu {
36 | animation: dmenu_joke 30s linear 0ms 1 !important;
37 | }
38 |
39 | @keyframes dmenu {
40 | 0% {
41 | opacity: 0.1;
42 | transform: translateY(25%) scale(0.9);
43 | }
44 |
45 | 40% {
46 | transform: translateY(-2%) scale(1);
47 | opacity: 1;
48 | }
49 |
50 | 60% {
51 | transform: translateY(0px);
52 | }
53 | }
54 |
55 | @keyframes dmenu_joke {
56 | 0% {
57 | transform: translateY(50%) scale(1);
58 | }
59 |
60 | 5% {
61 | transform: translateY(-50%) scale(1);
62 | }
63 |
64 | 10% {
65 | transform: scale(4) rotate3d(90deg, 5deg, 720deg);
66 | }
67 |
68 | 15% {
69 | transform: scale(1) translateX(-500%);
70 | }
71 |
72 | 20% {
73 | transform: translateX(500%);
74 | }
75 |
76 | 25% {
77 | transform: translateY(0px);
78 | }
79 |
80 | 30% {
81 | transform: translateY(50%) scale(2) rotate(1500deg);
82 | }
83 |
84 | 35% {
85 | transform: translateY(-100%) scale(0.5);
86 | }
87 |
88 | 40% {
89 | transform: translateX(50%) scale(1.1);
90 | }
91 |
92 | 45% {
93 | transform: rotate(180deg) scale3d(2, -2, -1);
94 | }
95 |
96 | 50% {
97 | transform: rotateZ(10deg) translateY(-2%) scale(1) scale3d(1, 1, 1);
98 | }
99 |
100 | 55% {
101 | transform: translateY(100%) rotateZ(-10deg);
102 | }
103 |
104 | 60% {
105 | transform: translateY(-100%) rotate(15deg) rotateZ(0deg);
106 | }
107 |
108 | 65% {
109 | transform: translateY(50%) scale(1.1);
110 | }
111 |
112 | 70% {
113 | transform: translateX(-100%) scale(0.5);
114 | }
115 |
116 | 75% {
117 | transform: scale3d(1, 2, 5) translateX(0px);
118 | }
119 |
120 | 80% {
121 | transform: translateY(50%) scale3d(1, 1, 1);
122 | }
123 |
124 | 85% {
125 | transform: translateY(0%) scale(10);
126 | }
127 |
128 | 90% {
129 | transform: translateY(-500%);
130 | }
131 |
132 | 95% {
133 | transform: translateY(500%);
134 | }
135 |
136 | 100% {
137 | transform: translateY(0%);
138 | }
139 | }
140 |
141 | .dmenu-top {
142 | display: flex;
143 | flex-direction: row;
144 | border-bottom: var(--border-size) solid var(--color-base02);
145 | }
146 |
147 | .dmenu-input {
148 | border: none !important;
149 | background: var(--color-base01) !important;
150 | color: var(--color-text) !important;
151 | flex-grow: 1;
152 | padding: var(--dmenu-padding) !important;
153 | font-size: var(--dmenu-font-size) !important;
154 | border-top-right-radius: 3px !important;
155 | }
156 |
157 | .dmenu-input:focus {
158 | outline-width: 0;
159 | outline: none;
160 | }
161 |
162 | .dmenu-title {
163 | background: var(--color-base01);
164 | color: var(--color-accent);
165 | line-height: inherit;
166 | padding-left: var(--dmenu-padding);
167 | padding: 3px !important;
168 | margin-left: 0px !important;
169 | border-right: var(--border-size) solid var(--color-base02);
170 | border-top-left-radius: 3px !important;
171 | }
172 |
173 | .dmenu-title:hover {
174 | cursor: auto !important;
175 | }
176 |
177 | .dmenu-itemlist {
178 | overflow-y: auto;
179 | }
180 |
181 | .dmenu .hidden {
182 | visibility: hidden;
183 | }
184 |
185 | .dmenu-row {
186 | padding: var(--dmenu-padding);
187 | background: var(--color-base00);
188 | color: var(--color-text) !important;
189 | display: flex;
190 | flex-direction: row;
191 | border: none;
192 | }
193 |
194 | .dmenu-row:nth-child(even) {
195 | background: var(--color-base01);
196 | }
197 |
198 | .dmenu-row:hover {
199 | background: var(--color-base02);
200 | cursor: pointer;
201 | }
202 |
203 | .dmenu-content {
204 | margin-right: 1em;
205 | display: inline !important;
206 | }
207 |
208 | .dmenu-meta {
209 | flex-grow: 10;
210 | color: var(--color-base03);
211 | }
212 |
213 | .dmenu-score {
214 | margin-right: 0.5rem;
215 | color: var(--color-accent);
216 | }
217 |
218 | .dmenu-selected .dmenu-score {
219 | color: var(--color-base01) !important;
220 | }
221 |
222 | .dmenu-selected {
223 | background: var(--color-accent) !important;
224 | color: var(--color-base01) !important;
225 | }
226 |
--------------------------------------------------------------------------------
/games/pong.js:
--------------------------------------------------------------------------------
1 | // Powered by Broodje56's magic sauce
2 | // This code is brought to you by Broodje56, the ultimate breadboss 🥖👑
3 |
4 | const PONG_BALL_RADIUS = 4;
5 | const PONG_PADDLE_WIDTH = 5;
6 | const PONG_PADDLE_HEIGHT = 40;
7 | const PONG_PADDLE_SPEED = 0.2;
8 | const PONG_BALL_SPEED = 0.1;
9 |
10 | const SPEEDUP_FACTOR = 1.05;
11 | const MAX_SPEED = 0.5;
12 |
13 | class PongWidget extends GameBase {
14 | ball;
15 | leftY;
16 | rightY;
17 | leftUp = false;
18 | leftDown = false;
19 |
20 | get title() {
21 | return "Pong++";
22 | }
23 |
24 | get options() {
25 | return [GameOption.slider("speed", "Speed:", 10, 300, 100)];
26 | }
27 |
28 | async onGameStart() {
29 | const w = this.canvas.width;
30 | const h = this.canvas.height;
31 |
32 | this.ball = {
33 | x: w / 2,
34 | y: h / 2,
35 | dx:
36 | PONG_BALL_SPEED *
37 | this.getOpt("speed") *
38 | 0.01 *
39 | (Math.random() > 0.5 ? 1 : -1),
40 | dy:
41 | PONG_BALL_SPEED * this.getOpt("speed") * 0.01 * (Math.random() * 2 - 1),
42 | };
43 |
44 | this.leftY = (h - PONG_PADDLE_HEIGHT) / 2;
45 | this.rightY = (h - PONG_PADDLE_HEIGHT) / 2;
46 |
47 | this.leftUp = false;
48 | this.leftDown = false;
49 | }
50 |
51 | drawRoundedRect(ctx, x, y, width, height, radius) {
52 | ctx.beginPath();
53 | ctx.moveTo(x + radius, y);
54 | ctx.lineTo(x + width - radius, y);
55 | ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
56 | ctx.lineTo(x + width, y + height - radius);
57 | ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
58 | ctx.lineTo(x + radius, y + height);
59 | ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
60 | ctx.lineTo(x, y + radius);
61 | ctx.quadraticCurveTo(x, y, x + radius, y);
62 | ctx.closePath();
63 | ctx.fill();
64 | }
65 |
66 | onGameDraw(ctx, dt) {
67 | const w = this.canvas.width;
68 | const h = this.canvas.height;
69 |
70 | ctx.fillStyle = getThemeVar("--color-base01");
71 | ctx.fillRect(0, 0, w, h);
72 |
73 | const speed = PONG_PADDLE_SPEED * this.getOpt("speed") * 0.01;
74 |
75 | if (this.leftUp) this.leftY -= speed * dt;
76 | if (this.leftDown) this.leftY += speed * dt;
77 | this.leftY = Math.max(0, Math.min(h - PONG_PADDLE_HEIGHT, this.leftY));
78 |
79 | const aiCenter = this.rightY + PONG_PADDLE_HEIGHT / 2;
80 | const diff = this.ball.y - aiCenter;
81 | const maxMove = speed * dt * 0.8;
82 | if (Math.abs(diff) > maxMove) {
83 | this.rightY += Math.sign(diff) * maxMove;
84 | } else {
85 | this.rightY += diff;
86 | }
87 | this.rightY = Math.max(0, Math.min(h - PONG_PADDLE_HEIGHT, this.rightY));
88 |
89 | let b = this.ball;
90 | b.x += b.dx * dt;
91 | b.y += b.dy * dt;
92 |
93 | if (b.y < PONG_BALL_RADIUS || b.y > h - PONG_BALL_RADIUS) {
94 | b.dy *= -1;
95 | }
96 |
97 | // Ball Hit by left paddle
98 | if (
99 | b.x < PONG_PADDLE_WIDTH + PONG_BALL_RADIUS &&
100 | b.y > this.leftY &&
101 | b.y < this.leftY + PONG_PADDLE_HEIGHT
102 | ) {
103 | b.dx *= -1;
104 | b.x = PONG_PADDLE_WIDTH + PONG_BALL_RADIUS;
105 |
106 | b.dx *= SPEEDUP_FACTOR;
107 | b.dy *= SPEEDUP_FACTOR;
108 |
109 | b.dx = Math.sign(b.dx) * Math.min(Math.abs(b.dx), MAX_SPEED);
110 | b.dy = Math.sign(b.dy) * Math.min(Math.abs(b.dy), MAX_SPEED);
111 |
112 | this.score++;
113 | }
114 |
115 | // Ball Hit by right paddle
116 | if (
117 | b.x > w - PONG_PADDLE_WIDTH - PONG_BALL_RADIUS &&
118 | b.y > this.rightY &&
119 | b.y < this.rightY + PONG_PADDLE_HEIGHT
120 | ) {
121 | b.dx *= -1;
122 | b.x = w - PONG_PADDLE_WIDTH - PONG_BALL_RADIUS;
123 |
124 | b.dx *= SPEEDUP_FACTOR;
125 | b.dy *= SPEEDUP_FACTOR;
126 |
127 | b.dx = Math.sign(b.dx) * Math.min(Math.abs(b.dx), MAX_SPEED);
128 | b.dy = Math.sign(b.dy) * Math.min(Math.abs(b.dy), MAX_SPEED);
129 | }
130 |
131 | if (b.x < 0) {
132 | this.stopGame();
133 | }
134 |
135 | if (b.x > w) {
136 | this.onGameStart();
137 | }
138 |
139 | ctx.fillStyle = getThemeVar("--color-text");
140 | ctx.beginPath();
141 | ctx.arc(b.x, b.y, PONG_BALL_RADIUS, 0, Math.PI * 2);
142 | ctx.fill();
143 |
144 | ctx.fillStyle = getThemeVar("--color-accent");
145 | this.drawRoundedRect(
146 | ctx,
147 | 0,
148 | this.leftY,
149 | PONG_PADDLE_WIDTH,
150 | PONG_PADDLE_HEIGHT,
151 | 3,
152 | );
153 | this.drawRoundedRect(
154 | ctx,
155 | w - PONG_PADDLE_WIDTH,
156 | this.rightY,
157 | PONG_PADDLE_WIDTH,
158 | PONG_PADDLE_HEIGHT,
159 | 3,
160 | );
161 |
162 | ctx.strokeStyle = getThemeVar("--color-text");
163 | ctx.setLineDash([4, 5]);
164 | ctx.beginPath();
165 | ctx.moveTo(w / 2, 0);
166 | ctx.lineTo(w / 2, h);
167 | ctx.stroke();
168 | ctx.setLineDash([]);
169 | }
170 |
171 | onKeyDown(e) {
172 | if (e.code === "ArrowUp") this.leftUp = true;
173 | if (e.code === "ArrowDown") this.leftDown = true;
174 | }
175 |
176 | onKeyUp(e) {
177 | if (e.code === "ArrowUp") this.leftUp = false;
178 | if (e.code === "ArrowDown") this.leftDown = false;
179 | }
180 | }
181 |
182 | registerWidget(new PongWidget());
183 |
--------------------------------------------------------------------------------
/widgets/clock.js:
--------------------------------------------------------------------------------
1 | // gemaakt door Lou (Flying_dinonugget) analog clock gestolen van Lukaz.vb
2 | class ClockWidget extends WidgetBase {
3 | #interval;
4 |
5 | get category() {
6 | return "other";
7 | }
8 |
9 | get name() {
10 | return "ClockWidget";
11 | }
12 |
13 | async createContent() {
14 | this.element.classList.add("smpp-widget-transparent");
15 | let clockContainer = document.createElement("div");
16 | clockContainer.classList.add("clock-widget");
17 |
18 | let container = document.createElement("div");
19 | container.classList.add("clock-container");
20 | clockContainer.appendChild(container);
21 |
22 | let clockFace = document.createElement("div");
23 | clockFace.classList.add("clock-face");
24 | container.appendChild(clockFace);
25 |
26 | let minuteHand = document.createElement("div");
27 | minuteHand.classList.add("clock-hand", "minute-hand");
28 | clockFace.appendChild(minuteHand);
29 |
30 | let hourHand = document.createElement("div");
31 | hourHand.classList.add("clock-hand", "hour-hand");
32 | clockFace.appendChild(hourHand);
33 |
34 | let secondHand = document.createElement("div");
35 | secondHand.classList.add("clock-hand", "second-hand");
36 | clockFace.appendChild(secondHand);
37 |
38 | const centerCircle = document.createElement("div");
39 | centerCircle.classList.add("clock-center");
40 | clockFace.appendChild(centerCircle);
41 |
42 | let bottomContainer = document.createElement("div");
43 | bottomContainer.classList.add("clock-bottom");
44 | container.appendChild(bottomContainer);
45 |
46 | let timeEl = document.createElement("div");
47 | timeEl.classList.add("digital-time");
48 | timeEl.innerText = "00:00";
49 | bottomContainer.appendChild(timeEl);
50 |
51 | const update = () => {
52 | const now = new Date();
53 | const hours = now.getHours();
54 | const minutes = now.getMinutes();
55 | const seconds = now.getSeconds();
56 | const milliseconds = now.getMilliseconds();
57 |
58 | const timeText =
59 | (hours < 10 ? "0" : "") +
60 | hours +
61 | ":" +
62 | (minutes < 10 ? "0" : "") +
63 | minutes;
64 | if (timeEl.innerText != timeText) {
65 | // only update the time if it has changed. This allows the time text to be selected and has possibly better performance
66 | timeEl.innerText = timeText;
67 | }
68 |
69 | const totalSeconds =
70 | hours * 3600 + minutes * 60 + seconds + milliseconds / 1000;
71 | const totalMinutes =
72 | hours * 60 + minutes + seconds / 60 + milliseconds / (1000 * 60);
73 | const totalHours =
74 | hours +
75 | minutes / 60 +
76 | seconds / (60 * 60) +
77 | milliseconds / (1000 * 60 * 60);
78 |
79 | let secondsAngle = totalSeconds * 6;
80 | let minutesAngle = totalMinutes * 6;
81 | let hoursAngle = totalHours * 30;
82 |
83 | if (secondsAngle > 270) secondsAngle -= 360;
84 | if (minutesAngle > 270) minutesAngle -= 360;
85 | if (hoursAngle > 270) hoursAngle -= 360;
86 |
87 | secondHand.style.transform = `translate(-50%, -100%) rotate(${secondsAngle}deg)`;
88 | minuteHand.style.transform = `translate(-50%, -100%) rotate(${minutesAngle}deg)`;
89 | hourHand.style.transform = `translate(-50%, -100%) rotate(${hoursAngle}deg)`;
90 |
91 | requestAnimationFrame(update);
92 | };
93 |
94 | requestAnimationFrame(update);
95 |
96 | return clockContainer;
97 | }
98 |
99 | async createPreview() {
100 | let div = document.createElement("div");
101 | div.classList.add("clock-widget-preview");
102 |
103 | let title = document.createElement("div");
104 | title.classList.add("clock-preview-title");
105 | title.innerText = "Clock";
106 | div.appendChild(title);
107 |
108 | let container = document.createElement("div");
109 | container.classList.add("clock-container");
110 | div.appendChild(container);
111 |
112 | let clockFace = document.createElement("div");
113 | clockFace.classList.add("clock-face");
114 | container.appendChild(clockFace);
115 |
116 | let minuteHand = document.createElement("div");
117 | minuteHand.classList.add("clock-hand", "minute-hand");
118 | minuteHand.style.transform = "rotate(0deg)";
119 | clockFace.appendChild(minuteHand);
120 |
121 | let hourHand = document.createElement("div");
122 | hourHand.classList.add("clock-hand", "hour-hand");
123 | hourHand.style.transform = "rotate(0deg)";
124 | clockFace.appendChild(hourHand);
125 |
126 | let secondHand = document.createElement("div");
127 | secondHand.classList.add("clock-hand", "second-hand");
128 | secondHand.style.transform = "rotate(0deg)";
129 | clockFace.appendChild(secondHand);
130 |
131 | const centerCircle = document.createElement("div");
132 | centerCircle.classList.add("clock-center");
133 | clockFace.appendChild(centerCircle);
134 |
135 | let secondsAngle = 50;
136 | let minutesAngle = 120;
137 | let hoursAngle = 240;
138 |
139 | secondHand.style.transform = `translate(-50%, -100%) rotate(${secondsAngle}deg)`;
140 | minuteHand.style.transform = `translate(-50%, -100%) rotate(${minutesAngle}deg)`;
141 | hourHand.style.transform = `translate(-50%, -100%) rotate(${hoursAngle}deg)`;
142 |
143 | return div;
144 | }
145 |
146 | async onThemeChange() {}
147 | }
148 |
149 | registerWidget(new ClockWidget());
150 |
--------------------------------------------------------------------------------
/fixes-utils/utils.js:
--------------------------------------------------------------------------------
1 | const DEBUG = false;
2 |
3 | /// Get a value inside an object by path
4 | //
5 | ///```js
6 | /// const ob = { sub1: { sub2: "hello" } };
7 | /// getByPath(ob, "sub1.sub2") === "hello"
8 | ///```
9 | function getByPath(object, path) {
10 | if (!path) {
11 | return object;
12 | }
13 | let ob = object;
14 | for (let node of path.split(".")) {
15 | ob = ob[node];
16 | if (ob === undefined) {
17 | throw `getByPath: ${node} did not exist in path ${path}`;
18 | }
19 | }
20 | return ob;
21 | }
22 |
23 | /// set a value inside an object by path
24 | function setByPath(object, path, value) {
25 | let ob = object;
26 | const pathSplit = path.split(".");
27 | for (let i = 0; i < pathSplit.length - 1; i++) {
28 | ob = ob[pathSplit[i]];
29 | if (ob === undefined) {
30 | throw `setByPath: ${pathSplit[i]} did not exist in path ${path}`;
31 | }
32 | }
33 | ob[pathSplit[pathSplit.length - 1]] = value;
34 | }
35 |
36 | /// Fills missing fields in an object with values from a default object.
37 | function fillObjectWithDefaults(object, defaults) {
38 | if (!object) {
39 | object = {};
40 | }
41 |
42 | for (const key of Object.keys(defaults)) {
43 | if (typeof defaults[key] === "object" && defaults[key] !== null) {
44 | object[key] = fillObjectWithDefaults(object[key], defaults[key]);
45 | }
46 |
47 | if (object[key] === undefined) {
48 | object[key] = defaults[key];
49 | }
50 | }
51 |
52 | return object;
53 | }
54 |
55 | function openURL(url, new_window = false) {
56 | if (new_window) {
57 | let a = document.createElement("a");
58 | a.href = url;
59 | a.rel = "noopener noreferrer";
60 | a.target = "_blank";
61 | a.click();
62 | return;
63 | }
64 | window.location = url;
65 | }
66 |
67 | function delay(ms) {
68 | return new Promise((resolve) => setTimeout(resolve, ms)); // no hate please 👉👈 // GRRRR
69 | }
70 |
71 | async function clearAllData() {
72 | localStorage.clear();
73 | await browser.runtime.sendMessage({
74 | action: "clearLocalStorage",
75 | });
76 | location.reload();
77 | }
78 |
79 | function unbloat() {
80 | document.body.innerHTML = "";
81 | }
82 |
83 | function getExtensionImage(name) {
84 | return chrome.runtime.getURL(`media/${name}`);
85 | }
86 |
87 | function sendDebug(...messages) {
88 | if (DEBUG) {
89 | console.log(...messages);
90 | }
91 | }
92 |
93 | function getSchoolName() {
94 | try {
95 | const schoolName = window.location.hostname.split(".")[0];
96 | if (!schoolName) {
97 | throw new Error("Failed to extract school name");
98 | }
99 | return schoolName;
100 | } catch (error) {
101 | console.error(error.message);
102 | return null;
103 | }
104 | }
105 |
106 | function getCurrentDate() {
107 | return new Date().toISOString().split("T")[0];
108 | }
109 |
110 | function getFutureDate(days) {
111 | return new Date(Date.now() + days * 86400000).toISOString().split("T")[0];
112 | }
113 |
114 | function randomChance(probability) {
115 | if (probability <= 0) return false;
116 | if (probability >= 1) return true;
117 | return Math.random() < probability;
118 | }
119 |
120 | function isAbsoluteUrl(url) {
121 | return /^(https?:\/\/|data:image\/)/i.test(url);
122 | }
123 |
124 | function getPfpLink(username) {
125 | let firstInitial;
126 | let secondInitial;
127 | if (username) {
128 | const parts = username.trim().split(/\s+/);
129 | firstInitial = parts[0][0].toUpperCase();
130 | secondInitial = parts.length > 1 ? parts[1][0].toUpperCase() : "";
131 | } else {
132 | // Mr. Unknown
133 | firstInitial = "M";
134 | secondInitial = "U";
135 | }
136 | return `https://userpicture20.smartschool.be/User/Userimage/hashimage/hash/initials_${
137 | firstInitial + secondInitial
138 | }/plain/1/res/128`;
139 | }
140 |
141 | function getUserId() {
142 | let userId;
143 | // get UID
144 | try {
145 | // try get it from a magic element
146 | sendDebug("Trying to get plannerUrl from DOM...");
147 | const plannerUrl = document
148 | .getElementById("datePickerMenu")
149 | .getAttribute("plannerurl");
150 | sendDebug("Found plannerUrl from DOM:", plannerUrl);
151 |
152 | const expirationDate = new Date(); // store UID as cookie
153 | expirationDate.setDate(expirationDate.getDate() + 30);
154 | document.cookie = `plannerUrl=${plannerUrl};expires=${expirationDate.toUTCString()};path=/`;
155 | sendDebug("Stored plannerUrl in cookies with 30 day expiration.");
156 |
157 | userId = plannerUrl.split("/")[4];
158 | sendDebug("Extracted userId from plannerUrl:", userId);
159 | } catch (e) {
160 | // read it from a cookie cuz the magic is unreliable af
161 | sendDebug("Failed to get plannerUrl from DOM. Error:", e.message);
162 | sendDebug("Trying to get plannerUrl from cookies...");
163 |
164 | const cookies = document.cookie.split(";");
165 | const plannerUrlCookie = cookies.find((cookie) =>
166 | cookie.trim().startsWith("plannerUrl=")
167 | );
168 |
169 | if (plannerUrlCookie) {
170 | sendDebug("Retrieved plannerUrl from cookies" + plannerUrlCookie);
171 | const plannerUrl = plannerUrlCookie.split("=")[1];
172 | sendDebug("Found plannerUrl in cookies:", plannerUrl);
173 |
174 | userId = plannerUrl.split("/")[4];
175 | sendDebug("Extracted userId from cookie plannerUrl:", userId);
176 | } else {
177 | console.error(
178 | "UID is fucked, refresh 5 keer en als het dan niet werkt vraag hulp op discord @JJorne"
179 | );
180 | }
181 | }
182 | if (userId) {
183 | return userId;
184 | } else {
185 | console.error("No userID? womp womp");
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/games/snake.js:
--------------------------------------------------------------------------------
1 | class Point {
2 | x;
3 | y;
4 | constructor(x, y) {
5 | this.x = x;
6 | this.y = y;
7 | }
8 |
9 | add(other) {
10 | return new Point(this.x + other.x, this.y + other.y);
11 | }
12 | equal(other) {
13 | return this.x === other.x && this.y === other.y;
14 | }
15 | }
16 |
17 | const DIR_UP = new Point(0, -1);
18 | const DIR_LEFT = new Point(-1, 0);
19 | const DIR_RIGHT = new Point(1, 0);
20 | const DIR_DOWN = new Point(0, 1);
21 |
22 | const CELL_COUNT = 15;
23 | const FRAME_TIME = 400;
24 | class SnakeWidget extends GameBase {
25 | #targetDir;
26 | #curDir;
27 | #counter;
28 | #snake;
29 | #food;
30 |
31 | #backgroundCanvas;
32 |
33 | constructor() {
34 | super();
35 | }
36 |
37 | get title() {
38 | return "Snake++";
39 | }
40 | get options() {
41 | return [
42 | GameOption.slider("speed", "Speed:", 10, 300, 100),
43 | GameOption.slider("size", "Size:", 100, 500, 100),
44 | ];
45 | }
46 |
47 | #tick() {
48 | this.#curDir = this.#targetDir;
49 | const head = this.#snake[this.#snake.length - 1];
50 | const newHead = head.add(this.#curDir);
51 |
52 | // bounds check
53 | if (
54 | newHead.x < 0 ||
55 | newHead.y < 0 ||
56 | newHead.x >= this.#getCellCount() ||
57 | newHead.y >= this.#getCellCount()
58 | ) {
59 | this.stopGame();
60 | return;
61 | }
62 | // check if we hit ourselves
63 | if (this.#snake.find((p) => p.equal(newHead))) {
64 | this.stopGame();
65 | return;
66 | }
67 |
68 | if (newHead.equal(this.#food)) {
69 | this.#spawnFood();
70 | this.score += 1;
71 | } else {
72 | this.#snake.shift(); // remove tail
73 | }
74 |
75 | // add new head
76 | this.#snake.push(newHead);
77 | }
78 |
79 | #calcCelRad() {
80 | return this.canvas.width / (this.#getCellCount() * 2);
81 | }
82 |
83 | #getRandomFieldPos() {
84 | const cellCount = this.#getCellCount();
85 | return new Point(
86 | Math.floor(Math.random() * cellCount),
87 | Math.floor(Math.random() * cellCount)
88 | );
89 | }
90 |
91 | #spawnFood() {
92 | this.#food = this.#getRandomFieldPos();
93 | while (this.#snake.find((p) => p.equal(this.#food))) {
94 | this.#food = this.#getRandomFieldPos();
95 | }
96 | }
97 |
98 | defaultSettings() {
99 | return { score: 0, options: [], enableGrid: true };
100 | }
101 |
102 | async onGameStart() {
103 | const cellCount = this.#getCellCount();
104 | if (!this.#backgroundCanvas) {
105 | const bg = document.createElement("canvas");
106 | bg.height = this.canvas.width;
107 | bg.width = this.canvas.width;
108 | this.#backgroundCanvas = bg;
109 | }
110 | // redraw background in case of slider change.
111 | const bgctx = this.#backgroundCanvas.getContext("2d", { alpha: false });
112 |
113 | this.#drawBg(bgctx);
114 |
115 | this.#counter = 0;
116 | this.#curDir = DIR_DOWN;
117 | this.#targetDir = DIR_DOWN;
118 | this.#snake = [
119 | new Point(Math.floor(cellCount / 2), Math.floor(cellCount / 2)),
120 | ];
121 | this.#spawnFood();
122 | }
123 |
124 | async onThemeChange() {
125 | if (!this.#backgroundCanvas) {
126 | return;
127 | }
128 | const bgctx = this.#backgroundCanvas.getContext("2d", { alpha: false });
129 | this.#drawBg(bgctx);
130 | }
131 |
132 | #drawDot(ctx, dot) {
133 | ctx.strokeWidth = 0;
134 | ctx.beginPath();
135 | const celRad = this.#calcCelRad();
136 | ctx.arc(
137 | Math.floor(celRad + dot.x * celRad * 2.0),
138 | Math.floor(celRad + dot.y * celRad * 2.0),
139 | celRad * 0.9,
140 | 0,
141 | Math.PI * 2
142 | );
143 | ctx.fill();
144 | }
145 |
146 | #getCellCount() {
147 | return Math.round(CELL_COUNT * Math.sqrt(this.getOpt("size") * 0.01));
148 | }
149 |
150 | #drawBg(ctx) {
151 | const cellCount = this.#getCellCount();
152 | const celRad = this.#calcCelRad();
153 | ctx.strokeWidth = 0;
154 | ctx.fillStyle = getThemeVar("--color-base01");
155 | ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
156 | if (this.settings.enableGrid) {
157 | for (let y = 0; y < cellCount; y++) {
158 | for (let x = 0; x < cellCount; x++) {
159 | if ((x + y) % 2 == 0) {
160 | ctx.fillStyle = `${getThemeVar("--color-base03")}50`;
161 | } else {
162 | ctx.fillStyle = `${getThemeVar("--color-base02")}50`;
163 | }
164 | ctx.beginPath();
165 | ctx.arc(
166 | celRad + x * celRad * 2,
167 | celRad + y * celRad * 2,
168 | celRad * 0.7,
169 | 0,
170 | Math.PI * 2
171 | );
172 | ctx.fill();
173 | }
174 | }
175 | }
176 | }
177 |
178 | onGameDraw(ctx, dt) {
179 | this.#counter += dt;
180 | if (this.#counter * this.getOpt("speed") * 0.02 >= FRAME_TIME) {
181 | this.#tick();
182 | this.#counter = 0;
183 | }
184 | ctx.drawImage(this.#backgroundCanvas, 0, 0);
185 |
186 | ctx.fillStyle = getThemeVar("--color-text");
187 | for (let part of this.#snake) {
188 | this.#drawDot(ctx, part);
189 | }
190 | ctx.fillStyle = getThemeVar("--color-accent");
191 | this.#drawDot(ctx, this.#food);
192 | }
193 | onKeyDown(e) {
194 | switch (e.key) {
195 | case "ArrowUp":
196 | if (this.#curDir !== DIR_DOWN) {
197 | this.#targetDir = DIR_UP;
198 | }
199 | break;
200 | case "ArrowDown":
201 | if (this.#curDir !== DIR_UP) {
202 | this.#targetDir = DIR_DOWN;
203 | }
204 | break;
205 | case "ArrowLeft":
206 | if (this.#curDir !== DIR_RIGHT) {
207 | this.#targetDir = DIR_LEFT;
208 | }
209 | break;
210 | case "ArrowRight":
211 | if (this.#curDir !== DIR_LEFT) {
212 | this.#targetDir = DIR_RIGHT;
213 | }
214 | break;
215 | }
216 | }
217 | }
218 | registerWidget(new SnakeWidget());
219 |
--------------------------------------------------------------------------------
/styles/smpp-styles/delijn.css:
--------------------------------------------------------------------------------
1 | .lijnCard {
2 | background-color: var(--color-base01);
3 | border: 3px solid var(--color-base03);
4 | align-self: center;
5 | padding: 10px;
6 | border-radius: 20px;
7 | width: 100%;
8 | height: 100%;
9 | display: flex;
10 | flex-direction: column;
11 | }
12 |
13 | .enableAnimations .lijnCard {
14 | animation: delijnCardAppear 0.3s ease-in-out 0ms 1;
15 | }
16 |
17 | @keyframes delijnCardAppear {
18 | 0% {
19 | transform: translateY(-50%);
20 | opacity: 0;
21 | }
22 |
23 | 50% {
24 | transform: translateY(10%);
25 | opacity: 1;
26 | }
27 |
28 | 100% {
29 | transform: translateY(0);
30 | }
31 | }
32 |
33 | .halteTitle {
34 | font-size: 16px;
35 | font-weight: 600;
36 | margin-bottom: 2px !important;
37 | }
38 |
39 | .halteLijnen {
40 | display: flex;
41 | flex-direction: row;
42 | gap: 10px;
43 | }
44 |
45 | .lijnNumber {
46 | border: 3px solid var(--color-base02);
47 | font-weight: 700;
48 | width: 55px;
49 | text-align: center;
50 | font-size: 20px;
51 | border-radius: 10px;
52 | padding: 4px;
53 | }
54 |
55 | .lijnCard.lijnCardCancelled .lijnCardBottom .timeUntilDeparture {
56 | color: var(--color-red);
57 | }
58 |
59 | .lijnCard.lijnCardNoData .arrivalTimeDeviation {
60 | color: var(--color-red);
61 | }
62 |
63 | .lijnCard.lijnCardCancelled .lijnCardBottom .time,
64 | .lijnCard.lijnCardCancelled .lijnCardTop .lijnDestination {
65 | text-decoration: line-through;
66 | text-decoration-color: var(--color-red);
67 | text-decoration-thickness: 3px;
68 | }
69 |
70 | .lijnNumber.halteLijnNumber {
71 | font-size: 12px;
72 | width: 35px;
73 | border-radius: 7.5px;
74 | border-width: 2px;
75 | }
76 |
77 | .enableAnimations .lijnCardHalte {
78 | transition-property: background-color, transform, scale;
79 | transition: 0.2s ease-in-out;
80 | }
81 |
82 | .enableAnimations .lijnCardHalte:hover {
83 | transform: scale(1.05);
84 | }
85 |
86 | .halteDirections {
87 | opacity: 0.9;
88 | font-size: 14px;
89 | margin-bottom: 8px;
90 | }
91 |
92 | .lijnCardHalte:hover {
93 | background-color: var(--color-base02) !important;
94 | cursor: pointer !important;
95 | }
96 |
97 | .lijnDestination {
98 | margin-bottom: 0px !important;
99 | font-size: 17px;
100 | vertical-align: middle;
101 | align-self: center;
102 | margin-left: auto;
103 | font-weight: 700;
104 | text-align: right;
105 | }
106 |
107 | .lijnDestination.lijnDestinationLong {
108 | font-size: 15.5px;
109 | }
110 |
111 | .lijnCardBottom {
112 | display: flex;
113 | flex-direction: row;
114 | }
115 |
116 | .time {
117 | font-size: 20px;
118 | }
119 |
120 | .arrivalTimeDeviation {
121 | font-size: 14px;
122 | color: var(--color-accent);
123 | text-align: right;
124 | margin-bottom: 5px;
125 | font-weight: 450;
126 | height: 19px;
127 | }
128 |
129 | .lijnCardTop {
130 | display: flex;
131 | flex-direction: row;
132 | width: 100%;
133 | justify-content: space-between;
134 | height: 40px;
135 | }
136 |
137 | .timeUntilDeparture {
138 | font-size: 20px;
139 | font-weight: 500;
140 | color: var(--color-accent);
141 | text-align: right;
142 | flex-grow: 1;
143 | }
144 |
145 | .delijnSearchButton {
146 | height: 40px !important;
147 | width: 40px !important;
148 | background: var(--color-base01) !important;
149 | border-radius: 10px !important;
150 | border: 2px solid var(--color-base03) !important;
151 | display: flex !important;
152 | align-items: center !important;
153 | justify-content: center;
154 | flex-grow: 0 !important;
155 | }
156 |
157 | .delijnSearchButton:hover {
158 | background-color: var(--color-base02) !important;
159 | border-color: var(--color-accent) !important;
160 | }
161 |
162 | .enableAnimations .delijnSearchButton {
163 | transition-property: background-color, border-color, transform, animation,
164 | origin;
165 | transition: 0.2s ease-in-out !important;
166 | }
167 |
168 | .enableAnimations .delijnSearchButton:hover > .delijnSearchIcon {
169 | transform-origin: bottom bottom;
170 | animation: searchIconJiggle 0.5s ease-in-out 0ms 1;
171 | }
172 |
173 | @keyframes searchIconJiggle {
174 | 33% {
175 | rotate: -5deg;
176 | }
177 |
178 | 50% {
179 | rotate: 6deg;
180 | }
181 |
182 | 66% {
183 | rotate: -5deg;
184 | }
185 |
186 | 100% {
187 | rotate: 0deg;
188 | }
189 | }
190 |
191 | .delijnInfoContainer {
192 | justify-content: center;
193 | font-size: 16px !important;
194 | font-weight: bold;
195 | text-align: center;
196 | }
197 |
198 | #delijnTopContainer {
199 | display: flex;
200 | flex-direction: row;
201 | align-items: center;
202 | gap: 1rem !important;
203 | }
204 |
205 | .popupinput.halteInput {
206 | flex-grow: 1;
207 | height: 40px !important;
208 | margin: 0px !important;
209 | font-size: 16px !important;
210 | }
211 |
212 | #delijncontainer {
213 | display: flex;
214 | flex-direction: column;
215 | gap: 12.5px !important;
216 | }
217 |
218 | #delijnBottomContainer {
219 | display: flex;
220 | flex-direction: column;
221 | gap: 12.5px;
222 | }
223 |
224 | .showMoreHaltesButton {
225 | align-self: center;
226 | text-align: center;
227 | font-weight: 560;
228 | background-color: var(--color-base01);
229 | width: 70%;
230 | border: 2px solid var(--color-base02);
231 | border-radius: 20px !important;
232 | padding: 5px !important;
233 | font-size: 18px !important;
234 | }
235 |
236 | .showMoreHaltesButton:hover {
237 | background-color: var(--color-base02);
238 | border-color: var(--color-accent);
239 | font-weight: 800;
240 | letter-spacing: 0.5px;
241 | width: 77%;
242 | cursor: pointer;
243 | }
244 |
245 | .enableAnimations .showMoreHaltesButton {
246 | animation: delijnCardAppear 0.3s ease-in-out 0ms 1;
247 | transition-property: background-color, border-color, width, font-weight,
248 | letter-spacing;
249 | transition: 0.2s ease-in-out !important;
250 | }
251 |
252 | .delijnInfoContainerHidden {
253 | display: none;
254 | }
255 |
256 | .delijnInfoContainerVisible {
257 | display: flex;
258 | }
259 |
260 | .delijn-preview-title {
261 | text-align: center;
262 | font-size: 2.5rem;
263 | font-weight: 700;
264 | margin-bottom: 0.5rem;
265 | }
266 | .delijn-icon-128-container {
267 | display: flex;
268 | align-items: center;
269 | justify-content: center;
270 | }
271 |
272 | .delijn-icon-128 {
273 | width: 128px;
274 | }
275 |
--------------------------------------------------------------------------------
/styles/fixes/login.css:
--------------------------------------------------------------------------------
1 | html {
2 | --loginpage-image: url("");
3 | --show-options: none;
4 | }
5 |
6 | .login-app__platform-indicator {
7 | background-color: transparent !important;
8 | padding: 20px !important;
9 | margin: 10px !important;
10 | height: 100px !important;
11 | text-align: center !important;
12 | }
13 |
14 | .login-app__bottom {
15 | background-image: none !important;
16 | }
17 |
18 | .login-app__school-logo {
19 | display: none !important;
20 | }
21 |
22 | .login-app__title--sign-in-with,
23 | .attachmentList__list .attachment__title__label {
24 | color: var(--color-text) !important;
25 | }
26 |
27 | .login-app__container,
28 | .login-app {
29 | background-position: center !important;
30 | background-size: cover !important;
31 | flex-grow: 0 !important;
32 | }
33 |
34 | .login-app__left > * {
35 | display: none !important;
36 | }
37 |
38 | .login-app__right,
39 | .login-app__top {
40 | box-shadow: none !important;
41 | width: 400px;
42 | height: auto;
43 | }
44 |
45 | .login-app__top {
46 | backdrop-filter: blur(10px) !important;
47 | background: var(--darken-background) !important;
48 | border-radius: 2.5rem !important;
49 | border: 3px solid var(--color-base03) !important;
50 | }
51 |
52 | .login-app__bottom {
53 | display: none !important;
54 | }
55 |
56 | .logintitle {
57 | font-size: 36px !important;
58 | font-weight: 650 !important;
59 | margin-bottom: 0px !important;
60 | color: var(--color-text) !important;
61 | }
62 |
63 | .login-app__link,
64 | .login-app__link--password {
65 | color: var(--color-text) !important;
66 | }
67 |
68 | a.smscButton[href^="/login/sso/init"],
69 | .login-app__title--sign-in-with {
70 | display: var(--show-options) !important;
71 | border-radius: 10px !important;
72 | width: 80% !important;
73 | align-self: center !important;
74 | }
75 |
76 | .login-app__form > form {
77 | margin-bottom: 5px !important;
78 | display: grid !important;
79 | }
80 |
81 | .login-app__title--separator {
82 | display: grid !important;
83 | margin-bottom: 0px !important;
84 | }
85 |
86 | body {
87 | background-image: var(--loginpage-image) !important;
88 | background-position: center !important;
89 | background-size: cover !important;
90 | }
91 |
92 | .showLeftNav:has(.login-app) {
93 | align-items: center !important;
94 | justify-content: center !important;
95 | }
96 |
97 | #smscMain.showLeftNav {
98 | display: flex !important;
99 | flex-direction: row !important;
100 | align-items: flex-start !important;
101 | }
102 |
103 | .float-label__input {
104 | border-radius: 40px !important;
105 | height: 40px !important;
106 | font-size: 15px !important;
107 | margin: 10px !important;
108 | margin-left: 0px !important;
109 | }
110 |
111 | .login-app .smscButton.blue:not(#pushWizardPopupConfirmButton) {
112 | border-radius: 9999px !important;
113 | width: 90% !important;
114 | height: 40px !important;
115 | color: var(--color-text) !important;
116 | border-color: var(--color-base02) !important;
117 | background-color: var(--color-base01) !important;
118 | font-weight: 450;
119 | box-shadow: none !important;
120 | justify-self: center;
121 | }
122 |
123 | .enableAnimations
124 | .login-app
125 | .smscButton.blue:not(#pushWizardPopupConfirmButton) {
126 | transition-property: background-color, border-color, box-shadow, width,
127 | font-weigt, margin-left;
128 | transition: 0.3s !important;
129 | }
130 |
131 | .login-app .smscButton.blue:hover:not(#pushWizardPopupConfirmButton:hover) {
132 | background-color: var(--color-base02) !important;
133 | border-color: var(--color-base03) !important;
134 | box-shadow: none !important;
135 | width: 100% !important;
136 | font-weight: 700 !important;
137 | margin-left: 0% !important;
138 | }
139 |
140 | .modern-ui .float-label__input:focus + label > span,
141 | .modern-ui .float-label__input:valid + label > span {
142 | font-size: 18px !important;
143 | left: 5px !important;
144 | top: -70px !important;
145 | color: var(--color-text) !important;
146 | }
147 |
148 | .login-app__form {
149 | color: var(--color-text) !important;
150 | display: flex !important;
151 | flex-direction: column !important;
152 | }
153 |
154 | .white_text_button {
155 | color: var(--color-base01) !important;
156 | background-color: var(--color-base01);
157 | color: var(--color-text) !important;
158 | border: 2px solid var(--color-base02) !important;
159 | border-radius: 5px !important;
160 | margin: 10px !important;
161 | font-size: 15px !important;
162 | }
163 |
164 | .enableAnimations .white_text_button {
165 | transition: ease-in-out !important;
166 | transition-duration: 0.1s !important;
167 | }
168 |
169 | .white_text_button:hover {
170 | background-color: var(--color-base02) !important;
171 | border-color: var(--color-base03) !important;
172 | }
173 |
174 | .login-app__infoblock--black {
175 | color: var(--color-text) !important;
176 | }
177 |
178 | .login-app__title--separator:after,
179 | .login-app__title--separator:before {
180 | display: none !important;
181 | }
182 |
183 | span.login-app__title.login-app__title--sign-in-with {
184 | display: none !important;
185 | }
186 |
187 | .splashtextcontainer {
188 | position: absolute;
189 | left: 50%;
190 | top: 50%;
191 | transform-origin: left !important;
192 | transform: translateX(180px) translateY(-240px) rotate(30deg) !important;
193 | /* Moves right by 200px and rotates */
194 | text-align: center;
195 | z-index: 99999;
196 | }
197 |
198 | .splashtext {
199 | color: var(--color-splashtext) !important;
200 | font-size: 30px !important;
201 | left: -50% !important;
202 | position: relative !important;
203 | }
204 |
205 | .enableAnimations .splashtext {
206 | animation: scale_up_and_down 0.58s linear infinite;
207 | }
208 |
209 | @keyframes scale_up_and_down {
210 | 0% {
211 | transform: scale(100%);
212 | }
213 |
214 | 50% {
215 | transform: scale(105%);
216 | }
217 |
218 | 100% {
219 | transform: scale(100%);
220 | }
221 | }
222 |
223 | #showmore {
224 | width: 80% !important;
225 | justify-self: center !important;
226 | border-radius: 10px !important;
227 | margin: 0px !important;
228 | }
229 |
230 | .login-app__password .form__eye-icon {
231 | top: 1.55rem !important;
232 | }
233 |
234 | #smscMain.showLeftNav:has(.login-app) {
235 | align-items: center !important;
236 | }
237 |
238 | .showLeftNav {
239 | background-color: transparent !important;
240 | }
241 |
--------------------------------------------------------------------------------
/styles/smpp-styles/plant.css:
--------------------------------------------------------------------------------
1 | #plantWidget {
2 | text-align: center;
3 | display: flex;
4 | flex-direction: column;
5 | justify-content: center;
6 | }
7 | #plantPreviewWidget {
8 | justify-content: center;
9 | align-items: center;
10 | display: flex;
11 | flex-direction: column;
12 | gap: 20px;
13 | }
14 | #plant-preview-title {
15 | font-size: 2.5rem;
16 | font-weight: 700;
17 | }
18 | #buttondivforplant {
19 | display: flex;
20 | flex-direction: row;
21 | gap: 15px;
22 | justify-content: space-evenly;
23 | margin: 0px 5px 0px 5px;
24 | }
25 |
26 | #watering_button {
27 | border: 3px solid var(--color-base03);
28 | background-color: var(--color-base02);
29 | fill: var(--color-accent);
30 | border-radius: 10px;
31 | padding: 2.5px;
32 | flex: 1;
33 | height: 50px;
34 | }
35 |
36 | #watering_button.disabled {
37 | fill: var(--color-base03);
38 | }
39 | #watering_button.disabled:hover {
40 | cursor: not-allowed;
41 | }
42 | .enableAnimations #watering_button {
43 | transition: ease-in-out;
44 | transition-duration: 0.1s;
45 | }
46 |
47 | #watering_button:hover:not(.disabled) {
48 | background-color: var(--color-base03);
49 | border-color: var(--color-accent);
50 | cursor: pointer;
51 | }
52 |
53 | #time_difference_last_watered {
54 | font-weight: bold;
55 | display: flex;
56 | align-items: center;
57 | flex-direction: column;
58 | justify-content: center;
59 | text-align: center;
60 | position: relative;
61 | top: 50%;
62 | flex: 1;
63 | vertical-align: center;
64 | max-height: 50px !important;
65 | }
66 |
67 | #glass-container {
68 | flex: 1;
69 | border: 3px solid var(--color-base03);
70 | border-radius: 10px;
71 | background-color: rgba(255, 255, 255, 0.05);
72 | /* semi-transparent background */
73 | position: relative;
74 | overflow: hidden;
75 | }
76 |
77 | #glass-fill {
78 | width: 100%;
79 | background: linear-gradient(
80 | 0deg,
81 | rgba(2, 112, 182, 1) 0%,
82 | rgba(56, 146, 219, 1) 40%,
83 | rgba(77, 166, 240, 1) 100%
84 | );
85 | position: absolute;
86 | bottom: 0;
87 | }
88 |
89 | .enableAnimations #glass-fill {
90 | transition: height 1.5s ease-in-out;
91 | /* smooth transition */
92 | /*lukas wat een stoeffer dat jij bent :/ */
93 | }
94 |
95 | #remove-button-bottom-div {
96 | display: flex;
97 | flex-direction: row;
98 | justify-content: center;
99 | }
100 |
101 | #remove_button_info {
102 | border: 3px solid var(--color-base03);
103 | background-color: var(--color-base02);
104 | border-radius: 50%;
105 | padding: 2.5px 5px 2.5px 5px;
106 | position: relative;
107 | align-self: center;
108 | height: 32px;
109 | width: 32px;
110 | font-size: 17px;
111 | text-align: center;
112 | position: absolute;
113 | right: 32px;
114 | }
115 | #remove-button-div {
116 | align-items: center;
117 | justify-content: center;
118 | flex-direction: column;
119 | display: flex;
120 | }
121 | .enableAnimations #remove_button_info {
122 | transition: ease-in-out;
123 | transition-duration: 0.1s;
124 | }
125 |
126 | #removeplantButton {
127 | border: 3px solid var(--color-base03);
128 | background-color: var(--color-base02);
129 | border-radius: 10px;
130 | padding: 2.5px 5px 2.5px 5px;
131 | position: relative;
132 | align-self: center;
133 | height: 32px;
134 | font-size: 17px;
135 | margin-top: 10px;
136 | margin-bottom: 10px;
137 | font-weight: 600;
138 | }
139 |
140 | .enableAnimations #removeplantButton {
141 | transition-property: background-color, border-color;
142 | transition: 0.2s ease-in-out;
143 | }
144 |
145 | #removeplantButton:hover {
146 | background-color: var(--color-base03);
147 | border-color: var(--color-accent);
148 | cursor: pointer;
149 | }
150 |
151 | #remove_button_info:hover {
152 | background-color: var(--color-base03);
153 | border-color: var(--color-accent);
154 | cursor: pointer;
155 | }
156 |
157 | #plant_remove_info_text {
158 | top: -45px;
159 | background-color: var(--color-base02);
160 | border: 2px solid var(--color-base03);
161 | box-shadow: rgba(0, 0, 0, 0.5) 0px 0.333rem 1.333rem;
162 | padding: 5px;
163 | border-radius: 10px;
164 | position: relative;
165 | opacity: 0;
166 | text-align: center;
167 | font-size: 13px;
168 | font-weight: 400;
169 | }
170 |
171 | .enableAnimations #plant_remove_info_text {
172 | transition-duration: 0.2s;
173 | transition-delay: 0.2s;
174 | transition-timing-function: ease-in-out;
175 | }
176 |
177 | #plant_streak {
178 | color: var(--color-accent) !important;
179 | font-size: 50px !important;
180 | font-weight: 800 !important;
181 | text-align: center !important;
182 | }
183 |
184 | .enableAnimations #plant_the_plant_svg {
185 | transition-duration: 0.4s;
186 | }
187 |
188 | #riseMask {
189 | y: 0% !important;
190 | }
191 |
192 | /* Rising/draining effect */
193 | .enableAnimations #riseMask {
194 | transition: transform 0.4s cubic-bezier(0.52, 0.56, 0.29, 0.98) !important;
195 | y: 100% !important;
196 | /* Animate transform */
197 | /* Start hidden at the bottom */
198 | }
199 |
200 | .enableAnimations #plant_the_plant_svg:hover #riseMask {
201 | transform: translateY(-125%);
202 | /* Rise up on hover */
203 | }
204 |
205 | .leaves-base {
206 | fill: var(--color-base03) !important;
207 | }
208 | .leaves {
209 | fill: rgb(90, 143, 1);
210 | }
211 | #hand {
212 | fill: var(--color-base02);
213 | }
214 |
215 | .enableAnimations #hand {
216 | transition-duration: 0.3s;
217 | }
218 |
219 | #plant_the_plant_svg:hover #hand {
220 | fill: var(--color-base03);
221 | }
222 |
223 | #plant_the_plant_svg:hover {
224 | cursor: pointer !important;
225 | }
226 |
227 | #water_title {
228 | font-size: 15px !important;
229 | font-weight: 600 !important;
230 | margin: 0px !important;
231 | }
232 |
233 | #water_time {
234 | font-size: 15px !important;
235 | font-weight: 400 !important;
236 | margin: 0px !important;
237 | width: 150% !important;
238 | }
239 |
240 | .plantConfirmationButton {
241 | border-color: var(--color-red) !important;
242 | color: var(--color-red) !important;
243 | }
244 | .smpp-widget-preview div:has(.planttheplantbutton) {
245 | width: 100% !important;
246 | }
247 | .planttheplantbutton {
248 | background-color: transparent !important;
249 | border: none !important;
250 | width: 100% !important;
251 | }
252 |
253 | #remove-button-top-div {
254 | height: 0px;
255 | width: 90%;
256 | }
257 |
258 | .plant1,
259 | .plant2,
260 | .plant3,
261 | .plant4,
262 | .plant5,
263 | .plant6,
264 | .plant7,
265 | .plant8 {
266 | aspect-ratio: 1;
267 | width: 100%;
268 | }
269 | .plant1 {
270 | scale: 0.2;
271 | }
272 | .plant2 {
273 | scale: 0.5;
274 | }
275 | .plant3 {
276 | scale: 0.6;
277 | }
278 | .plant4 {
279 | scale: 0.8;
280 | }
281 | .plant5 {
282 | scale: 0.9;
283 | }
284 |
--------------------------------------------------------------------------------
/main-features/modules/images.js:
--------------------------------------------------------------------------------
1 | class ImageSelector {
2 | constructor(name) {
3 | this.name = name;
4 |
5 | this.clearButton = null;
6 | this.linkInput = null;
7 | this.linkInputContainer = this.createLinkImageInputContainer();
8 |
9 | this.fileInput = null;
10 | this.fileInputButton = null;
11 | this.fileInputContainer = this.createImageFileInputContainer();
12 |
13 | this.fullContainer = this.createFullFileInput();
14 | this._bindEvents();
15 | }
16 |
17 | readFileAsDataURL(file) {
18 | return new Promise((resolve, reject) => {
19 | const reader = new FileReader();
20 | reader.onerror = () => {
21 | reader.abort();
22 | reject(new Error("Failed to read file"));
23 | };
24 | reader.onload = () => resolve(reader.result);
25 | reader.readAsDataURL(file);
26 | });
27 | }
28 |
29 | onStore() {}
30 |
31 | createImageFileInputContainer() {
32 | const fileInputContainer = document.createElement("div");
33 | fileInputContainer.classList.add("file-input-container");
34 |
35 | this.fileInput = document.createElement("input");
36 | this.fileInput.style.display = "none";
37 | this.fileInput.tabindex = "-1";
38 | this.fileInput.type = "file";
39 | this.fileInput.accept = "image/*";
40 |
41 | this.fileInputButton = document.createElement("button");
42 | this.fileInputButton.type = "button";
43 | this.fileInputButton.classList.add("smpp-file-input-button");
44 | this.fileInputButton.setAttribute("aria-label", "Choose image file");
45 | this.fileInputButton.innerHTML = fileInputIconSvg;
46 |
47 | this.fileInputButton.addEventListener("click", (e) => {
48 | e.preventDefault();
49 | this.fileInput.click();
50 | });
51 |
52 | fileInputContainer.appendChild(this.fileInput);
53 | fileInputContainer.appendChild(this.fileInputButton);
54 | return fileInputContainer;
55 | }
56 |
57 | createLinkImageInputContainer() {
58 | const linkInputContainer = document.createElement("div");
59 | linkInputContainer.classList.add("link-input-container");
60 |
61 | this.clearButton = document.createElement("button");
62 | this.clearButton.type = "button";
63 | this.clearButton.tabIndex = "-1";
64 | this.clearButton.classList.add("smpp-link-clear-button");
65 | this.clearButton.setAttribute("aria-label", "Clear link");
66 | this.clearButton.innerHTML = ``;
71 |
72 | this.linkInput = createTextInput(null, "Link");
73 |
74 | linkInputContainer.appendChild(this.linkInput);
75 | linkInputContainer.appendChild(this.clearButton);
76 | return linkInputContainer;
77 | }
78 |
79 | createFullFileInput() {
80 | const container = document.createElement("div");
81 | container.classList.add("full-file-input-container");
82 | container.appendChild(this.linkInputContainer);
83 | container.appendChild(this.fileInputContainer);
84 | return container;
85 | }
86 |
87 | _bindEvents() {
88 | this.fileInput.addEventListener("change", async () => {
89 | if (this.fileInput.files && this.fileInput.files.length > 0) {
90 | this.fileInputButton.classList.add("active");
91 | } else {
92 | this.fileInputButton.classList.remove("active");
93 | }
94 | await this.storeImage();
95 | });
96 |
97 | this.linkInput.addEventListener("change", async () => {
98 | if ((this.linkInput.value || "").trim() !== "") {
99 | this.clearButton.classList.add("active");
100 | } else {
101 | this.clearButton.classList.remove("active");
102 | }
103 | await this.storeImage();
104 | });
105 |
106 | this.clearButton.addEventListener("click", async (e) => {
107 | e.preventDefault();
108 | this.linkInput.value = "";
109 | this.clearButton.classList.remove("active");
110 | this.fileInputButton.classList.remove("active");
111 | await this.storeImage();
112 | });
113 | }
114 |
115 | async storeImage() {
116 | let data = await browser.runtime.sendMessage({
117 | action: "getImage",
118 | id: this.name,
119 | });
120 |
121 | if (!data) data = { imageData: null, link: "", type: "default" };
122 |
123 | const file = this.fileInput.files && this.fileInput.files[0];
124 | if (file) {
125 | const dataUrl = await this.readFileAsDataURL(file);
126 | data.imageData = dataUrl;
127 | data.link = file.name;
128 | data.type = "file";
129 |
130 | this.fileInput.value = "";
131 | } else {
132 | const linkValue = (this.linkInput.value || "").trim();
133 | if (linkValue === "") {
134 | data.type = "default";
135 | data.link = "";
136 | data.imageData = null;
137 | } else {
138 | data.link = linkValue;
139 | if (isAbsoluteUrl(linkValue)) {
140 | data.type = "link";
141 | data.imageData = linkValue;
142 | } else {
143 | data.type = "default";
144 | data.imageData = null;
145 | }
146 | }
147 | }
148 |
149 | await browser.runtime.sendMessage({
150 | action: "setImage",
151 | id: this.name,
152 | data,
153 | });
154 | this.loadImageData();
155 | this.onStore();
156 | }
157 |
158 | async loadImageData() {
159 | let data = await browser.runtime.sendMessage({
160 | action: "getImage",
161 | id: this.name,
162 | });
163 |
164 | if (!data) data = { link: "", type: "default" };
165 |
166 | this.linkInput.value = data.link || "";
167 |
168 | if (data.type === "file") {
169 | this.fileInputButton.classList.add("active");
170 | } else {
171 | this.fileInputButton.classList.remove("active");
172 | }
173 |
174 | if ((this.linkInput.value || "").trim() !== "") {
175 | this.clearButton.classList.add("active");
176 | } else {
177 | this.clearButton.classList.remove("active");
178 | }
179 | }
180 | }
181 |
182 | // Returns { url: string|null, type: file, link, default }
183 | async function getImageURL(id, onDefault) {
184 | let image;
185 | try {
186 | image = await browser.runtime.sendMessage({ action: "getImage", id });
187 | } catch (err) {
188 | console.warn("[getImageURL] Failed to get image from background:", err);
189 | return { url: onDefault(), type: null };
190 | }
191 |
192 | // Default
193 | if (image.type === "default") {
194 | return { url: onDefault(), type: image.type };
195 | }
196 |
197 | // Link
198 | if (image.type === "link") {
199 | return { url: image.imageData, type: image.type };
200 | }
201 |
202 | // WARNING, THIS IS CURSED BUT I DON'T CARE!
203 | if (isFirefox) {
204 | return { url: image.imageData, type: image.type };
205 | }
206 |
207 | // File (base64)
208 | try {
209 | const res = await fetch(image.imageData);
210 | const blob = await res.blob();
211 |
212 | const objectURL = URL.createObjectURL(blob);
213 |
214 | return { url: objectURL, type: image.type };
215 | } catch (err) {
216 | console.warn("[getImageURL] Failed to create Blob URL:", err);
217 | return { url: onDefault(), type: image.type };
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/main-features/quick-menu/quick.js:
--------------------------------------------------------------------------------
1 | let quicks = [];
2 |
3 | let links = [];
4 | let vakken = [];
5 | let goto_items = [];
6 |
7 | if (document.querySelector(".topnav")) {
8 | fetch_links();
9 | fetch_vakken();
10 | scrape_goto();
11 | }
12 |
13 | function quick_cmd_list() {
14 | let cmd_list = [];
15 | for (let i = 0; i < quicks.length; i++) {
16 | cmd_list.push({ value: quicks[i].name, meta: "quick: " + quicks[i].url });
17 | }
18 | return cmd_list;
19 | }
20 |
21 | function add_quick(name, url) {
22 | let quick = { name: name.toLowerCase(), url: url };
23 | for (let i = 0; i < quicks.length; i++) {
24 | if (quicks[i].name == name) {
25 | quicks[i] = quick;
26 | quick_save();
27 | return;
28 | }
29 | }
30 | quicks.push(quick);
31 | quick_save();
32 | }
33 | function remove_quick(name) {
34 | for (let i = 0; i < quicks.length; i++) {
35 | if (quicks[i].name == name) {
36 | quicks.splice(i, 1);
37 | quick_save();
38 | return;
39 | }
40 | }
41 | }
42 |
43 | async function quick_load() {
44 | const quicks = await browser.runtime.sendMessage({
45 | action: "getSetting",
46 | name: "other.quicks",
47 | });
48 | if (!quicks) {
49 | return [];
50 | }
51 | return quicks;
52 | }
53 |
54 | async function quick_save() {
55 | await browser.runtime.sendMessage({
56 | action: "setSetting",
57 | name: "other.quicks",
58 | data: quicks,
59 | });
60 | }
61 |
62 | function add_quick_interactive() {
63 | let cmd_list = quick_cmd_list();
64 | dmenu(
65 | cmd_list,
66 | function(name, shift) {
67 | value_list = [];
68 | for (let i = 0; i < quicks.length; i++) {
69 | if (quicks[i].name == name) {
70 | value_list = [{ value: quicks[i].url }];
71 | break;
72 | }
73 | }
74 | dmenu(
75 | value_list,
76 | function(value, shift) {
77 | if (!value.startsWith("http")) {
78 | value = "https://" + value;
79 | }
80 | add_quick(name, value);
81 | },
82 | "value:"
83 | );
84 | },
85 | "name:"
86 | );
87 | }
88 |
89 | function remove_quick_interactive() {
90 | let cmd_list = quick_cmd_list();
91 | dmenu(
92 | cmd_list,
93 | function(name, shift) {
94 | remove_quick(name);
95 | },
96 | "name:"
97 | );
98 | }
99 |
100 | async function fetch_links() {
101 | links = [];
102 | let response = await fetch("/links/api/v1/");
103 | const contentType = response.headers.get("content-type");
104 | if (response.ok && contentType && contentType.includes("application/json")) {
105 | let response_data = await response.json();
106 | for (let i = 0; i < response_data.length; i++) {
107 | links.push({
108 | url: response_data[i].url,
109 | value: response_data[i].name.toLowerCase(),
110 | meta: "link",
111 | });
112 | }
113 | } else {
114 | console.error("Fetching links failed (" + response.status + " http code)");
115 | links = [];
116 | }
117 | }
118 |
119 | async function fetch_vakken() {
120 | vakken = [];
121 | let response = await fetch("/Topnav/getCourseConfig");
122 | const contentType = response.headers.get("content-type");
123 | if (response.ok && contentType && contentType.includes("application/json")) {
124 | let response_data = await response.json();
125 | for (let i = 0; i < response_data.own.length; i++) {
126 | let vak = response_data.own[i];
127 | let meta = "vak";
128 | if (vak.descr != "") {
129 | meta += " [ " + vak.descr + " ]";
130 | }
131 | vakken.push({ url: vak.url, value: vak.name.toLowerCase(), meta: meta });
132 | }
133 | } else {
134 | console.error("Fetching vakken failed (" + response.status + " http code)");
135 | vakken = [];
136 | }
137 | }
138 |
139 | function scrape_goto() {
140 | goto_items = [];
141 | let goto_items_html = document.querySelectorAll(
142 | ".js-shortcuts-container > a"
143 | );
144 | for (let i = 0; i < goto_items_html.length; i++) {
145 | const item = goto_items_html[i];
146 | goto_items.push({
147 | url: item.href,
148 | value: item.innerText.toLowerCase().trim(),
149 | meta: "goto",
150 | });
151 | }
152 | }
153 |
154 | async function do_qm(opener = "") {
155 | let cmd_list = quick_cmd_list()
156 | .concat(goto_items)
157 | .concat(vakken)
158 | .concat(links)
159 | .concat([
160 | "home",
161 | "quick add",
162 | "quick remove",
163 | "unbloat",
164 | "config",
165 | "clearsettings",
166 | "discord",
167 | "toggle performance mode",
168 | "gcbeta",
169 | "dizzy",
170 | "breakdmenu",
171 | "glass",
172 | "ridge",
173 | "reset plant",
174 | ]);
175 |
176 | if (dmenuConfig.toplevelConfig) {
177 | cmd_list = cmd_list.concat(await getDMenuOptionsForSettings(true));
178 | }
179 |
180 | dmenu(
181 | cmd_list,
182 | async function(cmd) {
183 | switch (cmd) {
184 | case "unbloat":
185 | unbloat();
186 | return;
187 | case "breakdmenu":
188 | await browser.runtime.sendMessage({
189 | action: "setSetting",
190 | name: "other.dmenu",
191 | data: {
192 | itemScore: false,
193 | },
194 | });
195 | return;
196 | case "set background":
197 | dmenu(
198 | [],
199 | function(url) {
200 | set_background(url);
201 | store_background(url);
202 | },
203 | "bg url:"
204 | );
205 | return;
206 | case "config":
207 | dmenu(
208 | await getDMenuOptionsForSettings(false),
209 | function(cmd, shift) {
210 | dmenuEditConfig(cmd);
211 | },
212 | "config: "
213 | );
214 | return;
215 | case "quick add":
216 | add_quick_interactive();
217 | return;
218 | case "quick remove":
219 | remove_quick_interactive();
220 | return;
221 | case "discord":
222 | openURL("https://discord.com/invite/qCHZYepDqZ");
223 | return;
224 | case "home":
225 | openURL("/");
226 | return;
227 | case "clearsettings":
228 | clearAllData();
229 | return;
230 | case "glass":
231 | document.body.classList.add("glass");
232 | return;
233 | case "ridge":
234 | document.body.classList.add("ridge");
235 | return;
236 | case "gcbeta":
237 | openGlobalChat(null, true);
238 | return;
239 | case "dizzy":
240 | const styleEl = document.createElement("style");
241 | styleEl.innerText = `
242 | *{
243 | transition: transform 10s !important;
244 | }
245 | *:hover{
246 | transform: rotate(360deg) !important;
247 | }`;
248 | document.body.appendChild(styleEl);
249 | return;
250 | case "reset plant":
251 | resetPlant()
252 | return;
253 | case "plant data":
254 | console.log(await browser.runtime.sendMessage({
255 | action: "getPlantAppData",
256 | }));
257 | default:
258 | break;
259 | }
260 | if (cmd.startsWith("config.")) {
261 | dmenuEditConfig(cmd);
262 | }
263 |
264 | for (let i = 0; i < quicks.length; i++) {
265 | const quick = quicks[i];
266 | if (quick.name == cmd) {
267 | openURL(quick.url, true);
268 | return;
269 | }
270 | }
271 | for (let i = 0; i < links.length; i++) {
272 | if (links[i].value == cmd) {
273 | openURL(links[i].url, true);
274 | }
275 | }
276 | for (let i = 0; i < goto_items.length; i++) {
277 | if (goto_items[i].value == cmd) {
278 | openURL(goto_items[i].url);
279 | }
280 | }
281 | for (let i = 0; i < vakken.length; i++) {
282 | if (vakken[i].value == cmd) {
283 | openURL(vakken[i].url);
284 | }
285 | }
286 | },
287 | "quick:",
288 | (opener = opener)
289 | );
290 | }
291 |
--------------------------------------------------------------------------------
/background-scripts/background-script.js:
--------------------------------------------------------------------------------
1 | if (typeof browser === "undefined") {
2 | var browser = chrome;
3 | }
4 |
5 | let globalsInitialized = false;
6 |
7 | import {
8 | getSettingsData,
9 | getSettingsOptions,
10 | getDelijnColorData,
11 | getDefaultSettings,
12 | getPlantAppData,
13 | getCustomThemeData,
14 | getAllThemes,
15 | getTheme,
16 | setImage,
17 | getImage,
18 | } from "./data-background-script.js";
19 | import { fetchWeatherData, fetchDelijnData } from "./api-background-script.js";
20 | import { initGlobals } from "./json-loader.js";
21 |
22 | ///
23 | ///```js
24 | /// const ob = { sub1: { sub2: "hello" } };
25 | /// getByPath(ob, "sub1.sub2") === "hello"
26 | ///```
27 | function getByPath(object, path) {
28 | if (!path) {
29 | return object;
30 | }
31 | let ob = object;
32 | for (let node of path.split(".")) {
33 | ob = ob[node];
34 | if (ob === undefined) {
35 | throw `getByPath: ${node} did not exist in path ${path}`;
36 | }
37 | }
38 | return ob;
39 | }
40 |
41 | function setByPath(object, path, value) {
42 | let ob = object;
43 | const pathSplit = path.split(".");
44 | for (let i = 0; i < pathSplit.length - 1; i++) {
45 | ob = ob[pathSplit[i]];
46 | if (ob === undefined) {
47 | throw `setByPath: ${pathSplit[i]} did not exist in path ${path}`;
48 | }
49 | }
50 | ob[pathSplit[pathSplit.length - 1]] = value;
51 | }
52 |
53 |
54 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
55 | handleMessage(message, sendResponse);
56 | return true; // keeps channel open
57 | });
58 |
59 | async function handleMessage(message, sendResponse) {
60 | if (!globalsInitialized) {
61 | await initGlobals();
62 | globalsInitialized = true;
63 | }
64 | try {
65 | // General
66 | if (message.action === "clearLocalStorage") {
67 | browser.storage.local.clear();
68 | sendResponse({ success: true });
69 | console.log("Cleared browser storage");
70 | }
71 | // Themes
72 | if (message.action === "getAllThemes") {
73 | const allThemes = getAllThemes();
74 | sendResponse(allThemes);
75 | console.log("All themes sent.");
76 | }
77 | if (message.action === "getTheme") {
78 | let theme;
79 | try {
80 | theme = getTheme(message.theme);
81 | sendResponse(theme);
82 | console.log(`Theme ${message.theme} sent.`);
83 | } catch (error) {
84 | console.error(error);
85 | theme = getTheme("error");
86 | sendResponse(theme);
87 | console.error(`Invalid theme requested, sent "error" theme`);
88 | }
89 | }
90 | // Custom theme
91 | if (message.action === "getCustomThemeData") {
92 | const customThemeData = await getCustomThemeData();
93 | sendResponse(customThemeData);
94 | console.log("Custom theme data data sent.");
95 | }
96 | if (message.action === "setCustomThemeData") {
97 | await browser.storage.local.set({ customThemeData: message.data });
98 | sendResponse({ success: true });
99 | console.log("Custom theme data saved.");
100 | }
101 | // Images
102 | if (message.action === "setImage") {
103 | await setImage(message.id, message.data);
104 | sendResponse({ success: true });
105 | console.log(`Image with id:${message.id} saved.`);
106 | }
107 | if (message.action === "getImage") {
108 | const image = await getImage(message.id);
109 | sendResponse(image || null);
110 | console.log(`Image with id:${message.id} sent.`);
111 | }
112 | // Weather
113 | if (message.action === "fetchWeatherData") {
114 | const weatherData = await fetchWeatherData(message.location);
115 | sendResponse(weatherData);
116 | console.log("Weather data fetched and sent.");
117 | }
118 | // Delijn
119 | if (message.action === "fetchDelijnData") {
120 | const delijnData = await fetchDelijnData(message.url);
121 | sendResponse(delijnData);
122 | console.log("Delijn appdata fetched and sent.");
123 | }
124 | if (message.action === "getDelijnColorData") {
125 | let delijnColorData = await getDelijnColorData();
126 | sendResponse(delijnColorData);
127 | console.log("Delijn color data fetched and sent.");
128 | }
129 | // Plant
130 | if (message.action === "setPlantAppData") {
131 | await browser.storage.local.set({ plantAppData: message.data });
132 | sendResponse({ success: true });
133 | }
134 | if (message.action === "getPlantAppData") {
135 | const plantAppData = await getPlantAppData();
136 | sendResponse(plantAppData);
137 | console.log("Plant appdata sent.");
138 | }
139 |
140 | // Settings
141 | if (message.action === "setSetting") {
142 | const settingsData = await getSettingsData();
143 | setByPath(settingsData, message.name, message.data);
144 | await browser.storage.local.set({ settingsData: settingsData });
145 | sendResponse({ success: true });
146 | }
147 | if (message.action === "getSetting") {
148 | const settingsData = await getSettingsData();
149 | sendResponse(getByPath(settingsData, message.name));
150 | console.log(
151 | "Setting " + message.name + "sent"
152 | );
153 | }
154 | if (message.action === "setSettingsData") {
155 | await browser.storage.local.set({ settingsData: message.data });
156 | sendResponse({ success: true });
157 | console.log("Settings data saved.");
158 | }
159 | if (message.action === "getSettingsData") {
160 | let settingsData = await getSettingsData();
161 | console.log(settingsData);
162 | sendResponse(settingsData);
163 | console.log("Settings data sent.");
164 | }
165 | if (message.action === "getSettingsTemplate") {
166 | const settingsOptions = await getSettingsOptions();
167 | sendResponse(getByPath(settingsOptions, message.name));
168 | console.log("Settings options sent.");
169 | }
170 |
171 | // Widgets
172 | if (message.action === "getWidgetLayout") {
173 | console.log("loading widget layout");
174 | const data = await browser.storage.local.get("widgets");
175 | sendResponse(data.widgets);
176 | }
177 | if (message.action === "setWidgetLayout") {
178 | console.log("saving widget layout");
179 | await browser.storage.local.set({ widgets: message.layout });
180 | sendResponse({ success: true });
181 | }
182 | if (message.action === "getWidgetData") {
183 | console.log("loading widget data");
184 | const widgetId = "Game." + message.widget;
185 | let data = await browser.storage.local.get(widgetId);
186 | data = data[widgetId];
187 | sendResponse(data);
188 | }
189 | if (message.action === "setWidgetData") {
190 | console.log("saving widget data");
191 | let data = {};
192 | data["Game." + message.widget] = message.data;
193 | await browser.storage.local.set(data);
194 | sendResponse({ success: true });
195 | }
196 | // Migration
197 | if (message.action === "getDelijnAppData") {
198 | // for migration, NEVER use this!!!
199 | const delijnAppData = await browser.storage.local.get("delijnAppData");
200 | // await browser.storage.local.remove("delijnAppData");
201 | sendResponse(delijnAppData);
202 | console.log("delijnAppData sent.");
203 | }
204 | if (message.action === "getWeatherAppData") {
205 | // for migration, NEVER use this!!!
206 | const weatherAppData = await browser.storage.local.get("weatherAppData");
207 | // await browser.storage.local.remove("weatherAppData");
208 | sendResponse(weatherAppData);
209 | console.log("weatherAppData sent.");
210 | }
211 | if (message.action === "getBackgroundImage") {
212 | // for migration, NEVER use this!!!
213 | const backgroundImage = await browser.storage.local.get(
214 | "backgroundImage"
215 | );
216 | // await browser.storage.local.remove("backgroundImage");
217 | sendResponse(backgroundImage);
218 | console.log("Background image sent.");
219 | }
220 | } catch (err) {
221 | console.error("Service worker error:", err);
222 | sendResponse({ error: err.message || String(err) });
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/media/icons/top-nav/gc-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
103 |
--------------------------------------------------------------------------------
/main-features/quick-menu/dmenu.js:
--------------------------------------------------------------------------------
1 | let active_dmenu = null;
2 | let dmenuConfig = null;
3 |
4 | // Called by apply
5 | async function reloadDMenuConfig() {
6 | dmenuConfig = await browser.runtime.sendMessage({
7 | action: "getSetting",
8 | name: "other.dmenu",
9 | });
10 | }
11 |
12 | // Load the config if it wasn't already loaded
13 | async function loadDMenuConfig() {
14 | if (!dmenuConfig) {
15 | await reloadDMenuConfig();
16 | }
17 | }
18 |
19 | class DMenu {
20 | openerEl;
21 | menuEl;
22 |
23 | #endFunc;
24 | #inputEl;
25 | #itemListEl;
26 | #userText;
27 | #selectedIndex;
28 |
29 | constructor(
30 | itemList,
31 | endFunc = undefined,
32 | title = "dmenu:",
33 | openerEl = undefined,
34 | ) {
35 | this.endFunc = endFunc;
36 | this.openerEl = openerEl;
37 |
38 | this.userText = "";
39 | this.selectedIndex = 0;
40 |
41 | this.#mkDmenu(itemList, title);
42 | this.inputEl.focus();
43 | }
44 | isOpen() {
45 | return this.menuEl != null;
46 | }
47 | close() {
48 | if (!this.isOpen()) {
49 | return;
50 | }
51 | this.menuEl.remove();
52 | this.menuEl = null;
53 | }
54 |
55 | #accept(row = undefined) {
56 | if (row == undefined) {
57 | row = this.itemListEl.childNodes[this.selectedIndex];
58 | }
59 | let content = this.inputEl.value;
60 | if (row != undefined && !row.classList.contains("hidden")) {
61 | content = row.dataset.content;
62 | }
63 |
64 | if (this.endFunc != undefined && content !== "") {
65 | this.endFunc(content);
66 | }
67 | this.close();
68 | }
69 |
70 | #onKeydown(e) {
71 | if (e.key == "Enter") {
72 | this.#accept();
73 | } else if (e.key == "Escape") {
74 | this.close();
75 | } else if ((e.key == "Tab" && e.shiftKey) || e.key == "ArrowUp") {
76 | this.#selPrev();
77 | } else if ((e.key == "Tab" && !e.shiftKey) || e.key == "ArrowDown") {
78 | this.#selNext();
79 | } else {
80 | return;
81 | }
82 | e.preventDefault();
83 | }
84 | #oninput() {
85 | this.#sort();
86 | return;
87 | }
88 |
89 | #selNext() {
90 | this.#select(this.selectedIndex + 1);
91 | }
92 | #selPrev() {
93 | this.#select(this.selectedIndex - 1);
94 | }
95 |
96 | #validIndex(index) {
97 | if (index < 0 || index >= this.itemListEl.childNodes.length) {
98 | return false;
99 | }
100 |
101 | return !this.itemListEl.childNodes[index].classList.contains("hidden");
102 | }
103 |
104 | #select(index) {
105 | if (!this.#validIndex(index)) {
106 | return;
107 | }
108 | let row = this.itemListEl.childNodes[this.selectedIndex];
109 | row.classList.remove("dmenu-selected");
110 | this.selectedIndex = index;
111 |
112 | let newrow = this.itemListEl.childNodes[this.selectedIndex];
113 | newrow.classList.add("dmenu-selected");
114 | newrow.scrollIntoView(false);
115 | }
116 |
117 | #sort() {
118 | this.selectedIndex = 0;
119 | let searchq = this.inputEl.value;
120 |
121 | // update scores
122 | let items = [];
123 | for (let node of this.itemListEl.childNodes) {
124 | let score = this.#matchScore(node.dataset.content, searchq);
125 |
126 | if (score == 0 && searchq != "") {
127 | node.classList.add("hidden");
128 | } else {
129 | node.classList.remove("hidden");
130 | }
131 | items.push({ score: score, htmlNode: node });
132 |
133 | if (dmenuConfig.itemScore) {
134 | node.getElementsByClassName("dmenu-score")[0].innerText = score;
135 | }
136 | }
137 |
138 | let sortedItems = items.sort(function(a, b) {
139 | if (a.score < b.score) return 1;
140 | if (a.score > b.score) return -1;
141 | return 0;
142 | });
143 |
144 | for (let i = 0; i < sortedItems.length; i++) {
145 | let item = sortedItems[i];
146 | if (i == 0) {
147 | item.htmlNode.classList.add("dmenu-selected");
148 | } else {
149 | item.htmlNode.classList.remove("dmenu-selected");
150 | }
151 |
152 | this.itemListEl.appendChild(item.htmlNode);
153 | }
154 | }
155 |
156 | #matchScore(str, match) {
157 | str = str.toLowerCase();
158 | match = match.toLowerCase();
159 | let score = 0;
160 | let mi = 0;
161 | let i = 0;
162 | let streak = 0;
163 | let streakStartI = 0;
164 | while (true) {
165 | if (i >= str.length || mi >= match.length) {
166 | break;
167 | }
168 | if (str[i] == match[mi]) {
169 | score +=
170 | (streak + 1) *
171 | (streak + 1) *
172 | ((str.length - streakStartI) / str.length);
173 | mi += 1;
174 | streak += 1;
175 | } else {
176 | streakStartI = i;
177 | streak = 0;
178 | }
179 | i++;
180 | }
181 | if (mi < match.length) {
182 | return 0;
183 | }
184 | if (score < 0) {
185 | score = 0;
186 | }
187 | return score;
188 | }
189 |
190 | #mkRow(item, parent) {
191 | let cmd;
192 | let meta = undefined;
193 | if (typeof item == "string") {
194 | cmd = item.toLowerCase();
195 | } else {
196 | cmd = item.value;
197 | meta = item.meta;
198 | }
199 | let row = document.createElement("div");
200 | row.classList.add("dmenu-row");
201 | row.innerHTML =
202 | '';
203 | row.getElementsByClassName("dmenu-content")[0].innerText = cmd;
204 | row.dataset.content = cmd;
205 | if (meta != undefined) {
206 | row.getElementsByClassName("dmenu-meta")[0].innerText = meta;
207 | }
208 |
209 | if (dmenuConfig.itemScore) {
210 | row.getElementsByClassName("dmenu-score")[0].innerText = "0";
211 | }
212 |
213 | let klass = this;
214 | row.addEventListener("click", function(e) {
215 | klass.#accept(row);
216 | });
217 | parent.appendChild(row);
218 | return row;
219 | }
220 |
221 | #mkDmenu(itemList, title) {
222 | this.menuEl = document.createElement("div");
223 | this.menuEl.classList.add("dmenu");
224 | if (dmenuConfig.centered) {
225 | this.menuEl.classList.add("dmenu-centered");
226 | }
227 |
228 | let top = document.createElement("div");
229 | top.classList.add("dmenu-top");
230 |
231 | let dtitle = document.createElement("label");
232 | dtitle.innerText = title;
233 | dtitle.classList.add("dmenu-title");
234 | top.appendChild(dtitle);
235 |
236 | this.inputEl = document.createElement("input");
237 | this.inputEl.type = "text";
238 | this.inputEl.classList.add("dmenu-input");
239 | let klass = this;
240 | this.inputEl.addEventListener("keydown", (e) => {
241 | klass.#onKeydown(e);
242 | });
243 | this.inputEl.addEventListener("input", (e) => {
244 | klass.#oninput();
245 | });
246 | top.appendChild(this.inputEl);
247 |
248 | this.menuEl.appendChild(top);
249 |
250 | this.itemListEl = document.createElement("div");
251 | this.itemListEl.classList.add("dmenu-itemlist");
252 | this.menuEl.appendChild(this.itemListEl);
253 |
254 | let first = true;
255 | for (let item of itemList) {
256 | let row = this.#mkRow(item, this.itemListEl);
257 | if (first) {
258 | row.classList.add("dmenu-selected");
259 | first = false;
260 | }
261 | }
262 |
263 | document.body.appendChild(this.menuEl);
264 | }
265 | }
266 |
267 | function dmenu(
268 | itemList,
269 | endFunc = undefined,
270 | title = "dmenu:",
271 | opener = undefined,
272 | ) {
273 | if (active_dmenu !== null && active_dmenu.isOpen()) {
274 | active_dmenu.close();
275 | }
276 | let menu = new DMenu(itemList, endFunc, title, opener);
277 | active_dmenu = menu;
278 | return menu;
279 | }
280 |
281 | function createQuickMenuButton() {
282 | const quickMenuButton = document.createElement("button");
283 | quickMenuButton.id = "quick-menu-button";
284 | quickMenuButton.className = "topnav__btn";
285 | quickMenuButton.innerHTML = "Quick";
286 | quickMenuButton.addEventListener("click", function() {
287 | do_qm(quickMenuButton);
288 | });
289 | return quickMenuButton;
290 | }
291 |
292 | document.addEventListener("click", function(e) {
293 | if (active_dmenu == null || !active_dmenu.isOpen()) {
294 | return;
295 | }
296 | if (
297 | !active_dmenu.menuEl.contains(e.target) &&
298 | e.target != active_dmenu.openerEl
299 | ) {
300 | active_dmenu.close();
301 | e.preventDefault();
302 | }
303 | });
304 |
--------------------------------------------------------------------------------
/games/games.js:
--------------------------------------------------------------------------------
1 | const GAME_OPTION_TYPE_SLIDER = 0;
2 |
3 | class GameOption {
4 | name;
5 | title;
6 | type;
7 | min;
8 | max;
9 | def;
10 |
11 | static slider(name, title, min = 0, max = 0, def = 0) {
12 | let go = new GameOption();
13 | go.name = name;
14 | go.title = title;
15 | go.type = GAME_OPTION_TYPE_SLIDER;
16 | go.min = min;
17 | go.def = def;
18 | go.max = max;
19 | return go;
20 | }
21 | }
22 |
23 | class GameBase extends WidgetBase {
24 | canvas;
25 | menu;
26 | score;
27 | playing;
28 | hasPlayedAtLeastOnce;
29 | #hiScore;
30 | #requestStopGame;
31 | #optionValues;
32 | #optionElements = {};
33 | #lastTs;
34 | #ctx;
35 |
36 | #scoreEl;
37 | #buttonEl;
38 |
39 | get category() {
40 | return "games";
41 | }
42 |
43 | constructor() {
44 | document.addEventListener("keydown", async (e) => {
45 | if (e.repeat) {
46 | return;
47 | }
48 | if (this.playing) {
49 | await this.onKeyDown(e);
50 | } else if (this.hasPlayedAtLeastOnce) {
51 | if (e.code === "Space") {
52 | await this.#startGame();
53 | }
54 | }
55 | });
56 | document.addEventListener("keyup", async (e) => {
57 | if (this.playing) {
58 | await this.onKeyUp(e);
59 | }
60 | });
61 | super();
62 | }
63 |
64 | // Start Protected (Use these functions in sub classes)
65 | getOpt(name) {
66 | return this.#optionValues[name];
67 | }
68 |
69 | stopGame() {
70 | this.#requestStopGame = true;
71 | if (this.score > this.#hiScore) {
72 | this.#hiScore = this.score;
73 | }
74 | }
75 | // End protected
76 |
77 | #updateOpt(name, value) {
78 | const displayEl = this.#optionElements[name].display;
79 | const inputEl = this.#optionElements[name].input;
80 | inputEl.value = value;
81 |
82 | let displayValue = Math.round(value / 10);
83 | displayValue /= 10;
84 | if (displayValue == Math.round(displayValue)) {
85 | displayValue += ".0";
86 | }
87 | displayEl.innerText = displayValue + "x";
88 | displayEl.classList.add("game-option-value");
89 |
90 | this.#optionValues[name] = value * 1;
91 | }
92 |
93 | #updateScore() {
94 | this.#scoreEl.innerText = "High Score: " + this.#hiScore;
95 | }
96 |
97 | #draw(ts) {
98 | if (!this.#lastTs) {
99 | this.#lastTs = ts;
100 | }
101 | const deltaTime = ts - this.#lastTs;
102 | this.#lastTs = ts;
103 | this.onGameDraw(this.#ctx, deltaTime);
104 | if (this.#requestStopGame) {
105 | setTimeout(async () => {
106 | this.playing = false;
107 | this.setSetting("score", this.#hiScore);
108 |
109 | this.canvas.style.display = "none";
110 | this.menu.style.display = "flex";
111 | this.#buttonEl.innerText = "Try Again (Space)";
112 | this.hasPlayedAtLeastOnce = true;
113 | this.lastTs = undefined;
114 | }, 500);
115 | return;
116 | }
117 |
118 | if (this.playing) {
119 | requestAnimationFrame((ts) => {
120 | this.#draw(ts);
121 | });
122 | }
123 | }
124 | #tick() {}
125 |
126 | async #startGame() {
127 | this.canvas.style.display = "block";
128 | this.menu.style.display = "none";
129 | this.#requestStopGame = false;
130 | this.#ctx = this.canvas.getContext("2d", { alpha: false });
131 | this.playing = true;
132 | this.score = 0;
133 | await this.onGameStart();
134 |
135 | this.#lastTs = undefined;
136 | window.requestAnimationFrame((ts) => {
137 | this.#draw(ts);
138 | });
139 | }
140 |
141 | defaultSettings() {
142 | return { score: 0, options: [] };
143 | }
144 |
145 | async createContent() {
146 | this.#optionValues = {};
147 |
148 | let div = document.createElement("div");
149 | div.classList.add("game-container");
150 |
151 | this.canvas = document.createElement("canvas");
152 | this.canvas.width = 300;
153 | this.canvas.height = 300;
154 | this.canvas.classList.add("game-canvas");
155 | this.canvas.style.display = "none";
156 | this.canvas.addEventListener("click", async (e) => {
157 | if (this.playing) {
158 | await this.onMouse(e);
159 | }
160 | });
161 | div.appendChild(this.canvas);
162 |
163 | let menuTop = document.createElement("div");
164 | menuTop.classList.add("game-menu-top");
165 | let menuBottom = document.createElement("div");
166 | menuBottom.classList.add("game-menu-bottom");
167 | let menu = document.createElement("div");
168 | menu.classList.add("game-menu");
169 |
170 | let title = document.createElement("h2");
171 | title.classList.add("game-title");
172 | title.innerText = this.title.endsWith("++")
173 | ? this.title
174 | : this.title + "++";
175 | menuTop.appendChild(title);
176 |
177 | this.#scoreEl = document.createElement("span");
178 | this.#scoreEl.classList.add("game-score");
179 | menuTop.appendChild(this.#scoreEl);
180 |
181 | for (let opt of this.options) {
182 | let label = document.createElement("label");
183 | label.classList.add("game-slider-label");
184 | label.innerText = opt.title;
185 | menuBottom.appendChild(label);
186 |
187 | if (opt.type == GAME_OPTION_TYPE_SLIDER) {
188 | let sliderCont = document.createElement("div");
189 | sliderCont.classList.add("game-slide-container");
190 | let slider = document.createElement("input");
191 | slider.type = "range";
192 | slider.min = opt.min;
193 | slider.max = opt.max;
194 | slider.value = 0;
195 | slider.classList.add("game-slider");
196 | sliderCont.appendChild(slider);
197 |
198 | let display = document.createElement("span");
199 | sliderCont.appendChild(display);
200 | this.#optionElements[opt.name] = { display: display, input: slider };
201 |
202 | slider.addEventListener("input", (e) => {
203 | this.#updateOpt(opt.name, e.target.value);
204 | });
205 | slider.addEventListener("change", async (e) => {
206 | await this.setSetting("options", this.#optionValues);
207 | });
208 |
209 | menuBottom.appendChild(sliderCont);
210 | }
211 | }
212 |
213 | this.#buttonEl = document.createElement("button");
214 | this.#buttonEl.classList.add("game-button");
215 | this.#buttonEl.innerText = "Play";
216 | this.#buttonEl.addEventListener("click", async (e) => {
217 | await this.#startGame();
218 | });
219 | menuBottom.appendChild(this.#buttonEl);
220 |
221 | menu.appendChild(menuTop);
222 | menu.appendChild(menuBottom);
223 | this.menu = menu;
224 | div.appendChild(menu);
225 | this.onSettingsChange();
226 | return div;
227 | }
228 |
229 | async onSettingsChange() {
230 | if (this.constructor.name == "SnakeWidget") {
231 | if (window.localStorage.getItem("snakehighscore")) {
232 | this.settings = await migrateSnake();
233 | }
234 | } else if (this.constructor.name == "FlappyWidget") {
235 | if (window.localStorage.getItem("flappyhighscore")) {
236 | this.settings = await migrateFlappy();
237 | }
238 | }
239 | for (let opt of this.options) {
240 | let value = this.settings.options[opt.name];
241 | if (!value) {
242 | value = opt.def;
243 | }
244 | this.#updateOpt(opt.name, value);
245 | }
246 |
247 | this.#hiScore = this.settings.score;
248 | this.#updateScore();
249 | }
250 | async createPreview() {
251 | let div = document.createElement("div");
252 | div.classList.add("game-container");
253 |
254 | let menuTop = document.createElement("div");
255 | menuTop.classList.add("game-menu-top");
256 | let menuBottom = document.createElement("div");
257 | menuBottom.classList.add("game-menu-bottom");
258 | let menu = document.createElement("div");
259 | menu.classList.add("game-menu");
260 |
261 | let title = document.createElement("h2");
262 | title.classList.add("game-title");
263 | title.innerText = this.title.endsWith("++")
264 | ? this.title
265 | : this.title + "++";
266 | menuTop.appendChild(title);
267 |
268 | let buttonEl = document.createElement("button");
269 | buttonEl.classList.add("game-button");
270 | buttonEl.innerText = "Play";
271 |
272 | menuBottom.appendChild(buttonEl);
273 |
274 | menu.appendChild(menuTop);
275 | menu.appendChild(menuBottom);
276 | this.menu = menu;
277 | div.appendChild(menu);
278 |
279 | return div;
280 | }
281 |
282 | // Override us
283 |
284 | // (required)
285 | get title() {}
286 |
287 | async onGameStart() {}
288 | // Called when the game to update (same on all devices)
289 | onGameTick() {}
290 | // Called when the game needs to render a new frame (dt is time since last
291 | // frame)
292 | onGameDraw(ctx, deltaTime) {}
293 | async onKeyDown(e) {}
294 | async onKeyUp(e) {}
295 | async onMouse(e) {}
296 |
297 | get tickSpeed() {
298 | return 60;
299 | }
300 |
301 | get options() {
302 | return [];
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/background-scripts/data/delijn-kleuren.json:
--------------------------------------------------------------------------------
1 | {
2 | "kleuren": [
3 | {
4 | "code": "TU",
5 | "hex": "0099AA",
6 | "links": [
7 | {
8 | "rel": "detail",
9 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/TU"
10 | }
11 | ],
12 | "omschrijving": "Turkoois",
13 | "rgb": {
14 | "blauw": 170,
15 | "groen": 153,
16 | "rood": 0
17 | }
18 | },
19 | {
20 | "code": "RZ",
21 | "hex": "FF88AA",
22 | "links": [
23 | {
24 | "rel": "detail",
25 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/RZ"
26 | }
27 | ],
28 | "omschrijving": "Roze",
29 | "rgb": {
30 | "blauw": 170,
31 | "groen": 136,
32 | "rood": 255
33 | }
34 | },
35 | {
36 | "code": "OR",
37 | "hex": "EE8822",
38 | "links": [
39 | {
40 | "rel": "detail",
41 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/OR"
42 | }
43 | ],
44 | "omschrijving": "Oranje",
45 | "rgb": {
46 | "blauw": 34,
47 | "groen": 136,
48 | "rood": 238
49 | }
50 | },
51 | {
52 | "code": "RO",
53 | "hex": "BB0022",
54 | "links": [
55 | {
56 | "rel": "detail",
57 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/RO"
58 | }
59 | ],
60 | "omschrijving": "Rood",
61 | "rgb": {
62 | "blauw": 34,
63 | "groen": 0,
64 | "rood": 187
65 | }
66 | },
67 | {
68 | "code": "PA",
69 | "hex": "991199",
70 | "links": [
71 | {
72 | "rel": "detail",
73 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/PA"
74 | }
75 | ],
76 | "omschrijving": "Paars",
77 | "rgb": {
78 | "blauw": 153,
79 | "groen": 17,
80 | "rood": 153
81 | }
82 | },
83 | {
84 | "code": "MA",
85 | "hex": "DD0077",
86 | "links": [
87 | {
88 | "rel": "detail",
89 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/MA"
90 | }
91 | ],
92 | "omschrijving": "Magenta",
93 | "rgb": {
94 | "blauw": 119,
95 | "groen": 0,
96 | "rood": 221
97 | }
98 | },
99 | {
100 | "code": "LB",
101 | "hex": "AACCEE",
102 | "links": [
103 | {
104 | "rel": "detail",
105 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/LB"
106 | }
107 | ],
108 | "omschrijving": "Lichtblauw",
109 | "rgb": {
110 | "blauw": 238,
111 | "groen": 204,
112 | "rood": 170
113 | }
114 | },
115 | {
116 | "code": "GE",
117 | "hex": "FFCC11",
118 | "links": [
119 | {
120 | "rel": "detail",
121 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/GE"
122 | }
123 | ],
124 | "omschrijving": "Geel",
125 | "rgb": {
126 | "blauw": 17,
127 | "groen": 204,
128 | "rood": 255
129 | }
130 | },
131 | {
132 | "code": "GR",
133 | "hex": "229922",
134 | "links": [
135 | {
136 | "rel": "detail",
137 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/GR"
138 | }
139 | ],
140 | "omschrijving": "Groen",
141 | "rgb": {
142 | "blauw": 34,
143 | "groen": 153,
144 | "rood": 34
145 | }
146 | },
147 | {
148 | "code": "MU",
149 | "hex": "77CCAA",
150 | "links": [
151 | {
152 | "rel": "detail",
153 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/MU"
154 | }
155 | ],
156 | "omschrijving": "Munt",
157 | "rgb": {
158 | "blauw": 170,
159 | "groen": 204,
160 | "rood": 119
161 | }
162 | },
163 | {
164 | "code": "KA",
165 | "hex": "995511",
166 | "links": [
167 | {
168 | "rel": "detail",
169 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/KA"
170 | }
171 | ],
172 | "omschrijving": "Kastanje",
173 | "rgb": {
174 | "blauw": 17,
175 | "groen": 85,
176 | "rood": 153
177 | }
178 | },
179 | {
180 | "code": "BL",
181 | "hex": "1199DD",
182 | "links": [
183 | {
184 | "rel": "detail",
185 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/BL"
186 | }
187 | ],
188 | "omschrijving": "Blauw",
189 | "rgb": {
190 | "blauw": 221,
191 | "groen": 153,
192 | "rood": 17
193 | }
194 | },
195 | {
196 | "code": "ZA",
197 | "hex": "FFCCAA",
198 | "links": [
199 | {
200 | "rel": "detail",
201 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/ZA"
202 | }
203 | ],
204 | "omschrijving": "Zalm",
205 | "rgb": {
206 | "blauw": 170,
207 | "groen": 204,
208 | "rood": 255
209 | }
210 | },
211 | {
212 | "code": "BO",
213 | "hex": "771133",
214 | "links": [
215 | {
216 | "rel": "detail",
217 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/BO"
218 | }
219 | ],
220 | "omschrijving": "Bordeaux",
221 | "rgb": {
222 | "blauw": 51,
223 | "groen": 17,
224 | "rood": 119
225 | }
226 | },
227 | {
228 | "code": "KI",
229 | "hex": "444411",
230 | "links": [
231 | {
232 | "rel": "detail",
233 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/KI"
234 | }
235 | ],
236 | "omschrijving": "Kaki",
237 | "rgb": {
238 | "blauw": 17,
239 | "groen": 68,
240 | "rood": 68
241 | }
242 | },
243 | {
244 | "code": "DB",
245 | "hex": "0044BB",
246 | "links": [
247 | {
248 | "rel": "detail",
249 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/DB"
250 | }
251 | ],
252 | "omschrijving": "Donkerblauw",
253 | "rgb": {
254 | "blauw": 187,
255 | "groen": 68,
256 | "rood": 0
257 | }
258 | },
259 | {
260 | "code": "LG",
261 | "hex": "BBDD00",
262 | "links": [
263 | {
264 | "rel": "detail",
265 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/LG"
266 | }
267 | ],
268 | "omschrijving": "Lichtgroen",
269 | "rgb": {
270 | "blauw": 0,
271 | "groen": 221,
272 | "rood": 187
273 | }
274 | },
275 | {
276 | "code": "PE",
277 | "hex": "005555",
278 | "links": [
279 | {
280 | "rel": "detail",
281 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/PE"
282 | }
283 | ],
284 | "omschrijving": "Petrol",
285 | "rgb": {
286 | "blauw": 85,
287 | "groen": 85,
288 | "rood": 0
289 | }
290 | },
291 | {
292 | "code": "ST",
293 | "hex": "8899AA",
294 | "links": [
295 | {
296 | "rel": "detail",
297 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/ST"
298 | }
299 | ],
300 | "omschrijving": "Staal",
301 | "rgb": {
302 | "blauw": 170,
303 | "groen": 153,
304 | "rood": 136
305 | }
306 | },
307 | {
308 | "code": "WI",
309 | "hex": "FFFFFF",
310 | "links": [
311 | {
312 | "rel": "detail",
313 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/WI"
314 | }
315 | ],
316 | "omschrijving": "Wit",
317 | "rgb": {
318 | "blauw": 255,
319 | "groen": 255,
320 | "rood": 255
321 | }
322 | },
323 | {
324 | "code": "GD",
325 | "hex": "FFDD00",
326 | "links": [
327 | {
328 | "rel": "detail",
329 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/GD"
330 | }
331 | ],
332 | "omschrijving": "GeelDeLijn",
333 | "rgb": {
334 | "blauw": 0,
335 | "groen": 221,
336 | "rood": 255
337 | }
338 | },
339 | {
340 | "code": "ZW",
341 | "hex": "000000",
342 | "links": [
343 | {
344 | "rel": "detail",
345 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/ZW"
346 | }
347 | ],
348 | "omschrijving": "Zwart",
349 | "rgb": {
350 | "blauw": 0,
351 | "groen": 0,
352 | "rood": 0
353 | }
354 | },
355 | {
356 | "code": "CR",
357 | "hex": "C5AA77",
358 | "links": [
359 | {
360 | "rel": "detail",
361 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/CR"
362 | }
363 | ],
364 | "omschrijving": "Crème",
365 | "rgb": {
366 | "blauw": 119,
367 | "groen": 170,
368 | "rood": 197
369 | }
370 | },
371 | {
372 | "code": "BD",
373 | "hex": "000099",
374 | "links": [
375 | {
376 | "rel": "detail",
377 | "url": "https://api.delijn.be/DLKernOpenData/api/v1/kleuren/BD"
378 | }
379 | ],
380 | "omschrijving": "BlauwDeLijn",
381 | "rgb": {
382 | "blauw": 153,
383 | "groen": 0,
384 | "rood": 0
385 | }
386 | }
387 | ]
388 | }
389 |
--------------------------------------------------------------------------------