├── .eslintignore ├── .eslintrc.cjs ├── .github ├── CODE_OF_CONDUCT.md └── workflows │ ├── codeql-analysis.yml │ └── lint.yml ├── .gitignore ├── .prettierrc.json ├── .stylelintrc.json ├── .vscode └── settings.json ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── bun.lockb ├── manifests ├── chrome.json ├── debug.json └── firefox.json ├── package.json ├── privacy_policy.md ├── scripts ├── build.ts ├── constants.ts ├── dev.ts ├── package.ts ├── run_build.ts ├── sveltePlugin.ts ├── utils.ts ├── version.ts └── web-ext.d.ts ├── src ├── background.ts ├── components │ ├── button.svelte │ └── loader.svelte ├── content │ ├── end.ts │ ├── highlightjs │ │ ├── LICENSE │ │ └── highlight.min.js │ ├── loader.ts │ ├── pages │ │ ├── Course │ │ │ ├── Course.svelte │ │ │ ├── Course.ts │ │ │ ├── course-group.svelte │ │ │ └── task-modal.svelte │ │ ├── Error │ │ │ ├── Error.svelte │ │ │ └── Error.ts │ │ ├── Exam.ts │ │ ├── Logged.ts │ │ ├── Login.ts │ │ ├── Main │ │ │ ├── Main.svelte │ │ │ ├── Main.ts │ │ │ └── menu-item.svelte │ │ ├── Page.ts │ │ ├── Results.ts │ │ └── Task.ts │ ├── start.ts │ └── utils.ts ├── dev │ ├── dev.svelte │ ├── index.html │ └── index.ts ├── events.ts ├── icon.png ├── messages.ts ├── settings.ts ├── settings │ ├── index.html │ ├── index.ts │ └── settings.svelte ├── themes │ ├── assets │ │ ├── background-dark.PNG │ │ ├── background-light.PNG │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── aag.svg │ │ │ ├── ag1.svg │ │ │ ├── compile.svg │ │ │ ├── faq.svg │ │ │ ├── osy.svg │ │ │ ├── pa1.svg │ │ │ ├── pa2.svg │ │ │ ├── pdp.svg │ │ │ ├── pjv.svg │ │ │ ├── ps1.svg │ │ │ ├── pyt.svg │ │ │ ├── settings.svg │ │ │ └── unknown.svg │ │ ├── notifications_active.svg │ │ ├── notifications_none.svg │ │ ├── p.svg │ │ ├── shibboleth-inverted.png │ │ ├── spinner.svg │ │ └── turret.ogg │ ├── automatic.css │ ├── common.css │ ├── dark.css │ ├── light.css │ ├── loading │ │ ├── off.css │ │ └── on.css │ ├── orig-dark.css │ └── svelte.css └── types.d.ts ├── tsconfig.json └── update.json /.eslintignore: -------------------------------------------------------------------------------- 1 | src/content/highlightjs 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 4 | parser: "@typescript-eslint/parser", 5 | plugins: ["@typescript-eslint"], 6 | root: true, 7 | env: { 8 | browser: true, 9 | webextensions: true, 10 | }, 11 | rules: { 12 | "@typescript-eslint/ban-ts-comment": "warn", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at keombre8@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [stable] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [stable] 9 | schedule: 10 | - cron: '0 12 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['javascript'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: oven-sh/setup-bun@v1 11 | - run: bun install 12 | - run: bun lint 13 | env: 14 | CI: true 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./vscode/ipch/* 2 | test_pages/ 3 | out 4 | node_modules 5 | build 6 | .env 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "plugins": ["prettier-plugin-svelte"], 4 | "overrides": [ 5 | { 6 | "files": "*.svelte", 7 | "options": { 8 | "parser": "svelte" 9 | } 10 | }, 11 | { 12 | "files": "*.json", 13 | "options": { 14 | "parser": "json", 15 | "tabWidth": 2 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "rules": { 4 | "selector-class-pattern": ".*", 5 | "color-hex-length": "long", 6 | "alpha-value-notation": "number" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "debug.json": "jsonc" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | This is a database of all the information about developing the extension I can think of. 4 | 5 | ## Test Pages 6 | 7 | When developing, you can place the HTML files gathered from issues into `test_pages/`. 8 | 9 | The folder structure follows the search params of the URL, e.g. `?X=Course&Cou=123` maps to `test_pages/course/123.html`. 10 | 11 | - `?X=[folder_name]` 12 | - `?Cou=[file_name]` 13 | 14 | The file can optionally include a comment in the first line describing what the page is. 15 | 16 | ```html 17 | 18 | ...the rest of the file 19 | ``` 20 | 21 | Then, when running `bun dev`, if you add the `ptmock.localhost 127.0.0.1` entry into your `/etc/hosts`, you can visit `http://ptmock.localhost:3000/`, which will contain a list of all the test pages with their comments. If you open one of them, the URL and the page should be the same as if you went to Progtest. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Keombre 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Progtest Themes 2 | 3 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/stars/eoofjghfpdplnjhbfflfnfogdjnedgjf)](https://chrome.google.com/webstore/detail/progtest-themes/eoofjghfpdplnjhbfflfnfogdjnedgjf) 4 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/eoofjghfpdplnjhbfflfnfogdjnedgjf)](https://chrome.google.com/webstore/detail/progtest-themes/eoofjghfpdplnjhbfflfnfogdjnedgjf) 5 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/users/eoofjghfpdplnjhbfflfnfogdjnedgjf?label=chrome%20users&logo=google-chrome&logoColor=fff)](https://chrome.google.com/webstore/detail/progtest-themes/eoofjghfpdplnjhbfflfnfogdjnedgjf) 6 | [![GitHub All Releases](https://img.shields.io/github/downloads/keombre/progtest-theme/total?label=firefox%20download&logo=mozilla-firefox&logoColor=fff)](https://github.com/keombre/progtest-theme/releases/latest) 7 | [![CodeFactor](https://www.codefactor.io/repository/github/keombre/progtest-theme/badge)](https://www.codefactor.io/repository/github/keombre/progtest-theme/overview) 8 | 9 | **ProgTest Themes** is a WebExtension for Google Chrome and Mozilla Firefox which works as a theme manager for [ProgTest](https://progtest.fit.cvut.cz). It adds other useful features, such as syntax highlighting or notifications. 10 | 11 | ## Project Status 12 | This is a college student project. 13 | 14 | I am very grateful for every pull request that happens here. It makes the FIT life that much better. 15 | 16 | Thank you. ♥️ 17 | 18 | Unfortunately, both me, the original author, as well as the maintainers that have come after me have had their personal goals change in a very short span of time. This project is something you focus on only during your study years and is very hard to keep updated once you finish school. 19 | 20 | If you, the user, a bug fixer, or even better, an innovator want to help maintain this project, send me an email or open an issue. 21 | 22 | After all, there is no limit to the number of managers in the Chrome WebStore and every helpful hand is more than welcome! 23 | 24 | ## Download 25 | 26 | [Chrome Web Store](https://chrome.google.com/webstore/detail/progtest-themes/eoofjghfpdplnjhbfflfnfogdjnedgjf) (supports any Chromium browser, e.g. Opera, Brave, new Microsoft Edge, etc.) 27 | 28 | [Firefox Addon](https://github.com/keombre/progtest-theme/releases/latest) 29 | 30 | ## Building from source 31 | 32 | In order to build from source, you are going to need `bun`. 33 | 34 | The current version of **ProgTest Themes** has been successfully compiled using `bun 1.0.6`. 35 | 36 | 1. Install all dev dependencies using `bun install` 37 | 2. Run `bun dev` to get a hot-reloading version of the extension in `build/`. Alternatively, run `bun build:chrome` or `bun build:firefox` to build it just once (and with the proper manifests; the dev version uses `manifests/debug.json`). 38 | 3. Load the extension to your browser 39 | - `chrome://extensions` in Chrome 40 | - `about:addons` (if building) / `about:debugging#/runtime/this-firefox` (if developing) in Firefox 41 | 42 | ## Privacy policy 43 | 44 | In short, we don't collect any data, but you can read it in full [here](https://github.com/keombre/progtest-theme/blob/stable/privacy_policy.md). 45 | 46 | ## Creating new theme 47 | 48 | Checkout from [primer branch](https://github.com/keombre/progtest-theme/tree/primer). The project there is restructured to allow multiple theme engines. To see how it all works look at `src/content/end.js` and into `src\themes`. 49 | 50 | Good luck! 51 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/bun.lockb -------------------------------------------------------------------------------- /manifests/chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ProgTest Themes", 3 | "version": "1.2.0", 4 | "description": "Theme manager for ProgTest", 5 | "manifest_version": 3, 6 | "content_scripts": [ 7 | { 8 | "run_at": "document_start", 9 | "js": ["content/start.js", "content/highlightjs/highlight.min.js"], 10 | "css": ["themes/loading/on.css"], 11 | "matches": ["https://progtest.fit.cvut.cz/*"], 12 | "all_frames": true 13 | }, 14 | { 15 | "run_at": "document_end", 16 | "js": ["content/end.js"], 17 | "matches": ["https://progtest.fit.cvut.cz/*"], 18 | "all_frames": true 19 | } 20 | ], 21 | "web_accessible_resources": [ 22 | { 23 | "resources": ["content/loader.js"], 24 | "matches": ["https://progtest.fit.cvut.cz/*"] 25 | }, 26 | { 27 | "resources": ["themes/*"], 28 | "matches": ["https://progtest.fit.cvut.cz/*"] 29 | } 30 | ], 31 | "background": { "service_worker": "background.js" }, 32 | "permissions": ["storage", "tabs"], 33 | "host_permissions": [ 34 | "https://progtest.fit.cvut.cz/*", 35 | "https://courses.fit.cvut.cz/data/courses-all.json" 36 | ], 37 | "action": { 38 | "default_title": "", 39 | "default_icon": "icon.png", 40 | "default_popup": "settings/index.html" 41 | }, 42 | "icons": { "128": "icon.png" } 43 | } 44 | -------------------------------------------------------------------------------- /manifests/debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ProgTest Themes", 3 | "version": "101.0.0", 4 | "description": "Theme manager for ProgTest", 5 | "manifest_version": 3, 6 | "content_scripts": [ 7 | { 8 | "run_at": "document_start", 9 | "js": ["content/start.js", "content/highlightjs/highlight.min.js"], 10 | "css": ["themes/loading/on.css"], 11 | "matches": [ 12 | "https://progtest.fit.cvut.cz/*", 13 | "http://ptmock.localhost/*" 14 | ], 15 | "all_frames": true 16 | }, 17 | { 18 | "run_at": "document_end", 19 | "js": ["content/end.js"], 20 | "matches": [ 21 | "https://progtest.fit.cvut.cz/*", 22 | "http://ptmock.localhost/*" 23 | ], 24 | "all_frames": true 25 | } 26 | ], 27 | "web_accessible_resources": [ 28 | { 29 | "resources": ["content/loader.js"], 30 | "matches": ["https://progtest.fit.cvut.cz/*", "http://ptmock.localhost/*"] 31 | }, 32 | { 33 | "resources": ["themes/*"], 34 | "matches": ["https://progtest.fit.cvut.cz/*", "http://ptmock.localhost/*"] 35 | } 36 | ], 37 | "background": { 38 | // Firefox 39 | "scripts": ["background.js"] 40 | 41 | // Chrome 42 | // "service_worker": "background.js" 43 | }, 44 | "permissions": ["storage", "tabs"], 45 | "host_permissions": [ 46 | "https://progtest.fit.cvut.cz/*", 47 | "http://ptmock.localhost/", 48 | "https://courses.fit.cvut.cz/data/courses-all.json" 49 | ], 50 | "action": { 51 | "default_title": "", 52 | "default_icon": "icon.png", 53 | "default_popup": "settings/index.html" 54 | }, 55 | "icons": { "128": "icon.png" }, 56 | "browser_specific_settings": { 57 | "gecko": { 58 | "id": "progtest-themes@debug.dev", 59 | "update_url": "https://raw.githubusercontent.com/keombre/progtest-theme/stable/update.json" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /manifests/firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ProgTest Themes", 3 | "version": "1.2.0", 4 | "description": "Theme manager for ProgTest", 5 | "manifest_version": 3, 6 | "content_scripts": [ 7 | { 8 | "run_at": "document_start", 9 | "js": ["content/start.js", "content/highlightjs/highlight.min.js"], 10 | "css": ["themes/loading/on.css"], 11 | "matches": ["https://progtest.fit.cvut.cz/*"], 12 | "all_frames": true 13 | }, 14 | { 15 | "run_at": "document_end", 16 | "js": ["content/end.js"], 17 | "matches": ["https://progtest.fit.cvut.cz/*"], 18 | "all_frames": true 19 | } 20 | ], 21 | "web_accessible_resources": [ 22 | { 23 | "resources": ["content/loader.js"], 24 | "matches": ["https://progtest.fit.cvut.cz/*"] 25 | }, 26 | { 27 | "resources": ["themes/*"], 28 | "matches": ["https://progtest.fit.cvut.cz/*"] 29 | } 30 | ], 31 | "background": { "scripts": ["background.js"] }, 32 | "permissions": ["storage", "tabs"], 33 | "host_permissions": [ 34 | "https://progtest.fit.cvut.cz/*", 35 | "https://courses.fit.cvut.cz/data/courses-all.json" 36 | ], 37 | "action": { 38 | "default_title": "", 39 | "default_icon": "icon.png", 40 | "default_popup": "settings/index.html" 41 | }, 42 | "icons": { "128": "icon.png" }, 43 | "browser_specific_settings": { 44 | "gecko": { 45 | "id": "progtest-themes@keombre", 46 | "update_url": "https://raw.githubusercontent.com/keombre/progtest-theme/stable/update.json" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "progtest-theme", 3 | "version": "1.2.0", 4 | "description": "Theme manager for ProgTest", 5 | "main": "src/background.js", 6 | "scripts": { 7 | "build": "bun run scripts/run_build.ts", 8 | "build:firefox": "bun run build && bun run scripts/package.ts --firefox", 9 | "build:chrome": "bun run build && bun run scripts/package.ts --chrome", 10 | "dev": "bun run scripts/dev.ts", 11 | "lint": "stylelint **/*.css; eslint .", 12 | "lint:fix": "prettier --write .; stylelint **/*.css --fix; eslint . --fix", 13 | "sign:firefox": "bun run build && bun run scripts/package.ts --firefox --sign", 14 | "version": "bun run scripts/version.ts" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/keombre/progtest-theme.git" 19 | }, 20 | "author": "keombre", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/keombre/progtest-theme/issues" 24 | }, 25 | "homepage": "https://github.com/keombre/progtest-theme", 26 | "devDependencies": { 27 | "@types/archiver": "^5.3.4", 28 | "@types/chrome": "^0.0.248", 29 | "@types/minimist": "^1.2.4", 30 | "@types/web": "^0.0.117", 31 | "@typescript-eslint/eslint-plugin": "^6.8.0", 32 | "@typescript-eslint/parser": "^6.8.0", 33 | "archiver": "^6.0.1", 34 | "bun-types": "^1.0.6", 35 | "eslint": "^8.51.0", 36 | "minimist": ">=1.2.8", 37 | "normalize.css": "^8.0.1", 38 | "prettier": "^3.0.3", 39 | "prettier-plugin-svelte": "^3.0.3", 40 | "stylelint": "^15.11.0", 41 | "stylelint-config-standard": "^34.0.0", 42 | "typescript": "^5.2.2", 43 | "web-ext": "^7.8.0" 44 | }, 45 | "dependencies": { 46 | "classnames": "^2.3.2", 47 | "iconify-icon": "^1.0.8", 48 | "svelte": "^4.2.2", 49 | "svelte-preprocess": "^5.0.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /privacy_policy.md: -------------------------------------------------------------------------------- 1 | PRIVACY POLICY 2 | ============== 3 | 4 | What information do we collect? 5 | ------------------------------- 6 | 7 | Any collected information from site https://progtest.fit.cvut.cz may be saved to the browser. Your information does not leave 8 | your browser and is not shared with any other site. 9 | Additional information may be collected by your browser developer to provide usage information for this extension. Please 10 | refer to your browser's developer for further information. 11 | 12 | How do we use your information? 13 | ------------------------------- 14 | 15 | All locally stored information is used to personalize your site experience. No personal information (including your username and 16 | password) is saved. 17 | 18 | Do we disclose the information we collect to outside parties? 19 | ------------------------------- 20 | 21 | No, and we never will. This is an open-source project with no intent to ever be monetized. 22 | 23 | Changes to our policy 24 | --------------------- 25 | 26 | If we decide to change our privacy policy, we will post those changes on this page. Policy changes will apply only to information 27 | collected after the date of the change. This policy was last modified on January 22, 2020. 28 | 29 | Your consent 30 | ------------ 31 | 32 | By using our extension, you consent to our privacy policy. 33 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { BUILD_DIR, ENTRYPOINTS, SRC_DIR } from "./constants"; 2 | import { mkdir, rm, cp } from "fs/promises"; 3 | import { copyDirectory } from "./utils"; 4 | import { sveltePlugin } from "./sveltePlugin"; 5 | 6 | export async function build(options: { verbose: boolean; clean: boolean }) { 7 | if (options.clean) { 8 | await rm(BUILD_DIR, { recursive: true, force: true }); 9 | } 10 | 11 | try { 12 | await mkdir(BUILD_DIR); 13 | } catch (e) { 14 | if ((e as ErrnoException)?.code !== "EEXIST") { 15 | console.error(e); 16 | } 17 | } 18 | 19 | console.log("Building .js files"); 20 | const buildOutput = await Bun.build({ 21 | entrypoints: Array(...ENTRYPOINTS), 22 | outdir: BUILD_DIR, 23 | plugins: [sveltePlugin], 24 | }); 25 | if (!buildOutput.success) { 26 | console.error("Build failed:", buildOutput); 27 | return; 28 | } 29 | console.log("Build finished", buildOutput); 30 | 31 | console.log("Copying other files"); 32 | await copyDirectory(SRC_DIR, BUILD_DIR, { 33 | filter: (path) => { 34 | if (path.endsWith("highlight.min.js")) { 35 | return true; 36 | } 37 | if ( 38 | path.endsWith(".js") || 39 | path.endsWith(".ts") || 40 | path.endsWith(".svelte") 41 | ) { 42 | return false; 43 | } 44 | return true; 45 | }, 46 | verbose: options.verbose, 47 | }); 48 | await cp( 49 | `./node_modules/normalize.css/normalize.css`, 50 | `${BUILD_DIR}/external/normalize.css`, 51 | ); 52 | await cp( 53 | `./node_modules/iconify-icon/dist/iconify-icon.min.js`, 54 | `${BUILD_DIR}/external/iconify-icon.min.js`, 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /scripts/constants.ts: -------------------------------------------------------------------------------- 1 | export const SRC_DIR = "./src" as const; 2 | export const ENTRYPOINTS = [ 3 | "./src/background.ts", 4 | "./src/content/end.ts", 5 | "./src/content/loader.ts", 6 | "./src/content/start.ts", 7 | "./src/settings/index.ts", 8 | "./src/dev/index.ts", 9 | ] as const; 10 | 11 | export const BUILD_DIR = "./build" as const; 12 | export const OUT_DIR = "./out" as const; 13 | 14 | export const CHROME_MANIFEST = "./manifests/chrome.json" as const; 15 | export const FIREFOX_MANIFEST = "./manifests/firefox.json" as const; 16 | -------------------------------------------------------------------------------- /scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import { watch } from "fs"; 2 | import { cp } from "fs/promises"; 3 | import { build } from "./build"; 4 | import { walkDir } from "./utils"; 5 | 6 | const watchedExtensions = [".ts", ".js", ".json", ".html", ".css", ".svelte"]; 7 | 8 | async function additionalDevSteps() { 9 | await cp("./manifests/debug.json", "./build/manifest.json"); 10 | } 11 | 12 | console.log("Building extension..."); 13 | build({ verbose: false, clean: true }) 14 | .then(additionalDevSteps) 15 | .then(() => { 16 | const exit = () => { 17 | watchers.forEach((w) => w.close()); 18 | process.exit(0); 19 | }; 20 | 21 | const onChange = async (eventType: string, filename: string | null) => { 22 | if (!watchedExtensions.some((ext) => filename?.endsWith(ext))) { 23 | console.log(`Ignoring change in ${filename}`); 24 | return; 25 | } 26 | if (eventType === "error") { 27 | console.error(`Error occurred with ${filename}, exiting...`); 28 | exit(); 29 | } 30 | console.log(`${eventType} detected in ${filename}, rebuilding...`); 31 | await build({ verbose: false, clean: false }); 32 | await additionalDevSteps(); 33 | console.log("Extension rebuilt"); 34 | }; 35 | 36 | const watchers = [ 37 | watch("./src/", { recursive: true }, onChange), 38 | watch("./manifests/", { recursive: true }, onChange), 39 | ]; 40 | process.on("SIGINT", exit); 41 | console.log("Watching for changes..."); 42 | }) 43 | .then(() => { 44 | const server = Bun.serve({ 45 | async fetch(req: Request) { 46 | const url = new URL(req.url); 47 | console.log(`Serving ${url}`); 48 | 49 | // test page list endpoint 50 | if (url.pathname === "/test_pages") { 51 | const pages: { url: string; description: string }[] = []; 52 | for await (const filePath of walkDir("./test_pages")) { 53 | const file = await Bun.file(filePath).text(); 54 | const description = file 55 | .split("\n")[0] 56 | .replace("", "") 58 | .trim(); 59 | 60 | const splitPath = filePath.split("/"); 61 | const folderName = splitPath.slice(-2)[0]; 62 | const fileName = splitPath.slice(-1)[0]; 63 | 64 | const url = new URL("/index.php", req.url); 65 | url.searchParams.set("X", folderName); 66 | url.searchParams.set( 67 | "Cou", 68 | fileName.replace(".html", ""), 69 | ); 70 | 71 | pages.push({ 72 | url: url.toString(), 73 | description: description.includes( 74 | 'xml version="1.0" encoding="utf-8"', 75 | ) 76 | ? filePath 77 | : description, 78 | }); 79 | } 80 | return Response.json(pages); 81 | } 82 | 83 | // test page endpoints 84 | if (url.pathname.startsWith("/test_pages")) { 85 | return new Response(Bun.file(`.${url.pathname}`)); 86 | } 87 | if (url.pathname === "/index.php") { 88 | return new Response( 89 | Bun.file( 90 | `./test_pages/${url.searchParams.get( 91 | "X", 92 | )}/${url.searchParams.get("Cou")}.html`, 93 | ), 94 | ); 95 | } 96 | if (url.pathname.endsWith("css.css")) { 97 | return fetch("https://progtest.fit.cvut.cz/css.css"); 98 | } 99 | if (url.pathname.endsWith("shared.js")) { 100 | return fetch("https://progtest.fit.cvut.cz/shared.js"); 101 | } 102 | 103 | // dev server index 104 | if (url.pathname === "/") { 105 | return Response.redirect("/dev/index.html"); 106 | } 107 | 108 | return new Response(Bun.file(`./build/${url.pathname}`)); 109 | }, 110 | }); 111 | console.log(`Dev server running on http://localhost:${server.port}/`); 112 | }); 113 | -------------------------------------------------------------------------------- /scripts/package.ts: -------------------------------------------------------------------------------- 1 | import { cp, mkdir, rm } from "fs/promises"; 2 | import webExt from "web-ext"; 3 | import { 4 | BUILD_DIR, 5 | CHROME_MANIFEST, 6 | FIREFOX_MANIFEST, 7 | OUT_DIR, 8 | } from "./constants"; 9 | import { zipDirectory } from "./utils"; 10 | import minimist from "minimist"; 11 | 12 | const MANIFEST_TARGET = `${BUILD_DIR}/manifest.json` as const; 13 | const FIREFOX_DIST_DIR = `${OUT_DIR}/firefox` as const; 14 | const CHROME_DIST_DIR = `${OUT_DIR}/chrome` as const; 15 | 16 | async function pack(distDir: string, useSystemUtilities: boolean) { 17 | await rm(distDir, { recursive: true }); 18 | await mkdir(distDir); 19 | await zipDirectory(BUILD_DIR, `${distDir}/progtest_themes.zip`, { 20 | useSystemUtilities, 21 | }); 22 | } 23 | 24 | async function signFirefox() { 25 | if (!process.env.WEB_EXT_API_KEY || !process.env.WEB_EXT_API_SECRET) { 26 | throw new Error( 27 | "WEB_EXT_API_KEY and WEB_EXT_API_SECRET must be set in the environment", 28 | ); 29 | } 30 | 31 | await cp(FIREFOX_MANIFEST, MANIFEST_TARGET); 32 | await rm(FIREFOX_DIST_DIR, { recursive: true }); 33 | await mkdir(FIREFOX_DIST_DIR); 34 | await webExt.cmd.sign( 35 | { 36 | sourceDir: BUILD_DIR, 37 | artifactsDir: FIREFOX_DIST_DIR, 38 | apiKey: process.env.WEB_EXT_API_KEY, 39 | apiSecret: process.env.WEB_EXT_API_SECRET, 40 | channel: "unlisted", 41 | /** upcoming Mozilla API */ 42 | // useSubmissionApi: true, 43 | // amoBaseUrl: "https://addons.mozilla.org/api/v5", 44 | }, 45 | { shouldExitProgram: false }, 46 | ); 47 | } 48 | 49 | async function packChrome(useSystemUtilities: boolean) { 50 | cp(CHROME_MANIFEST, MANIFEST_TARGET); 51 | pack(CHROME_DIST_DIR, useSystemUtilities); 52 | } 53 | 54 | async function packFirefox(useSystemUtilities: boolean) { 55 | cp(FIREFOX_MANIFEST, MANIFEST_TARGET); 56 | pack(FIREFOX_DIST_DIR, useSystemUtilities); 57 | } 58 | 59 | async function main() { 60 | const args = minimist(process.argv.slice(2), { 61 | boolean: ["chrome", "firefox", "sign", "system"], 62 | default: { 63 | chrome: false, 64 | firefox: false, 65 | sign: false, 66 | system: false, 67 | }, 68 | }); 69 | 70 | if (args.chrome) { 71 | if (args.sign) { 72 | throw new Error("Cannot sign Chrome extension"); 73 | } 74 | await packChrome(args.system); 75 | return; 76 | } 77 | 78 | if (args.firefox) { 79 | if (args.sign) { 80 | if (args.system) { 81 | throw new Error("System utilities cannot be used with signing"); 82 | } 83 | await signFirefox(); 84 | } else { 85 | await packFirefox(args.system); 86 | } 87 | return; 88 | } 89 | 90 | console.log( 91 | "Use '--chrome' or '--firefox' to build a specific browser extension", 92 | ); 93 | console.log("Use '--firefox --sign' to create a signed Firefox extension"); 94 | console.log("Use '--system' to use system utilities for zipping"); 95 | } 96 | 97 | main(); 98 | -------------------------------------------------------------------------------- /scripts/run_build.ts: -------------------------------------------------------------------------------- 1 | import { build } from "./build"; 2 | 3 | build({ verbose: true, clean: true }); 4 | -------------------------------------------------------------------------------- /scripts/sveltePlugin.ts: -------------------------------------------------------------------------------- 1 | import { compile, preprocess } from "svelte/compiler"; 2 | import { readFile } from "fs/promises"; 3 | import svelte from "svelte-preprocess"; 4 | import { BunPlugin } from "bun"; 5 | 6 | export const sveltePlugin: BunPlugin = { 7 | name: "svelte loader", 8 | async setup(builder) { 9 | builder.onLoad({ filter: /\.svelte$/ }, async ({ path }) => { 10 | const preprocessed = await preprocess( 11 | await readFile(path, "utf8"), 12 | svelte(), 13 | { 14 | filename: path, 15 | }, 16 | ); 17 | 18 | return { 19 | // Use the preprocessor of your choice. 20 | contents: compile(preprocessed.code, { 21 | filename: path, 22 | generate: "dom", 23 | dev: Bun.main.endsWith("dev.ts"), 24 | }).js.code, 25 | loader: "js", 26 | }; 27 | }); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | import archiver from "archiver"; 2 | import { cp, rm, exists, mkdir, readdir, stat } from "fs/promises"; 3 | import { createWriteStream } from "fs"; 4 | import { BUILD_DIR } from "./constants"; 5 | 6 | export async function* walkDir(dir: string): AsyncGenerator { 7 | const files = await readdir(dir); 8 | for (const file of files) { 9 | const path = `${dir}/${file}`; 10 | const stats = await stat(path); 11 | if (stats.isDirectory()) { 12 | yield* walkDir(path); 13 | } else { 14 | yield path; 15 | } 16 | } 17 | } 18 | 19 | export async function copyDirectory( 20 | from: string, 21 | to: string, 22 | options?: { 23 | filter?: (path: string) => boolean; 24 | verbose?: boolean; 25 | }, 26 | ) { 27 | // cut trailing slash if present 28 | from = from.replace(/\/$/, ""); 29 | to = to.replace(/\/$/, ""); 30 | 31 | for await (const filePath of walkDir(from)) { 32 | if (options?.filter && !options.filter(filePath)) { 33 | continue; 34 | } 35 | 36 | const directoryPath = filePath 37 | .substring(0, filePath.lastIndexOf("/")) 38 | .replace(from, to); 39 | if (!(await exists(directoryPath))) { 40 | options?.verbose && 41 | console.log(`Creating directory ${directoryPath}`); 42 | await mkdir(directoryPath, { recursive: true }); 43 | } 44 | 45 | const toPath = filePath.replace(from, to); 46 | options?.verbose && console.log(`Copying ${filePath} to ${toPath}`); 47 | await cp(filePath, toPath); 48 | } 49 | } 50 | 51 | export async function zipDirectory( 52 | sourceDir: string, 53 | outPath: string, 54 | options?: { 55 | zipDestPath?: string; 56 | useSystemUtilities?: boolean; 57 | }, 58 | ) { 59 | if (options?.useSystemUtilities) { 60 | console.info("Zipping with system utilities"); 61 | await rm(outPath, { force: true }); 62 | // . + ./ => ../ 63 | const proc = Bun.spawn(["zip", "-r", `.${outPath}`, "."], { 64 | cwd: BUILD_DIR, 65 | }); 66 | await proc.exited; 67 | const output = await new Response(proc.stdout).text(); 68 | const error = await new Response(proc.stderr).text(); 69 | console.log(output); 70 | console.error(error); 71 | } else { 72 | console.log(`Zipping ${sourceDir} to ${outPath}`); 73 | const archive = archiver("zip", { zlib: { level: 9 } }); 74 | 75 | const stream = createWriteStream(outPath); 76 | stream.on("error", (err) => { 77 | console.error(err); 78 | throw err; 79 | }); 80 | 81 | archive 82 | .on("error", (err) => { 83 | console.error(err); 84 | throw err; 85 | }) 86 | .directory(sourceDir, options?.zipDestPath ?? false) 87 | .pipe(stream); 88 | 89 | await archive.finalize(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /scripts/version.ts: -------------------------------------------------------------------------------- 1 | import minimist from "minimist"; 2 | import { CHROME_MANIFEST, FIREFOX_MANIFEST } from "./constants"; 3 | import { format, resolveConfig } from "prettier"; 4 | 5 | async function writeFormattedJson( 6 | filePath: string, 7 | content: Data extends object ? Data : never, 8 | ) { 9 | const prettierConfig = await resolveConfig(filePath); 10 | const output = await format(JSON.stringify(content), { 11 | parser: "json", 12 | ...prettierConfig, 13 | }); 14 | Bun.write(filePath, output); 15 | } 16 | 17 | async function incrementVersionInFile( 18 | filePath: string, 19 | type: "major" | "minor" | "patch", 20 | ) { 21 | const file = Bun.file(filePath); 22 | const content = await file.json(); 23 | const version = content.version as string; 24 | const [major, minor, patch] = version.split(".").map((v) => parseInt(v)); 25 | switch (type) { 26 | case "major": 27 | content.version = `${major + 1}.0.0`; 28 | break; 29 | case "minor": 30 | content.version = `${major}.${minor + 1}.0`; 31 | break; 32 | case "patch": 33 | content.version = `${major}.${minor}.${patch + 1}`; 34 | break; 35 | } 36 | 37 | console.log(`Updating ${filePath} to ${content.version}`); 38 | writeFormattedJson(filePath, content); 39 | } 40 | 41 | async function addUpdateToList(updateFile: string) { 42 | const packageJson = await Bun.file("./package.json").json(); 43 | const version = packageJson.version as string; 44 | 45 | const firefoxManifest = await Bun.file(FIREFOX_MANIFEST).json(); 46 | const appId = firefoxManifest.applications.gecko.id; 47 | 48 | const updateEntry = { 49 | version, 50 | update_link: `https://github.com/keombre/progtest-theme/releases/download/${version}/progtest_themes-${version}-an+fx.xpi`, 51 | }; 52 | 53 | const updateJson = await Bun.file(updateFile).json(); 54 | const addon = updateJson.addons[appId]; 55 | if (!addon) { 56 | updateJson.addons[appId] = { updates: [updateEntry] }; 57 | } else { 58 | updateJson.addons[appId].updates.push(updateEntry); 59 | } 60 | 61 | console.log(`Adding ${version} to ${updateFile}`); 62 | writeFormattedJson(updateFile, updateJson); 63 | } 64 | 65 | async function main() { 66 | const args = minimist(process.argv.slice(2), { 67 | boolean: ["major", "minor", "patch"], 68 | default: { 69 | major: false, 70 | minor: false, 71 | patch: false, 72 | }, 73 | }); 74 | 75 | if ( 76 | (args.major && args.minor) || 77 | (args.major && args.patch) || 78 | (args.minor && args.patch) 79 | ) { 80 | throw new Error("Cannot increment multiple numbers at the same time"); 81 | } 82 | 83 | if (!args.major && !args.minor && !args.patch) { 84 | throw new Error( 85 | "Must specify a version increment type ('--major', '--minor', or '--patch')", 86 | ); 87 | } 88 | 89 | const type = args.major ? "major" : args.minor ? "minor" : "patch"; 90 | 91 | for (const file of [ 92 | CHROME_MANIFEST, 93 | FIREFOX_MANIFEST, 94 | "./manifests/debug.json", 95 | "./package.json", 96 | ]) { 97 | await incrementVersionInFile(file, type); 98 | } 99 | 100 | await addUpdateToList("./update.json"); 101 | } 102 | 103 | main(); 104 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from "./messages"; 2 | import { DEFAULT_SETTINGS } from "./settings"; 3 | 4 | async function migrateSettingNames(): Promise { 5 | return new Promise((resolve) => { 6 | chrome.storage.sync.get( 7 | [ 8 | "selectedTheme", 9 | "autoHide", 10 | "notifications", 11 | "highlighting", 12 | "sounds", 13 | ], 14 | function (items) { 15 | if (items === undefined) { 16 | return resolve(); 17 | } 18 | if (items.selectedTheme) { 19 | items.theme = items.selectedTheme; 20 | delete items.selectedTheme; 21 | } 22 | if (items.autoHide) { 23 | items.autohideResults = items.autoHide; 24 | delete items.autoHide; 25 | } 26 | if (items.notifications) { 27 | items.showNotifications = items.notifications; 28 | delete items.notifications; 29 | } 30 | if (items.highlighting) { 31 | items.syntaxHighlighting = items.highlighting; 32 | delete items.highlighting; 33 | } 34 | if (items.sounds) { 35 | items.playSounds = items.sounds; 36 | delete items.sounds; 37 | } 38 | chrome.storage.sync.set(items, resolve); 39 | }, 40 | ); 41 | }); 42 | } 43 | 44 | const syncSettings = async () => { 45 | console.log("Syncing PTT settings"); 46 | const localSettings = await chrome.storage.local.get(DEFAULT_SETTINGS); 47 | const syncSettings = await chrome.storage.sync.get(DEFAULT_SETTINGS); 48 | console.log("localSettings", localSettings); 49 | console.log("syncSettings", syncSettings); 50 | if (localSettings !== syncSettings) { 51 | if ( 52 | localSettings === DEFAULT_SETTINGS && 53 | syncSettings !== DEFAULT_SETTINGS 54 | ) { 55 | await chrome.storage.sync.set(localSettings); 56 | } 57 | if ( 58 | localSettings !== DEFAULT_SETTINGS && 59 | syncSettings === DEFAULT_SETTINGS 60 | ) { 61 | await chrome.storage.local.set(syncSettings); 62 | } 63 | } 64 | }; 65 | 66 | chrome.runtime.onInstalled.addListener(async () => { 67 | await migrateSettingNames(); 68 | syncSettings(); 69 | }); 70 | chrome.runtime.onStartup.addListener(async () => { 71 | await migrateSettingNames(); 72 | syncSettings(); 73 | }); 74 | chrome.storage.sync.onChanged.addListener(syncSettings); 75 | chrome.storage.local.onChanged.addListener(syncSettings); 76 | 77 | chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { 78 | console.log("message", message); 79 | if (message.type === MessageType.GET_SETTINGS) { 80 | chrome.storage.local.get(DEFAULT_SETTINGS, (settings) => { 81 | sendResponse(settings); 82 | }); 83 | } 84 | return true; 85 | }); 86 | -------------------------------------------------------------------------------- /src/components/button.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/components/loader.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | {#if isVisible} 28 |
29 |
30 |
31 |
32 | 47 |
48 | {/if} 49 | 50 | 109 | -------------------------------------------------------------------------------- /src/content/end.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from "../messages"; 2 | import { ExtensionSettings } from "../settings"; 3 | import { Course } from "./pages/Course/Course"; 4 | import { ErrorPage } from "./pages/Error/Error"; 5 | import { Exam } from "./pages/Exam"; 6 | import { Logged } from "./pages/Logged"; 7 | import { Login } from "./pages/Login"; 8 | import { Main } from "./pages/Main/Main"; 9 | import { Results } from "./pages/Results"; 10 | import { Task } from "./pages/Task"; 11 | import { pttLoadedEvent } from "../events"; 12 | import { Page } from "./pages/Page"; 13 | 14 | const getMessage = (classes: string[], message: string) => { 15 | const div = document.createElement("div"); 16 | div.classList.add(...classes); 17 | div.innerHTML = message.replace(/\n/g, "
"); 18 | return div; 19 | }; 20 | 21 | const main = async (settings: ExtensionSettings) => { 22 | if (!["/", "/index.php"].includes(window.location.pathname)) { 23 | console.log("unknown page", window.location.pathname); 24 | return; 25 | } 26 | 27 | const args = new URLSearchParams(window.location.search); 28 | 29 | // show message for new users 30 | const message = getMessage( 31 | ["install-message"], 32 | "PTT theme couldn't load.\nTry to force refresh the page.\n(Ctrl+Shift+R / Cmd+Shift+R)", 33 | ); 34 | message.style.fontSize = "40px"; 35 | document.body.prepend(message); 36 | 37 | document.body.removeAttribute("bgcolor"); 38 | document.body.removeAttribute("text"); 39 | 40 | let page: Page; 41 | try { 42 | if (document.querySelector("select[name=UID_UNIVERSITY]") != null) { 43 | page = new Login(); 44 | } else if (args.has("X")) { 45 | switch (args.get("X")) { 46 | case "FAQ": 47 | case "Preset": 48 | case "CompilersDryRuns": 49 | case "Extra": 50 | case "KNT": 51 | case "TaskGrp": { 52 | page = new Logged(settings); 53 | break; 54 | } 55 | case "KNTQ": { 56 | page = new Exam(settings); 57 | break; 58 | } 59 | case "Course": { 60 | page = new Course(settings); 61 | break; 62 | } 63 | case "Results": { 64 | page = new Results(settings); 65 | break; 66 | } 67 | case "Compiler": 68 | case "DryRun": 69 | case "Task": 70 | case "TaskU": { 71 | page = new Task(settings); 72 | break; 73 | } 74 | case "Main": { 75 | page = new Main(settings); 76 | break; 77 | } 78 | default: { 79 | // determine if site is really main 80 | const navlink = 81 | document.querySelector("span.navlink"); // first time login 82 | if ( 83 | document.querySelector( 84 | 'span.navLink > a.navLink[href="?X=Main"]', 85 | ) || 86 | (navlink && navlink.innerText.includes("Než")) 87 | ) { 88 | page = new Logged(settings); 89 | } else { 90 | page = new Main(settings); 91 | } 92 | } 93 | } 94 | } else { 95 | page = new Main(settings); 96 | } 97 | 98 | await page.initialise(); 99 | } catch (e) { 100 | if (e instanceof Error) { 101 | page = new ErrorPage(e); 102 | await page.initialise(); 103 | } else { 104 | throw e; 105 | } 106 | } 107 | return page; 108 | }; 109 | 110 | const replaceStyles = (theme: string) => { 111 | if (theme === "orig") return Promise.resolve(); 112 | 113 | document.head.querySelectorAll('link[href$="/css.css"]').forEach((e) => { 114 | e.remove(); 115 | }); 116 | 117 | const link = document.createElement("link"); 118 | link.setAttribute("rel", "stylesheet"); 119 | link.setAttribute("type", "text/css"); 120 | 121 | link.setAttribute( 122 | "href", 123 | chrome.runtime.getURL("themes/" + theme + ".css"), 124 | ); 125 | document.getElementsByTagName("head")[0].appendChild(link); 126 | return new Promise((resolve) => { 127 | link.onload = resolve; 128 | }); 129 | }; 130 | 131 | const addLoadingOffStyle = () => { 132 | const link = document.createElement("link"); 133 | link.setAttribute("rel", "stylesheet"); 134 | link.setAttribute("type", "text/css"); 135 | 136 | link.setAttribute("href", chrome.runtime.getURL("themes/loading/off.css")); 137 | document.getElementsByTagName("head")[0].appendChild(link); 138 | return new Promise((resolve) => { 139 | link.onload = resolve; 140 | }); 141 | }; 142 | 143 | chrome.runtime.sendMessage( 144 | { type: MessageType.GET_SETTINGS }, 145 | async (settings: ExtensionSettings) => { 146 | console.log("PTT end with settings:", settings); 147 | await Promise.all([ 148 | replaceStyles(settings.theme).then(() => { 149 | console.log("PTT styles loaded"); 150 | }), 151 | !["orig", "orig-dark"].includes(settings.theme) && main(settings), 152 | ]); 153 | await addLoadingOffStyle(); 154 | console.log("PTT loaded"); 155 | document.dispatchEvent(pttLoadedEvent); 156 | }, 157 | ); 158 | -------------------------------------------------------------------------------- /src/content/highlightjs/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006, Ivan Sagalaev 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of highlight.js nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /src/content/loader.ts: -------------------------------------------------------------------------------- 1 | import Loader from "../components/loader.svelte"; 2 | 3 | const loaderElement = document.createElement("pttloader"); 4 | document.documentElement.appendChild(loaderElement); 5 | new Loader({ 6 | target: loaderElement, 7 | }); 8 | -------------------------------------------------------------------------------- /src/content/pages/Course/Course.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
38 | 46 | {#each courseGroups as group} 47 | 48 | {/each} 49 | {#if taskItem} 50 | 51 | {/if} 52 |
53 | 54 | 118 | -------------------------------------------------------------------------------- /src/content/pages/Course/course-group.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | {#if group.taskGrp.length > 0} 35 | 84 | {/if} 85 | 86 | 222 | -------------------------------------------------------------------------------- /src/content/pages/Course/task-modal.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | {#if showModal} 43 |
44 | {#await task then data} 45 | 46 | 47 | 109 | {/await} 110 |
111 | {/if} 112 | 113 | 319 | -------------------------------------------------------------------------------- /src/content/pages/Error/Error.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 |

An unexpected error occurred

12 |

The following error has occurred:

13 | {error.toString()} 14 | {#if error.stack} 15 | {#each error.stack.split("\n") as line} 16 | {line} 17 | {/each} 18 | {/if} 19 |

20 | Try going back, if 21 | that doesn't help, 22 | file an issue on GitHub. 25 |

26 |
27 | 28 | 39 | -------------------------------------------------------------------------------- /src/content/pages/Error/Error.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "../Page"; 2 | import ErrorComponent from "./Error.svelte"; 3 | 4 | export class ErrorPage implements Page { 5 | constructor(private error: Error) {} 6 | 7 | async initialise() { 8 | document.title = "Error | ProgTest"; 9 | 10 | const center = document.querySelector("body > center"); 11 | if (center) { 12 | center.style.display = "none"; 13 | } 14 | 15 | const container = document.createElement("div"); 16 | document.body.insertBefore(container, center); 17 | new ErrorComponent({ 18 | target: container, 19 | props: { error: this.error }, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/content/pages/Exam.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionSettings } from "../../settings"; 2 | import { Logged } from "./Logged"; 3 | 4 | export class Exam extends Logged { 5 | constructor(settings: ExtensionSettings) { 6 | super(settings); 7 | } 8 | 9 | async initialise(): Promise { 10 | await super.initialise(); 11 | 12 | // normalize html 13 | document 14 | .querySelectorAll( 15 | 'form[name="form1"] table tr:nth-child(n+4) td.rCell, form[name="form1"] table tr:nth-child(n+4) td.rbCell', 16 | ) 17 | .forEach((e) => { 18 | const radio = e.querySelector('input[type="radio"]'); 19 | if (!radio) { 20 | return; 21 | } 22 | const dot = e.previousElementSibling?.querySelector(".redBox"); 23 | if (dot) { 24 | dot.parentElement?.removeChild(dot); 25 | radio.classList.add("radio-red"); 26 | } 27 | e.classList.add("radio"); 28 | const label = document.createElement("span"); 29 | label.classList.add("radio-label"); 30 | const remove: HTMLElement[] = []; 31 | e.childNodes.forEach((f) => { 32 | if (f.nodeName == "#text") { 33 | label.innerHTML += f.textContent; 34 | e.replaceChild(label, f); 35 | } else if (f instanceof HTMLElement) { 36 | label.innerHTML += f.outerHTML; 37 | remove.push(f); 38 | } 39 | }); 40 | remove.forEach((g) => g.remove()); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/content/pages/Logged.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionSettings } from "../../settings"; 2 | import { Page } from "./Page"; 3 | 4 | interface LoggedTask { 5 | subject: string; 6 | link: string; 7 | name: string; 8 | seen: boolean; 9 | } 10 | 11 | export class Logged implements Page { 12 | topButton = ` 13 | 14 | 15 | 16 | `; 17 | 18 | tButton: HTMLElement; 19 | header: HTMLElement; 20 | oldScroll: number | undefined; 21 | 22 | constructor(protected settings: ExtensionSettings) { 23 | this.tButton = document.getElementById("upTop") as HTMLElement; 24 | const header = document.querySelector("body > table"); 25 | if (!header) { 26 | throw new Error("Header not found"); 27 | } 28 | this.header = header; 29 | } 30 | 31 | async initialise() { 32 | this.header.className += " navbar"; 33 | // add scroll to top button 34 | document.body.innerHTML += this.topButton; 35 | if (this.tButton) { 36 | this.tButton.addEventListener("click", () => { 37 | this.tButton.removeAttribute("style"); 38 | document.body.scrollIntoView({ 39 | block: "start", 40 | behavior: "smooth", 41 | }); 42 | }); 43 | } 44 | 45 | if (typeof this.header != "undefined" && this.header != null) { 46 | window.onscroll = this.scrollCheck.bind(this); 47 | window.addEventListener("load", this.scrollCheck.bind(this)); 48 | } 49 | 50 | window.addEventListener("beforeunload", this.scrollHigh.bind(this)); 51 | 52 | this.displayBell(); 53 | this.highlightCode(); 54 | this.notifications(); 55 | } 56 | 57 | displayBell() { 58 | if (!window.localStorage || !this.settings.showNotifications) { 59 | return; 60 | } 61 | const bell = document.createElement("div"); 62 | bell.classList.add("notify", "off"); 63 | bell.addEventListener("click", Logged.notifyToggle.bind(this)); 64 | const logout = document.querySelector('.navLink[href*="Logout"]'); 65 | logout?.parentNode?.insertBefore(bell, logout); 66 | 67 | document.addEventListener("click", (e) => { 68 | if (e.target != bell) { 69 | document 70 | .getElementsByClassName("notifications")[0] 71 | .classList.add("notifications-hide"); 72 | } 73 | }); 74 | 75 | const notify = document.createElement("div"); 76 | notify.classList.add("notifications", "notifications-hide"); 77 | notify.innerHTML = "Žádná upozornění"; 78 | document.body.insertBefore(notify, document.body.firstElementChild); 79 | } 80 | 81 | scrollCheck() { 82 | if ( 83 | document.body.scrollTop > 40 || 84 | document.documentElement.scrollTop > 40 85 | ) { 86 | this.scrollLow(); 87 | } else { 88 | this.scrollHigh(); 89 | } 90 | 91 | this.oldScroll = window.scrollY; 92 | } 93 | 94 | scrollHigh() { 95 | if (this.header && this.header.getAttribute("style")) { 96 | this.header.removeAttribute("style"); 97 | } 98 | if (this.tButton && this.tButton.getAttribute("style")) { 99 | this.tButton.removeAttribute("style"); 100 | } 101 | } 102 | 103 | scrollLow() { 104 | if (this.header && !this.header.getAttribute("style")) { 105 | this.header.style.padding = "0px 16px"; 106 | } 107 | if ( 108 | this.tButton && 109 | !this.tButton.getAttribute("style") && 110 | this.oldScroll && 111 | (this.oldScroll <= window.scrollY || !this.oldScroll) 112 | ) { 113 | this.tButton.style.transform = "scale(1)"; 114 | } 115 | } 116 | 117 | highlightCode() { 118 | document 119 | .querySelectorAll("pre, code, tt") 120 | .forEach((block) => { 121 | if (this.settings.syntaxHighlighting) { 122 | window.hljs.highlightBlock(block); 123 | } else { 124 | block.classList.add("hljs"); 125 | } 126 | }); 127 | } 128 | 129 | async notifications() { 130 | if (!window.localStorage || !this.settings.showNotifications) { 131 | return; 132 | } 133 | 134 | const tasks = await Logged.taskSpider(); 135 | 136 | if (!localStorage.tasks) { 137 | localStorage.tasks = JSON.stringify( 138 | tasks?.map((e) => { 139 | e["seen"] = true; 140 | return e; 141 | }), 142 | ); 143 | } else { 144 | const localTasks = JSON.parse(localStorage.tasks) as LoggedTask[]; 145 | const notify = 146 | tasks?.filter((t) => { 147 | return !localTasks.some((e) => e.link === t.link); 148 | }) ?? []; 149 | this.displayNotifications( 150 | notify?.concat(localTasks.filter((e) => e.seen == false)) ?? [], 151 | ); 152 | localStorage.tasks = JSON.stringify(localTasks.concat(notify)); 153 | } 154 | } 155 | 156 | displayNotifications(elems: LoggedTask[]) { 157 | if (!elems.length) { 158 | return; 159 | } 160 | document 161 | .getElementsByClassName("notify")[0] 162 | .classList.replace("off", "on"); 163 | const frame = document.getElementsByClassName("notifications")[0]; 164 | frame.innerHTML = ""; 165 | elems.forEach((e) => { 166 | const node = document.createElement("a"); 167 | node.href = e.link; 168 | node.innerHTML = `${e.subject} Nová úloha:
${e.name}`; 169 | node.addEventListener("click", Logged.notifySeen.bind(this)); 170 | frame.appendChild(node); 171 | }); 172 | } 173 | 174 | static getLinksFromHTML(text: string, href: string) { 175 | text = text.replace(/]*>([\S\s]*?)<\/script>/gim, ""); 176 | const doc = new DOMParser().parseFromString(text, "text/html"); 177 | const allLinks = doc.querySelectorAll( 178 | `.butLink[href*="${href}"]`, 179 | ); 180 | const links: HTMLAnchorElement[] = []; 181 | allLinks.forEach((link) => { 182 | if (link.href && !link.href.includes("javascript:")) { 183 | links.push(link); 184 | } 185 | }); 186 | return links; 187 | } 188 | 189 | static async taskSpider() { 190 | const main = await fetch( 191 | new URL( 192 | "index.php?X=Main", 193 | window.location.protocol + "//" + window.location.hostname, 194 | ), 195 | ); 196 | if (!main.ok || main.redirected) { 197 | return []; 198 | } 199 | 200 | const mainText = await main.text(); 201 | const subjects = Logged.getLinksFromHTML(mainText, "Course"); 202 | if (!subjects) { 203 | return []; 204 | } 205 | const tasks: LoggedTask[] = []; 206 | 207 | for (const e of subjects) { 208 | const course = await fetch(e.href); 209 | if (!course.ok || course.redirected) { 210 | return; 211 | } 212 | const text = await course.text(); 213 | const taskLinks = Logged.getLinksFromHTML(text, "TaskGrp"); 214 | if (!taskLinks) { 215 | return; 216 | } 217 | 218 | taskLinks.forEach((f) => { 219 | const url = new URL(f.href); 220 | const name = (f.parentNode?.parentNode?.parentNode?.parentNode 221 | ?.firstElementChild || undefined) as 222 | | HTMLElement 223 | | undefined; 224 | if (!name) { 225 | return; 226 | } 227 | tasks.push({ 228 | subject: e.innerText, 229 | link: "/" + url.search, 230 | name: name?.innerText, 231 | seen: false, 232 | }); 233 | }); 234 | } 235 | return tasks; 236 | } 237 | 238 | static notifyToggle() { 239 | document 240 | .getElementsByClassName("notifications")[0] 241 | .classList.toggle("notifications-hide"); 242 | } 243 | 244 | static notifySeen(event: MouseEvent) { 245 | const localTasks = JSON.parse(localStorage.tasks) as LoggedTask[]; 246 | const target = event.target; 247 | if (!(target instanceof HTMLElement)) { 248 | return; 249 | } 250 | const linkNode = target.nodeName == "A" ? target : target.parentElement; 251 | if (!(linkNode instanceof HTMLAnchorElement)) { 252 | return; 253 | } 254 | const link = new URL(linkNode.href); 255 | localTasks.map((e) => { 256 | if (e.link == "/" + link.search) { 257 | e.seen = true; 258 | } 259 | return e; 260 | }); 261 | localStorage.tasks = JSON.stringify(localTasks); 262 | const frame = document.getElementsByClassName("notifications")[0]; 263 | frame.removeChild(linkNode); 264 | if (!frame.childElementCount) { 265 | frame.innerHTML = "Žádná upozornění"; 266 | document 267 | .getElementsByClassName("notify")[0] 268 | .classList.replace("on", "off"); 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/content/pages/Login.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "./Page"; 2 | 3 | export class Login implements Page { 4 | langGlobe = ` 5 | 6 | 7 | 8 | `; 9 | 10 | async initialise() { 11 | const loginForm = document.getElementsByTagName("form")[0]; 12 | if (typeof loginForm != "undefined") { 13 | const title = document.createElement("div"); 14 | title.className = "app_name"; 15 | title.innerHTML = "FIT: ProgTest"; 16 | loginForm.parentElement?.insertBefore(title, loginForm); 17 | loginForm.className += " loginForm"; 18 | 19 | const uniselect = document.createElement("div"); 20 | uniselect.id = "uniSel"; 21 | 22 | document 23 | .querySelector( 24 | "#main > tbody > tr:nth-child(2) > td.rtbCell > select", 25 | ) 26 | ?.childNodes.forEach((e) => { 27 | if (!(e instanceof HTMLOptionElement)) return; 28 | const uni = document.createElement("div"); 29 | uni.innerText = e.innerText; 30 | uni.setAttribute("uni", e.value); 31 | uni.className = "uniVal"; 32 | uni.addEventListener("click", uniChange); 33 | uniselect.appendChild(uni); 34 | }); 35 | 36 | uniselect.children[0].setAttribute("active", "true"); 37 | 38 | loginForm.appendChild(uniselect); 39 | 40 | // add title mover 41 | document 42 | .querySelector("#ldap1 > td.ltCell.al > b") 43 | ?.addEventListener("click", moveInputLabel); 44 | document 45 | .querySelector("#ldap2 > td.al.lbCell > b") 46 | ?.addEventListener("click", moveInputLabel); 47 | 48 | const inputs = document.getElementsByTagName("input"); 49 | inputs[0].addEventListener("focus", loginFocus); 50 | inputs[1].addEventListener("focus", loginFocus); 51 | 52 | inputs[0].addEventListener("focusout", loginFocusOut); 53 | inputs[1].addEventListener("focusout", loginFocusOut); 54 | 55 | document.getElementsByName("lang")[0].outerHTML += this.langGlobe; 56 | 57 | // in firefox, when opening the page, load the selected login type 58 | // @ts-expect-error browser is only defined in Chrome and thus is not in types as a global variable 59 | if (typeof browser !== "undefined") { 60 | document 61 | .querySelector("#uniSel > .uniVal") 62 | ?.click(); 63 | } 64 | } 65 | } 66 | } 67 | 68 | export const moveInputLabel = (event: MouseEvent) => { 69 | const target = event.target; 70 | if (!(target instanceof HTMLElement)) { 71 | return; 72 | } 73 | target.setAttribute("moved", "true"); 74 | const label = target.parentNode?.parentNode?.children[1].children[0]; 75 | if (!(label instanceof HTMLElement)) { 76 | return; 77 | } 78 | label.focus(); 79 | }; 80 | 81 | export const loginFocusOut = (event: FocusEvent) => { 82 | const target = event.target; 83 | if (!(target instanceof HTMLInputElement)) { 84 | return; 85 | } 86 | if (target?.value == "") { 87 | target.parentNode?.parentNode?.children[0].children[0].removeAttribute( 88 | "moved", 89 | ); 90 | } 91 | }; 92 | 93 | export const loginFocus = (event: FocusEvent) => { 94 | const target = event.target; 95 | if (!(target instanceof HTMLInputElement)) { 96 | return; 97 | } 98 | const login = target?.parentNode?.parentNode?.children[0].children[0]; 99 | if (login instanceof HTMLElement) { 100 | login.click(); 101 | } 102 | }; 103 | 104 | export const uniChange = (event: MouseEvent) => { 105 | const target = event.target; 106 | if (!(target instanceof HTMLElement)) { 107 | return; 108 | } 109 | 110 | let c = 0, 111 | i = 0; 112 | document.getElementById("uniSel")?.childNodes.forEach((e) => { 113 | if (!(e instanceof HTMLElement)) return; 114 | e.removeAttribute("active"); 115 | if (e == target) { 116 | i = c; 117 | } 118 | c++; 119 | }); 120 | 121 | target?.setAttribute("active", "true"); 122 | 123 | const select = document.querySelector( 124 | 'select[name="UID_UNIVERSITY"]', 125 | ); 126 | if (select) { 127 | select.selectedIndex = i; 128 | const trigger = new Event("change"); 129 | select.dispatchEvent(trigger); 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /src/content/pages/Main/Main.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 | {#each sortedSubjects as [semester, subjects]} 41 | 42 | 50 | {#if expanded[semester]} 51 |
52 | {#each subjects as subject} 53 | 54 | {/each} 55 |
56 | {/if} 57 | {/each} 58 |
59 | {#each settings as item} 60 | 61 | {/each} 62 |
63 |
64 | 65 | 158 | -------------------------------------------------------------------------------- /src/content/pages/Main/Main.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionSettings } from "../../../settings"; 2 | import { Logged } from "../Logged"; 3 | import MainComponent from "./Main.svelte"; 4 | 5 | export type MenuItem = { 6 | title: string; 7 | text: string; 8 | icon: string; 9 | link: string; 10 | subjectHomepage?: string; 11 | footer?: string; 12 | }; 13 | 14 | export type Subjects = Record; 15 | 16 | type ParsedItem = Pick; 17 | 18 | type CoursesInfo = { 19 | semester: string; 20 | generatedAt: string; 21 | courses: Record< 22 | string, 23 | { 24 | department: number; 25 | nameCs: string; 26 | nameEn: string; 27 | programmeType: string; 28 | classesLang: string; 29 | season: string; 30 | homepage: string; 31 | grades: string; 32 | pagesRepo: string; 33 | active: boolean; 34 | } 35 | >; 36 | }; 37 | 38 | export class Main extends Logged { 39 | orderC = 1000; 40 | constructor(settings: ExtensionSettings) { 41 | super(settings); 42 | } 43 | 44 | async initialise() { 45 | await super.initialise(); 46 | 47 | const center = document.querySelector("body > center"); 48 | if (center) { 49 | center.style.display = "none"; 50 | } 51 | 52 | // collect all elements 53 | const items = parseItems(); 54 | if (items.length === 0) { 55 | if (!center) return; 56 | center.style.display = "block"; 57 | return; 58 | } 59 | 60 | // get subject URLs from courses 61 | const subjectInfo: CoursesInfo = await fetch( 62 | "https://courses.fit.cvut.cz/data/courses-all.json", 63 | { 64 | method: "GET", 65 | mode: "cors", 66 | credentials: "omit", 67 | }, 68 | ).then((response) => response.json()); 69 | 70 | const subjects: Subjects = {}; 71 | const settings: MenuItem[] = []; 72 | items.forEach((item) => { 73 | const icon = getMenuIcon(item.title); 74 | 75 | // create semester code from subject code 76 | const semester = item.text.substring( 77 | item.text.indexOf("(") + 1, 78 | item.text.indexOf(")"), 79 | ); 80 | 81 | // settings have no semester 82 | if (semester === "") { 83 | // overrides 84 | if (item.title === "FAQ") { 85 | item.text = "Často kladené dotazy"; 86 | } 87 | if (item.title === item.text) { 88 | item.text = ""; 89 | } 90 | settings.push({ 91 | title: item.title, 92 | text: item.text, 93 | icon, 94 | link: item.link, 95 | }); 96 | return; 97 | } 98 | 99 | const footer = "20" + semester; 100 | const semesterKey = `B${semester.split("/")[0]}${ 101 | semester.includes("ZS") ? 1 : 2 102 | }`; 103 | const subjectHomepage = 104 | subjectInfo.courses[item.title]?.homepage ?? 105 | `https://courses.fit.cvut.cz/${item.title}`; 106 | 107 | if (!subjects[semesterKey]) { 108 | subjects[semesterKey] = []; 109 | } 110 | subjects[semesterKey].push({ 111 | title: item.title, 112 | text: item.text.substring(0, item.text.indexOf("(")), 113 | icon, 114 | link: item.link, 115 | subjectHomepage, 116 | footer, 117 | }); 118 | }); 119 | 120 | const container = document.createElement("div"); 121 | document.body.insertBefore(container, center); 122 | new MainComponent({ 123 | target: container, 124 | props: { subjects, settings }, 125 | }); 126 | } 127 | } 128 | 129 | function parseItems(): ParsedItem[] { 130 | return [ 131 | ...document.querySelectorAll( 132 | "body > center > table > tbody > tr", 133 | ), 134 | ] 135 | .map((e: HTMLElement) => { 136 | const ch = e.children[1]?.children[0]?.children[0]?.children[0]; 137 | if (!(ch instanceof HTMLAnchorElement)) { 138 | console.error("Subject button not found"); 139 | return null; 140 | } 141 | const firstChild = e.children[0]; 142 | if (!(firstChild instanceof HTMLElement)) { 143 | console.error("Subject name not found"); 144 | return null; 145 | } 146 | return { 147 | title: ch.innerText, 148 | text: firstChild.innerText, 149 | link: ch.href, 150 | }; 151 | }) 152 | .filter((e) => e !== null) as ParsedItem[]; 153 | } 154 | 155 | function getMenuIcon(title: string) { 156 | return ( 157 | { 158 | "BI-AAG": "icon-aag", 159 | "BI-AG1": "icon-ag1", 160 | "BI-OSY": "icon-osy", 161 | "BI-PA1": "icon-pa1", 162 | "BI-PA2": "icon-pa2", 163 | "BI-PJV": "icon-pjv", 164 | "BI-PS1": "icon-ps1", 165 | "BI-PYT": "icon-pyt", 166 | "NI-PDP": "icon-pdp", 167 | Nastavení: "icon-setting", 168 | Překladače: "icon-compile", 169 | FAQ: "icon-faq", 170 | }[title] || "icon-unknown" 171 | ); 172 | } 173 | -------------------------------------------------------------------------------- /src/content/pages/Main/menu-item.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | 24 | 77 | -------------------------------------------------------------------------------- /src/content/pages/Page.ts: -------------------------------------------------------------------------------- 1 | export interface Page { 2 | initialise(): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/content/pages/Results.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionSettings } from "../../settings"; 2 | import { Logged } from "./Logged"; 3 | 4 | export class Results extends Logged { 5 | constructor(settings: ExtensionSettings) { 6 | super(settings); 7 | } 8 | 9 | async initialise() { 10 | await super.initialise(); 11 | 12 | let styles = ""; 13 | 14 | // add selector to duplicate parent elements 15 | document.querySelectorAll("span.dupC").forEach((e) => { 16 | const parent = e.parentNode; 17 | if (parent instanceof HTMLElement) { 18 | parent.className += " dupCpar"; 19 | } 20 | }); 21 | 22 | // mark number of columns 23 | const c: number[] = []; 24 | let i = 0; 25 | const qsel = document.querySelector("tr.resHdr:nth-child(1)"); 26 | if (qsel != null) { 27 | qsel.childNodes.forEach((e) => { 28 | if (!(e instanceof HTMLElement)) return; 29 | const colspan = e.getAttribute("colspan"); 30 | i += colspan ? parseInt(colspan) : 1; 31 | c.push(i); 32 | }); 33 | c.pop(); 34 | c.shift(); 35 | c.forEach((e) => { 36 | styles += 37 | "tr.resRow > td:nth-child(" + 38 | (e + 1) + 39 | ") {border-left: thin solid rgba(0, 0, 0, 0.125);font-weight: 500;}"; 40 | }); 41 | 42 | styles += "tr.resRow > td:last-child {font-weight: 500;}"; 43 | } 44 | 45 | const styleSheet = document.createElement("style"); 46 | styleSheet.innerText = styles; 47 | document.head.appendChild(styleSheet); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/content/start.ts: -------------------------------------------------------------------------------- 1 | import { MessageType } from "../messages"; 2 | import { ExtensionSettings } from "../settings"; 3 | 4 | const setTitle = () => { 5 | document.title = document.title 6 | .replace("@progtest.fit.cvut.cz -", " |") 7 | .replace("progtest.fit.cvut.cz - ", ""); 8 | }; 9 | 10 | const setFavicon = () => { 11 | const favicon: HTMLLinkElement = 12 | document.querySelector("link[rel*='icon']") || 13 | document.createElement("link"); 14 | favicon.type = "image/x-icon"; 15 | favicon.rel = "shortcut icon"; 16 | favicon.href = chrome.runtime.getURL("./themes/assets/favicon.ico"); 17 | document.head.appendChild(favicon); 18 | }; 19 | 20 | const addLoader = () => { 21 | const script = document.createElement("script"); 22 | script.setAttribute("type", "text/javascript"); 23 | script.setAttribute("src", chrome.runtime.getURL("content/loader.js")); 24 | document.documentElement.appendChild(script); 25 | }; 26 | 27 | addLoader(); 28 | chrome.runtime.sendMessage( 29 | { type: MessageType.GET_SETTINGS }, 30 | function (settings: ExtensionSettings) { 31 | if (settings === undefined) { 32 | throw new Error("Did not receive settings from background script"); 33 | } 34 | console.log("PTT start with settings:", settings); 35 | if (settings.theme == "orig" || settings.theme == "orig-dark") { 36 | return; 37 | } 38 | 39 | setTitle(); 40 | setTimeout(setFavicon, 0); 41 | }, 42 | ); 43 | -------------------------------------------------------------------------------- /src/content/utils.ts: -------------------------------------------------------------------------------- 1 | export const buildLink = (arg: string) => 2 | new URL( 3 | "index.php?" + arg, 4 | window.location.protocol + "//" + window.location.host, 5 | ); 6 | 7 | export function getCourseId() { 8 | const args = new URLSearchParams(window.location.search); 9 | return args.get("Cou"); 10 | } 11 | -------------------------------------------------------------------------------- /src/dev/dev.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

Dev page

7 | 8 |
9 |

Extension pages

10 |

11 | Settings 12 |

13 |
14 | 15 |
16 |

Test pages

17 | {#await data} 18 |

Loading...

19 | {:then list} 20 | {#each list as item} 21 |

22 | {item.description} 23 |

24 | {/each} 25 | {:catch error} 26 |

Error: {error.message}

27 |

28 | Check that you have test pages in 29 | test_pages/. 30 |

31 | {/await} 32 |
33 |
34 | 35 | 49 | -------------------------------------------------------------------------------- /src/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dev | Progtest Themes 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/dev/index.ts: -------------------------------------------------------------------------------- 1 | import Dev from "./dev.svelte"; 2 | 3 | // settings page 4 | new Dev({ target: document.body }); 5 | -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | export const pttLoadedEvent = new Event("pttLoaded"); 2 | -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/src/icon.png -------------------------------------------------------------------------------- /src/messages.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | GET_SETTINGS = "GET_SETTINGS", 3 | GET_LOADER = "GET_LOADER", 4 | } 5 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | export interface ExtensionSettings { 2 | theme: "automatic" | "orig" | "orig-dark" | "dark" | "light"; 3 | autohideResults: boolean; 4 | showNotifications: boolean; 5 | syntaxHighlighting: boolean; 6 | playSounds: boolean; 7 | } 8 | 9 | export const DEFAULT_SETTINGS: ExtensionSettings = { 10 | theme: "automatic", 11 | autohideResults: true, 12 | showNotifications: true, 13 | syntaxHighlighting: true, 14 | playSounds: true, 15 | } as const; 16 | -------------------------------------------------------------------------------- /src/settings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Settings | Progtest Themes 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/settings/index.ts: -------------------------------------------------------------------------------- 1 | import Settings from "./settings.svelte"; 2 | 3 | // on click, Open Progtest in new tab 4 | if (typeof chrome === "object") { 5 | chrome.tabs.query( 6 | { active: true, currentWindow: true }, 7 | function callback(tabs) { 8 | if ( 9 | tabs[0]?.url?.indexOf("://progtest.fit.cvut.cz") == -1 && 10 | tabs[0].url.indexOf("://ptmock.localhost") == -1 11 | ) { 12 | if (tabs[0].url.indexOf("://newtab") != -1) { 13 | if (tabs[0].id !== undefined) { 14 | chrome.tabs.update(tabs[0].id, { 15 | url: "https://progtest.fit.cvut.cz/", 16 | }); 17 | } 18 | window.close(); 19 | } else { 20 | chrome.tabs.create({ 21 | url: "https://progtest.fit.cvut.cz/", 22 | }); 23 | } 24 | } 25 | }, 26 | ); 27 | } 28 | 29 | // settings page 30 | new Settings({ target: document.body }); 31 | -------------------------------------------------------------------------------- /src/themes/assets/background-dark.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/src/themes/assets/background-dark.PNG -------------------------------------------------------------------------------- /src/themes/assets/background-light.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/src/themes/assets/background-light.PNG -------------------------------------------------------------------------------- /src/themes/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/src/themes/assets/favicon.ico -------------------------------------------------------------------------------- /src/themes/assets/icons/aag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/themes/assets/icons/ag1.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 22 | 24 | 26 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/themes/assets/icons/compile.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/themes/assets/icons/faq.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 17 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /src/themes/assets/icons/osy.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 41 | 42 | 43 | 44 | 45 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/themes/assets/icons/pa1.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/themes/assets/icons/pa2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/themes/assets/icons/pdp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 17 | 30 | 43 | 44 | -------------------------------------------------------------------------------- /src/themes/assets/icons/pjv.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/themes/assets/icons/ps1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/themes/assets/icons/pyt.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 37 | 39 | 40 | 42 | image/svg+xml 43 | 45 | 46 | 47 | 48 | 49 | 51 | 54 | 61 | 62 | 65 | 69 | 70 | 73 | 82 | 83 | 86 | 93 | 94 | 95 | 99 | 103 | 107 | 112 | 116 | 120 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/themes/assets/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/themes/assets/icons/unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 48 | 50 | 53 | 60 | 61 | 64 | 68 | 69 | 72 | 81 | 82 | 85 | 92 | 93 | 94 | 98 | 102 | 106 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/themes/assets/notifications_active.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/themes/assets/notifications_none.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/themes/assets/p.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/themes/assets/shibboleth-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/src/themes/assets/shibboleth-inverted.png -------------------------------------------------------------------------------- /src/themes/assets/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/themes/assets/turret.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keombre/progtest-theme/db224994bc5c60e460b7125b1f190579b1b8a3fe/src/themes/assets/turret.ogg -------------------------------------------------------------------------------- /src/themes/automatic.css: -------------------------------------------------------------------------------- 1 | @import url("./light.css") screen and not (prefers-color-scheme: dark); 2 | @import url("./dark.css") screen and (prefers-color-scheme: dark); 3 | -------------------------------------------------------------------------------- /src/themes/light.css: -------------------------------------------------------------------------------- 1 | @import "./common.css"; 2 | 3 | .uniVal { 4 | color: #2e2e2e; 5 | } 6 | 7 | #loadWrapper { 8 | background: #fff; 9 | } 10 | 11 | #shib1 > td > img { 12 | background: none; 13 | padding-left: 0; 14 | } 15 | 16 | body { 17 | color: #000000; 18 | background-color: #fff; 19 | background-image: url("./assets/background-light.PNG"); 20 | } 21 | 22 | table { 23 | background-color: #fff; 24 | } 25 | 26 | select { 27 | outline-color: #2e2e2e; 28 | } 29 | 30 | .app_name { 31 | text-shadow: 0 0 1px rgba(0, 0, 0, 0.25); 32 | color: #464646; 33 | } 34 | 35 | .app_name > b { 36 | color: #212121; 37 | } 38 | 39 | .loginForm > center > div > div > table:before { 40 | background: #26a69a; 41 | } 42 | 43 | .loginForm table { 44 | background-color: #fff !important; 45 | } 46 | 47 | .loginForm .insBox { 48 | background: #fff; 49 | } 50 | 51 | .loginForm div.outButton.w120 input, 52 | .loginForm a.butLink { 53 | outline-color: #004d40; 54 | } 55 | 56 | .loginForm select[name="lang"] { 57 | border-bottom: thin solid rgba(0, 0, 0, 0.25); 58 | color: #353535; 59 | } 60 | 61 | #langGlobe { 62 | fill: #424242; 63 | } 64 | 65 | .loginForm #ldap2 > td:nth-child(1) > b, 66 | .loginForm #ldap1 > td:nth-child(1) > b { 67 | color: #585858; 68 | background: #fff; 69 | } 70 | 71 | .loginForm input.std.w200 { 72 | border-bottom: thin solid rgba(0, 0, 0, 0.3); 73 | } 74 | 75 | /** header */ 76 | 77 | body > table { 78 | background-color: #343a40 !important; 79 | } 80 | 81 | body > table td { 82 | background-color: #343a40 !important; 83 | } 84 | 85 | #refProgress { 86 | background-color: rgb(255, 193, 7) !important; 87 | } 88 | 89 | .notifications { 90 | background: #fff; 91 | border-color: #eee; 92 | } 93 | 94 | .notifications > a { 95 | color: #000; 96 | border-color: #d5d5d5; 97 | } 98 | 99 | .notifications > a:hover { 100 | background: #ececec; 101 | } 102 | 103 | /** subject select */ 104 | 105 | .subject { 106 | border: thin solid #d1d1d1; 107 | background-color: #fff; 108 | } 109 | 110 | .subject:hover { 111 | background-color: #fafafa; 112 | } 113 | 114 | .subject-title { 115 | color: #000; 116 | } 117 | 118 | .subject-body { 119 | color: #414141; 120 | } 121 | 122 | .subject-footer { 123 | background-color: #f0f0f0; 124 | border-top: thin solid #d5d5d5; 125 | color: #000; 126 | } 127 | 128 | .subject-homepage { 129 | color: white !important; 130 | background-color: #607d8b; 131 | border-color: #455a64; 132 | } 133 | 134 | .subject-homepage:hover { 135 | background-color: #546e7a; 136 | border-color: #455a64; 137 | } 138 | 139 | /** task select */ 140 | 141 | .course_grp { 142 | border: thin solid #d1d1d1; 143 | background-color: #fff; 144 | } 145 | 146 | .course_grp > *:nth-child(even) { 147 | background: rgba(0, 0, 0, 0.05); 148 | } 149 | 150 | .course_link { 151 | background: #fff; 152 | color: #424242; 153 | } 154 | 155 | .course_link.course_disabled span.course_link_name, 156 | span.course_link { 157 | color: #b1b1b1; 158 | } 159 | 160 | .course_link.course_disabled { 161 | border-color: #676767; 162 | } 163 | 164 | .course_unknown_grp > span.course_title { 165 | color: #404040; 166 | } 167 | 168 | span span.course_link_score { 169 | background: #b0bec5; 170 | } 171 | 172 | span.course_link_score_sum { 173 | color: #464248 !important; 174 | border-bottom: thin solid rgba(0, 0, 0, 0.1) !important; 175 | } 176 | 177 | a.course_link:hover { 178 | background-color: rgba(0, 0, 0, 0.1); 179 | } 180 | 181 | .modal { 182 | background: #fff; 183 | box-shadow: 0 5px 30px #494949; 184 | border: thin solid #ababab; 185 | } 186 | 187 | .modal-close { 188 | background: #e0e0e0; 189 | } 190 | 191 | .modal-close:hover { 192 | background: #bdbdbd; 193 | } 194 | 195 | .modal-header { 196 | background: #eeeeee; 197 | border-bottom: thin solid #ababab; 198 | } 199 | 200 | .modal-score-my::after { 201 | color: #8b8b8b; 202 | } 203 | 204 | .modal-score-max { 205 | color: #525252; 206 | } 207 | 208 | .modal-deadline-late { 209 | color: #a00000; 210 | } 211 | 212 | .modal-line { 213 | color: #000; 214 | border-bottom: thin solid #e2e2e2; 215 | } 216 | 217 | .modal-line:hover { 218 | background: #e0e0e0; 219 | } 220 | 221 | .modal-spinner { 222 | background: url(./assets/spinner.svg), 223 | radial-gradient(ellipse at center, #fffa 40%, #fff0 70%); 224 | } 225 | 226 | .mtask-score-my::after { 227 | color: #8b8b8b; 228 | } 229 | 230 | .mtask-score-max { 231 | color: #525252; 232 | } 233 | 234 | /** item header */ 235 | 236 | table.header { 237 | border-bottom: solid thin rgba(0, 0, 0, 0.175); 238 | } 239 | 240 | .outBox > table:not(.header) { 241 | background-color: #fff !important; 242 | } 243 | 244 | /** item body */ 245 | 246 | .ltCell, 247 | .tCell, 248 | .rtCell, 249 | .lCell, 250 | .rCell, 251 | .lbCell, 252 | .rbCell, 253 | .bCell, 254 | .ltbCell, 255 | .rtbCell, 256 | .lrtCell, 257 | .lrbCell, 258 | .Cell, 259 | .tbCell { 260 | background-color: rgba(0, 0, 0, 0.03) !important; 261 | } 262 | 263 | .lrtbCell { 264 | background-color: #fff; 265 | } 266 | 267 | li.testRes a { 268 | color: #3949a2; 269 | background-color: #fff; 270 | } 271 | 272 | li.testRes { 273 | border-top: thin solid rgba(0, 0, 0, 0.2); 274 | } 275 | 276 | /** item colored header */ 277 | 278 | .lrtbSepCell, 279 | .ltbSepCell, 280 | .rtbSepCell, 281 | .tbSepCell { 282 | background-color: #e8e8e8; 283 | color: #000; 284 | } 285 | 286 | /** list box */ 287 | 288 | .menuListDis { 289 | color: #3c3c3c; 290 | } 291 | 292 | body > center > table > tbody > tr:nth-child(even) { 293 | background-color: rgba(0, 0, 0, 0.03); 294 | } 295 | 296 | /** results table */ 297 | 298 | tr.resRow td { 299 | /*border-top: thin solid rgba(0, 0, 0, 0.125);*/ 300 | border-top: thin solid #eee; 301 | } 302 | 303 | tr.resRow:nth-child(even) { 304 | /*background-color: rgba(0, 0, 0, 0.025);*/ 305 | background-color: #f8f8f8; 306 | } 307 | 308 | .hdrCell { 309 | color: #000000; 310 | } 311 | 312 | tr.resHdr td > a { 313 | color: #000000; 314 | } 315 | 316 | /** buttons */ 317 | 318 | .radio input[type="radio"] { 319 | background: #f6f6f6; 320 | border: 1px solid #b0b0b0; 321 | } 322 | .radio input[type="radio"]:checked { 323 | background-color: #3197ee; 324 | box-shadow: inset 0 0 0 4px #f6f6f6; 325 | } 326 | .radio input[type="radio"]:focus { 327 | outline: none; 328 | border-color: #3197ee; 329 | } 330 | .radio input[type="radio"]:disabled { 331 | box-shadow: inset 0 0 0 4px #e9e9e9; 332 | border-color: #b0b0b0; 333 | background: #d0d0d0; 334 | } 335 | .radio input[type="radio"]:disabled:checked { 336 | box-shadow: inset 0 0 0 4px #e9e9e9; 337 | border-color: #b0b0b0; 338 | background: #66bb6a; 339 | } 340 | .radio input[type="radio"].radio-red:disabled:checked { 341 | background: #ef5350; 342 | border-color: #ef5350 !important; 343 | } 344 | .radio input[type="radio"].radio-red { 345 | box-shadow: inset 0 0 0 4px #66bb6a; 346 | } 347 | 348 | input.std, 349 | input.stdDis, 350 | input.test, 351 | input.testDis, 352 | input.flt, 353 | input.fltDis, 354 | input.seek, 355 | input.seekDis, 356 | input.err { 357 | color: #1e1e1e; 358 | border: thin solid rgba(0, 0, 0, 0.125); 359 | } 360 | 361 | input.std, 362 | input.test { 363 | background-color: rgba(0, 0, 0, 0.075); 364 | } 365 | 366 | input.err { 367 | background-color: #f13636; 368 | } 369 | 370 | .updButton, 371 | a.butLink { 372 | background-color: #607d8b; 373 | border-color: #455a64 !important; 374 | text-shadow: 0 1px 0 #455a64; 375 | } 376 | 377 | /* 378 | 379 | Original highlight.js style (c) Ivan Sagalaev 380 | 381 | */ 382 | 383 | .hljs { 384 | display: block; 385 | width: fit-content; 386 | overflow-x: auto; 387 | padding: 0.5em; 388 | background: #f0f0f0; 389 | font-family: var(--font-monospace); 390 | } 391 | 392 | code.hljs, 393 | tt.hljs { 394 | display: inline; 395 | padding: 0.1em; 396 | } 397 | 398 | /* Base color: saturation 0; */ 399 | 400 | .hljs, 401 | .hljs-subst { 402 | color: #444; 403 | } 404 | 405 | .hljs-comment { 406 | color: #888888; 407 | } 408 | 409 | .hljs-keyword, 410 | .hljs-attribute, 411 | .hljs-selector-tag, 412 | .hljs-meta-keyword, 413 | .hljs-doctag, 414 | .hljs-name { 415 | font-weight: bold; 416 | } 417 | 418 | /* User color: hue: 0 */ 419 | 420 | .hljs-type, 421 | .hljs-string, 422 | .hljs-number, 423 | .hljs-selector-id, 424 | .hljs-selector-class, 425 | .hljs-quote, 426 | .hljs-template-tag, 427 | .hljs-deletion { 428 | color: #880000; 429 | } 430 | 431 | .hljs-title, 432 | .hljs-section { 433 | color: #880000; 434 | font-weight: bold; 435 | } 436 | 437 | .hljs-regexp, 438 | .hljs-symbol, 439 | .hljs-variable, 440 | .hljs-template-variable, 441 | .hljs-link, 442 | .hljs-selector-attr, 443 | .hljs-selector-pseudo { 444 | color: #bc6060; 445 | } 446 | 447 | /* Language color: hue: 90; */ 448 | 449 | .hljs-literal { 450 | color: #78a960; 451 | } 452 | 453 | .hljs-built_in, 454 | .hljs-bullet, 455 | .hljs-code, 456 | .hljs-addition { 457 | color: #397300; 458 | } 459 | 460 | /* Meta color: hue: 200 */ 461 | 462 | .hljs-meta { 463 | color: #1f7199; 464 | } 465 | 466 | .hljs-meta-string { 467 | color: #4d99bf; 468 | } 469 | 470 | /* Misc effects */ 471 | 472 | .hljs-emphasis { 473 | font-style: italic; 474 | } 475 | 476 | .hljs-strong { 477 | font-weight: bold; 478 | } 479 | -------------------------------------------------------------------------------- /src/themes/loading/off.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Show hidden content after page has loaded 3 | * This file is injected as part of end.ts content script, 4 | * right after the extension loads all content 5 | */ 6 | body::before { 7 | display: none !important; 8 | position: relative !important; 9 | } 10 | -------------------------------------------------------------------------------- /src/themes/loading/on.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Hide all content while page is loading 3 | * This file is injected as part of document_start content script 4 | */ 5 | body::before { 6 | content: ""; 7 | display: block; 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | width: 100vw; 12 | height: 100vh; 13 | background: #1c1e1f; 14 | z-index: 100; 15 | } 16 | -------------------------------------------------------------------------------- /src/themes/svelte.css: -------------------------------------------------------------------------------- 1 | @layer light, dark; 2 | 3 | @layer light { 4 | :root { 5 | --background-color: hsl(0 0% 100%); 6 | --foreground-color--muted: hsl(240 7% 14%); 7 | --foreground-color: hsl(240 10% 3.9%); 8 | --divider-color: #eee; 9 | } 10 | } 11 | 12 | @layer dark { 13 | @media screen and (prefers-color-scheme: dark) { 14 | :root { 15 | --background-color: hsl(240 10% 3.9%); 16 | --foreground-color--muted: hsl(0 0% 80%); 17 | --foreground-color: hsl(0 0% 98%); 18 | --divider-color: hsl(0 0% 15%); 19 | } 20 | } 21 | } 22 | 23 | :root { 24 | font-family: "Inter", sans-serif; 25 | } 26 | 27 | @supports (font-variation-settings: normal) { 28 | :root { 29 | font-family: "Inter var", sans-serif; 30 | } 31 | } 32 | 33 | * { 34 | font-size: 14px; 35 | margin: 0; 36 | padding: 0; 37 | box-sizing: border-box; 38 | border: none; 39 | } 40 | 41 | ul { 42 | margin: 0 8px; 43 | } 44 | 45 | :global(button) { 46 | all: unset; 47 | outline: revert; 48 | } 49 | 50 | :global(button::-moz-focus-inner) { 51 | border: 0; 52 | padding: 0; 53 | } 54 | 55 | #loadWrapper { 56 | display: none; 57 | } 58 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | // TODO: Migrate to using highlight.js when each page is in its own file 3 | hljs?: HLJSApi; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*", "./package.json"], 3 | "compilerOptions": { 4 | // add Bun type definitions 5 | "types": [ 6 | "@emotion/css", 7 | "archiver", 8 | "bun-types", 9 | "chrome", 10 | "minimist", 11 | "svelte", 12 | "web", 13 | "./src/types.d.ts" 14 | ], 15 | 16 | // enable latest features 17 | "lib": ["DOM", "ESNext"], 18 | "module": "esnext", 19 | "target": "ES6", 20 | 21 | "resolveJsonModule": true, 22 | 23 | // if TS 5.x+ 24 | "moduleResolution": "bundler", 25 | "noEmit": true, 26 | "allowImportingTsExtensions": true, 27 | "moduleDetection": "force", 28 | // if TS 4.x or earlier 29 | // "moduleResolution": "nodenext", 30 | 31 | "allowJs": true, // allow importing `.js` from `.ts` 32 | 33 | // best practices 34 | "strict": true, 35 | "forceConsistentCasingInFileNames": true, 36 | "skipLibCheck": true, 37 | "composite": true, 38 | "downlevelIteration": true, 39 | "allowSyntheticDefaultImports": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "progtest-themes@keombre": { 4 | "updates": [ 5 | { 6 | "version": "1.0.27", 7 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.27/progtest_themes-1.0.27-an+fx.xpi" 8 | }, 9 | { 10 | "version": "1.0.28", 11 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.28/progtest_themes-1.0.28-an+fx.xpi" 12 | }, 13 | { 14 | "version": "1.0.29", 15 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.29/progtest_themes-1.0.29-an+fx.xpi" 16 | }, 17 | { 18 | "version": "1.0.31", 19 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.31/progtest_themes-1.0.31-an+fx.xpi" 20 | }, 21 | { 22 | "version": "1.0.32", 23 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.32/progtest_themes-1.0.32-an+fx.xpi" 24 | }, 25 | { 26 | "version": "1.0.33", 27 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.33/progtest_themes-1.0.33-an+fx.xpi" 28 | }, 29 | { 30 | "version": "1.0.34", 31 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.34/progtest_themes-1.0.34-an+fx.xpi" 32 | }, 33 | { 34 | "version": "1.0.35", 35 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.0.35/progtest_themes-1.0.35-an+fx.xpi" 36 | }, 37 | { 38 | "version": "1.1.0", 39 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.0/progtest_themes-1.1.0-an+fx.xpi" 40 | }, 41 | { 42 | "version": "1.1.1", 43 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.1/progtest_themes-1.1.1-an+fx.xpi" 44 | }, 45 | { 46 | "version": "1.1.2", 47 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.2/progtest_themes-1.1.2-an+fx.xpi" 48 | }, 49 | { 50 | "version": "1.1.3", 51 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.3/progtest_themes-1.1.3-an+fx.xpi" 52 | }, 53 | { 54 | "version": "1.1.4", 55 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.4/progtest_themes-1.1.4-an+fx.xpi" 56 | }, 57 | { 58 | "version": "1.1.5", 59 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.5/progtest_themes-1.1.5-an+fx.xpi" 60 | }, 61 | { 62 | "version": "1.1.6", 63 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/1.1.6/progtest_themes-1.1.6-an+fx.xpi" 64 | }, 65 | { 66 | "version": "1.2.0", 67 | "update_link": "https://github.com/keombre/progtest-theme/releases/download/v1.2.0/progtest-themes-v1.2.0-firefox.xpi" 68 | } 69 | ] 70 | } 71 | } 72 | } 73 | --------------------------------------------------------------------------------