├── .github ├── FUNDING.yml └── workflows │ ├── dev.yml │ └── main.yml ├── icons ├── icon@128px.png ├── icon@16px.png ├── icon@256px.png ├── icon@300px.png ├── icon@32px.png ├── icon@48px.png ├── icon@512px.png └── icon@64px.png ├── src ├── package.json ├── jsconfig.json ├── scripts │ ├── theme-store.js │ ├── microsoft-login.js │ ├── login.js │ ├── books.js │ └── grades │ │ ├── list.js │ │ └── backup.js ├── package-lock.json ├── globals.d.ts ├── styles │ ├── studyguide.css │ ├── gamification.css │ └── today │ │ └── today.css ├── background.js └── service-worker.js ├── popup ├── src │ ├── assets │ │ ├── logo.png │ │ ├── fa-regular-400.ttf │ │ ├── decorations │ │ │ ├── lego.png │ │ │ ├── waves.png │ │ │ ├── stripes.png │ │ │ ├── zig-zag.png │ │ │ └── polka-dot.png │ │ └── variables.css │ ├── main.js │ ├── stores │ │ └── store.js │ ├── components │ │ ├── Icon.vue │ │ ├── setting-types │ │ │ ├── ColorSetting.vue │ │ │ ├── LinkToOptionsTab.vue │ │ │ ├── ColorOverrideSetting.vue │ │ │ ├── Text.vue │ │ │ ├── Slider.vue │ │ │ └── SingleChoice.vue │ │ ├── sheets │ │ │ └── ImageUrlSheet.vue │ │ ├── Chip.vue │ │ ├── TopAppBar.vue │ │ ├── ThemeColors.vue │ │ ├── Dialog.vue │ │ ├── inputs │ │ │ ├── TextInput.vue │ │ │ ├── SegmentedButton.vue │ │ │ └── ColorPicker.vue │ │ ├── NavigationBar.vue │ │ ├── MagisterThemePreview.vue │ │ ├── DialogFullscreen.vue │ │ ├── InputText.vue │ │ ├── SwitchInput.vue │ │ ├── BottomSheet.vue │ │ ├── IconInput.vue │ │ ├── ThemePresets.vue │ │ ├── NavigationRail.vue │ │ ├── ShortcutsEditor.vue │ │ ├── ImageInput.vue │ │ ├── About.vue │ │ ├── CustomCssEditor.vue │ │ └── ThemePreviewImage.vue │ └── composables │ │ └── chrome.js ├── dist │ ├── assets │ │ ├── waves-BpDb7oY5.js │ │ ├── stripes-CrSqOAWt.js │ │ ├── waves-BYx-PK3B.png │ │ ├── polka-dot-Bk_kYWko.js │ │ ├── stripes-BhwmhtOl.png │ │ ├── polka-dot-vnfSTs_N.png │ │ ├── fa-regular-400-BMFokQJ2.ttf │ │ ├── zig-zag-BPQTWn5Y.js │ │ └── lego-B9XBXV1o.js │ └── index.html ├── jsconfig.json ├── index.html ├── vite.config.js └── package.json ├── _locales ├── en │ └── messages.json ├── de │ └── messages.json ├── fr │ └── messages.json └── nl │ └── messages.json ├── .gitignore ├── LICENSE ├── README.md ├── updates.json ├── manifest.json └── manifest-firefox.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["paypal.me/QkeleQ10"] 2 | -------------------------------------------------------------------------------- /icons/icon@128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@128px.png -------------------------------------------------------------------------------- /icons/icon@16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@16px.png -------------------------------------------------------------------------------- /icons/icon@256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@256px.png -------------------------------------------------------------------------------- /icons/icon@300px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@300px.png -------------------------------------------------------------------------------- /icons/icon@32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@32px.png -------------------------------------------------------------------------------- /icons/icon@48px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@48px.png -------------------------------------------------------------------------------- /icons/icon@512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@512px.png -------------------------------------------------------------------------------- /icons/icon@64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/icons/icon@64px.png -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/chrome": "^0.0.326" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /popup/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/logo.png -------------------------------------------------------------------------------- /popup/dist/assets/waves-BpDb7oY5.js: -------------------------------------------------------------------------------- 1 | const e=""+new URL("waves-BYx-PK3B.png",import.meta.url).href;export{e as default}; 2 | -------------------------------------------------------------------------------- /popup/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /popup/dist/assets/stripes-CrSqOAWt.js: -------------------------------------------------------------------------------- 1 | const t=""+new URL("stripes-BhwmhtOl.png",import.meta.url).href;export{t as default}; 2 | -------------------------------------------------------------------------------- /popup/dist/assets/waves-BYx-PK3B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/dist/assets/waves-BYx-PK3B.png -------------------------------------------------------------------------------- /popup/src/assets/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/fa-regular-400.ttf -------------------------------------------------------------------------------- /popup/dist/assets/polka-dot-Bk_kYWko.js: -------------------------------------------------------------------------------- 1 | const o=""+new URL("polka-dot-vnfSTs_N.png",import.meta.url).href;export{o as default}; 2 | -------------------------------------------------------------------------------- /popup/dist/assets/stripes-BhwmhtOl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/dist/assets/stripes-BhwmhtOl.png -------------------------------------------------------------------------------- /popup/src/assets/decorations/lego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/decorations/lego.png -------------------------------------------------------------------------------- /popup/src/assets/decorations/waves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/decorations/waves.png -------------------------------------------------------------------------------- /popup/src/stores/store.js: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | 3 | export const store = reactive({ 4 | currentlyHovered: '' 5 | }) -------------------------------------------------------------------------------- /popup/dist/assets/polka-dot-vnfSTs_N.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/dist/assets/polka-dot-vnfSTs_N.png -------------------------------------------------------------------------------- /popup/src/assets/decorations/stripes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/decorations/stripes.png -------------------------------------------------------------------------------- /popup/src/assets/decorations/zig-zag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/decorations/zig-zag.png -------------------------------------------------------------------------------- /popup/src/assets/decorations/polka-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/src/assets/decorations/polka-dot.png -------------------------------------------------------------------------------- /popup/dist/assets/fa-regular-400-BMFokQJ2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QkeleQ10/Study-Tools/HEAD/popup/dist/assets/fa-regular-400-BMFokQJ2.ttf -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Study Tools for Magister" 4 | }, 5 | "appDesc": { 6 | "message": "An extension that improves various aspects of and solves many issues in Magister." 7 | } 8 | } -------------------------------------------------------------------------------- /_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Study Tools für Magister" 4 | }, 5 | "appDesc": { 6 | "message": "Eine Erweiterung, die verschiedene Aspekte verbessert und viele Probleme in Magister löst." 7 | } 8 | } -------------------------------------------------------------------------------- /_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Study Tools pour Magister" 4 | }, 5 | "appDesc": { 6 | "message": "Une extension qui améliore divers aspects et résout de nombreux problèmes dans Magister." 7 | } 8 | } -------------------------------------------------------------------------------- /_locales/nl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "appName": { 3 | "message": "Study Tools voor Magister" 4 | }, 5 | "appDesc": { 6 | "message": "Een extensie die verschillende aspecten van Magister verbetert en problemen ermee oplost." 7 | } 8 | } -------------------------------------------------------------------------------- /src/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "checkJs": true, 5 | "types": [ 6 | "chrome" 7 | ] 8 | }, 9 | "include": [ 10 | "./**/*.js", 11 | "./globals.d.ts" 12 | ] 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /src/scripts/theme-store.js: -------------------------------------------------------------------------------- 1 | popstate() 2 | window.addEventListener('popstate', popstate) 3 | async function popstate() { 4 | // Only run on the theme store 5 | if (! await awaitElement('meta#theme-store-st')) return 6 | // Provide the page with this extension's ID 7 | element('meta', `st-${chrome.runtime.id}`, document.head) 8 | } -------------------------------------------------------------------------------- /popup/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Study Tools-configuratiepaneel 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /popup/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | import path from 'path'; 4 | import Components from 'unplugin-vue-components/vite'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | Components({}), 11 | ], 12 | base: '', 13 | resolve: { 14 | alias: { 15 | '@': path.resolve(__dirname, './src') 16 | }, 17 | extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'] 18 | } 19 | }); -------------------------------------------------------------------------------- /popup/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Study Tools-configuratiepaneel 9 | 10 | 11 | 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /popup/src/components/Icon.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /popup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "study-tools-popup", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "host": "vite --host", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vueuse/core": "^12.7.0", 13 | "vue": "^3.5.22", 14 | "vue-slider-component": "^4.1.0-beta.7" 15 | }, 16 | "devDependencies": { 17 | "@vitejs/plugin-vue": "^5.2.1", 18 | "unplugin-vue-components": "^28.4.0", 19 | "vite": "^6.4.1" 20 | }, 21 | "browserslist": [ 22 | "last 2 Chrome versions", 23 | "last 2 Firefox versions", 24 | "last 2 Edge versions" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Create beta release 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Indicate beta version 17 | run: | 18 | sed -i '3s/.*/"message": "Study Tools voor Magister BETA"/' _locales/nl/messages.json 19 | sed -i '3s/.*/"message": "Study Tools for Magister BETA"/' _locales/en/messages.json 20 | 21 | - name: Upload as artifacts 22 | uses: actions/upload-artifact@v4 23 | with: 24 | name: Beta release 25 | path: | 26 | _locales 27 | icons 28 | popup/dist 29 | src/scripts 30 | src/strings 31 | src/styles 32 | src/service-worker.js 33 | manifest.json 34 | updates.json 35 | -------------------------------------------------------------------------------- /src/scripts/microsoft-login.js: -------------------------------------------------------------------------------- 1 | init() 2 | 3 | async function magisterLogin() { 4 | const forceLogoutTimestamp = await getFromStorage('force-logout', 'local') 5 | 6 | if (!syncedStorage['magisterLogin-enabled'] || !syncedStorage['magisterLogin-email'] || (forceLogoutTimestamp && Math.abs(new Date().getTime() - forceLogoutTimestamp) <= 30000)) return 7 | 8 | let signInButton = await awaitElement(`div.table[data-test-id="${syncedStorage['magisterLogin-email']}"]`) 9 | if (signInButton) signInButton.click() 10 | } 11 | 12 | // Run when the extension and page are loaded 13 | async function init() { 14 | popstate() 15 | 16 | window.addEventListener('popstate', popstate) 17 | window.addEventListener('hashchange', popstate) 18 | window.addEventListener('locationchange', popstate) 19 | } 20 | 21 | // Run when the URL changes 22 | async function popstate() { 23 | const href = document.location.href.split('?')[0] 24 | 25 | if (document.location.href.includes('accounts.magister.net')) magisterLogin() 26 | } -------------------------------------------------------------------------------- /popup/src/components/setting-types/ColorSetting.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 24 | 40 | 41 | -------------------------------------------------------------------------------- /popup/src/components/setting-types/LinkToOptionsTab.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Quinten Althues 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /popup/src/components/sheets/ImageUrlSheet.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Create releases 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Upload Chromium build 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: Chromium release 20 | path: | 21 | _locales 22 | icons 23 | popup/dist 24 | src/scripts 25 | src/strings 26 | src/styles 27 | src/service-worker.js 28 | manifest.json 29 | updates.json 30 | 31 | - name: Convert manifest for Firefox 32 | run: | 33 | mv manifest.json manifest-chromium.json 34 | mv manifest-firefox.json manifest.json 35 | 36 | - name: Upload Firefox build 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: Firefox release 40 | path: | 41 | _locales 42 | icons 43 | popup/dist 44 | src/scripts 45 | src/strings 46 | src/styles 47 | src/background.js 48 | manifest.json 49 | updates.json 50 | 51 | - name: Restore Chromium manifest 52 | run: | 53 | mv manifest.json manifest-firefox.json 54 | mv manifest-chromium.json manifest.json -------------------------------------------------------------------------------- /popup/src/components/Chip.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /popup/src/components/TopAppBar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 36 | 37 | -------------------------------------------------------------------------------- /src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "@types/chrome": "^0.0.326" 9 | } 10 | }, 11 | "node_modules/@types/chrome": { 12 | "version": "0.0.326", 13 | "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.326.tgz", 14 | "integrity": "sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw==", 15 | "dev": true, 16 | "license": "MIT", 17 | "dependencies": { 18 | "@types/filesystem": "*", 19 | "@types/har-format": "*" 20 | } 21 | }, 22 | "node_modules/@types/filesystem": { 23 | "version": "0.0.36", 24 | "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", 25 | "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", 26 | "dev": true, 27 | "license": "MIT", 28 | "dependencies": { 29 | "@types/filewriter": "*" 30 | } 31 | }, 32 | "node_modules/@types/filewriter": { 33 | "version": "0.0.33", 34 | "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", 35 | "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", 36 | "dev": true, 37 | "license": "MIT" 38 | }, 39 | "node_modules/@types/har-format": { 40 | "version": "1.2.16", 41 | "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", 42 | "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", 43 | "dev": true, 44 | "license": "MIT" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | // globals.d.ts 2 | /// 3 | /// 4 | 5 | // globals.d.ts 6 | interface HTMLElement { 7 | /** 8 | * Creates a child element under this HTMLElement. 9 | * @template {keyof HTMLElementTagNameMap} K 10 | * @param {K} tagName - The element's tag name. 11 | * @param {CreateElementAttributes & Record} [attributes] - Attributes to set 12 | * @returns {HTMLElementTagNameMap[K]} 13 | */ 14 | createChildElement( 15 | tagName: K, 16 | attributes?: CreateElementAttributes & Record 17 | ): HTMLElementTagNameMap[K]; 18 | 19 | /** 20 | * Creates a sibling element under the parent of this element. 21 | * @template {keyof HTMLElementTagNameMap} K 22 | * @param {K} tagName - The element's tag name. 23 | * @param {CreateElementAttributes & Record} [attributes] - Attributes to set on the sibling. 24 | * @returns {HTMLElementTagNameMap[K]} 25 | */ 26 | createSiblingElement( 27 | tagName: K, 28 | attributes?: CreateElementAttributes & Record 29 | ): HTMLElementTagNameMap[K]; 30 | 31 | /** 32 | * Sets multiple attributes, properties, styles, classes, etc. on this element. 33 | * @param {CreateElementAttributes & Record} attributes - An object of attributes and properties. 34 | * @returns {void} 35 | */ 36 | setAttributes(attributes: CreateElementAttributes & Record): void; 37 | } 38 | 39 | interface Date { 40 | getWeek(): number; 41 | getHoursWithDecimals(): number; 42 | 43 | getFormattedDay(): string; 44 | getFormattedTime(): string; 45 | 46 | addDays(days: number): Date; 47 | 48 | isToday(offset?: number): boolean; 49 | isTomorrow(offset?: number): boolean; 50 | isYesterday(offset?: number): boolean; 51 | } 52 | 53 | interface Array { 54 | random(seed); 55 | mode(); 56 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Study Tools logo 3 |

4 | 5 | # Study Tools voor Magister 6 | 7 | Een extensie die verschillende aspecten van Magister verbetert en problemen ermee oplost. 8 | 9 | ## Functionaliteiten 10 | 11 | - Aangepaste uiterlijk: Instelbaar donker/licht thema, aangepaste accentkleur en speciale decoraties. 12 | - Start: Weg met het rommelige Vandaag, op pagina Start zie je met widgets alles in één oogopslag. 13 | - Automatisch inloggen: Compleet instelbaar op basis van jouw behoeften en je school. 14 | - Studiewijzers: Verschillende weergave-opties voor efficiëntie en overzichtelijkheid. 15 | - Cijfercalculator: Wat moet ik halen en wat kom ik te staan? Inclusief grafiek! 16 | - Statistieken: Vergelijk je cijfers en je rooster op verschillende manieren met vrienden. 17 | - Cijferback-up: Altijd bij je cijfers kunnen, zelfs als je school ze besluit te sluiten. 18 | 19 | En nog veel meer! Ik voeg regelmatig nieuwe functies toe. Feedback wordt op prijs gesteld! 20 | 21 | ## Screenshots 22 | 23 | ![Start](https://github.com/QkeleQ10/Study-Tools/assets/65854503/56dd1a44-e02c-4d31-8c9f-d34afb92da18) 24 | ![Cijfercalculator](https://github.com/QkeleQ10/Study-Tools/assets/65854503/5ab2b1ec-73f7-4864-b3cc-8f232e9a3090) 25 | 26 | 27 | Study Tools beta logo 28 | 29 | ## Bètaversie installeren 30 | 1. Kopieer even je huidige instellingen naar je klembord. Dat kan via het tabblad 'Over' in de pop-up! 31 | 2. Deactiveer de stabiele extensie. 32 | 3. [Installeer de bèta-extensie](https://chromewebstore.google.com/u/1/detail/study-tools-voor-magister/dlmdgkhbbclpolcofdlhlpdpiobmklmd). 33 | 4. Plak je instellingen in de bèta-extensie. Dat kan onder hetzelfde menuutje in de pop-up. 34 | 35 | ## Licentie 36 | 37 | Het project is verkrijgbaar onder de MIT license. 38 | -------------------------------------------------------------------------------- /src/scripts/login.js: -------------------------------------------------------------------------------- 1 | login(); 2 | 3 | async function login() { 4 | chrome.runtime.sendMessage({ action: 'popstateDetected' }); 5 | 6 | const footerNotice = element('div', 'bottom-st', null, { 7 | innerHTML: "\xa0|\xa0Autom. inloggen aan", 8 | title: "Meer informatie over automatisch inloggen", 9 | style: "cursor: pointer;", 10 | }); 11 | document.querySelector('footer>.bottom-company-logo, footer>*:last-child')?.before(footerNotice); 12 | 13 | let autoLoginDisclaimer = "Study Tools is actief en er wordt een poging gedaan om automatisch in te loggen. \n\nControleer je instellingen als het inloggen niet slaagt." 14 | footerNotice.addEventListener('click', () => notify('dialog', autoLoginDisclaimer, [{ 15 | innerText: "Instellingen", 16 | primary: true, 17 | onclick: () => chrome.runtime.sendMessage({ action: 'openOptions', data: 'tab=login' }), 18 | }])); 19 | 20 | if (!syncedStorage['magisterLogin-enabled'] || !syncedStorage['magisterLogin-username']) { 21 | footerNotice.innerHTML = "\xa0|\xa0Autom. inloggen uit"; 22 | autoLoginDisclaimer = "Study Tools is actief, maar automatisch inloggen is nog niet ingesteld. \n\nGebruik onderstaande knop om de instellingen te openen."; 23 | return; 24 | } 25 | 26 | const forceLogoutTimestamp = await getFromStorage('force-logout', 'local') 27 | if (forceLogoutTimestamp && Math.abs(new Date().getTime() - forceLogoutTimestamp) <= 30000) { 28 | footerNotice.innerHTML = "\xa0|\xa0Autom. inloggen uit"; 29 | autoLoginDisclaimer = "Study Tools is actief, maar automatisch inloggen is tijdelijk gepauzeerd omdat je handmatig hebt uitgelogd. \n\nDe volgende keer zal er weer automatisch worden ingelogd."; 30 | return; 31 | } 32 | 33 | const usernameInput = await awaitElement('#username'); 34 | usernameInput.value = syncedStorage['magisterLogin-username']; 35 | usernameInput.dispatchEvent(new Event('input')); 36 | 37 | const usernameSubmit = await awaitElement('#username_submit'); 38 | usernameSubmit.click(); 39 | } -------------------------------------------------------------------------------- /popup/src/components/ThemeColors.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 49 | 50 | -------------------------------------------------------------------------------- /popup/src/components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | 31 | -------------------------------------------------------------------------------- /popup/src/components/inputs/TextInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 25 | 26 | -------------------------------------------------------------------------------- /updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "3.12.0": "Nieuwe widget- en thema-editor.", 3 | "3.11.0": "Magister Wrapped reimagined", 4 | "3.10.0": "Geavanceerde thema-instellingen!", 5 | "3.9.0": "Studiewijzers vernieuwd, met nieuwe manier van rangschikken", 6 | "3.8.0": "Achtergrondafbeeldingen, betere gebruiksvriendelijkheid en aanbevelingen.", 7 | "3.7.0": "Widgets in tegel- en lijstvorm, hernoembare leermiddelen en een nieuwe themakiezer.", 8 | "3.6.0": "Magister Wrapped is er! Beter laat dan nooit?", 9 | "3.5.0": "Cijferstatistieken hebben een make-over gekregen!", 10 | "3.4.0": "De cijfercalculator heeft een make-over gekregen!", 11 | "3.3.5": "Neem eens een kijkje in de nieuwe optie- en statistiekschermen op pagina Start!", 12 | "3.3.0": "Pagina Start is wederom helemaal verbeterd. Je wordt niet meer gefeliciteerd wanneer het je verjaardag helemaal niet is.", 13 | "3.2.0": "Scherm 'Vandaag' is nu 'Start', dit keer met aanpasbare widgets. Ook véél andere wijzigingen!", 14 | "3.1.0": "Snelkoppelingen in de zijbalk zijn terug! Veel verbeteringen voor alle onderdelen.", 15 | "3.0.3": "Studiewijzers vernieuwd, nu met zoekbalk en verbergen (bedankt voor de suggesties!)", 16 | "3.0.0": "Configuratiepaneel vanaf nul opnieuw opgebouwd. Kleurinstellingen zijn zonder vernieuwen zichtbaar in Magister.", 17 | "2.6.6": "Keuze tussen foutloze en snelle back-ups.", 18 | "2.6.5": "Je kunt nu een aangepaste profielfoto uploaden (dankjewel voor de suggestie, Jonas van Leeuwen!).", 19 | "2.6.4": "Voorbereidingen voor toetsweek (cijferback-up en aankondigingen).", 20 | "2.6.2": "Veel kleine verbeteringen. Verbeterde ondersteuning voor andere scholen.", 21 | "2.6.1": "Hotfix voor een aantal instellingen en bestandsmappen in Studiewijzers.", 22 | "2.6.0": "Gamificatie en notities zijn vanaf nu in bèta. Er zijn ook heel veel uiterlijkverbeteringen.", 23 | "2.5.2": "Sneltoetsen! Houd 'S' ingedrukt en kies een nummer.", 24 | "2.5.1": "Bug fixes, stijlverbeteringen en meer begroetingen. Overlappende agenda-items gefikst.", 25 | "2.5.0": "Cijferstatistieken toegevoegd aan het cijferoverzicht en verscheidene updates aan uiterlijk en stabiliteit.", 26 | "2.4.1": "Verbeteringen aan stabiliteit en uiterlijk.", 27 | "2.4.0": "Je cijferoverzicht kan nu worden geback-upt! Ook is het configuratiepaneel helemaal opnieuw gebouwd.", 28 | "2.3.2": "Updates aan onder andere het configuratiepaneel en stabiliteit.", 29 | "2.3.1": "Cijfercalculator overzichtelijker, rooster 'Vandaag' updatet nu live, veel kleine aanpassingen.", 30 | "2.3.0": "Configuratiepaneel heeft nu twee delen en is overzichtelijker, het scala aan opties uitgebreid.", 31 | "2.2.2": "Verbeterde arcering van huidige afspraak, update-meldingen, problemen opgelost.", 32 | "2.2.1": "Problemen opgelost, stabiliteit verbeterd.", 33 | "2.2.0": "Optie om pagina 'Vandaag' een make-over te geven, cijfercalculator, HEEL VEEL kleinere aanpassingen." 34 | } 35 | -------------------------------------------------------------------------------- /popup/src/components/setting-types/ColorOverrideSetting.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 53 | 54 | -------------------------------------------------------------------------------- /popup/src/components/NavigationBar.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 64 | 65 | -------------------------------------------------------------------------------- /popup/src/components/MagisterThemePreview.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /popup/src/components/DialogFullscreen.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | 37 | -------------------------------------------------------------------------------- /popup/src/components/InputText.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 43 | 44 | -------------------------------------------------------------------------------- /src/scripts/books.js: -------------------------------------------------------------------------------- 1 | // Run at start and when the URL changes 2 | popstate() 3 | window.addEventListener('popstate', popstate) 4 | async function popstate() { 5 | if (document.location.href.split('?')[0].includes('/leermiddelen')) booksList() 6 | } 7 | 8 | async function booksList() { 9 | let bookNames = syncedStorage['books'] || {} 10 | 11 | const bookEntries = await awaitElement('#leermiddelen-container tr[data-ng-repeat="leermiddel in items"]', true) 12 | 13 | for (const bookEntry of bookEntries) { 14 | const ean = bookEntry.querySelector('td[data-ng-bind="leermiddel.EAN"]').innerText; 15 | const titleCell = bookEntry.querySelector('td>a[data-ng-bind="leermiddel.Titel"]'); 16 | const originalTitle = `${titleCell.innerText}`; 17 | 18 | titleCell.title = originalTitle; 19 | 20 | if (bookNames[ean]?.length > 1) titleCell.innerText = bookNames[ean]; 21 | 22 | titleCell.parentElement.createChildElement('button', { 23 | class: 'st-button icon', 24 | 'data-icon': '', 25 | title: i18n('renameX', { item: originalTitle }), 26 | style: 'position: absolute; top: 50%; right: 4px; translate: 0 -50%; opacity: .5;' 27 | }) 28 | .addEventListener('click', () => { 29 | const dialog = new Dialog({ closeText: i18n('cancel') }) 30 | dialog.body.createChildElement('h3', { 31 | class: 'st-section-heading', 32 | innerText: i18n('renameX', { item: originalTitle }) 33 | }); 34 | dialog.body.createChildElement('input', { 35 | class: 'st-input', 36 | type: 'text', 37 | value: bookNames[ean] || '', 38 | placeholder: originalTitle, 39 | style: 'width: 100%; box-sizing: border-box; margin-top: 8px; padding: 4px;' 40 | }) 41 | dialog.buttonsWrapper.createChildElement('button', { innerText: i18n('save'), class: 'st-button primary', 'data-icon': '' }).addEventListener('click', () => { 42 | const input = dialog.body.querySelector('input'); 43 | const result = input.value.trim(); 44 | dialog.close(); 45 | if (result?.length) { 46 | bookNames[ean] = result; 47 | titleCell.innerText = bookNames[ean]; 48 | saveToStorage('books', bookNames); 49 | } else { 50 | delete bookNames[ean]; 51 | titleCell.innerText = originalTitle; 52 | saveToStorage('books', bookNames); 53 | } 54 | sortBookEntries(); 55 | }); 56 | dialog.show(); 57 | }) 58 | } 59 | 60 | sortBookEntries(); 61 | 62 | function sortBookEntries() { 63 | const container = bookEntries[0].parentElement; 64 | const entriesArray = Array.from(bookEntries); 65 | entriesArray.sort((a, b) => { 66 | const titleA = a.querySelector('td>a[data-ng-bind="leermiddel.Titel"]').innerText.toLowerCase(); 67 | const titleB = b.querySelector('td>a[data-ng-bind="leermiddel.Titel"]').innerText.toLowerCase(); 68 | return titleA.localeCompare(titleB); 69 | }); 70 | entriesArray.forEach(entry => container.appendChild(entry)); 71 | } 72 | } -------------------------------------------------------------------------------- /popup/src/components/setting-types/Text.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 40 | 41 | -------------------------------------------------------------------------------- /popup/src/components/setting-types/Slider.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 61 | 62 | -------------------------------------------------------------------------------- /popup/src/components/SwitchInput.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 44 | 45 | -------------------------------------------------------------------------------- /popup/src/components/BottomSheet.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | 44 | -------------------------------------------------------------------------------- /popup/src/components/IconInput.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 68 | 69 | -------------------------------------------------------------------------------- /popup/src/components/ThemePresets.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 60 | 61 | -------------------------------------------------------------------------------- /popup/src/components/NavigationRail.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 75 | 76 | -------------------------------------------------------------------------------- /popup/src/composables/chrome.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { ref, onMounted, watchEffect, isProxy, toRaw } from 'vue' 3 | import { useDebounceFn } from '@vueuse/core' 4 | 5 | import settings from '../../public/settings.js' 6 | 7 | const browser = window.browser || window.chrome 8 | 9 | export function useSyncedStorage() { 10 | let syncedStorage = ref({}) 11 | 12 | onMounted(() => { 13 | if (browser?.storage?.sync) { 14 | browser.storage.sync.get() 15 | .then(value => { 16 | syncedStorage.value = value 17 | 18 | // Set all undefined settings to their default values 19 | settings.forEach(category => { 20 | category.settings.forEach(setting => { 21 | if (typeof syncedStorage.value[setting.id] === 'undefined') { 22 | syncedStorage.value[setting.id] = setting.default 23 | } 24 | }) 25 | }) 26 | 27 | browser.storage.sync.onChanged.addListener(changes => { 28 | for (let key in changes) { 29 | if (syncedStorage.value[key] !== changes[key].newValue) 30 | syncedStorage.value[key] = changes[key].newValue 31 | } 32 | }) 33 | }) 34 | 35 | // Store the current version number 36 | syncedStorage.value['v'] = browser?.runtime?.getManifest()?.version 37 | } 38 | }) 39 | 40 | const debouncedFn = useDebounceFn(() => { 41 | if (browser?.storage?.sync) { 42 | let toStore = { ...syncedStorage.value } 43 | if (isProxy(toStore)) toStore = toRaw(toStore) 44 | browser.storage.sync.set(toStore) 45 | } 46 | }, 250, { maxWait: 2000 }) 47 | 48 | const updateTheme = () => { 49 | const themeFixed = syncedStorage.value['ptheme']?.split(',') 50 | const themeAuto = themeFixed?.[0] === 'auto' 51 | let currentTheme = themeFixed 52 | 53 | if (themeAuto && window.matchMedia?.('(prefers-color-scheme: dark)').matches) { currentTheme[0] = 'dark' } 54 | else if (themeAuto) currentTheme[0] = 'light' 55 | 56 | document.documentElement.setAttribute('theme', (currentTheme?.[0] || 'light')) 57 | document.documentElement.style.setProperty('--palette-primary-hue', (currentTheme?.[1] || 207)) 58 | document.documentElement.style.setProperty('--palette-primary-saturation', `${currentTheme?.[2] || 95}%`) 59 | document.documentElement.style.setProperty('--palette-primary-luminance', `${currentTheme?.[3] || 55}%`) 60 | } 61 | 62 | watchEffect(() => { 63 | let toStore = { ...syncedStorage.value } 64 | debouncedFn() 65 | updateTheme() 66 | }) 67 | 68 | return syncedStorage 69 | } 70 | 71 | export function useLocalStorage() { 72 | let localStorage = ref({}) 73 | 74 | onMounted(() => { 75 | if (browser?.storage?.local) { 76 | browser.storage.local.get() 77 | .then(value => { 78 | localStorage.value = value 79 | }) 80 | 81 | browser.storage.local.onChanged.addListener(changes => { 82 | for (let key in changes) { 83 | if (localStorage.value[key] !== changes[key].newValue) 84 | localStorage.value[key] = changes[key].newValue 85 | } 86 | }) 87 | } 88 | }) 89 | 90 | watchEffect(() => { 91 | let toStore = { ...localStorage.value } 92 | if (isProxy(toStore)) toStore = toRaw(toStore) 93 | if (browser?.storage) browser.storage.local.set(toStore) 94 | }) 95 | 96 | return localStorage 97 | } 98 | 99 | export function useManifest() { 100 | let manifest = ref({}) 101 | 102 | onMounted(() => { 103 | if (browser?.runtime?.getManifest) 104 | manifest.value = browser.runtime.getManifest() 105 | }) 106 | 107 | return { manifest } 108 | } 109 | 110 | export function useExtension() { 111 | let extension = ref({}) 112 | 113 | onMounted(() => { 114 | extension.value = browser?.extension 115 | }) 116 | 117 | return { extension } 118 | } -------------------------------------------------------------------------------- /popup/dist/assets/zig-zag-BPQTWn5Y.js: -------------------------------------------------------------------------------- 1 | const e="data:image/png;base64,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";export{e as default}; 2 | -------------------------------------------------------------------------------- /popup/src/components/inputs/SegmentedButton.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_appName__", 4 | "description": "__MSG_appDesc__", 5 | "version": "3.15.1", 6 | "default_locale": "nl", 7 | "icons": { 8 | "16": "icons/icon@16px.png", 9 | "32": "icons/icon@32px.png", 10 | "48": "icons/icon@48px.png", 11 | "64": "icons/icon@64px.png", 12 | "128": "icons/icon@128px.png", 13 | "256": "icons/icon@256px.png", 14 | "300": "icons/icon@300px.png", 15 | "512": "icons/icon@512px.png" 16 | }, 17 | "minimum_chrome_version": "109", 18 | "background": { 19 | "service_worker": "src/service-worker.js", 20 | "type": "module" 21 | }, 22 | "content_scripts": [ 23 | { 24 | "matches": [ 25 | "*://*.magister.net/*", 26 | "*://login.microsoftonline.com/*/oauth2/authorize*" 27 | ], 28 | "js": [ 29 | "src/scripts/util.js", 30 | "src/scripts/api.js" 31 | ], 32 | "run_at": "document_start" 33 | }, 34 | { 35 | "matches": [ 36 | "*://*.magister.net/*" 37 | ], 38 | "js": [ 39 | "src/scripts/style.js" 40 | ], 41 | "css": [ 42 | "src/styles/main.css", 43 | "src/styles/today/today.css", 44 | "src/styles/today/schedule.css", 45 | "src/styles/today/widgets.css", 46 | "src/styles/gamification.css", 47 | "src/styles/grades.css", 48 | "src/styles/studyguide.css" 49 | ], 50 | "run_at": "document_start" 51 | }, 52 | { 53 | "matches": [ 54 | "*://*.magister.net/magister/*" 55 | ], 56 | "js": [ 57 | "src/scripts/main.js", 58 | "src/scripts/today/schedule.js", 59 | "src/scripts/today/widgets.js", 60 | "src/scripts/today/today.js", 61 | "src/scripts/gamification.js", 62 | "src/scripts/grades/grades.js", 63 | "src/scripts/grades/list.js", 64 | "src/scripts/grades/backup.js", 65 | "src/scripts/grades/statistics.js", 66 | "src/scripts/grades/calculator.js", 67 | "src/scripts/studyguide.js", 68 | "src/scripts/books.js" 69 | ], 70 | "run_at": "document_end" 71 | }, 72 | { 73 | "matches": [ 74 | "*://accounts.magister.net/account/login*" 75 | ], 76 | "js": [ 77 | "src/scripts/login.js" 78 | ], 79 | "run_at": "document_end" 80 | }, 81 | { 82 | "matches": [ 83 | "*://login.microsoftonline.com/*/oauth2/authorize*" 84 | ], 85 | "js": [ 86 | "src/scripts/microsoft-login.js" 87 | ], 88 | "run_at": "document_end" 89 | }, 90 | { 91 | "matches": [ 92 | "*://study-tools.nl/*" 93 | ], 94 | "js": [ 95 | "src/scripts/util.js", 96 | "src/scripts/theme-store.js" 97 | ] 98 | } 99 | ], 100 | "web_accessible_resources": [ 101 | { 102 | "resources": [ 103 | "src/strings/nl.json", 104 | "src/strings/en.json", 105 | "src/strings/fr.json", 106 | "src/strings/de.json", 107 | "src/strings/sv.json", 108 | "src/strings/la.json" 109 | ], 110 | "matches": [ 111 | "*://*.magister.net/*", 112 | "*://study-tools.nl/*" 113 | ] 114 | } 115 | ], 116 | "action": { 117 | "default_icon": { 118 | "16": "icons/icon@16px.png", 119 | "32": "icons/icon@32px.png", 120 | "48": "icons/icon@48px.png", 121 | "64": "icons/icon@64px.png", 122 | "128": "icons/icon@128px.png", 123 | "256": "icons/icon@256px.png", 124 | "300": "icons/icon@300px.png", 125 | "512": "icons/icon@512px.png" 126 | }, 127 | "default_popup": "popup/dist/index.html?type=popup", 128 | "default_title": "__MSG_appName__\nKlik om te configureren" 129 | }, 130 | "options_page": "popup/dist/index.html?type=page", 131 | "options_ui": { 132 | "page": "popup/dist/index.html?type=options", 133 | "open_in_tab": true 134 | }, 135 | "permissions": [ 136 | "storage", 137 | "webRequest" 138 | ], 139 | "host_permissions": [ 140 | "*://*.magister.net/*" 141 | ] 142 | } -------------------------------------------------------------------------------- /manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "__MSG_appName__", 4 | "description": "__MSG_appDesc__", 5 | "version": "3.15.1", 6 | "default_locale": "nl", 7 | "icons": { 8 | "16": "icons/icon@16px.png", 9 | "32": "icons/icon@32px.png", 10 | "48": "icons/icon@48px.png", 11 | "64": "icons/icon@64px.png", 12 | "128": "icons/icon@128px.png", 13 | "256": "icons/icon@256px.png", 14 | "300": "icons/icon@300px.png", 15 | "512": "icons/icon@512px.png" 16 | }, 17 | "browser_specific_settings": { 18 | "gecko": { 19 | "id": "studytools@qkeleq10.dev" 20 | } 21 | }, 22 | "background": { 23 | "scripts": [ 24 | "src/background.js" 25 | ], 26 | "type": "module" 27 | }, 28 | "content_scripts": [ 29 | { 30 | "matches": [ 31 | "*://*.magister.net/*", 32 | "*://login.microsoftonline.com/*/oauth2/authorize*" 33 | ], 34 | "js": [ 35 | "src/scripts/api.js", 36 | "src/scripts/util.js" 37 | ], 38 | "run_at": "document_start" 39 | }, 40 | { 41 | "matches": [ 42 | "*://*.magister.net/*" 43 | ], 44 | "js": [ 45 | "src/scripts/style.js" 46 | ], 47 | "css": [ 48 | "src/styles/main.css", 49 | "src/styles/today/today.css", 50 | "src/styles/today/schedule.css", 51 | "src/styles/today/widgets.css", 52 | "src/styles/gamification.css", 53 | "src/styles/grades.css", 54 | "src/styles/studyguide.css" 55 | ], 56 | "run_at": "document_start" 57 | }, 58 | { 59 | "matches": [ 60 | "*://*.magister.net/magister/*" 61 | ], 62 | "js": [ 63 | "src/scripts/main.js", 64 | "src/scripts/today/schedule.js", 65 | "src/scripts/today/widgets.js", 66 | "src/scripts/today/today.js", 67 | "src/scripts/gamification.js", 68 | "src/scripts/grades/grades.js", 69 | "src/scripts/grades/list.js", 70 | "src/scripts/grades/backup.js", 71 | "src/scripts/grades/statistics.js", 72 | "src/scripts/grades/calculator.js", 73 | "src/scripts/studyguide.js", 74 | "src/scripts/books.js" 75 | ], 76 | "run_at": "document_end" 77 | }, 78 | { 79 | "matches": [ 80 | "*://accounts.magister.net/account/login*" 81 | ], 82 | "js": [ 83 | "src/scripts/login.js" 84 | ], 85 | "run_at": "document_end" 86 | }, 87 | { 88 | "matches": [ 89 | "*://login.microsoftonline.com/*/oauth2/authorize*" 90 | ], 91 | "js": [ 92 | "src/scripts/microsoft-login.js" 93 | ], 94 | "run_at": "document_end" 95 | }, 96 | { 97 | "matches": [ 98 | "*://study-tools.nl/*" 99 | ], 100 | "js": [ 101 | "src/scripts/util.js", 102 | "src/scripts/theme-store.js" 103 | ] 104 | } 105 | ], 106 | "web_accessible_resources": [ 107 | { 108 | "resources": [ 109 | "src/strings/nl.json", 110 | "src/strings/en.json", 111 | "src/strings/fr.json", 112 | "src/strings/de.json", 113 | "src/strings/sv.json", 114 | "src/strings/la.json" 115 | ], 116 | "matches": [ 117 | "*://*.magister.net/*", 118 | "*://study-tools.nl/*" 119 | ] 120 | } 121 | ], 122 | "action": { 123 | "default_icon": { 124 | "16": "icons/icon@16px.png", 125 | "32": "icons/icon@32px.png", 126 | "48": "icons/icon@48px.png", 127 | "64": "icons/icon@64px.png", 128 | "128": "icons/icon@128px.png", 129 | "256": "icons/icon@256px.png", 130 | "300": "icons/icon@300px.png", 131 | "512": "icons/icon@512px.png" 132 | }, 133 | "default_popup": "popup/dist/index.html?type=popup", 134 | "default_title": "__MSG_appName__\nKlik om te configureren" 135 | }, 136 | "options_ui": { 137 | "page": "popup/dist/index.html?type=options", 138 | "open_in_tab": true 139 | }, 140 | "permissions": [ 141 | "storage", 142 | "webRequest" 143 | ], 144 | "host_permissions": [ 145 | "*://*.magister.net/*" 146 | ] 147 | } -------------------------------------------------------------------------------- /popup/dist/assets/lego-B9XBXV1o.js: -------------------------------------------------------------------------------- 1 | const A="data:image/png;base64,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";export{A as default}; 2 | -------------------------------------------------------------------------------- /popup/src/components/ShortcutsEditor.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 92 | 93 | -------------------------------------------------------------------------------- /popup/src/components/inputs/ColorPicker.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 75 | 76 | -------------------------------------------------------------------------------- /popup/src/assets/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --palette-primary-hue: 207; 3 | --palette-primary-saturation: 95%; 4 | --palette-primary-luminance: 55%; 5 | --palette-primary: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) var(--palette-primary-luminance)); 6 | 7 | --palette-secondary-hue: calc(var(--palette-primary-hue) + 30); 8 | --palette-secondary-saturation: 50%; 9 | 10 | --palette-neutral-hue: var(--palette-primary-hue); 11 | --palette-neutral-saturation: 30%; 12 | 13 | --palette-neutral-variant-hue: var(--palette-neutral-hue); 14 | --palette-neutral-variant-saturation: 30%; 15 | 16 | --color-primary: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 40%); 17 | --color-primary-container: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 90%); 18 | --color-on-primary: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 100%); 19 | --color-on-primary-container: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 10%); 20 | --color-secondary: hsl(var(--palette-secondary-hue) var(--palette-secondary-saturation) 40%); 21 | --color-secondary-container: hsl(var(--palette-secondary-hue) var(--palette-secondary-saturation) 90%); 22 | --color-on-secondary-container: hsl(var(--palette-secondary-hue) var(--palette-secondary-saturation) 10%); 23 | --color-surface: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 98%); 24 | --color-surface-container-lowest: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 100%); 25 | --color-surface-container-low: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 96%); 26 | --color-surface-container: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 94%); 27 | --color-surface-container-high: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 92%); 28 | --color-surface-container-highest: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 90%); 29 | --color-surface-variant: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 90%); 30 | --color-on-surface: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 10%); 31 | --color-on-surface-variant: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 30%); 32 | --color-outline: hsl(var(--palette-neutral-variant-hue) var(--palette-neutral-variant-saturation) 50%); 33 | --color-outline-variant: hsl(var(--palette-neutral-variant-hue) var(--palette-neutral-variant-saturation) 80%); 34 | --color-shadow: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 0%); 35 | --color-scrim: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 0%); 36 | 37 | --typescale-headline-small: 400 24px/32px 'Noto Sans', sans-serif; 38 | --typescale-title-large: 400 22px/28px 'Noto Sans', sans-serif; 39 | --typescale-title-small: 500 14px/20px 'Noto Sans', sans-serif; 40 | --typescale-label-large: 500 14px/20px 'Noto Sans', sans-serif; 41 | --typescale-label-medium: 500 12px/16px 'Noto Sans', sans-serif; 42 | --typescale-body-large: 400 16px/24px 'Noto Sans', sans-serif; 43 | --typescale-body-medium: 400 14px/20px 'Noto Sans', sans-serif; 44 | --typescale-body-small: 400 12px/16px 'Noto Sans', sans-serif; 45 | 46 | --mg-blue: hsl(207, 95%, 55%); 47 | --mg-orange: hsl(30, 100%, 51%); 48 | --mg-alt-green: hsl(161deg, 51%, 41%); 49 | --mg-alt-yellow: hsl(40deg, 51%, 41%); 50 | --mg-alt-red: hsl(360deg, 51%, 41%); 51 | --mg-alt-pink: hsl(331deg, 51%, 41%); 52 | --mg-alt-purple: hsl(266deg, 51%, 41%); 53 | 54 | --mg-bk-light-1: #ffffff; 55 | --mg-bk-light-2: #ffffff; 56 | --mg-fg-light: #000; 57 | --mg-br-light: #ededed; 58 | --mg-bk-dark-1: #121212; 59 | --mg-bk-dark-2: #161616; 60 | --mg-fg-dark: #fff; 61 | --mg-br-dark: #2e2e2e; 62 | 63 | color-scheme: only light; 64 | } 65 | 66 | :root[theme~=dark] { 67 | --color-primary: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 80%); 68 | --color-primary-container: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 30%); 69 | --color-on-primary: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 20%); 70 | --color-on-primary-container: hsl(var(--palette-primary-hue) var(--palette-primary-saturation) 90%); 71 | --color-secondary: hsl(var(--palette-secondary-hue) var(--palette-secondary-saturation) 80%); 72 | --color-secondary-container: hsl(var(--palette-secondary-hue) var(--palette-secondary-saturation) 30%); 73 | --color-on-secondary-container: hsl(var(--palette-secondary-hue) var(--palette-secondary-saturation) 90%); 74 | --color-surface: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 6%); 75 | --color-surface-container-lowest: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 4%); 76 | --color-surface-container-low: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 10%); 77 | --color-surface-container: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 12%); 78 | --color-surface-container-high: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 17%); 79 | --color-surface-container-highest: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 22%); 80 | --color-surface-variant: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 30%); 81 | --color-on-surface: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 90%); 82 | --color-on-surface-variant: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 80%); 83 | --color-outline: hsl(var(--palette-neutral-variant-hue) var(--palette-neutral-variant-saturation) 60%); 84 | --color-outline-variant: hsl(var(--palette-neutral-variant-hue) var(--palette-neutral-variant-saturation) 30%); 85 | --color-shadow: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 0%); 86 | --color-scrim: hsl(var(--palette-neutral-hue) var(--palette-neutral-saturation) 0%); 87 | 88 | color-scheme: only dark; 89 | } -------------------------------------------------------------------------------- /popup/src/components/setting-types/SingleChoice.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 47 | 48 | -------------------------------------------------------------------------------- /src/scripts/grades/list.js: -------------------------------------------------------------------------------- 1 | class GradeListPane extends Pane { 2 | id = 'cl'; 3 | icon = ''; 4 | 5 | #div1; 6 | #div2; 7 | 8 | sortingOptions = [ 9 | { 10 | id: 'date_desc', label: i18n('cl.sortByDateDesc'), comparator: (a, b) => { 11 | const dateA = new Date(a.DatumIngevoerd); 12 | const dateB = new Date(b.DatumIngevoerd); 13 | return dateB.getTime() - dateA.getTime(); 14 | } 15 | }, 16 | { 17 | id: 'date_asc', label: i18n('cl.sortByDateAsc'), comparator: (a, b) => { 18 | const dateA = new Date(a.DatumIngevoerd); 19 | const dateB = new Date(b.DatumIngevoerd); 20 | return dateA.getTime() - dateB.getTime(); 21 | } 22 | }, 23 | { 24 | id: 'result_desc', label: i18n('cl.sortByResultDesc'), comparator: (a, b) => { 25 | const resultA = Number(a.CijferStr?.replace(',', '.')); 26 | const resultB = Number(b.CijferStr?.replace(',', '.')); 27 | if (isNaN(resultA) && isNaN(resultB)) return a.CijferStr.localeCompare(b.CijferStr); 28 | if (isNaN(resultA)) return 1; 29 | if (isNaN(resultB)) return -1; 30 | return resultB - resultA; 31 | } 32 | }, 33 | { 34 | id: 'result_asc', label: i18n('cl.sortByResultAsc'), comparator: (a, b) => { 35 | const resultA = Number(a.CijferStr?.replace(',', '.')); 36 | const resultB = Number(b.CijferStr?.replace(',', '.')); 37 | if (isNaN(resultA) && isNaN(resultB)) return a.CijferStr.localeCompare(b.CijferStr); 38 | if (isNaN(resultA)) return 1; 39 | if (isNaN(resultB)) return -1; 40 | return resultA - resultB; 41 | } 42 | }, 43 | ]; 44 | sortingOption = this.sortingOptions[0]; 45 | 46 | constructor(parentElement) { 47 | super(parentElement); 48 | 49 | this.element.id = 'st-grade-recents-pane'; 50 | this.element.classList.remove('st-hidden'); 51 | 52 | this.#div1 = this.element.createChildElement('div', { class: 'st-div', style: 'margin-bottom: 16px' }); 53 | this.#div1.createChildElement('h3', { class: 'st-section-heading', innerText: i18n('cl.title') }); 54 | const select = this.#div1.createChildElement('select', { class: 'st-select', style: 'width: 100%' }); 55 | for (const option of this.sortingOptions) { 56 | const optionElement = select.createChildElement('option', { value: option.id, innerText: option.label }); 57 | if (option === this.sortingOption) { 58 | optionElement.selected = true; 59 | } 60 | } 61 | select.addEventListener('change', () => { 62 | const selectedOption = this.sortingOptions.find(option => option.id === select.value); 63 | if (selectedOption && selectedOption !== this.sortingOption) { 64 | this.sortingOption = selectedOption; 65 | this.redraw(); 66 | } 67 | }); 68 | this.element.createChildElement('hr'); 69 | this.#div2 = this.element.createChildElement('div', { class: 'st-div' }); 70 | } 71 | 72 | show() { 73 | this.redraw(); 74 | super.show(); 75 | } 76 | 77 | async redraw() { 78 | this.progressBar.dataset.visible = 'true'; 79 | 80 | this.#div2.innerHTML = ''; 81 | 82 | this.#div1.firstElementChild.innerText = this.sortingOption.id === 'date_desc' ? i18n('cl.recents') : i18n('cl.title'); 83 | 84 | const recentGrades = await magisterApi.gradesRecent(currentGradeTable.grades.length); 85 | 86 | const grades = currentGradeTable.grades.filter(g => g.CijferStr?.length > 0 && g.CijferKolom?.KolomSoort !== 2 && !syncedStorage['ignore-grade-columns'].includes(g.CijferKolom?.KolomKop || 'undefined')); 87 | grades.sort(this.sortingOption.comparator); 88 | 89 | // === EMPTY === 90 | 91 | if (grades.length === 0) { 92 | this.#div2.createChildElement('p', { innerText: i18n('cl.emptyDesc') }); 93 | this.progressBar.dataset.visible = 'false'; 94 | return; 95 | } 96 | 97 | // === LIST === 98 | 99 | const list = this.#div2.createChildElement('ul', { class: 'st-grade-list' }); 100 | for (const grade of grades) { 101 | const recentGrade = recentGrades.find(rg => rg.kolomId === grade.CijferKolom.Id); 102 | 103 | const gradeItem = list.createChildElement('li', { class: 'st-grade-item' }); 104 | 105 | const col1 = gradeItem.createChildElement('div') 106 | col1.createChildElement('div', { class: 'st-subject', innerText: grade.Vak?.Omschrijving || '-' }) 107 | if (grade.CijferKolom.WerkInformatieOmschrijving || grade.CijferKolom.KolomOmschrijving || recentGrade?.omschrijving) col1.createChildElement('div', { innerText: grade.CijferKolom.WerkInformatieOmschrijving || grade.CijferKolom.KolomOmschrijving || recentGrade?.omschrijving || '-' }) 108 | col1.createChildElement('div', { innerText: makeTimestamp(grade.DatumIngevoerd) }); 109 | 110 | const col2 = gradeItem.createChildElement('div') 111 | col2.createChildElement('div', { innerText: grade.CijferStr, classList: grade.IsVoldoende === false ? ['st-insufficient'] : [] }) 112 | if (grade.CijferKolom?.Weging ?? recentGrade) col2.createChildElement('div', { innerText: (grade.CijferKolom?.Weging ?? recentGrade?.weegfactor ?? '?') + 'x' }); 113 | 114 | if ( 115 | new Date(grade.DatumIngevoerd) >= new Date(new Date(localStorage['st-grade-last-viewed'] || 0)) 116 | && new Date(grade.DatumIngevoerd) >= new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000) 117 | ) 118 | gradeItem.classList.add('st-highlight'); 119 | 120 | gradeItem.classList.add('st-clickable'); 121 | gradeItem.addEventListener('click', () => { 122 | const dialog = new GradeDetailDialog(grade, currentGradeTable.identifier.year); 123 | dialog.show(); 124 | }); 125 | } 126 | 127 | localStorage['st-grade-last-viewed'] = new Date().toISOString(); 128 | 129 | this.progressBar.dataset.visible = 'false'; 130 | } 131 | } -------------------------------------------------------------------------------- /popup/src/components/ImageInput.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 108 | 109 | -------------------------------------------------------------------------------- /src/styles/studyguide.css: -------------------------------------------------------------------------------- 1 | #st-sw-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | gap: 8px; 6 | height: auto; 7 | max-height: min(100%, calc(100vh - 125px)); 8 | overflow-y: auto; 9 | } 10 | 11 | .st-sw-col { 12 | flex: 1 1 0px; 13 | display: flex; 14 | flex-wrap: wrap; 15 | gap: 8px; 16 | padding: 1px; 17 | width: 100%; 18 | } 19 | 20 | #st-sw-search { 21 | position: absolute; 22 | top: 70px; 23 | right: 25px; 24 | z-index: 1000; 25 | } 26 | 27 | .st-sw-item.hidden, 28 | .st-sw-subject.hidden { 29 | display: none; 30 | } 31 | 32 | .st-sw-item.hidden-item:before, 33 | .st-sw-item-default.hidden-item>.st-sw-item-default-desc:before { 34 | content: ''; 35 | font-family: 'Font Awesome 6 Pro'; 36 | font-weight: 500; 37 | font-size: 14px; 38 | vertical-align: -2px; 39 | margin-right: 8px; 40 | } 41 | 42 | #st-sw-item-hider { 43 | position: absolute; 44 | top: 73px; 45 | right: 154px; 46 | z-index: 2; 47 | } 48 | 49 | #st-sw-hidden-items { 50 | display: none; 51 | flex-direction: column; 52 | position: absolute; 53 | bottom: 20px; 54 | right: 25px; 55 | width: 300px; 56 | border-radius: var(--st-border-radius); 57 | overflow: hidden; 58 | } 59 | 60 | #st-sw-hidden-items.st-expanded { 61 | display: flex; 62 | border: var(--st-border); 63 | animation: expandIn 150ms both; 64 | } 65 | 66 | #studiewijzer-detail-container dna-page-header { 67 | clip-path: inset(0 15% 0 0); 68 | } 69 | 70 | #st-sw-hidden-items-button { 71 | position: absolute; 72 | bottom: 20px; 73 | right: 25px; 74 | border: none; 75 | background-color: transparent; 76 | opacity: .75; 77 | font: 14px var(--st-font-family-secondary); 78 | background-color: var(--st-background-secondary); 79 | border: var(--st-border); 80 | transition: background-color 200ms, color 200ms, border 200ms, padding 200ms, margin 200ms, transform 200ms; 81 | } 82 | 83 | #st-sw-hidden-items-button.st-collapsed { 84 | animation: shrinkOut 150ms, displayOut 150ms both; 85 | pointer-events: none; 86 | } 87 | 88 | .st-sw-subject { 89 | flex: 1 0 50%; 90 | box-sizing: border-box; 91 | display: flex; 92 | flex-direction: column; 93 | border-radius: var(--st-border-radius); 94 | border: var(--st-border); 95 | overflow: hidden; 96 | } 97 | 98 | .st-sw-subject:not(:has(button:not(.hidden))) { 99 | display: none; 100 | } 101 | 102 | .st-sw-items-wrapper { 103 | display: flex; 104 | flex-wrap: wrap; 105 | flex-direction: column; 106 | } 107 | 108 | .st-sw-items-wrapper>*:not(:first-child) { 109 | border-top: var(--st-border); 110 | } 111 | 112 | .st-sw-items-wrapper[data-flex-row=true] { 113 | flex-direction: row; 114 | } 115 | 116 | .st-sw-subject button, 117 | #st-sw-hidden-items button { 118 | position: relative; 119 | outline: 0; 120 | border: none; 121 | color: var(--st-foreground-primary); 122 | text-align: left; 123 | cursor: pointer; 124 | } 125 | 126 | .st-sw-item-default, 127 | .st-sw-subject-headline { 128 | --padding-block: 12px; 129 | display: flex; 130 | flex-direction: column; 131 | grid-column: 1 / -1; 132 | grid-row: 1; 133 | padding: 12px; 134 | padding-block: var(--padding-block); 135 | font: 600 16px/22px var(--st-font-family-secondary); 136 | background-color: var(--st-highlight-primary); 137 | color: var(--st-foreground-primary); 138 | } 139 | 140 | .st-sw-subject-headline { 141 | border-bottom: var(--st-border); 142 | } 143 | 144 | .st-sw-item-default-desc { 145 | font: 12px var(--st-font-family-secondary); 146 | } 147 | 148 | .st-sw-item-default-desc[data-2nd]:after { 149 | content: attr(data-2nd); 150 | position: absolute; 151 | width: 100%; 152 | bottom: var(--padding-block); 153 | left: 12px; 154 | background-color: var(--st-highlight-primary); 155 | opacity: 0; 156 | } 157 | 158 | .st-sw-item-default:hover .st-sw-item-default-desc[data-2nd]:after { 159 | opacity: 1; 160 | } 161 | 162 | .st-sw-item { 163 | flex: 1 1 0px; 164 | padding: 10px 12px; 165 | font: 12px var(--st-font-family-secondary); 166 | background-color: var(--st-background-secondary); 167 | } 168 | 169 | .st-sw-item[data-2nd]:hover { 170 | color: transparent; 171 | } 172 | 173 | .st-sw-item[data-2nd]:after { 174 | content: attr(data-2nd); 175 | position: absolute; 176 | height: 100%; 177 | width: 100%; 178 | top: 0; 179 | left: 0; 180 | padding: 10px 12px; 181 | background-color: var(--st-background-secondary); 182 | color: var(--st-foreground-primary); 183 | opacity: 0; 184 | } 185 | 186 | .st-sw-item[data-2nd]:hover:after { 187 | opacity: 1; 188 | } 189 | 190 | .st-current, 191 | .st-sw-2 { 192 | font-weight: 700; 193 | } 194 | 195 | .st-obsolete, 196 | .st-obsolete span, 197 | .st-sw-0 { 198 | color: #888 !important 199 | } 200 | 201 | #st-sw-container button:hover, 202 | #st-sw-container button:focus, 203 | #st-sw-container button.st-sw-selected { 204 | filter: brightness(var(--st-hover-brightness)); 205 | } 206 | 207 | .st-current-sw>div>div>footer.endlink, 208 | .st-current-sw>div>h3, 209 | .st-current-sw>div>h3>b { 210 | background: var(--st-highlight-primary); 211 | font-weight: 700 212 | } 213 | 214 | .widget .st-sw-item-default, 215 | .widget .st-sw-subject-headline { 216 | --padding-block: 4px; 217 | font-size: 14px; 218 | } 219 | 220 | #st-hb-sheet>div { 221 | display: flex; 222 | flex-direction: column; 223 | gap: 6px; 224 | padding: 8px; 225 | margin-inline: -4px; 226 | box-shadow: 0 0 8px 0 rgba(var(--st-shadow-value), var(--st-shadow-value), var(--st-shadow-value), var(--st-shadow-alpha)); 227 | background-color: var(--st-background-secondary); 228 | border-radius: var(--st-border-radius); 229 | border: var(--st-border); 230 | } 231 | 232 | #st-hb-sheet-heading { 233 | align-items: start; 234 | gap: 4px; 235 | font-size: 16px; 236 | margin-bottom: 0; 237 | } 238 | 239 | #st-hb-sheet-heading[data-description]:after { 240 | font-size: 12px; 241 | text-align: left; 242 | } 243 | 244 | @media (height < 700px) { 245 | 246 | .st-sw-item-default, 247 | .st-sw-subject-headline { 248 | --padding-block: 8px; 249 | } 250 | } 251 | 252 | @media (width > 768px) { 253 | div.view>#st-sw-container { 254 | flex-direction: row; 255 | gap: 12px; 256 | margin-inline: 24px; 257 | } 258 | } -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import settings from '../popup/dist/settings.js' 2 | 3 | let apiUserId, 4 | apiUserToken, 5 | apiUserTokenDate 6 | 7 | const settingsToClear = [ 8 | 'auto-theme', 'theme-fixed', 'theme-day', 'theme-night', 'openedPopup', 'updates', 'beta', 'magister-shortcuts', 'magister-shortcuts-today', 'magister-sw-grid', 'magister-sw-sort', 'magister-sw-period', 'magister-sw-display', 'magister-ag-large', 'magister-subjects', 'magister-appbar-hidePicture', 'appbar-hide-actions', 'magister-appbar-zermelo', 'magister-appbar-zermelo-url', 'magister-css-border-radius', 'magister-css-dark-invert', 'magister-css-experimental', 'magister-css-hue', 'magister-css-luminance', 'magister-css-saturation', 'magister-css-theme', 'magister-op-oldgrey', 'magister-periods', 'periods', 'magister-shortcut-keys', 'magister-shortcut-keys-master', 'magister-shortcut-keys-today', 'magister-subjects', 'magister-sw-thisWeek', 'magister-vd-overhaul', 'magister-vd-enabled', 'magister-vd-subjects', 'magister-vd-grade', 'magister-vd-agendaHeight', 'magister-vd-deblue', 'magister-vd-gradewidget', 'magisterLogin-password', 'magisterLogin-method', 'magister-gamification-beta', 'gamification-enabled', 'magister-cf-calculator', 'magister-cf-statistics', 'magister-cf-backup', 'magister-cf-failred', 'notes-enabled', 'notes', 'st-notes', 'vd-enabled', 'vd-schedule-days', 'vd-schedule-extra-day', 'vd-schedule-zoom', 'vd-subjects-display', 'start-stats', 'teacher-names', 'version', 'disable-css', 'hotkeys-today', 'start-widgets', 'dark-image', 'light-image', 'subjects', 'hidden-studyguides', 'color', 'start-schedule-days', 'v', 'special' 9 | ] 10 | 11 | startListenCredentials() 12 | setDefaults() 13 | console.info("Service worker running!") 14 | 15 | async function startListenCredentials() { 16 | // Initialise the three variables 17 | apiUserId = (await browser.storage.sync.get('user-id'))?.['user-id'] || null 18 | apiUserToken = (await browser.storage.local.get('token'))?.['token'] || null 19 | apiUserTokenDate = (await browser.storage.local.get('token-date'))?.['token-date'] || null 20 | 21 | browser.webRequest.onBeforeSendHeaders.addListener(async e => { 22 | let userIdWas = apiUserId 23 | let userTokenWas = apiUserToken 24 | if (e.url.split('/personen/')[1]?.split('/')[0].length > 2) { 25 | apiUserId = e.url.split('/personen/')[1].split('/')[0] 26 | browser.storage.sync.set({ 'user-id': apiUserId }) 27 | if (userIdWas !== apiUserId) console.info(`User ID changed from ${userIdWas} to ${apiUserId}.`) 28 | } 29 | let authObject = Object.values(e.requestHeaders).find(obj => obj.name === 'Authorization') 30 | if (authObject) { 31 | apiUserToken = authObject.value 32 | apiUserTokenDate = new Date() 33 | browser.storage.local.set({ 'token': apiUserToken }) 34 | browser.storage.local.set({ 'token-date': apiUserTokenDate.getTime() }) 35 | if (userTokenWas !== apiUserToken) console.info(`User token changed between ${new Date().toLocaleDateString()} and now.`) 36 | } 37 | 38 | }, { urls: ['*://*.magister.net/*'] }, ['requestHeaders']) 39 | 40 | console.info("Intercepting HTTP request information to extract token and userId...%c\n\nVrees niet, dit is alleen nodig zodat de extensie API-verzoeken kan maken naar Magister. Deze gegevens blijven op je apparaat. Dit wordt momenteel alleen gebruikt voor de volgende onderdelen:\n" + ["cijferexport", "widgets startpagina", "rooster startpagina", "puntensysteem"].join(', ') + "\n\nen in de toekomst eventueel ook voor:\n" + [].join(', '), "font-size: .8em") 41 | } 42 | 43 | async function setDefaults() { 44 | let syncedStorage = await browser.storage.sync.get() 45 | let diff = {} 46 | 47 | // Check each setting to see if its value has been defined. If not, set it to the default value. 48 | settings.forEach(category => { 49 | category.settings.forEach(setting => { 50 | if (typeof syncedStorage[setting.id] === 'undefined') { 51 | if (setting.id === 'wallpaper' && syncedStorage['backdrop']?.length > 5) diff[setting.id] = 'custom,' + syncedStorage['backdrop'] 52 | else diff[setting.id] = setting.default 53 | } 54 | }) 55 | }) 56 | 57 | if (Object.keys(diff).length > 0) { 58 | setTimeout(() => browser.storage.sync.set(diff), 200) 59 | console.info("Set the following storage.sync keys to their default values:", diff) 60 | } 61 | 62 | if (settingsToClear.some(key => Object.keys(syncedStorage).includes(key))) { 63 | browser.storage.sync.remove(settingsToClear) 64 | console.info("Redundant storage.sync keys removed to free up space.") 65 | } 66 | } 67 | 68 | browser.runtime.onMessage.addListener((request, sender, sendResponse) => { 69 | switch (request.action) { 70 | case 'popstateDetected': 71 | console.info("Popstate detected, service worker revived for 30 seconds.") 72 | return 0 73 | 74 | case 'waitForRequestCompleted': 75 | console.info(`Request completion notification requested by ${sender.url}.`) 76 | browser.webRequest.onCompleted.addListener((details) => { 77 | sendResponse({ status: 'completed', details: details }) 78 | console.info(`Request completion notification sent to ${sender.url}.`) 79 | }, { urls: ['*://*.magister.net/api/personen/*/aanmeldingen/*/cijfers/extracijferkolominfo/*'] }) 80 | setTimeout(() => { 81 | sendResponse({ status: 'timeout' }) 82 | console.warn(`Request completion notification requested by ${sender.url} has timed out.`) 83 | }, 5000) 84 | return true 85 | 86 | case 'uninstallSelf': 87 | chrome.management.uninstallSelf({ showConfirmDialog: false }, () => { window.location.reload() }) 88 | break 89 | 90 | case 'openOptions': 91 | chrome.tabs.create({ url: `index.html?${request.data}` }); 92 | break; 93 | 94 | default: 95 | return 0 96 | } 97 | }) 98 | 99 | browser.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { 100 | switch (request.action) { 101 | case 'addPersonalTheme': 102 | const obj = request.obj 103 | const storedThemes = Object.values((await chrome.storage.local.get('storedThemes')).storedThemes) 104 | if (!storedThemes || storedThemes.length >= 9) return 105 | 106 | storedThemes.push(obj) 107 | 108 | //TODO: only if not exist 109 | 110 | await chrome.storage.local.set({ 'storedThemes': storedThemes }) 111 | break 112 | 113 | default: 114 | return 0 115 | } 116 | }) 117 | -------------------------------------------------------------------------------- /src/styles/gamification.css: -------------------------------------------------------------------------------- 1 | #st-wrapped-invoke { 2 | width: 50px; 3 | padding: 0; 4 | margin-inline: auto; 5 | background-image: linear-gradient(35deg, #ffffff, #7cc4ff9c); 6 | background-clip: text; 7 | -webkit-background-clip: text; 8 | text-fill-color: transparent; 9 | color: transparent; 10 | border: none; 11 | border-radius: var(--st-border-radius); 12 | outline: none; 13 | font: 600 30px/50px 'Font Awesome 6 Pro'; 14 | text-align: center; 15 | opacity: 0.5; 16 | cursor: pointer; 17 | transition: scale 200ms, opacity 200ms; 18 | } 19 | 20 | #st-wrapped-invoke.spinning { 21 | animation: spin 1s linear infinite; 22 | } 23 | 24 | @keyframes spin { 25 | to { 26 | rotate: 359.9deg; 27 | } 28 | } 29 | 30 | #st-wrapped-invoke:hover, 31 | #st-wrapped-invoke:focus-visible { 32 | scale: 1.1; 33 | opacity: 1; 34 | transition: scale 100ms, opacity 100ms; 35 | } 36 | 37 | #st-wrapped-invoke:focus-visible { 38 | outline: 2px solid var(--st-foreground-primary); 39 | } 40 | 41 | #st-wrapped-invoke-tip { 42 | position: absolute; 43 | top: 24px; 44 | left: 58px; 45 | height: auto; 46 | background-color: var(--st-accent-primary-dark); 47 | color: var(--st-contrast-accent); 48 | box-shadow: 0 0 8px 0 rgba(var(--st-shadow-value), var(--st-shadow-value), var(--st-shadow-value), var(--st-shadow-alpha)); 49 | z-index: 100; 50 | transform-origin: left center; 51 | transform: none; 52 | opacity: 1; 53 | padding: 7px 13px; 54 | border: 1px solid var(--st-contrast-accent); 55 | border-radius: var(--st-border-radius); 56 | font: 12px var(--st-font-family-secondary); 57 | transition: transform 200ms ease 0s, opacity 200ms ease 0s; 58 | } 59 | 60 | #st-wrapped-invoke-tip:after { 61 | content: ""; 62 | position: absolute; 63 | top: 50%; 64 | right: 100%; 65 | margin-top: -5px; 66 | border-width: 5px; 67 | border-style: solid; 68 | border-color: transparent var(--st-contrast-accent) transparent transparent; 69 | } 70 | 71 | #st-wrapped-invoke-tip.hidden { 72 | transform: scale(0); 73 | opacity: 0; 74 | } 75 | 76 | #st-wrapped { 77 | padding: 0; 78 | border-radius: 16px; 79 | border: 1px solid #444; 80 | outline: none; 81 | background-color: transparent; 82 | user-select: none; 83 | } 84 | 85 | #st-wrapped::backdrop { 86 | background-color: #121212f7; 87 | } 88 | 89 | #st-wrapped-years-wrapper { 90 | width: min(calc(100vw - 64px), 1300px); 91 | height: min(calc(100vh - 64px), 850px); 92 | overflow: hidden; 93 | 94 | container-type: inline-size; 95 | display: flex; 96 | gap: 32px; 97 | background-color: transparent; 98 | } 99 | 100 | .st-wrapped-year { 101 | --pattern: linear-gradient(to right, transparent, transparent); 102 | --gradient: linear-gradient(to right, #485563, #29323c); 103 | min-height: min(calc(100vh - 64px), 850px); 104 | max-height: min(calc(100vh - 64px), 850px); 105 | flex: 100% 0 0; 106 | 107 | display: grid; 108 | grid-template-columns: repeat(3, 1fr); 109 | grid-auto-rows: 28px; 110 | grid-auto-flow: row dense; 111 | gap: 16px; 112 | padding: 32px; 113 | padding-top: 40px; 114 | 115 | overflow-y: auto; 116 | border-radius: 16px; 117 | background-image: radial-gradient(at left top, #20283110, #1218201c), var(--pattern), var(--gradient); 118 | color: #ffffff; 119 | } 120 | 121 | @container (max-width: 1000px) { 122 | .st-wrapped-year { 123 | grid-template-columns: repeat(2, 1fr); 124 | } 125 | } 126 | 127 | .st-wrapped-year-title { 128 | grid-column: 1 / -1; 129 | font: 500 28px/2rem arboria, sans-serif; 130 | translate: 0 -8px; 131 | } 132 | 133 | .st-wrapped-card { 134 | position: relative; 135 | display: flex; 136 | flex-direction: column; 137 | align-items: center; 138 | justify-content: center; 139 | gap: 8px; 140 | padding: 32px; 141 | padding-top: 36px; 142 | 143 | background: #ffffff33; 144 | border-radius: 16px; 145 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); 146 | border: 1px solid #ffffff11; 147 | 148 | font: 500 22px arboria, sans-serif; 149 | color: #ffffff; 150 | text-align: left; 151 | text-wrap: balance; 152 | user-select: none; 153 | overflow: hidden; 154 | } 155 | 156 | .st-wrapped-card.grid { 157 | display: grid; 158 | gap: 12px; 159 | } 160 | 161 | .st-wrapped-card.interactable, 162 | .st-wrapped-card.external-link { 163 | cursor: pointer; 164 | transition: transform 200ms; 165 | } 166 | 167 | .st-wrapped-card.interactable:hover, 168 | .st-wrapped-card.external-link:hover { 169 | transform: scale(1.02); 170 | } 171 | 172 | .st-wrapped-card.interactable:active, 173 | .st-wrapped-card.external-link:active { 174 | transform: scale(0.97); 175 | } 176 | 177 | .st-wrapped-card.interactable[data-pages][data-page]::after { 178 | content: attr(data-page) '/' attr(data-pages); 179 | position: absolute; 180 | top: 12px; 181 | right: 12px; 182 | color: #ffffff66; 183 | font: 14px var(--st-font-family-secondary); 184 | } 185 | 186 | .st-wrapped-card.external-link::after { 187 | content: ''; 188 | position: absolute; 189 | top: 12px; 190 | right: 12px; 191 | color: #ffffff66; 192 | font: 500 16px "Font Awesome 6 Pro"; 193 | } 194 | 195 | .st-wrapped-card[data-icon]::before { 196 | content: attr(data-icon); 197 | position: absolute; 198 | top: 12px; 199 | left: 12px; 200 | color: #ffffff66; 201 | font: 600 16px "Font Awesome 6 Pro"; 202 | } 203 | 204 | .st-wrapped-card .st-w-metric { 205 | font: 500 42px arboria, sans-serif; 206 | color: #ffffff; 207 | text-align: center; 208 | } 209 | 210 | .st-wrapped-card .st-w-metric-med { 211 | font: 500 36px arboria, sans-serif; 212 | color: #ffffff; 213 | text-align: center; 214 | } 215 | 216 | .st-wrapped-card .st-w-text { 217 | font: 500 22px arboria, sans-serif; 218 | color: #ffffff; 219 | text-align: center; 220 | } 221 | 222 | .st-wrapped-card .st-w-text-small { 223 | font: 500 18px arboria, sans-serif; 224 | color: #ffffffdd; 225 | text-align: center; 226 | } 227 | 228 | .st-wrapped-card .st-w-text-tiny { 229 | font: 500 16px arboria, sans-serif; 230 | color: #ffffffdd; 231 | text-align: center; 232 | } 233 | 234 | .st-wrapped-card .st-w-line-vertical { 235 | height: 100%; 236 | border-left: 1px solid #ffffff33; 237 | } 238 | 239 | #st-wrapped .st-button.icon { 240 | color: #ffffff; 241 | } 242 | 243 | #st-wrapped-esc { 244 | position: absolute; 245 | top: 32px; 246 | right: 32px; 247 | } 248 | 249 | #st-wrapped-info { 250 | position: absolute; 251 | top: 32px; 252 | right: 68px; 253 | } 254 | 255 | #st-wrapped-next { 256 | position: absolute; 257 | top: 32px; 258 | right: 104px; 259 | } 260 | 261 | #st-wrapped-prev { 262 | position: absolute; 263 | top: 32px; 264 | right: 140px; 265 | } -------------------------------------------------------------------------------- /popup/src/components/About.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 131 | 132 | -------------------------------------------------------------------------------- /src/styles/today/today.css: -------------------------------------------------------------------------------- 1 | @property --hour-height { 2 | syntax: ""; 3 | inherits: true; 4 | initial-value: 110px; 5 | } 6 | 7 | @property --progress { 8 | syntax: ""; 9 | inherits: true; 10 | initial-value: 0; 11 | } 12 | 13 | #st-start button.st-widget-subitem:focus-visible, 14 | #st-start button.st-event:focus-visible { 15 | outline: 2px solid var(--st-foreground-accent); 16 | outline-offset: -2px; 17 | } 18 | 19 | #st-start { 20 | position: absolute; 21 | top: 0; 22 | left: 0; 23 | height: 100%; 24 | width: 100%; 25 | display: grid; 26 | grid-template: 27 | 'header widgets' auto 28 | 'schedule widgets' 1fr 29 | / 1fr 400px; 30 | overflow: hidden; 31 | transition: grid-template 200ms, grid-template-columns 200ms; 32 | } 33 | 34 | #st-start-header-text-wrapper { 35 | display: grid; 36 | grid-template-columns: 100% 100%; 37 | height: 36px; 38 | overflow: hidden; 39 | 40 | background-color: transparent; 41 | border: none; 42 | text-align: left; 43 | 44 | container-type: size; 45 | 46 | &>* { 47 | grid-area: 1 / 1 / 2 / 2; 48 | text-wrap: nowrap; 49 | text-overflow: ellipsis; 50 | cursor: pointer; 51 | height: 100%; 52 | margin-bottom: 0; 53 | overflow: hidden; 54 | 55 | display: block; 56 | clip-path: inset(0 0 0 0); 57 | transition: display 1000ms allow-discrete; 58 | animation: wipeIn 1000ms 50ms both; 59 | 60 | &.not-today { 61 | font-style: normal; 62 | font-weight: 500; 63 | color: var(--st-foreground-secondary); 64 | } 65 | 66 | &::first-letter { 67 | text-transform: capitalize; 68 | } 69 | 70 | :focus-visible { 71 | outline: 2px solid var(--st-foreground-accent); 72 | outline-offset: -2px; 73 | } 74 | } 75 | } 76 | 77 | #st-start-header-short-title, 78 | #st-start-header-greeting { 79 | display: none; 80 | animation: wipeOut 1000ms both; 81 | } 82 | 83 | @container (width < 280px) { 84 | #st-start-header-title { 85 | display: none; 86 | animation: wipeOut 1000ms both; 87 | } 88 | 89 | #st-start-header-short-title { 90 | display: block; 91 | animation: wipeIn 1000ms 50ms both; 92 | } 93 | } 94 | 95 | #st-start-header-text-wrapper.greet { 96 | 97 | #st-start-header-title, 98 | #st-start-header-short-title { 99 | display: none; 100 | animation: wipeOut 1000ms both; 101 | } 102 | 103 | #st-start-header-greeting { 104 | display: block; 105 | animation: wipeIn 1000ms 50ms both; 106 | } 107 | } 108 | 109 | @keyframes wipeIn { 110 | from { 111 | clip-path: inset(0 100% 0 0); 112 | } 113 | 114 | to { 115 | clip-path: inset(0 0 0 0); 116 | } 117 | } 118 | 119 | @keyframes wipeOut { 120 | from { 121 | clip-path: inset(0 0 0 0); 122 | } 123 | 124 | to { 125 | clip-path: inset(0 0 0 100%); 126 | } 127 | } 128 | 129 | #st-start-header-buttons { 130 | margin-left: auto; 131 | display: flex; 132 | flex-wrap: nowrap; 133 | align-items: center; 134 | gap: 4px; 135 | } 136 | 137 | #st-start-header-buttons .st-button[data-icon]:before { 138 | font-weight: 600; 139 | } 140 | 141 | #st-start-today-offset-zero.emphasise { 142 | animation: 200ms teeter 3 linear, 1000ms pulsebig; 143 | } 144 | 145 | @keyframes teeter { 146 | 33% { 147 | transform: translate(-7%, 0) rotate(-9deg); 148 | } 149 | 150 | 66% { 151 | transform: translate(-5%, 0) rotate(4deg); 152 | } 153 | 154 | 66% { 155 | transform: translate(7%, 0) rotate(9deg); 156 | } 157 | } 158 | 159 | @keyframes pulsebig { 160 | 161 | 10%, 162 | 60% { 163 | scale: 1.5; 164 | translate: 0 -8px; 165 | } 166 | } 167 | 168 | #st-start-today-view { 169 | display: flex; 170 | flex-wrap: nowrap; 171 | margin-left: 8px; 172 | } 173 | 174 | .st-start-icon { 175 | display: inline-block; 176 | position: sticky !important; 177 | top: 50%; 178 | left: 50%; 179 | translate: -50% calc(-50% - 48px); 180 | width: 120px; 181 | height: max-content; 182 | font-size: 90px; 183 | text-align: center; 184 | opacity: .5; 185 | color: var(--st-foreground-secondary); 186 | } 187 | 188 | .st-start-disclaimer { 189 | display: inline-block; 190 | position: sticky; 191 | top: 50%; 192 | left: 50%; 193 | translate: -50% calc(-50% + 32px); 194 | width: max-content; 195 | height: max-content; 196 | font: 600 14px var(--st-font-family-secondary); 197 | text-align: center; 198 | opacity: .5; 199 | color: var(--st-foreground-secondary); 200 | } 201 | 202 | #st-start[data-widgets-collapsed=true] { 203 | grid-template: 204 | 'schedule widgets' 1fr 205 | / 1fr 0px; 206 | 207 | #st-start-fab { 208 | min-width: 128px; 209 | border-top-left-radius: var(--st-border-radius); 210 | box-shadow: 0 0 8px 0 rgba(var(--st-shadow-value), var(--st-shadow-value), var(--st-shadow-value), var(--st-shadow-alpha)); 211 | } 212 | } 213 | 214 | .st-teacher-names-list { 215 | display: grid; 216 | grid-auto-flow: column; 217 | grid-template-rows: repeat(20, auto); 218 | column-gap: 32px; 219 | 220 | div { 221 | display: flex; 222 | justify-content: space-between; 223 | align-items: baseline; 224 | gap: 8px; 225 | width: 100%; 226 | font-size: 12px; 227 | 228 | .st-input { 229 | height: auto; 230 | padding: 4px 8px; 231 | margin-bottom: 4px; 232 | font-size: 12px; 233 | } 234 | } 235 | } 236 | 237 | #st-start-fab { 238 | position: absolute; 239 | right: 0; 240 | bottom: 0; 241 | min-width: 400px; 242 | z-index: 2; 243 | 244 | display: flex; 245 | justify-content: center; 246 | align-items: center; 247 | gap: 4px; 248 | padding: 8px; 249 | 250 | color: var(--st-foreground-insignificant); 251 | background-color: var(--st-background-secondary); 252 | border-top: var(--st-border); 253 | border-left: var(--st-border); 254 | transition: min-width 200ms; 255 | 256 | .st-button { 257 | color: currentColor; 258 | 259 | &:hover { 260 | color: var(--st-foreground-accent); 261 | filter: brightness(var(--st-hover-brightness)); 262 | } 263 | } 264 | 265 | .st-widget-controls-button-group { 266 | color: currentColor; 267 | display: flex; 268 | align-items: center; 269 | gap: 4px; 270 | padding: 4px; 271 | 272 | border-radius: var(--st-border-radius); 273 | background-color: hsl(from var(--st-background-secondary) h s calc(l - 1)); 274 | 275 | #st-start-edit-zoom-reset { 276 | padding-inline: 0; 277 | width: 42px; 278 | background-color: hsl(from var(--st-background-secondary) h s calc(l - 1)); 279 | } 280 | } 281 | 282 | #st-sch-collapse-widgets { 283 | margin-left: auto; 284 | } 285 | } -------------------------------------------------------------------------------- /popup/src/components/CustomCssEditor.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 140 | 141 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | import settings from '../popup/dist/settings.js' 2 | 3 | let userId, 4 | userToken, 5 | userTokenDate 6 | 7 | const settingsToClear = [ 8 | 'auto-theme', 'theme-fixed', 'theme-day', 'theme-night', 'openedPopup', 'updates', 'beta', 'magister-shortcuts', 'magister-shortcuts-today', 'magister-sw-grid', 'magister-sw-sort', 'magister-sw-period', 'magister-sw-display', 'magister-ag-large', 'magister-subjects', 'magister-appbar-hidePicture', 'appbar-hide-actions', 'magister-appbar-zermelo', 'magister-appbar-zermelo-url', 'magister-css-border-radius', 'magister-css-dark-invert', 'magister-css-experimental', 'magister-css-hue', 'magister-css-luminance', 'magister-css-saturation', 'magister-css-theme', 'magister-op-oldgrey', 'magister-periods', 'periods', 'magister-shortcut-keys', 'magister-shortcut-keys-master', 'magister-shortcut-keys-today', 'magister-subjects', 'magister-sw-thisWeek', 'magister-vd-overhaul', 'magister-vd-enabled', 'magister-vd-subjects', 'magister-vd-grade', 'magister-vd-agendaHeight', 'magister-vd-deblue', 'magister-vd-gradewidget', 'magisterLogin-password', 'magisterLogin-method', 'magister-gamification-beta', 'gamification-enabled', 'magister-cf-calculator', 'magister-cf-statistics', 'magister-cf-backup', 'magister-cf-failred', 'notes-enabled', 'notes', 'st-notes', 'vd-enabled', 'vd-schedule-days', 'vd-schedule-extra-day', 'vd-schedule-zoom', 'vd-subjects-display', 'start-stats', 'teacher-names', 'version', 'disable-css', 'hotkeys-today', 'start-widgets', 'dark-image', 'light-image', 'subjects', 'hidden-studyguides', 'color', 'start-schedule-days', 'v', 'special' 9 | ] 10 | 11 | startListenCredentials() 12 | setDefaults() 13 | console.info("Service worker running!") 14 | addEventListener('activate', () => { 15 | startListenCredentials() 16 | setDefaults() 17 | console.info("Service worker running!") 18 | }) 19 | 20 | chrome.runtime.onStartup.addListener(() => { 21 | startListenCredentials() 22 | setDefaults() 23 | console.info("Browser started, service worker revived.") 24 | }) 25 | 26 | const keepAlive = () => setInterval(chrome.runtime.getPlatformInfo, 20e3) 27 | chrome.runtime.onStartup.addListener(keepAlive) 28 | keepAlive() 29 | 30 | async function startListenCredentials() { 31 | // Allow any context to use chrome.storage.session 32 | chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' }) 33 | 34 | // Initialise the three variables 35 | userId = (await chrome.storage.sync.get('user-id'))?.['user-id'] || null 36 | userToken = (await chrome.storage.session.get('token'))?.['token'] || null 37 | userTokenDate = (await chrome.storage.session.get('token-date'))?.['token-date'] || new Date(0) 38 | 39 | chrome.webRequest.onBeforeSendHeaders.addListener(async e => { 40 | // Return if the request was made by Study Tools itself 41 | if (Object.values(e.requestHeaders).find(header => header.name === 'X-Request-Source')?.value === 'study-tools') return 42 | 43 | console.info('Request caught!') 44 | 45 | let userIdWas = userId 46 | let userTokenWas = userToken 47 | let userTokenDateWas = userTokenDate 48 | 49 | let urlUserId = e.url.split('/personen/')[1]?.split('/')[0] 50 | if (urlUserId?.length > 2 && !urlUserId.includes('undefined')) { 51 | userId = urlUserId || userIdWas 52 | chrome.storage.sync.set({ 'user-id': userId }) 53 | if (userIdWas !== userId) console.info(`User ID changed from ${userIdWas} to ${userId}.`) 54 | } 55 | 56 | let authObject = Object.values(e.requestHeaders).find(header => header.name === 'Authorization') 57 | if (authObject) { 58 | userToken = authObject.value 59 | userTokenDate = new Date() 60 | chrome.storage.session.set({ 'token': userToken }) 61 | chrome.storage.session.set({ 'token-date': userTokenDate.getTime() }) 62 | if (userTokenWas !== userToken && new Date(userTokenDateWas).getTime() == 0) console.info(`User token gathered. Length: ${userToken.length}.`) 63 | else if (userTokenWas !== userToken) console.info(`User token changed since ${userTokenDate - userTokenDateWas} ms ago.`) 64 | } 65 | 66 | }, { urls: ['*://*.magister.net/*'] }, ['requestHeaders']) 67 | 68 | console.info("Intercepting HTTP request information to extract token and userId...%c\n\nVrees niet, dit is alleen nodig zodat de extensie API-verzoeken kan maken naar Magister. Deze gegevens blijven op je apparaat. Dit wordt momenteel alleen gebruikt voor de volgende onderdelen:\n" + ["cijferexport", "widgets startpagina", "rooster startpagina", "puntensysteem"].join(', ') + "\n\nen in de toekomst eventueel ook voor:\n" + [].join(', '), "font-size: .8em") 69 | } 70 | 71 | async function setDefaults() { 72 | let syncedStorage = await chrome.storage.sync.get() 73 | let diff = {} 74 | 75 | // Check each setting to see if its value has been defined. If not, set it to the default value. 76 | settings.forEach(category => { 77 | category.settings.forEach(setting => { 78 | if (typeof syncedStorage[setting.id] === 'undefined') { 79 | if (setting.id === 'wallpaper' && syncedStorage['backdrop']?.length > 5) diff[setting.id] = 'custom,' + syncedStorage['backdrop'] 80 | else diff[setting.id] = setting.default 81 | } 82 | }) 83 | }) 84 | 85 | if (Object.keys(diff).length > 0) { 86 | setTimeout(() => chrome.storage.sync.set(diff), 200) 87 | console.info("Set the following storage.sync keys to their default values:", diff) 88 | } 89 | 90 | if (settingsToClear.some(key => Object.keys(syncedStorage).includes(key))) { 91 | chrome.storage.sync.remove(settingsToClear) 92 | console.info("Redundant storage.sync keys removed to free up space.") 93 | } 94 | } 95 | 96 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 97 | switch (request.action) { 98 | case 'popstateDetected': 99 | console.info("Popstate detected, service worker revived.") 100 | return 0 101 | 102 | case 'waitForRequestCompleted': 103 | console.info(`Request completion notification requested by ${sender.url}.`) 104 | chrome.webRequest.onCompleted.addListener((details) => { 105 | sendResponse({ status: 'completed', details: details }) 106 | console.info(`Request completion notification sent to ${sender.url}.`) 107 | }, { urls: ['*://*.magister.net/api/personen/*/aanmeldingen/*/cijfers/extracijferkolominfo/*'] }) 108 | setTimeout(() => { 109 | sendResponse({ status: 'timeout' }) 110 | console.warn(`Request completion notification requested by ${sender.url} has timed out.`) 111 | }, 5000) 112 | return true 113 | 114 | case 'uninstallSelf': 115 | chrome.management.uninstallSelf({ showConfirmDialog: false }, () => { window.location.reload() }) 116 | break 117 | 118 | case 'openOptions': 119 | chrome.tabs.create({ url: `popup/dist/index.html?${request.data}` }); 120 | break; 121 | 122 | default: 123 | return 0 124 | } 125 | }) 126 | 127 | chrome.runtime.onMessageExternal.addListener(async (request, sender, sendResponse) => { 128 | switch (request.action) { 129 | case 'addPersonalTheme': 130 | const obj = request.obj 131 | const storedThemes = Object.values((await chrome.storage.local.get('storedThemes')).storedThemes) 132 | if (!storedThemes || storedThemes.length >= 9) return 133 | 134 | storedThemes.push(obj) 135 | 136 | //TODO: only if not exist 137 | 138 | await chrome.storage.local.set({ 'storedThemes': storedThemes }) 139 | break 140 | 141 | default: 142 | return 0 143 | } 144 | }) 145 | -------------------------------------------------------------------------------- /src/scripts/grades/backup.js: -------------------------------------------------------------------------------- 1 | class GradeBackupPane extends Pane { 2 | id = 'cb'; 3 | icon = ''; 4 | 5 | #div1; 6 | #div2; 7 | 8 | constructor(parentElement) { 9 | super(parentElement); 10 | 11 | this.element.id = 'st-grade-backup-pane'; 12 | this.#div1 = this.element.createChildElement('div', { class: 'st-div' }); 13 | this.element.createChildElement('hr'); 14 | this.#div2 = this.element.createChildElement('div', { class: 'st-div' }); 15 | } 16 | 17 | show() { 18 | this.redraw(); 19 | super.show(); 20 | } 21 | 22 | redraw() { 23 | this.#div1.innerHTML = ''; 24 | this.#div2.innerHTML = ''; 25 | 26 | this.#div1.createChildElement('h3', { class: 'st-section-heading', innerText: i18n('cb.export') }); 27 | this.#div2.createChildElement('h3', { class: 'st-section-heading', innerText: i18n('cb.import') }); 28 | 29 | const year = currentGradeTable?.identifier?.year; 30 | const { backupYear, backupDate } = currentGradeTable?.identifier || {}; 31 | 32 | if (currentGradeTable?.identifier?.year?.id) { 33 | this.#div1.createChildElement('p', { innerText: i18n('cb.exportDesc', { study: year.studie.code, period: year.lesperiode.code }) }); 34 | const exportButton = this.#div1.createChildElement('button', { id: 'st-grade-backup-export', class: 'st-button hero', innerText: i18n('cb.backUpThisTable'), 'data-icon': '' }); 35 | exportButton.addEventListener('click', async () => { 36 | if (!currentGradeTable?.identifier?.year?.id) return; 37 | await this.#exportGrades(currentGradeTable); 38 | }); 39 | } else if (backupYear && backupDate) { 40 | this.#div1.createChildElement('p', { innerText: i18n('cb.importedDesc', { study: backupYear.studie.code, period: backupYear.lesperiode.code, date: new Date(backupDate).toLocaleString(locale) }) }); 41 | } 42 | 43 | this.#div2.createChildElement('p', { innerText: i18n('cb.importDesc') }); 44 | const importButton = this.#div2.createChildElement('button', { id: 'st-grade-backup-import', class: 'st-button hero', innerText: i18n('cb.browse'), 'data-icon': '' }); 45 | const input = this.#div2.createChildElement('input', { type: 'file', accept: '.stgrades,application/json', style: 'display:none' }); 46 | importButton.addEventListener('click', () => input.click()); 47 | input.addEventListener('change', async (event) => { 48 | const file = event.target.files[0]; 49 | if (!file) return; 50 | 51 | const reader = new FileReader(); 52 | reader.onload = async (e) => { 53 | const json = JSON.parse(typeof e.target.result === 'string' ? e.target.result : new TextDecoder().decode(e.target.result)); 54 | await this.#importBackup(json); 55 | }; 56 | reader.readAsText(file); 57 | }); 58 | 59 | this.progressBar.dataset.visible = 'false'; 60 | } 61 | 62 | async #exportGrades(gradeTable) { 63 | return new Promise(async (resolve, reject) => { 64 | try { 65 | this.#div1.classList.add('st-disabled'); 66 | 67 | const grades = await this.#gatherGrades(gradeTable); 68 | const year = gradeTable.identifier.year; 69 | 70 | this.progressBar.dataset.visible = 'true'; 71 | 72 | let uri = `data:application/json;base64,${window.btoa(unescape(encodeURIComponent(JSON.stringify( 73 | { 74 | date: new Date(), 75 | year: { groep: { omschrijving: year.groep.omschrijving, code: year.groep.code }, studie: { code: year.studie.code }, lesperiode: { code: year.lesperiode.code } }, 76 | grades 77 | } 78 | ))))}`, 79 | a = element('a', 'st-grade-backup-temp', document.body, { 80 | download: `Cijferback-up ${year.studie.code} (${year.lesperiode.code}) ${(new Date).toLocaleString()}.stgrades`, 81 | href: uri, 82 | type: 'application/json' 83 | }); 84 | a.click(); 85 | a.remove(); 86 | 87 | setTimeout(() => { 88 | this.progressBar.dataset.visible = 'false'; 89 | }, 500); 90 | 91 | new Dialog({ innerText: i18n('cb.done') }).show(); 92 | 93 | setTimeout(() => { 94 | this.#div1.classList.remove('st-disabled'); 95 | resolve(); 96 | }, 10000) 97 | 98 | } catch (error) { 99 | reject(error); 100 | new Dialog({ innerText: i18n('cb.error') }).show(); 101 | } 102 | }); 103 | } 104 | 105 | async #gatherGrades(gradeTable) { 106 | return new Promise(async (resolve, reject) => { 107 | try { 108 | this.progressBar.firstElementChild.classList.remove('indeterminate'); 109 | this.progressBar.dataset.visible = 'true'; 110 | 111 | const grades = gradeTable.grades.filter(grade => grade.CijferKolom?.Id > 0); 112 | 113 | for (let i = 0; i < grades.length; i++) { 114 | this.progressBar.firstElementChild.style.width = `${(i / grades.length) * 100}%`; 115 | 116 | await new Promise((resolve) => { 117 | let delay = 5; 118 | if (i > 0 && i % 100 === 0) { 119 | delay = 8000; 120 | const dialog = new Dialog({ innerText: i18n('cb.avoidingRateLimit'), allowClose: false }); 121 | dialog.show(); 122 | setTimeout(() => dialog.close(), 8000); 123 | } else if (grades.length > 100) { 124 | delay = 20; 125 | } 126 | setTimeout(resolve, delay); 127 | }); 128 | 129 | const gradeColumnInfo = await magisterApi.gradesColumnInfo(gradeTable.identifier.year, grades[i].CijferKolom.Id); 130 | 131 | grades[i] = { 132 | ...grades[i], 133 | CijferKolom: { ...grades[i].CijferKolom, ...gradeColumnInfo } 134 | } 135 | } 136 | 137 | this.progressBar.firstElementChild.removeAttribute('style'); 138 | this.progressBar.firstElementChild.classList.add('indeterminate'); 139 | this.progressBar.dataset.visible = 'false'; 140 | 141 | resolve(grades); 142 | } catch (error) { 143 | reject(error); 144 | } 145 | }); 146 | } 147 | 148 | async #importBackup(json) { 149 | const { date, year, grades } = json; 150 | 151 | if (!date || !year || !grades || !Array.isArray(grades)) { 152 | new Dialog({ 153 | innerText: "Je back-up is ongeldig of in een verouderde indeling.\n\nJe kunt oude back-ups importeren via onderstaande website.", 154 | buttons: [{ innerText: 'Importeren via website', primary: true, href: 'https://qkeleq10.github.io/studytools/grades#grades' }] 155 | }).show(); 156 | return; 157 | } 158 | 159 | // this.close(); 160 | 161 | const aside = await awaitElement('aside'), container = await awaitElement('.container[id$=container]'), asideResizer = document.querySelector('#st-aside-resize'); 162 | aside.setAttribute('style', 'display:none;width:0px;'); 163 | container.style.paddingRight = '20px'; 164 | if (asideResizer) asideResizer.setAttribute('style', `display:none`); 165 | 166 | if (gradeTables.find(t => t.date?.getTime() === new Date(date).getTime())) { 167 | notify('snackbar', "Je hebt deze back-up al geïmporteerd."); 168 | return; 169 | } 170 | 171 | const newGradeTable = new GradeTable(grades, { backupDate: new Date(date), backupYear: year }); 172 | gradeTables.push(newGradeTable); 173 | 174 | const label = document.getElementById('st-grades-year-filter') 175 | .createChildElement('label', { 176 | class: 'st-checkbox-label icon', 177 | for: `st-year-filter-yearimport${new Date(date).getTime()}`, 178 | innerText: '', 179 | title: `Back-up van ${year.studie.code} (${year.lesperiode.code}) ${new Date(date).toLocaleString()}` 180 | }); 181 | const input = label.createChildElement('input', { id: `st-year-filter-yearimport${new Date(date).getTime()}`, class: 'st-checkbox-input', name: 'st-year-filter', type: 'radio' }); 182 | 183 | input.checked = true; 184 | 185 | input.addEventListener('change', () => { 186 | if (!input.checked) return; 187 | currentGradeTable?.destroy(); 188 | currentGradeTable = newGradeTable; 189 | currentGradeTable.draw(); 190 | }); 191 | 192 | currentGradeTable?.destroy(); 193 | currentGradeTable = newGradeTable; 194 | currentGradeTable.draw(); 195 | } 196 | } -------------------------------------------------------------------------------- /popup/src/components/ThemePreviewImage.vue: -------------------------------------------------------------------------------- 1 | 228 | 229 | 234 | 235 | --------------------------------------------------------------------------------