├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE.txt ├── README.md ├── assets ├── chrome-webstore.png ├── firefox.png ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png └── screenshot-4.png ├── build.sh ├── content-scripts ├── package-lock.json ├── package.json ├── ts │ ├── context │ │ └── index.ts │ ├── enums │ │ ├── enums.ts │ │ └── messages.ts │ ├── helper │ │ ├── blockBtn.ts │ │ └── index.ts │ ├── index.ts │ ├── interfaces │ │ ├── interfaces.ts │ │ ├── messages.ts │ │ └── storage.ts │ ├── observer │ │ ├── index.ts │ │ ├── observer.ts │ │ └── videoCreatorObserver.ts │ └── storage │ │ └── index.ts └── tsconfig.json ├── images ├── CB_icon.svg ├── CB_icon_128.png ├── CB_icon_16.png ├── CB_icon_32.png ├── CB_icon_48.png ├── CB_icon_96.png └── misc │ └── Burger.svg ├── manifest.json ├── manifest.json.firefox ├── service-worker ├── package-lock.json ├── package.json ├── ts │ ├── enums.ts │ ├── helper.ts │ ├── index.ts │ ├── interfaces │ │ ├── interfaces.ts │ │ └── storage.ts │ └── storage.ts └── tsconfig.json ├── setup.sh └── ui ├── package-lock.json ├── package.json ├── popup ├── index.html └── scripts │ └── index.ts ├── settings ├── index.html ├── scripts │ ├── donate.ts │ ├── enums.ts │ ├── faq.ts │ ├── helper.ts │ ├── importExport.ts │ ├── index.ts │ ├── interfaces │ │ ├── interfaces.ts │ │ └── storage.ts │ ├── navigation.ts │ └── settings.ts └── style │ ├── main.scss │ └── modules │ ├── _donate.scss │ ├── _faq.scss │ ├── _header.scss │ ├── _index.scss │ ├── _navigation.scss │ ├── _sections.scss │ ├── _switch.scss │ └── _variables.scss └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Git will always convert line endings to LF on checkout. 5 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Channel Blocker 4 | title: 'Fix: ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description: 11 | A clear and concise description of what the bug is. 12 | 13 | ## Steps to Reproduce: 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected Behavior: 21 | A clear description of what you expected to happen. 22 | 23 | ## Actual Behavior: 24 | A clear description of what actually happened. 25 | 26 | ## Proposed Solution: 27 | A suggested approach to fix the bug. 28 | Note: This section can be left empty if you are unsure of how to resolve the issue. 29 | 30 | ## Additional Information: 31 | Add any other context about the problem here. 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | ## Environment: 35 | Channel Blocker version: [version] 36 | Browser: [name and version] 37 | Operating System: [name and version] 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Channel Blocker 4 | title: 'Add:' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? Please describe. 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | 131 | # MacOS junk 132 | .DS_Store 133 | 134 | # Build folder 135 | dist 136 | dist-firefox 137 | bin -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The "Channel Blocker" Web Extension is distributed under the New BSD license: 2 | 3 | Copyright (c) 2024, Time Machine Development 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of Time Machine Development nor the names of its contributors 14 | may be used to endorse or promote products derived from this software 15 | without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY TIME MACHINE DEVELOPMENT ''AS IS'' AND ANY 18 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL TIME MACHINE DEVELOPMENT BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Channel-Blocker

4 | 5 |

A web extension allowing you to block YouTube™ videos and comments by blacklisting users and/or by using regular expressions.

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | 16 | --- 17 | 18 | ### About this Extension 19 | 20 | Channel Blocker enables the function of blocking anything you'd like. Block separate videos, users or whole channels with only one click! Naturally, the add-on supports regular expressions to ensure you the best customization possible. And another thing: no user information is collected. 21 | 22 | Do you like what we do? Do you want to support us and our work? A subscription on Patreon or a donation would help a lot at developing this tool as well as realizing our future projects. 23 | 24 | --- 25 | 26 | ### Screenshots 27 | 28 |

29 | 30 | 31 | 32 | 33 |

34 | 35 | ## Getting Started 36 | 37 | ### Installation 38 | 39 | Before you begin, ensure that you have [downloaded and installed Node.js and npm](https://nodejs.org/en/download/). 40 | 41 | This project is composed of three npm projects: `content-scripts`, `service-worker` and `ui`. 42 | 43 | To install all dependencies, simply run the following command: 44 | 45 | ``` 46 | ./setup.sh 47 | ``` 48 | 49 | ### Building the Extension 50 | 51 | To build the web extension, simply run the following command: 52 | 53 | ``` 54 | ./build.sh 55 | ``` 56 | 57 | This will create a `dist` folder that contains the web extension. 58 | -------------------------------------------------------------------------------- /assets/chrome-webstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/assets/chrome-webstore.png -------------------------------------------------------------------------------- /assets/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/assets/firefox.png -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/assets/screenshot-2.png -------------------------------------------------------------------------------- /assets/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/assets/screenshot-3.png -------------------------------------------------------------------------------- /assets/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/assets/screenshot-4.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | manifest_file="./manifest.json" 4 | manifest_file_firefox="./manifest.json.firefox" 5 | images_folder="./images" 6 | ui_folder="./ui" 7 | destination_folder="./dist" 8 | destination_folder_firefox="./dist-firefox" 9 | 10 | # Delete the existing dist folder 11 | rm -rf "$destination_folder" 12 | rm -rf "$destination_folder_firefox" 13 | 14 | # Run npm run build in content-scripts folder 15 | cd content-scripts 16 | npm run build 17 | cd .. 18 | 19 | # Run npm run build in service-worker folder 20 | cd service-worker 21 | npm run build 22 | cd .. 23 | 24 | # Run npm run build in ui folder 25 | cd ui 26 | npm run build 27 | cd .. 28 | 29 | # Create the destination folder if it doesn't exist 30 | mkdir -p "$destination_folder/images" 31 | 32 | # Copy the contents of the images folder 33 | cp "$manifest_file" "$destination_folder" 34 | cp -r "$images_folder"/* "$destination_folder/images" 35 | 36 | # Copy HTML files from the ui folder and its subfolders 37 | # find "$ui_folder/settings" -name "*.html" -exec cp {} "$destination_folder/settings/{}" \; 38 | # find "$ui_folder/popup" -name "*.html" -exec cp {} "$destination_folder/popup/{}" \; 39 | cp -v "$ui_folder"/settings/*.html "$destination_folder/ui/settings/" 40 | cp -v "$ui_folder"/popup/*.html "$destination_folder/ui/popup/" 41 | 42 | echo "Files copied to ./dist folder." 43 | 44 | mkdir -p ./bin 45 | 46 | cd "$destination_folder" 47 | 48 | zip -r -q -FS ../bin/ytc.zip * 49 | 50 | cd .. 51 | 52 | cp -r "$destination_folder" "$destination_folder_firefox" 53 | 54 | cp "$manifest_file_firefox" "$destination_folder_firefox/manifest.json" 55 | 56 | cd "$destination_folder_firefox" 57 | 58 | zip -r -q -FS ../bin/ytc.xpi * -------------------------------------------------------------------------------- /content-scripts/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "channel-blocker", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "channel-blocker", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/chrome": "^0.0.254", 13 | "chrome-types": "^0.1.246", 14 | "typescript": "^5.3.3" 15 | } 16 | }, 17 | "node_modules/@types/chrome": { 18 | "version": "0.0.254", 19 | "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.254.tgz", 20 | "integrity": "sha512-svkOGKwA+6ZZuk9xtrYun8MYpNY/9hD17rgZ19v3KunhsK1ZOKaMESw12/1AXLh1u3UPA8jQIRi2370DXv9wgw==", 21 | "dev": true, 22 | "dependencies": { 23 | "@types/filesystem": "*", 24 | "@types/har-format": "*" 25 | } 26 | }, 27 | "node_modules/@types/filesystem": { 28 | "version": "0.0.35", 29 | "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.35.tgz", 30 | "integrity": "sha512-1eKvCaIBdrD2mmMgy5dwh564rVvfEhZTWVQQGRNn0Nt4ZEnJ0C8oSUCzvMKRA4lGde5oEVo+q2MrTTbV/GHDCQ==", 31 | "dev": true, 32 | "dependencies": { 33 | "@types/filewriter": "*" 34 | } 35 | }, 36 | "node_modules/@types/filewriter": { 37 | "version": "0.0.32", 38 | "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.32.tgz", 39 | "integrity": "sha512-Kpi2GXQyYJdjL8mFclL1eDgihn1SIzorMZjD94kdPZh9E4VxGOeyjPxi5LpsM4Zku7P0reqegZTt2GxhmA9VBg==", 40 | "dev": true 41 | }, 42 | "node_modules/@types/har-format": { 43 | "version": "1.2.15", 44 | "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz", 45 | "integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==", 46 | "dev": true 47 | }, 48 | "node_modules/chrome-types": { 49 | "version": "0.1.251", 50 | "resolved": "https://registry.npmjs.org/chrome-types/-/chrome-types-0.1.251.tgz", 51 | "integrity": "sha512-QcQjFCebg1PGkR9Th2tNjQoxhLeSe1GEhDsUjR40wmXTrCdb44pO2Pl0Gwmt8/D6t9Mu7TlX8sU9F+B8OZTfmQ==", 52 | "dev": true 53 | }, 54 | "node_modules/typescript": { 55 | "version": "5.3.3", 56 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 57 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 58 | "dev": true, 59 | "bin": { 60 | "tsc": "bin/tsc", 61 | "tsserver": "bin/tsserver" 62 | }, 63 | "engines": { 64 | "node": ">=14.17" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /content-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "channel-blocker", 3 | "version": "1.0.0", 4 | "description": "A web extension allowing you to block YouTube™ videos and comments by blacklisting users and/or by using regular expressions.", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "tsc -w", 8 | "build": "tsc" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Time-Machine-Development/Channel-Blocker.git" 13 | }, 14 | "keywords": [ 15 | "channel", 16 | "blocker", 17 | "youtube", 18 | "chrome-extension" 19 | ], 20 | "author": "Marc Beyer", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/Time-Machine-Development/Channel-Blocker/issues" 24 | }, 25 | "homepage": "https://github.com/Time-Machine-Development/Channel-Blocker#readme", 26 | "devDependencies": { 27 | "@types/chrome": "^0.0.254", 28 | "chrome-types": "^0.1.246", 29 | "typescript": "^5.3.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /content-scripts/ts/context/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds a MutationObserver to monitor changes in the node of the current HTML document. 3 | * The provided callback function is called whenever there is a change in the <title> node. 4 | * 5 | * YouTube modifies this element when the context changes. 6 | */ 7 | async function addTitleChangeObserver(callback: () => void) { 8 | const observerOptions = { 9 | childList: true, 10 | subtree: true, 11 | characterData: true, 12 | }; 13 | 14 | const observer = new MutationObserver(() => { 15 | callback(); 16 | }); 17 | 18 | const titleElement = await getElement("title"); 19 | observer.observe(titleElement, observerOptions); 20 | } 21 | 22 | /** 23 | * Determines the current YouTube context based on the URL and returns it. 24 | * 25 | * @returns The current YouTube context. 26 | */ 27 | function getYTContext(): YTContext { 28 | const regExpContextMap: RegExpContextMap = { 29 | "^https://www\\.youtube\\.com/watch\\?.*$": YTContext.VIDEO, 30 | "^https://www\\.youtube\\.com/results\\?search_query=.*$": YTContext.SEARCH, 31 | "^https://www\\.youtube\\.com/(\\?(.)*|)$": YTContext.HOME, 32 | 33 | "^https://www\\.youtube\\.com/user/[^/]+/videos(\\?(.)*|)$": YTContext.CHANNEL_VIDEOS, 34 | "^https://www\\.youtube\\.com/channel/[^/]+/videos(\\?(.)*|)$": YTContext.CHANNEL_VIDEOS, 35 | "^https://www\\.youtube\\.com/@[^/]+/videos.*$": YTContext.CHANNEL_VIDEOS, 36 | 37 | "^https://www\\.youtube\\.com/user/[^/]*(\\?(.)*|)$": YTContext.CHANNEL_HOME, 38 | "^https://www\\.youtube\\.com/channel/[^/]*(\\?(.)*|)$": YTContext.CHANNEL_HOME, 39 | "^https://www\\.youtube\\.com/user/[^/]+/featured(\\?(.)*|)$": YTContext.CHANNEL_HOME, 40 | "^https://www\\.youtube\\.com/channel/[^/]+/featured(\\?(.)*|)$": YTContext.CHANNEL_HOME, 41 | "^https://www\\.youtube\\.com/@.*$": YTContext.CHANNEL_HOME, 42 | 43 | "^https://www\\.youtube\\.com/feed/trending(\\?(.)*|)$": YTContext.TRENDING, 44 | "^https://www\\.youtube\\.com/feed/explore(\\?(.)*|)$": YTContext.TRENDING, 45 | }; 46 | 47 | for (const regExp in regExpContextMap) { 48 | if (Object.prototype.hasOwnProperty.call(regExpContextMap, regExp)) { 49 | if (new RegExp(regExp).test(window.location.href)) { 50 | return regExpContextMap[regExp]; 51 | } 52 | } 53 | } 54 | 55 | return YTContext.OTHER; 56 | } 57 | -------------------------------------------------------------------------------- /content-scripts/ts/enums/enums.ts: -------------------------------------------------------------------------------- 1 | enum YTContext { 2 | VIDEO, 3 | SEARCH, 4 | CHANNEL_HOME, 5 | CHANNEL_VIDEOS, 6 | TRENDING, 7 | HOME, 8 | OTHER, 9 | } 10 | 11 | enum SettingsDesign { 12 | DETECT, 13 | DARK, 14 | LICHT, 15 | } 16 | -------------------------------------------------------------------------------- /content-scripts/ts/enums/messages.ts: -------------------------------------------------------------------------------- 1 | enum MessageType { 2 | ADD_BLOCKING_RULE, 3 | REMOVE_BLOCKING_RULE, 4 | IS_BLOCKED, 5 | STORAGE_CHANGED, 6 | REQUEST_SETTINGS, 7 | SETTINGS_CHANGED, 8 | } 9 | 10 | enum CommunicationRole { 11 | SERVICE_WORKER, 12 | CONTENT_SCRIPT, 13 | SETTINGS, 14 | } 15 | -------------------------------------------------------------------------------- /content-scripts/ts/helper/blockBtn.ts: -------------------------------------------------------------------------------- 1 | //generates the SVG of a block-btn 2 | function createBlockBtnSVG() { 3 | let svgURI = "http://www.w3.org/2000/svg"; 4 | let svg = document.createElementNS(svgURI, "svg"); 5 | 6 | svg.setAttribute("viewBox", "0 0 100 100"); 7 | 8 | let path = document.createElementNS(svgURI, "path"); 9 | path.setAttribute("d", "M 10,10 L 90,90 M 90,10 L 10,90"); 10 | path.setAttribute("style", "fill: transparent;stroke-linecap: round;stroke-width: 20;"); 11 | 12 | svg.appendChild(path); 13 | 14 | return svg; 15 | } 16 | 17 | //creates and returns a block-button and applies (optionally) passed style options style which blocks user/channel-name userChannelName which clicked 18 | function createBlockBtnElement(userChannelName: string) { 19 | let btn = document.createElement("button"); 20 | btn.setAttribute("class", "cb_block_button"); 21 | btn.setAttribute("type", "button"); 22 | btn.setAttribute("title", "Block '" + userChannelName + "' (Channel Blocker)"); 23 | 24 | btn.appendChild(createBlockBtnSVG()); 25 | 26 | return btn; 27 | } 28 | 29 | //adds a new Element with id "cb_style" and updates CSS depending on contentUIConfig (defined in config.js) 30 | function initBlockBtnCSS() { 31 | //if cb_style Element does not already exist add it to the head 32 | if (document.getElementById("cb_style") === null) { 33 | let style = document.createElement("style"); 34 | style.id = "cb_style"; 35 | document.head.appendChild(style); 36 | } 37 | 38 | //set new css rules 39 | updateBlockBtnCSS(); 40 | } 41 | 42 | //updates CSS depending on contentUIConfig (defined in config.js) 43 | function updateBlockBtnCSS() { 44 | //get the cb_style element 45 | let style = document.getElementById("cb_style") as HTMLStyleElement; 46 | 47 | if (style.sheet === null) return; 48 | 49 | //remove all old rules 50 | while (style.sheet.cssRules.length > 0) { 51 | style.sheet.deleteRule(0); 52 | } 53 | 54 | //define width, strokeColor and display depending on contentUIConfig (defined in config.js) 55 | 56 | //add the new rules 57 | if (buttonVisible) { 58 | style.sheet.insertRule(` 59 | .cb_block_button { 60 | background-color: Transparent; 61 | border: none; 62 | stroke: ${buttonColor}; 63 | cursor: pointer; 64 | margin: 0; 65 | padding: 0 0.5rem 0 0; 66 | } 67 | `); 68 | style.sheet.insertRule(` 69 | *:has(> .cb_block_button) { 70 | display: flex !important; 71 | align-items: center !important; 72 | flex-direction: row !important; 73 | flex-wrap: nowrap !important; 74 | } 75 | `); 76 | style.sheet.insertRule(` 77 | .cb_block_button + * { 78 | overflow: hidden; 79 | } 80 | `); 81 | style.sheet.insertRule(` 82 | .cb_block_button.cb_large { 83 | padding: 16px; 84 | } 85 | `); 86 | } else { 87 | style.sheet.insertRule(` 88 | .cb_block_button { 89 | display: none; 90 | } 91 | `); 92 | } 93 | 94 | style.sheet.insertRule(` 95 | .cb_block_button svg{ 96 | display: block; 97 | width: ${buttonSize * 0.1 - 2}px; 98 | } 99 | `); 100 | style.sheet.insertRule(` 101 | #items.blocked, 102 | .blocked { 103 | display: none !important; 104 | } 105 | `); 106 | } 107 | -------------------------------------------------------------------------------- /content-scripts/ts/helper/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously retrieves an element matching the provided query selector. 3 | * 4 | * @param querySelector The selector used to query for the desired element. 5 | * @returns A Promise that resolves with the found element. 6 | */ 7 | function getElement(querySelector: string, rootElement?: Element | Document): Promise<Element> { 8 | const root = rootElement ?? document; 9 | 10 | return new Promise((resolve) => { 11 | let searchedElement: Element | null = root.querySelector(querySelector); 12 | if (searchedElement !== null) return resolve(searchedElement); 13 | 14 | const observerOptions = { 15 | childList: true, 16 | subtree: true, 17 | }; 18 | 19 | const observer = new MutationObserver(() => { 20 | let searchedElement: Element | null = root.querySelector(querySelector); 21 | if (searchedElement !== null) { 22 | observer.disconnect(); 23 | return resolve(searchedElement); 24 | } 25 | }); 26 | observer.observe(document.body, observerOptions); 27 | }); 28 | } 29 | 30 | function getElementFromList(querySelectors: string[], rootElement?: Element | Document): Promise<{ element: Element; index: number }> { 31 | const root = rootElement ?? document; 32 | 33 | return new Promise((resolve) => { 34 | for (let index = 0; index < querySelectors.length; index++) { 35 | let searchedElement: Element | null = root.querySelector(querySelectors[index]); 36 | if (searchedElement !== null) return resolve({ element: searchedElement, index }); 37 | } 38 | 39 | const observerOptions = { 40 | childList: true, 41 | subtree: true, 42 | }; 43 | 44 | const observer = new MutationObserver(() => { 45 | for (let index = 0; index < querySelectors.length; index++) { 46 | let searchedElement: Element | null = root.querySelector(querySelectors[index]); 47 | if (searchedElement !== null) { 48 | observer.disconnect(); 49 | return resolve({ element: searchedElement, index }); 50 | } 51 | } 52 | }); 53 | observer.observe(document.body, observerOptions); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /content-scripts/ts/index.ts: -------------------------------------------------------------------------------- 1 | initBlockBtnCSS(); 2 | 3 | function loadContext() { 4 | const context = getYTContext(); 5 | handleContextChange(context); 6 | } 7 | 8 | addTitleChangeObserver(loadContext); 9 | 10 | loadContext(); 11 | -------------------------------------------------------------------------------- /content-scripts/ts/interfaces/interfaces.ts: -------------------------------------------------------------------------------- 1 | interface ObserverOptions { 2 | anchorSelector: string; 3 | videoTitle?: string[]; 4 | userChannelName?: string[]; 5 | commentContent?: string[]; 6 | insertBlockBtn?: ((element: HTMLElement, userChannelNameElement: HTMLElement, button: HTMLButtonElement) => void)[]; 7 | transformChannelName?: ((channelName: string) => string)[]; 8 | embeddedObserver?: string; 9 | } 10 | 11 | interface SubObserverOptions { 12 | targetSelector: string; 13 | anchorSelector: string; 14 | observerOptions?: ObserverOptions[]; 15 | subObserver?: SubObserverOptions[]; 16 | } 17 | 18 | interface RegExpContextMap { 19 | [key: string]: YTContext; 20 | } 21 | 22 | interface KeyValueMap { 23 | [key: string]: string; 24 | } 25 | -------------------------------------------------------------------------------- /content-scripts/ts/interfaces/messages.ts: -------------------------------------------------------------------------------- 1 | interface Message { 2 | sender: CommunicationRole; 3 | receiver: CommunicationRole; 4 | type: MessageType; 5 | content: any; 6 | } 7 | 8 | interface AddBlockingRuleMessage extends Message { 9 | content: { 10 | blockedChannel?: string; 11 | excludedChannel?: string; 12 | blockingChannelRegExp?: string; 13 | blockingCommentRegExp?: string; 14 | blockingVideoTitleRegExp?: string; 15 | caseInsensitive?: boolean; 16 | }; 17 | } 18 | 19 | interface RemoveBlockingRuleMessage extends Message { 20 | content: { 21 | blockedChannel?: string[]; 22 | excludedChannel?: string[]; 23 | blockingChannelRegExp?: string[]; 24 | blockingCommentRegExp?: string[]; 25 | blockingVideoTitleRegExp?: string[]; 26 | }; 27 | } 28 | 29 | interface IsBlockedMessage extends Message { 30 | content: { 31 | videoTitle?: string; 32 | userChannelName?: string; 33 | commentContent?: string; 34 | }; 35 | } 36 | 37 | interface StorageChangedMessage extends Message { 38 | content: undefined; 39 | } 40 | 41 | interface RequestSettingsMessage extends Message { 42 | content: undefined; 43 | } 44 | 45 | interface SettingsChangedMessage extends Message { 46 | content: { 47 | buttonVisible: boolean; 48 | buttonColor: string; 49 | buttonSize: number; 50 | animationSpeed: number; 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /content-scripts/ts/interfaces/storage.ts: -------------------------------------------------------------------------------- 1 | interface OldStorageObject { 2 | "0"?: { [key: string]: number }; 3 | "1"?: { [key: string]: number }; 4 | "2"?: { [key: string]: number }; 5 | "3"?: { [key: string]: number }; 6 | "4"?: { [key: string]: number }; 7 | content_ui: { 8 | "0": boolean; // button_visible 9 | "1": string; // button_color 10 | "2": number; // button_size 11 | "3": number; // animation_speed 12 | }; 13 | settings_ui: { 14 | "0": number; // design 15 | "1": boolean; // advanced_view 16 | "2": boolean; // open_popup 17 | }; 18 | } 19 | 20 | interface StorageObject { 21 | version: string; 22 | 23 | blockedChannels: string[]; 24 | excludedChannels: string[]; 25 | 26 | blockedVideoTitles: KeyValueMap; 27 | blockedChannelsRegExp: KeyValueMap; 28 | blockedComments: KeyValueMap; 29 | } 30 | 31 | interface SettingsStorageObject { 32 | version: string; 33 | 34 | settings: { 35 | design: SettingsDesign; 36 | advancedView: boolean; 37 | openPopup: boolean; 38 | buttonVisible: boolean; 39 | buttonColor: string; 40 | buttonSize: number; 41 | animationSpeed: number; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /content-scripts/ts/observer/index.ts: -------------------------------------------------------------------------------- 1 | let activeObserver: Observer[] = []; 2 | let curYTContext: YTContext = YTContext.OTHER; 3 | 4 | /** 5 | * Handles changes in the YouTube context and updates the active observer accordingly. 6 | * 7 | * @param {YTContext} context - The new YouTube context to handle. This can be one of the following: 8 | * - YTContext.HOME: The homepage (https://www.youtube.com/) 9 | * - YTContext.VIDEO: A video page (https://www.youtube.com/watch?v=<ID>) 10 | * - YTContext.SEARCH: A search results page (https://www.youtube.com/results?search_query=<INPUT>) 11 | * - YTContext.TRENDING: The trending page (https://www.youtube.com/feed/trending) 12 | * 13 | * @returns {void} 14 | */ 15 | function handleContextChange(context: YTContext) { 16 | if (curYTContext === context) return; 17 | curYTContext = context; 18 | 19 | while (activeObserver.length > 0) { 20 | activeObserver.pop()?.disconnect(); 21 | } 22 | 23 | switch (context) { 24 | case YTContext.HOME: 25 | //HomePage(https://www.youtube.com/) 26 | activeObserver = createHomeObserver(); 27 | break; 28 | case YTContext.VIDEO: 29 | //VideoPage(https://www.youtube.com/watch?v=<ID>) 30 | activeObserver = createVideoObserver(); 31 | break; 32 | case YTContext.SEARCH: 33 | //SearchPage(https://www.youtube.com/results?search_query=<INPUT>) 34 | activeObserver = createSearchObserver(); 35 | break; 36 | case YTContext.TRENDING: 37 | //TrendingPage(https://www.youtube.com/feed/trending) 38 | activeObserver = createTrendingObserver(); 39 | break; 40 | 41 | default: 42 | break; 43 | } 44 | } 45 | 46 | // The following functions create the observer for the YouTube pages 47 | // If something is broken, because of an update changing the structure of a page, 48 | // you most likely have to change the structure description here. 49 | 50 | /** 51 | * Creates observer for the video page (i.e.: YTContext.VIDEO: https://www.youtube.com/watch?v=<ID>) and returns them. 52 | * 53 | * @returns an array of observer for the video page. 54 | */ 55 | function createVideoObserver() { 56 | return [ 57 | new VideoCreatorObserver(), 58 | new Observer("div[class='ytp-endscreen-content']", [ 59 | { 60 | anchorSelector: "a", 61 | userChannelName: ["span[class='ytp-videowall-still-info-author']"], 62 | videoTitle: ["span[class='ytp-videowall-still-info-title']"], 63 | insertBlockBtn: [ 64 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 65 | // No block button is inserted 66 | }, 67 | ], 68 | transformChannelName: [ 69 | (userChannelName) => { 70 | return userChannelName.split(" • ")[0]; 71 | }, 72 | ], 73 | }, 74 | ]), 75 | new Observer("div#items[class='style-scope ytd-watch-next-secondary-results-renderer']", [ 76 | { 77 | anchorSelector: "ytd-compact-video-renderer", 78 | userChannelName: ["yt-formatted-string#text[class='style-scope ytd-channel-name']"], 79 | videoTitle: ["span#video-title[class=' style-scope ytd-compact-video-renderer style-scope ytd-compact-video-renderer']"], 80 | insertBlockBtn: [ 81 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 82 | element.querySelector("ytd-channel-name")?.insertAdjacentElement("beforebegin", button); 83 | }, 84 | ], 85 | embeddedObserver: "ytd-item-section-renderer", 86 | }, 87 | { 88 | anchorSelector: "ytd-item-section-renderer", 89 | embeddedObserver: "div#contents", 90 | }, 91 | ]), 92 | // New design (Video suggestions under main video) 93 | new Observer( 94 | "#bottom-grid #contents.ytd-rich-grid-renderer", 95 | [], 96 | [ 97 | { 98 | anchorSelector: "#contents", 99 | targetSelector: "ytd-rich-grid-row", 100 | observerOptions: [ 101 | { 102 | userChannelName: ["#channel-name a"], 103 | videoTitle: ["#video-title"], 104 | insertBlockBtn: [ 105 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 106 | element.querySelector("ytd-channel-name")?.insertAdjacentElement("beforebegin", button); 107 | }, 108 | ], 109 | anchorSelector: "ytd-rich-item-renderer", 110 | }, 111 | ], 112 | }, 113 | ] 114 | ), 115 | // Comments for the old and new design (Comments on the right of the main video) 116 | new Observer("#comments #contents.ytd-item-section-renderer", [ 117 | { 118 | anchorSelector: "ytd-comment-thread-renderer", 119 | userChannelName: ["#author-text", "#text-container"], 120 | commentContent: ["#content-text"], 121 | insertBlockBtn: [ 122 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 123 | element.querySelector("div#header-author")?.insertAdjacentElement("afterbegin", button); 124 | }, 125 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 126 | element.querySelector("span#author-comment-badge")?.insertAdjacentElement("beforebegin", button); 127 | }, 128 | ], 129 | transformChannelName: [ 130 | (userChannelName) => { 131 | return userChannelName.trim().substring(1); 132 | }, 133 | (userChannelName) => { 134 | return userChannelName.trim().substring(1); 135 | }, 136 | ], 137 | embeddedObserver: "div#contents", 138 | }, 139 | { 140 | anchorSelector: "ytd-comment-view-model", 141 | userChannelName: ["#author-text", "#text-container"], 142 | commentContent: ["#content-text"], 143 | insertBlockBtn: [ 144 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 145 | element.querySelector("div#header-author")?.insertAdjacentElement("afterbegin", button); 146 | }, 147 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 148 | element.querySelector("span#author-comment-badge")?.insertAdjacentElement("beforebegin", button); 149 | }, 150 | ], 151 | transformChannelName: [ 152 | (userChannelName) => { 153 | return userChannelName.trim().substring(1); 154 | }, 155 | (userChannelName) => { 156 | return userChannelName.trim().substring(1); 157 | }, 158 | ], 159 | }, 160 | { 161 | anchorSelector: "ytd-comment-renderer", 162 | userChannelName: [ 163 | "yt-formatted-string[class=' style-scope ytd-comment-renderer style-scope ytd-comment-renderer']", 164 | "yt-formatted-string#text[class='style-scope ytd-channel-name']", 165 | ], 166 | commentContent: ["yt-formatted-string#content-text"], 167 | insertBlockBtn: [ 168 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 169 | element.querySelector("div#header-author")?.insertAdjacentElement("afterbegin", button); 170 | }, 171 | (element: HTMLElement, userChannelName: HTMLElement, button: HTMLButtonElement) => { 172 | element.querySelector("span#author-comment-badge")?.insertAdjacentElement("beforebegin", button); 173 | }, 174 | ], 175 | transformChannelName: [ 176 | (userChannelName) => { 177 | return userChannelName.substring(1); 178 | }, 179 | (userChannelName) => { 180 | return userChannelName.substring(1); 181 | }, 182 | ], 183 | }, 184 | ]), 185 | ]; 186 | } 187 | 188 | /** 189 | * Creates observer for the trending page (i.e.: YTContext.TRENDING: https://www.youtube.com/feed/trending) and returns them. 190 | * 191 | * @returns an array of observer for the trending page. 192 | */ 193 | function createTrendingObserver() { 194 | return [ 195 | new Observer( 196 | "ytd-page-manager#page-manager", 197 | [], 198 | [ 199 | { 200 | targetSelector: "ytd-browse", 201 | anchorSelector: "div#contents[class='style-scope ytd-section-list-renderer']", 202 | subObserver: [ 203 | { 204 | targetSelector: "ytd-item-section-renderer", 205 | anchorSelector: "div#grid-container", 206 | observerOptions: [ 207 | { 208 | anchorSelector: "ytd-video-renderer", 209 | userChannelName: ["a[class='yt-simple-endpoint style-scope yt-formatted-string']"], 210 | videoTitle: ["yt-formatted-string[class='style-scope ytd-video-renderer']"], 211 | }, 212 | ], 213 | }, 214 | ], 215 | }, 216 | ] 217 | ), 218 | ]; 219 | } 220 | 221 | /** 222 | * Creates observer for the home page (i.e.: YTContext.HOME: https://www.youtube.com/) and returns them. 223 | * 224 | * @returns an array of observer for the home page. 225 | */ 226 | function createHomeObserver(): Observer[] { 227 | return [ 228 | new Observer( 229 | "ytd-browse #contents.ytd-rich-grid-renderer", 230 | [ 231 | { 232 | anchorSelector: "ytd-rich-item-renderer", 233 | userChannelName: ["yt-formatted-string#text"], 234 | videoTitle: ["yt-formatted-string#video-title"], 235 | }, 236 | ], 237 | [ 238 | { 239 | targetSelector: "ytd-rich-grid-row[class='style-scope ytd-rich-grid-renderer']", 240 | anchorSelector: "div#contents[class='style-scope ytd-rich-grid-row']", 241 | observerOptions: [ 242 | { 243 | anchorSelector: "ytd-rich-item-renderer[class='style-scope ytd-rich-grid-row']", 244 | userChannelName: [ 245 | "a[class='yt-simple-endpoint style-scope yt-formatted-string']", 246 | "yt-formatted-string#text[class='style-scope ytd-channel-name']", 247 | ], 248 | videoTitle: [ 249 | "yt-formatted-string#video-title[class='style-scope ytd-rich-grid-media']", 250 | "yt-formatted-string#video-title[class='style-scope ytd-ad-inline-playback-meta-block']", 251 | ], 252 | }, 253 | ], 254 | }, 255 | { 256 | targetSelector: "ytd-rich-section-renderer", 257 | anchorSelector: "div#contents", 258 | observerOptions: [ 259 | { 260 | anchorSelector: "ytd-rich-item-renderer", 261 | userChannelName: ["a[class='yt-simple-endpoint style-scope yt-formatted-string']"], 262 | videoTitle: ["yt-formatted-string#video-title[class='style-scope ytd-rich-grid-media']"], 263 | }, 264 | ], 265 | }, 266 | ] 267 | ), 268 | ]; 269 | } 270 | 271 | /** 272 | * Creates observer for the search page (i.e.: YTContext.SEARCH: https://www.youtube.com/results?search_query=<INPUT>) and returns them. 273 | * 274 | * @returns an array of observer for the search page. 275 | */ 276 | function createSearchObserver(): Observer[] { 277 | return [ 278 | new Observer( 279 | "ytd-search div#contents[class='style-scope ytd-section-list-renderer']", 280 | [], 281 | [ 282 | { 283 | targetSelector: "ytd-item-section-renderer", //"ytd-shelf-renderer[class='style-scope ytd-item-section-renderer']", 284 | anchorSelector: "div#contents[class=' style-scope ytd-item-section-renderer style-scope ytd-item-section-renderer']", 285 | observerOptions: [ 286 | { 287 | anchorSelector: "ytd-video-renderer[class='style-scope ytd-item-section-renderer']", 288 | userChannelName: ["yt-formatted-string#text[class='style-scope ytd-channel-name']"], 289 | videoTitle: ["yt-formatted-string[class='style-scope ytd-video-renderer']"], 290 | }, 291 | { 292 | anchorSelector: "ytd-video-renderer[class='style-scope ytd-vertical-list-renderer']", 293 | userChannelName: ["yt-formatted-string#text[class='style-scope ytd-channel-name']"], 294 | videoTitle: ["yt-formatted-string[class='style-scope ytd-video-renderer']"], 295 | }, 296 | { 297 | anchorSelector: "ytd-search-pyv-renderer[class='style-scope ytd-item-section-renderer']", 298 | userChannelName: ["a[class='yt-simple-endpoint style-scope yt-formatted-string']"], 299 | videoTitle: ["h3#video-title[class='style-scope ytd-promoted-video-renderer']"], 300 | }, 301 | { 302 | anchorSelector: "ytd-ad-slot-renderer[class='style-scope ytd-item-section-renderer']", 303 | userChannelName: [ 304 | "div#website-text[class='style-scope ytd-promoted-sparkles-web-renderer yt-simple-endpoint']", 305 | ], 306 | videoTitle: ["h3#title[class='style-scope ytd-promoted-sparkles-web-renderer yt-simple-endpoint']"], 307 | }, 308 | { 309 | anchorSelector: "ytd-playlist-renderer[class='style-scope ytd-item-section-renderer']", 310 | userChannelName: ["a[class='yt-simple-endpoint style-scope yt-formatted-string']"], 311 | videoTitle: ["span#video-title[class='style-scope ytd-playlist-renderer']"], 312 | }, 313 | { 314 | anchorSelector: "ytd-channel-renderer[class='style-scope ytd-item-section-renderer']", 315 | userChannelName: ["yt-formatted-string#text[class='style-scope ytd-channel-name']"], 316 | }, 317 | { 318 | anchorSelector: "ytd-grid-video-renderer", 319 | userChannelName: ["a[class='yt-simple-endpoint style-scope yt-formatted-string']"], 320 | videoTitle: ["a#video-title"], 321 | }, 322 | ], 323 | subObserver: [ 324 | { 325 | targetSelector: "ytd-shelf-renderer[class='style-scope ytd-item-section-renderer']", 326 | anchorSelector: "div#items", 327 | }, 328 | /* 329 | { 330 | targetSelector: "ytd-shelf-renderer[class='style-scope ytd-item-section-renderer']", 331 | anchorSelector: "div#items[class='style-scope yt-horizontal-list-renderer']", 332 | }, 333 | */ 334 | ], 335 | }, 336 | ] 337 | ), 338 | ]; 339 | } 340 | 341 | /** 342 | * Updates all active observers by calling their `update` method. 343 | */ 344 | function updateObserver() { 345 | for (let index = 0; index < activeObserver.length; index++) { 346 | activeObserver[index].update(); 347 | } 348 | } 349 | 350 | let buttonVisible: boolean = true; 351 | let buttonColor: string = "#717171"; 352 | let buttonSize: number = 142; 353 | let animationSpeed: number = 200; 354 | 355 | /** 356 | * Updates the settings for the button appearance and behavior based on the provided message. 357 | * 358 | * @param {SettingsChangedMessage} message - The message containing the new settings. 359 | */ 360 | function updateSettings(message: SettingsChangedMessage) { 361 | buttonVisible = message.content.buttonVisible; 362 | buttonColor = message.content.buttonColor; 363 | buttonSize = message.content.buttonSize; 364 | animationSpeed = message.content.animationSpeed; 365 | 366 | updateBlockBtnCSS(); 367 | } 368 | 369 | chrome.runtime.onMessage.addListener((message: Message, sender: chrome.runtime.MessageSender) => { 370 | if (message.receiver !== CommunicationRole.CONTENT_SCRIPT) return; 371 | 372 | switch (message.type) { 373 | case MessageType.STORAGE_CHANGED: 374 | updateObserver(); 375 | break; 376 | 377 | case MessageType.SETTINGS_CHANGED: 378 | updateSettings(message); 379 | break; 380 | 381 | default: 382 | break; 383 | } 384 | }); 385 | -------------------------------------------------------------------------------- /content-scripts/ts/observer/observer.ts: -------------------------------------------------------------------------------- 1 | class Observer { 2 | private target: string | Element; 3 | 4 | private observerOptions: ObserverOptions[]; 5 | private subObserver: SubObserverOptions[]; 6 | 7 | protected activeMutationObserver: MutationObserver[] = []; 8 | private isBlockedValidators: Function[] = []; 9 | 10 | constructor(targetSelector: string | Element, observerOptions: ObserverOptions[], subObserver?: SubObserverOptions[]) { 11 | this.target = targetSelector; 12 | this.observerOptions = observerOptions; 13 | this.subObserver = subObserver ?? []; 14 | 15 | this.addObserver(); 16 | } 17 | 18 | public disconnect() { 19 | while (this.activeMutationObserver.length > 0) { 20 | this.activeMutationObserver.pop()?.disconnect(); 21 | } 22 | } 23 | 24 | public update() { 25 | for (let index = 0; index < this.isBlockedValidators.length; index++) { 26 | this.isBlockedValidators[index](); 27 | } 28 | } 29 | 30 | protected async addObserver() { 31 | const element: Element = typeof this.target === "string" ? await getElement(this.target) : this.target; 32 | 33 | for (let index = 0; index < element.children.length; index++) { 34 | this.handleChild(element.children[index]); 35 | } 36 | 37 | const mainMutationObserver = new MutationObserver((mutationRecords: MutationRecord[]) => { 38 | for (let index = 0; index < mutationRecords.length; index++) { 39 | const mutationRecord = mutationRecords[index]; 40 | if (mutationRecord.type === "childList") { 41 | for (let index = 0; index < mutationRecord.addedNodes.length; index++) { 42 | this.handleChild(mutationRecord.addedNodes[index] as Element); 43 | } 44 | } 45 | } 46 | }); 47 | 48 | mainMutationObserver.observe(element, { childList: true }); 49 | this.activeMutationObserver.push(mainMutationObserver); 50 | } 51 | 52 | private handleChild(child: Element) { 53 | // Check for sub observer 54 | for (let index = 0; index < this.subObserver.length; index++) { 55 | const subObserver = this.subObserver[index]; 56 | if (child.matches(subObserver.targetSelector)) { 57 | getElement(subObserver.anchorSelector, child).then((target: Element) => { 58 | activeObserver.push(new Observer(target, subObserver.observerOptions ?? this.observerOptions, subObserver.subObserver)); 59 | }); 60 | } 61 | } 62 | 63 | for (let index = 0; index < this.observerOptions.length; index++) { 64 | const observerOption = this.observerOptions[index]; 65 | if (child.matches(observerOption.anchorSelector)) { 66 | this.addCharacterDataSelector(child, observerOption); 67 | } 68 | } 69 | } 70 | 71 | private async addCharacterDataSelector(element: Element, observerOption: ObserverOptions) { 72 | let userChannelName: string | undefined; 73 | let videoTitle: string | undefined; 74 | let commentContent: string | undefined; 75 | 76 | const checkIfElementIsBlocked = async () => { 77 | const blocked = await isBlocked({ userChannelName, videoTitle, commentContent }); 78 | 79 | element.classList.toggle("blocked", blocked); 80 | }; 81 | this.isBlockedValidators.push(checkIfElementIsBlocked); 82 | 83 | element.querySelectorAll("button[class='cb_block_button']").forEach((blockButton) => { 84 | blockButton.remove(); 85 | }); 86 | 87 | if (observerOption.userChannelName !== undefined) { 88 | const elementAndIndex = await getElementFromList(observerOption.userChannelName, element); 89 | const userChannelNameElement = elementAndIndex.element; 90 | let button = createBlockBtnElement(""); 91 | button.addEventListener("click", (mouseEvent) => { 92 | mouseEvent.preventDefault(); 93 | mouseEvent.stopPropagation(); 94 | 95 | if (userChannelName !== undefined) { 96 | blockUserChannel(userChannelName); 97 | } 98 | }); 99 | 100 | if (observerOption.insertBlockBtn) { 101 | observerOption.insertBlockBtn[elementAndIndex.index](element as HTMLElement, userChannelNameElement as HTMLElement, button); 102 | } else { 103 | userChannelNameElement.insertAdjacentElement("beforebegin", button); 104 | } 105 | 106 | const handleUserChannelName = () => { 107 | userChannelName = userChannelNameElement.textContent ?? undefined; 108 | if ( 109 | userChannelName !== undefined && 110 | observerOption.transformChannelName !== undefined && 111 | observerOption.transformChannelName[elementAndIndex.index] !== undefined 112 | ) { 113 | userChannelName = observerOption.transformChannelName[elementAndIndex.index](userChannelName); 114 | } 115 | button.setAttribute("title", "Block '" + userChannelName + "' (Channel Blocker)"); 116 | checkIfElementIsBlocked(); 117 | }; 118 | const mutationObserver = new MutationObserver(handleUserChannelName); 119 | mutationObserver.observe(userChannelNameElement, { childList: true, subtree: true, characterData: true }); 120 | this.activeMutationObserver.push(mutationObserver); 121 | handleUserChannelName(); 122 | } 123 | 124 | if (observerOption.videoTitle !== undefined) { 125 | const elementAndIndex = await getElementFromList(observerOption.videoTitle, element); 126 | const videoTitleElement = elementAndIndex.element; 127 | const handleVideoTitle = () => { 128 | videoTitle = videoTitleElement.textContent ?? undefined; 129 | checkIfElementIsBlocked(); 130 | }; 131 | const mutationObserver = new MutationObserver(handleVideoTitle); 132 | mutationObserver.observe(videoTitleElement, { childList: true, subtree: true, characterData: true }); 133 | this.activeMutationObserver.push(mutationObserver); 134 | handleVideoTitle(); 135 | } 136 | 137 | if (observerOption.commentContent !== undefined) { 138 | const elementAndIndex = await getElementFromList(observerOption.commentContent, element); 139 | const commentContentElement = elementAndIndex.element; 140 | const handleCommentContent = () => { 141 | commentContent = commentContentElement.textContent ?? undefined; 142 | checkIfElementIsBlocked(); 143 | }; 144 | const mutationObserver = new MutationObserver(handleCommentContent); 145 | mutationObserver.observe(commentContentElement, { childList: true, subtree: true, characterData: true }); 146 | this.activeMutationObserver.push(mutationObserver); 147 | handleCommentContent(); 148 | } 149 | 150 | if (observerOption.embeddedObserver !== undefined) { 151 | const target = element.querySelector(observerOption.embeddedObserver); 152 | if (target !== null) { 153 | activeObserver.push(new Observer(target, this.observerOptions, this.subObserver)); 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /content-scripts/ts/observer/videoCreatorObserver.ts: -------------------------------------------------------------------------------- 1 | class VideoCreatorObserver extends Observer { 2 | constructor() { 3 | super("", []); 4 | } 5 | 6 | protected async addObserver() { 7 | const ownerElement = (await getElement("#owner")) as HTMLDivElement; 8 | 9 | ownerElement.querySelectorAll("button[class='cb_block_button cb_large']").forEach((blockButton) => { 10 | blockButton.remove(); 11 | }); 12 | 13 | const channelNameElement = (await getElement("ytd-channel-name a", ownerElement)) as HTMLAnchorElement; 14 | let userChannelName = channelNameElement.textContent; 15 | 16 | let button = createBlockBtnElement(""); 17 | button.setAttribute("title", "Block '" + userChannelName + "' (Channel Blocker)"); 18 | button.classList.add("cb_large"); 19 | button.addEventListener("click", (mouseEvent) => { 20 | mouseEvent.preventDefault(); 21 | mouseEvent.stopPropagation(); 22 | 23 | if (userChannelName !== null) { 24 | blockUserChannel(userChannelName); 25 | } 26 | }); 27 | ownerElement.insertAdjacentElement("beforeend", button); 28 | 29 | const mainMutationObserver = new MutationObserver((mutationRecords: MutationRecord[]) => { 30 | userChannelName = channelNameElement.textContent; 31 | button.setAttribute("title", "Block '" + userChannelName + "' (Channel Blocker)"); 32 | }); 33 | 34 | mainMutationObserver.observe(channelNameElement, { childList: true }); 35 | this.activeMutationObserver.push(mainMutationObserver); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /content-scripts/ts/storage/index.ts: -------------------------------------------------------------------------------- 1 | async function isBlocked(content: { videoTitle?: string; userChannelName?: string; commentContent?: string }): Promise<boolean> { 2 | const message: IsBlockedMessage = { 3 | sender: CommunicationRole.CONTENT_SCRIPT, 4 | receiver: CommunicationRole.SERVICE_WORKER, 5 | type: MessageType.IS_BLOCKED, 6 | content, 7 | }; 8 | const sending = await sendMessage(message); 9 | return sending === true; 10 | } 11 | 12 | async function blockUserChannel(userChannelName: string) { 13 | const message: AddBlockingRuleMessage = { 14 | sender: CommunicationRole.CONTENT_SCRIPT, 15 | receiver: CommunicationRole.SERVICE_WORKER, 16 | type: MessageType.ADD_BLOCKING_RULE, 17 | content: { 18 | blockedChannel: userChannelName, 19 | }, 20 | }; 21 | const sending = await sendMessage(message); 22 | } 23 | 24 | (async function getSettings() { 25 | const message: RequestSettingsMessage = { 26 | sender: CommunicationRole.CONTENT_SCRIPT, 27 | receiver: CommunicationRole.SERVICE_WORKER, 28 | type: MessageType.REQUEST_SETTINGS, 29 | content: undefined, 30 | }; 31 | const sending = (await sendMessage(message)) as { 32 | buttonVisible: boolean; 33 | buttonColor: string; 34 | buttonSize: number; 35 | animationSpeed: number; 36 | }; 37 | 38 | buttonVisible = sending.buttonVisible; 39 | buttonColor = sending.buttonColor; 40 | buttonSize = sending.buttonSize; 41 | animationSpeed = sending.animationSpeed; 42 | 43 | updateBlockBtnCSS(); 44 | })(); 45 | 46 | async function sendMessage(message: Message) { 47 | return chrome.runtime.sendMessage(message); 48 | } 49 | -------------------------------------------------------------------------------- /content-scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "none", 5 | "outDir": "../dist/content-scripts", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["./**/*.ts"], 12 | "exclude": ["../dist"] 13 | } 14 | -------------------------------------------------------------------------------- /images/CB_icon.svg: -------------------------------------------------------------------------------- 1 | <svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <path fill-rule="evenodd" clip-rule="evenodd" d="M70.6971 153.002L162.5 100L137.901 85.798L70.6971 153.002ZM130.729 81.6569L116.465 73.4218L68.75 121.137V143.636L130.729 81.6569ZM68.75 109.823L109.293 69.2807L68.75 45.8734V109.823Z" fill="#FF3333"/> 3 | <path fill-rule="evenodd" clip-rule="evenodd" d="M200 100C200 155.228 155.228 200 100 200C44.7715 200 0 155.228 0 100C0 44.7715 44.7715 0 100 0C155.228 0 200 44.7715 200 100ZM184.091 101.136C184.091 146.951 146.951 184.091 101.136 184.091C80.8037 184.091 62.1795 176.776 47.7522 164.634L164.634 47.7522C176.776 62.1795 184.091 80.8037 184.091 101.136ZM153.275 36.6116C139.029 25.0855 120.889 18.1818 101.136 18.1818C55.3218 18.1818 18.1818 55.3218 18.1818 101.136C18.1818 120.889 25.0855 139.029 36.6116 153.275L153.275 36.6116Z" fill="#FF3333"/> 4 | </svg> 5 | -------------------------------------------------------------------------------- /images/CB_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/images/CB_icon_128.png -------------------------------------------------------------------------------- /images/CB_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/images/CB_icon_16.png -------------------------------------------------------------------------------- /images/CB_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/images/CB_icon_32.png -------------------------------------------------------------------------------- /images/CB_icon_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/images/CB_icon_48.png -------------------------------------------------------------------------------- /images/CB_icon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Time-Machine-Development/Channel-Blocker-Manifest-v3/d479baf40fb1c01357a7cd801ecc496dce8da022/images/CB_icon_96.png -------------------------------------------------------------------------------- /images/misc/Burger.svg: -------------------------------------------------------------------------------- 1 | <svg width="500" height="400" viewBox="0 0 500 400" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 | <rect y="160" width="500" height="80" rx="30" fill="white" /> 3 | <rect width="500" height="80" rx="30" fill="white" /> 4 | <rect y="320" width="500" height="80" rx="30" fill="white" /> 5 | </svg> -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Channel Blocker", 4 | "version": "3.0.5", 5 | 6 | "description": "Allows you to block YouTube™ videos and comments by blacklisting users and/or by using regular expressions.", 7 | 8 | "icons": { 9 | "16": "./images/CB_icon_16.png", 10 | "32": "./images/CB_icon_32.png", 11 | "48": "./images/CB_icon_48.png", 12 | "96": "./images/CB_icon_96.png", 13 | "128": "./images/CB_icon_128.png" 14 | }, 15 | 16 | "permissions": ["storage", "unlimitedStorage", "tabs"], 17 | 18 | "background": { 19 | "service_worker": "service-worker/index.js", 20 | "type": "module" 21 | }, 22 | 23 | "content_scripts": [ 24 | { 25 | "matches": ["*://www.youtube.com/*"], 26 | "js": [ 27 | "content-scripts/enums/enums.js", 28 | "content-scripts/enums/messages.js", 29 | "content-scripts/helper/index.js", 30 | "content-scripts/helper/blockBtn.js", 31 | "content-scripts/storage/index.js", 32 | "content-scripts/context/index.js", 33 | "content-scripts/observer/observer.js", 34 | "content-scripts/observer/videoCreatorObserver.js", 35 | "content-scripts/observer/index.js", 36 | "content-scripts/index.js" 37 | ] 38 | } 39 | ], 40 | 41 | "options_page": "./ui/settings/index.html", 42 | 43 | "action": { 44 | "default_title": "Channel Blocker (Configuration)", 45 | "default_icon": { 46 | "16": "./images/CB_icon_16.png", 47 | "32": "./images/CB_icon_32.png", 48 | "48": "./images/CB_icon_48.png", 49 | "96": "./images/CB_icon_96.png", 50 | "128": "./images/CB_icon_128.png" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /manifest.json.firefox: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Channel Blocker", 4 | "version": "3.0.5", 5 | "description": "Allows you to block YouTube™ videos and comments by blacklisting users and/or by using regular expressions.", 6 | "icons": { 7 | "16": "./images/CB_icon_16.png", 8 | "32": "./images/CB_icon_32.png", 9 | "48": "./images/CB_icon_48.png", 10 | "96": "./images/CB_icon_96.png", 11 | "128": "./images/CB_icon_128.png" 12 | }, 13 | "permissions": [ 14 | "storage", 15 | "unlimitedStorage", 16 | "tabs" 17 | ], 18 | "host_permissions": [ 19 | "*://*.youtube.com/*" 20 | ], 21 | "background": { 22 | "scripts": [ 23 | "service-worker/index.js" 24 | ], 25 | "type": "module" 26 | }, 27 | "content_scripts": [ 28 | { 29 | "matches": [ 30 | "*://*.youtube.com/*" 31 | ], 32 | "js": [ 33 | "content-scripts/enums/enums.js", 34 | "content-scripts/enums/messages.js", 35 | "content-scripts/helper/index.js", 36 | "content-scripts/helper/blockBtn.js", 37 | "content-scripts/storage/index.js", 38 | "content-scripts/context/index.js", 39 | "content-scripts/observer/observer.js", 40 | "content-scripts/observer/videoCreatorObserver.js", 41 | "content-scripts/observer/index.js", 42 | "content-scripts/index.js" 43 | ] 44 | } 45 | ], 46 | "options_ui": { 47 | "page": "./ui/settings/index.html", 48 | "open_in_tab": true 49 | }, 50 | "action": { 51 | "default_title": "Channel Blocker (Configuration)", 52 | "default_icon": { 53 | "16": "./images/CB_icon_16.png", 54 | "32": "./images/CB_icon_32.png", 55 | "48": "./images/CB_icon_48.png", 56 | "96": "./images/CB_icon_96.png", 57 | "128": "./images/CB_icon_128.png" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /service-worker/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "channel-blocker-service-worker", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "channel-blocker-service-worker", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/chrome": "^0.0.254", 13 | "chrome-types": "^0.1.246", 14 | "typescript": "^5.3.3" 15 | } 16 | }, 17 | "node_modules/@types/chrome": { 18 | "version": "0.0.254", 19 | "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.254.tgz", 20 | "integrity": "sha512-svkOGKwA+6ZZuk9xtrYun8MYpNY/9hD17rgZ19v3KunhsK1ZOKaMESw12/1AXLh1u3UPA8jQIRi2370DXv9wgw==", 21 | "dev": true, 22 | "dependencies": { 23 | "@types/filesystem": "*", 24 | "@types/har-format": "*" 25 | } 26 | }, 27 | "node_modules/@types/filesystem": { 28 | "version": "0.0.35", 29 | "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.35.tgz", 30 | "integrity": "sha512-1eKvCaIBdrD2mmMgy5dwh564rVvfEhZTWVQQGRNn0Nt4ZEnJ0C8oSUCzvMKRA4lGde5oEVo+q2MrTTbV/GHDCQ==", 31 | "dev": true, 32 | "dependencies": { 33 | "@types/filewriter": "*" 34 | } 35 | }, 36 | "node_modules/@types/filewriter": { 37 | "version": "0.0.32", 38 | "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.32.tgz", 39 | "integrity": "sha512-Kpi2GXQyYJdjL8mFclL1eDgihn1SIzorMZjD94kdPZh9E4VxGOeyjPxi5LpsM4Zku7P0reqegZTt2GxhmA9VBg==", 40 | "dev": true 41 | }, 42 | "node_modules/@types/har-format": { 43 | "version": "1.2.15", 44 | "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.15.tgz", 45 | "integrity": "sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==", 46 | "dev": true 47 | }, 48 | "node_modules/chrome-types": { 49 | "version": "0.1.252", 50 | "resolved": "https://registry.npmjs.org/chrome-types/-/chrome-types-0.1.252.tgz", 51 | "integrity": "sha512-0DGlB6DoLiBBAoz93MC+95FujdYGboabsNRsk7r8vtNi+E6GBO1TwQfqs+06MrQu/S78lnYpWpyIeShjEPWqjw==", 52 | "dev": true 53 | }, 54 | "node_modules/typescript": { 55 | "version": "5.3.3", 56 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 57 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 58 | "dev": true, 59 | "bin": { 60 | "tsc": "bin/tsc", 61 | "tsserver": "bin/tsserver" 62 | }, 63 | "engines": { 64 | "node": ">=14.17" 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /service-worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "channel-blocker-service-worker", 3 | "version": "1.0.0", 4 | "description": "The service worker of the channel blocker web extension.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/Time-Machine-Development/Channel-Blocker.git" 12 | }, 13 | "author": "Marc Beyer", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Time-Machine-Development/Channel-Blocker/issues" 17 | }, 18 | "homepage": "https://github.com/Time-Machine-Development/Channel-Blocker#readme", 19 | "devDependencies": { 20 | "@types/chrome": "^0.0.254", 21 | "chrome-types": "^0.1.246", 22 | "typescript": "^5.3.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /service-worker/ts/enums.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | ADD_BLOCKING_RULE, 3 | REMOVE_BLOCKING_RULE, 4 | IS_BLOCKED, 5 | STORAGE_CHANGED, 6 | REQUEST_SETTINGS, 7 | SETTINGS_CHANGED, 8 | } 9 | 10 | export enum CommunicationRole { 11 | SERVICE_WORKER, 12 | CONTENT_SCRIPT, 13 | SETTINGS, 14 | } 15 | 16 | export enum SettingsDesign { 17 | DETECT, 18 | DARK, 19 | LICHT, 20 | } 21 | -------------------------------------------------------------------------------- /service-worker/ts/helper.ts: -------------------------------------------------------------------------------- 1 | import { Tab } from "./interfaces/interfaces.js"; 2 | 3 | /** 4 | * Limits a value to a range between a minimum and a maximum value. 5 | * @param min The minimum value. 6 | * @param max The maximum value. 7 | * @param value The value to be clamped. 8 | * @returns 9 | */ 10 | export function clamp(min: number, max: number, value: number): number { 11 | return Math.max(min, Math.min(max, value)); 12 | } 13 | 14 | /** 15 | * Finds all config tab (There should normally just be one) and return them in a list. 16 | * @returns A list of config tabs. 17 | */ 18 | export async function getConfigTabs(): Promise<Tab[]> { 19 | let configTabs: Tab[] = []; 20 | const configURL = chrome.runtime.getURL("/*"); 21 | const tabs = await chrome.tabs.query({ url: configURL }); 22 | for (let index = 0; index < tabs.length; index++) { 23 | const tab = tabs[index]; 24 | if (tab.id !== undefined) configTabs.push(tab as Tab); 25 | } 26 | return configTabs; 27 | } 28 | 29 | /** 30 | * Adds mock data to the storage. 31 | * Just used for testing. 32 | */ 33 | export function mockOldStorageData() { 34 | console.log("Clear all data"); 35 | chrome.storage.local.clear(); 36 | 37 | console.log("Add mock data"); 38 | chrome.storage.local.set({ 39 | "0": { 40 | test: 53, 41 | test1: 53, 42 | "This is a test": 53, 43 | 42: 53, 44 | }, 45 | "1": { 46 | "[\\u03A0-\\u1CC0]": 1, 47 | is: 1, 48 | "case-insensitive": 0, 49 | "case-sensitive": 1, 50 | }, 51 | "2": { 52 | test: 1, 53 | "case-insensitive": 0, 54 | "case-sensitive": 1, 55 | }, 56 | "3": { 57 | test: 0, 58 | "case-insensitive": 0, 59 | "case-sensitive": 1, 60 | }, 61 | "4": { 62 | test: 53, 63 | }, 64 | content_ui: { 65 | "0": true, 66 | "1": "#717171", 67 | "2": 106, 68 | "3": 200, 69 | }, 70 | settings_ui: { 71 | "0": 0, 72 | "1": true, 73 | "2": true, 74 | }, 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /service-worker/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType } from "./enums.js"; 2 | import { getConfigTabs } from "./helper.js"; 3 | import { Message } from "./interfaces/interfaces.js"; 4 | import { 5 | handleAddBlockingRuleMessage, 6 | handleIsBlockedMessage, 7 | handleRemoveBlockingRuleMessage, 8 | handleRequestSettings, 9 | handleStorageChangedMessage, 10 | loadDataFromStorage, 11 | loadDataIfNecessary, 12 | sendSettingsChangedMessage, 13 | } from "./storage.js"; 14 | 15 | loadDataFromStorage(); 16 | initListeners(); 17 | 18 | /** 19 | * Adds a message listener and a browser cation listener. 20 | */ 21 | function initListeners() { 22 | chrome.runtime.onMessage.addListener( 23 | async (message: Message, sender: chrome.runtime.MessageSender, sendResponse: (response?: any) => void) => { 24 | if (message.receiver !== CommunicationRole.SERVICE_WORKER) return; 25 | 26 | loadDataIfNecessary(); 27 | 28 | switch (message.type) { 29 | case MessageType.ADD_BLOCKING_RULE: 30 | handleAddBlockingRuleMessage(message); 31 | break; 32 | case MessageType.REMOVE_BLOCKING_RULE: 33 | handleRemoveBlockingRuleMessage(message); 34 | break; 35 | case MessageType.IS_BLOCKED: 36 | sendResponse(handleIsBlockedMessage(message)); 37 | break; 38 | case MessageType.SETTINGS_CHANGED: 39 | sendSettingsChangedMessage(message); 40 | break; 41 | case MessageType.REQUEST_SETTINGS: 42 | sendResponse(handleRequestSettings(message)); 43 | break; 44 | case MessageType.STORAGE_CHANGED: 45 | handleStorageChangedMessage(message); 46 | break; 47 | 48 | default: 49 | break; 50 | } 51 | } 52 | ); 53 | 54 | //open config page as default behavior of clicking the browserAction-button 55 | chrome.action.onClicked.addListener(openConfig); 56 | } 57 | 58 | //if an open config page exists makes config tab active, otherwise creates new config tab and make it active 59 | async function openConfig() { 60 | const configTabs = await getConfigTabs(); 61 | if (configTabs.length > 0) { 62 | const tab = configTabs[0]; 63 | await chrome.tabs.update(tab.id, { 64 | active: true, 65 | }); 66 | 67 | chrome.windows.update(tab.windowId, { 68 | focused: true, 69 | }); 70 | } else { 71 | chrome.tabs.create({ 72 | active: true, 73 | url: "./ui/settings/index.html", 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /service-worker/ts/interfaces/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType } from "../enums.js"; 2 | 3 | export interface Message { 4 | sender: CommunicationRole; 5 | receiver: CommunicationRole; 6 | type: MessageType; 7 | content: any; 8 | } 9 | 10 | export interface AddBlockingRuleMessage extends Message { 11 | content: { 12 | blockedChannel?: string; 13 | excludedChannel?: string; 14 | blockingChannelRegExp?: string; 15 | blockingCommentRegExp?: string; 16 | blockingVideoTitleRegExp?: string; 17 | caseInsensitive?: boolean; 18 | }; 19 | } 20 | 21 | export interface RemoveBlockingRuleMessage extends Message { 22 | content: { 23 | blockedChannel?: string[]; 24 | excludedChannel?: string[]; 25 | blockingChannelRegExp?: string[]; 26 | blockingCommentRegExp?: string[]; 27 | blockingVideoTitleRegExp?: string[]; 28 | }; 29 | } 30 | 31 | export interface IsBlockedMessage extends Message { 32 | content: { 33 | videoTitle?: string; 34 | userChannelName?: string; 35 | commentContent?: string; 36 | }; 37 | } 38 | 39 | export interface StorageChangedMessage extends Message { 40 | content: undefined; 41 | } 42 | 43 | export interface RequestSettingsMessage extends Message { 44 | content: undefined; 45 | } 46 | 47 | export interface SettingsChangedMessage extends Message { 48 | content: { 49 | buttonVisible: boolean; 50 | buttonColor: string; 51 | buttonSize: number; 52 | animationSpeed: number; 53 | }; 54 | } 55 | 56 | export interface Tab extends chrome.tabs.Tab { 57 | id: number; 58 | } 59 | -------------------------------------------------------------------------------- /service-worker/ts/interfaces/storage.ts: -------------------------------------------------------------------------------- 1 | import { SettingsDesign } from "../enums"; 2 | 3 | export interface KeyValueMap { 4 | [key: string]: string; 5 | } 6 | 7 | export interface StorageObject { 8 | version: string; 9 | 10 | blockedChannels: string[]; 11 | excludedChannels: string[]; 12 | 13 | blockedVideoTitles: KeyValueMap; 14 | blockedChannelsRegExp: KeyValueMap; 15 | blockedComments: KeyValueMap; 16 | } 17 | 18 | export interface SettingsStorageObject { 19 | version: string; 20 | 21 | settings: { 22 | design: SettingsDesign; 23 | advancedView: boolean; 24 | openPopup: boolean; 25 | buttonVisible: boolean; 26 | buttonColor: string; 27 | buttonSize: number; 28 | animationSpeed: number; 29 | }; 30 | } 31 | 32 | export interface OldStorageObject { 33 | "0"?: { [key: string]: number }; // blocked channels 34 | "1"?: { [key: string]: number }; // video title RegExp 35 | "2"?: { [key: string]: number }; // channel name RegExp 36 | "3"?: { [key: string]: number }; // comment RegExp 37 | "4"?: { [key: string]: number }; // excluded channels 38 | content_ui: { 39 | "0": boolean; // button_visible 40 | "1": string; // button_color 41 | "2": number; // button_size 42 | "3": number; // animation_speed 43 | }; 44 | settings_ui: { 45 | "0": number; // design 46 | "1": boolean; // advanced_view 47 | "2": boolean; // open_popup 48 | }; 49 | } 50 | 51 | export interface CombinedStorageObject extends SettingsStorageObject, StorageObject {} 52 | -------------------------------------------------------------------------------- /service-worker/ts/storage.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType } from "./enums.js"; 2 | import { KeyValueMap, StorageObject, OldStorageObject, SettingsStorageObject } from "./interfaces/storage.js"; 3 | import { 4 | AddBlockingRuleMessage, 5 | IsBlockedMessage, 6 | RemoveBlockingRuleMessage, 7 | RequestSettingsMessage, 8 | SettingsChangedMessage, 9 | StorageChangedMessage, 10 | } from "./interfaces/interfaces.js"; 11 | import { clamp, getConfigTabs } from "./helper.js"; 12 | 13 | let defaultStorage: StorageObject = { 14 | version: "0", 15 | blockedChannels: [], 16 | blockedChannelsRegExp: {}, 17 | blockedComments: {}, 18 | blockedVideoTitles: {}, 19 | excludedChannels: [], 20 | }; 21 | 22 | const STORAGE_VERSION = "1.0"; 23 | 24 | let storageVersion: string | undefined = undefined; 25 | let settings = { 26 | buttonVisible: true, 27 | buttonColor: "#717171", 28 | buttonSize: 142, 29 | animationSpeed: 200, 30 | }; 31 | 32 | let blockedChannelsSet = new Set<string>(); 33 | let excludedChannels = new Set<string>(); 34 | 35 | let blockedChannelsRegExp: KeyValueMap = {}; 36 | let blockedComments: KeyValueMap = {}; 37 | let blockedVideoTitles: KeyValueMap = {}; 38 | 39 | export async function loadDataIfNecessary() { 40 | if (storageVersion != STORAGE_VERSION) { 41 | await loadDataFromStorage(); 42 | 43 | console.log("Reload data"); 44 | 45 | sendStorageChangedMessage(); 46 | sendSettingsChangedMessage(); 47 | } 48 | } 49 | 50 | /** 51 | * Load the blocking rules and settings. 52 | */ 53 | export async function loadDataFromStorage() { 54 | const result = await chrome.storage.local.get({ ...defaultStorage, settings }); 55 | 56 | const storageObject = result as StorageObject; 57 | console.log("Loaded stored data", storageObject); 58 | 59 | if (storageObject.version === "0") { 60 | convertOldStorage(); 61 | } else { 62 | for (let index = 0; index < storageObject.blockedChannels.length; index++) { 63 | blockedChannelsSet.add(storageObject.blockedChannels[index]); 64 | } 65 | for (let index = 0; index < storageObject.excludedChannels.length; index++) { 66 | excludedChannels.add(storageObject.excludedChannels[index]); 67 | } 68 | 69 | blockedChannelsRegExp = storageObject.blockedChannelsRegExp; 70 | blockedComments = storageObject.blockedComments; 71 | blockedVideoTitles = storageObject.blockedVideoTitles; 72 | } 73 | 74 | settings.buttonVisible = result.settings.buttonVisible; 75 | settings.buttonColor = result.settings.buttonColor; 76 | settings.buttonSize = result.settings.buttonSize; 77 | settings.animationSpeed = result.settings.animationSpeed; 78 | storageVersion = STORAGE_VERSION; 79 | } 80 | 81 | /** 82 | * Add a blocking rule to the storage and send a storage changed message to all tabs running YouTube. 83 | * @param message The message containing the blocking rule to add. 84 | */ 85 | export function handleAddBlockingRuleMessage(message: AddBlockingRuleMessage) { 86 | if (message.content.blockedChannel !== undefined) { 87 | blockedChannelsSet.add(message.content.blockedChannel); 88 | chrome.storage.local.set({ blockedChannels: Array.from(blockedChannelsSet) }); 89 | } 90 | if (message.content.blockingChannelRegExp !== undefined) { 91 | blockedChannelsRegExp[message.content.blockingChannelRegExp] = message.content.caseInsensitive ? "i" : ""; 92 | chrome.storage.local.set({ blockedChannelsRegExp }); 93 | } 94 | if (message.content.blockingCommentRegExp !== undefined) { 95 | blockedComments[message.content.blockingCommentRegExp] = message.content.caseInsensitive ? "i" : ""; 96 | chrome.storage.local.set({ blockedComments }); 97 | } 98 | if (message.content.blockingVideoTitleRegExp !== undefined) { 99 | blockedVideoTitles[message.content.blockingVideoTitleRegExp] = message.content.caseInsensitive ? "i" : ""; 100 | chrome.storage.local.set({ blockedVideoTitles }); 101 | } 102 | if (message.content.excludedChannel !== undefined) { 103 | excludedChannels.add(message.content.excludedChannel); 104 | chrome.storage.local.set({ excludedChannels: Array.from(excludedChannels) }); 105 | } 106 | 107 | sendStorageChangedMessage(); 108 | } 109 | 110 | /** 111 | * Remove a blocking rule from the storage and send a storage changed message to all tabs running YouTube. 112 | * @param message The message containing the blocking rule to remove. 113 | */ 114 | export function handleRemoveBlockingRuleMessage(message: RemoveBlockingRuleMessage) { 115 | if (message.content.blockedChannel !== undefined) { 116 | for (let index = 0; index < message.content.blockedChannel.length; index++) { 117 | blockedChannelsSet.delete(message.content.blockedChannel[index]); 118 | } 119 | chrome.storage.local.set({ blockedChannels: Array.from(blockedChannelsSet) }); 120 | } 121 | if (message.content.blockingChannelRegExp !== undefined) { 122 | for (let index = 0; index < message.content.blockingChannelRegExp.length; index++) { 123 | delete blockedChannelsRegExp[message.content.blockingChannelRegExp[index]]; 124 | } 125 | chrome.storage.local.set({ blockedChannelsRegExp }); 126 | } 127 | if (message.content.blockingCommentRegExp !== undefined) { 128 | for (let index = 0; index < message.content.blockingCommentRegExp.length; index++) { 129 | delete blockedComments[message.content.blockingCommentRegExp[index]]; 130 | } 131 | chrome.storage.local.set({ blockedComments }); 132 | } 133 | if (message.content.blockingVideoTitleRegExp !== undefined) { 134 | for (let index = 0; index < message.content.blockingVideoTitleRegExp.length; index++) { 135 | delete blockedVideoTitles[message.content.blockingVideoTitleRegExp[index]]; 136 | } 137 | chrome.storage.local.set({ blockedVideoTitles }); 138 | } 139 | if (message.content.excludedChannel !== undefined) { 140 | for (let index = 0; index < message.content.excludedChannel.length; index++) { 141 | excludedChannels.delete(message.content.excludedChannel[index]); 142 | } 143 | chrome.storage.local.set({ excludedChannels: Array.from(excludedChannels) }); 144 | } 145 | 146 | sendStorageChangedMessage(); 147 | } 148 | 149 | /** 150 | * Checks if the given userChannelName, videoTitle or commentContent matches any of the blocking rules. 151 | * @param message The message containing userChannelName, videoTitle or commentContent. 152 | * @returns 153 | */ 154 | export function handleIsBlockedMessage(message: IsBlockedMessage): boolean { 155 | if (message.content.userChannelName !== undefined) { 156 | if (excludedChannels.has(message.content.userChannelName)) return false; 157 | if (blockedChannelsSet.has(message.content.userChannelName)) return true; 158 | for (const key in blockedChannelsRegExp) { 159 | if (Object.prototype.hasOwnProperty.call(blockedChannelsRegExp, key)) { 160 | const regEgx = new RegExp(key, blockedChannelsRegExp[key]); 161 | if (regEgx.test(message.content.userChannelName)) return true; 162 | } 163 | } 164 | } 165 | if (message.content.videoTitle !== undefined) { 166 | for (const key in blockedVideoTitles) { 167 | if (Object.prototype.hasOwnProperty.call(blockedVideoTitles, key)) { 168 | const regEgx = new RegExp(key, blockedVideoTitles[key]); 169 | if (regEgx.test(message.content.videoTitle)) return true; 170 | } 171 | } 172 | } 173 | if (message.content.commentContent !== undefined) { 174 | for (const key in blockedComments) { 175 | if (Object.prototype.hasOwnProperty.call(blockedComments, key)) { 176 | const regEgx = new RegExp(key, blockedComments[key]); 177 | if (regEgx.test(message.content.commentContent)) return true; 178 | } 179 | } 180 | } 181 | return false; 182 | } 183 | 184 | /** 185 | * Reloads the storage and settings and sends a storage changed message to all tabs running YouTube. 186 | * @param message The StorageChangedMessage. 187 | */ 188 | export async function handleStorageChangedMessage(message: StorageChangedMessage) { 189 | await loadDataFromStorage(); 190 | sendStorageChangedMessage(); 191 | sendSettingsChangedMessage(); 192 | } 193 | 194 | /** 195 | * Returns the settings. 196 | * @param message The RequestSettingsMessage. 197 | * @returns The settings. 198 | */ 199 | export function handleRequestSettings(message: RequestSettingsMessage): { 200 | buttonVisible: boolean; 201 | buttonColor: string; 202 | buttonSize: number; 203 | animationSpeed: number; 204 | } { 205 | return settings; 206 | } 207 | 208 | /** 209 | * Sends a storage changed message to all tabs that have YouTube open and the config tab if an tab id is available. 210 | */ 211 | async function sendStorageChangedMessage() { 212 | const storageChangedMessage: StorageChangedMessage = { 213 | sender: CommunicationRole.SERVICE_WORKER, 214 | receiver: CommunicationRole.CONTENT_SCRIPT, 215 | type: MessageType.STORAGE_CHANGED, 216 | content: undefined, 217 | }; 218 | 219 | chrome.tabs.query({ url: "*://www.youtube.com/*" }, (tabs) => { 220 | for (let index = 0; index < tabs.length; index++) { 221 | const tab = tabs[index]; 222 | if (tab.id !== undefined) chrome.tabs.sendMessage(tab.id, storageChangedMessage); 223 | } 224 | }); 225 | 226 | const configTabs = await getConfigTabs(); 227 | for (let index = 0; index < configTabs.length; index++) { 228 | const tab = configTabs[index]; 229 | const storageChangedMessageForSettings = { 230 | sender: CommunicationRole.SERVICE_WORKER, 231 | receiver: CommunicationRole.SETTINGS, 232 | type: MessageType.STORAGE_CHANGED, 233 | content: undefined, 234 | }; 235 | chrome.tabs.sendMessage(tab.id, storageChangedMessageForSettings); 236 | } 237 | } 238 | 239 | /** 240 | * Sends a settings changed message to all tabs that have YouTube open. 241 | * If is gets a settings changed message it changes the receiver to CONTENT_SCRIPT. 242 | * @param message The settings changed message. 243 | */ 244 | export async function sendSettingsChangedMessage( 245 | message: SettingsChangedMessage = { 246 | sender: CommunicationRole.SETTINGS, 247 | receiver: CommunicationRole.CONTENT_SCRIPT, 248 | type: MessageType.SETTINGS_CHANGED, 249 | content: settings, 250 | } 251 | ) { 252 | message.receiver = CommunicationRole.CONTENT_SCRIPT; 253 | settings = message.content; 254 | 255 | chrome.tabs.query({ url: "*://www.youtube.com/*" }, (tabs) => { 256 | for (let index = 0; index < tabs.length; index++) { 257 | const tab = tabs[index]; 258 | if (tab.id !== undefined) chrome.tabs.sendMessage(tab.id, message); 259 | } 260 | }); 261 | } 262 | 263 | /** 264 | * Loads the old storage data and converts it to the new format. 265 | * It stores the new data and removes the old. 266 | */ 267 | function convertOldStorage() { 268 | const defaultOldStorage: OldStorageObject = { 269 | "0": {}, 270 | "1": {}, 271 | "2": {}, 272 | "3": {}, 273 | "4": {}, 274 | content_ui: { 275 | "0": true, 276 | "1": "#717171", 277 | "2": 106, 278 | "3": 200, 279 | }, 280 | settings_ui: { 281 | 0: -1, 282 | 1: false, 283 | 2: false, 284 | }, 285 | }; 286 | chrome.storage.local.get(defaultOldStorage).then((result) => { 287 | const storageObject = result as OldStorageObject; 288 | console.log("Loaded stored data", storageObject); 289 | 290 | if ( 291 | storageObject[0] === undefined || 292 | storageObject[1] === undefined || 293 | storageObject[2] === undefined || 294 | storageObject[3] === undefined || 295 | storageObject[4] === undefined 296 | ) { 297 | return; 298 | } 299 | 300 | // Add blocked channels 301 | for (const key in storageObject[0]) { 302 | if (Object.prototype.hasOwnProperty.call(storageObject[0], key)) { 303 | blockedChannelsSet.add(key); 304 | } 305 | } 306 | 307 | // Add blocked blockedVideoTitles 308 | for (const key in storageObject[1]) { 309 | if (Object.prototype.hasOwnProperty.call(storageObject[1], key)) { 310 | blockedVideoTitles[key] = storageObject[1][key] === 0 ? "i" : ""; 311 | } 312 | } 313 | 314 | // Add blocked blockedChannelsRegExp 315 | for (const key in storageObject[2]) { 316 | if (Object.prototype.hasOwnProperty.call(storageObject[2], key)) { 317 | blockedChannelsRegExp[key] = storageObject[2][key] === 0 ? "i" : ""; 318 | } 319 | } 320 | 321 | // Add blocked blockedComments 322 | for (const key in storageObject[3]) { 323 | if (Object.prototype.hasOwnProperty.call(storageObject[3], key)) { 324 | blockedComments[key] = storageObject[3][key] === 0 ? "i" : ""; 325 | } 326 | } 327 | 328 | // Add excluded channels 329 | for (const key in storageObject[4]) { 330 | if (Object.prototype.hasOwnProperty.call(storageObject[4], key)) { 331 | excludedChannels.add(key); 332 | } 333 | } 334 | 335 | // Add settings 336 | let settingsStorageObject: SettingsStorageObject = { 337 | version: STORAGE_VERSION, 338 | settings: { 339 | // The old format only had two designs. Dark: 0 and Light: 1. 340 | // Currently Device: 0 is the default, therefore adding 1 adjusts this. 341 | design: clamp(0, 2, storageObject.settings_ui[0] + 1), 342 | // No longer in use 343 | advancedView: storageObject.settings_ui[1], 344 | openPopup: storageObject.settings_ui[2], 345 | buttonVisible: storageObject.content_ui[0], 346 | buttonColor: storageObject.content_ui[1], 347 | // The old default was 106, but in the new implementation this is pretty small so add 36 to adjust. 348 | // Also clamp the value between 100 and 200. 349 | buttonSize: clamp(100, 200, storageObject.content_ui[2] + 36), 350 | animationSpeed: clamp(100, 200, storageObject.content_ui[3]), 351 | }, 352 | }; 353 | 354 | // Write data to storage 355 | chrome.storage.local 356 | .set({ 357 | version: STORAGE_VERSION, 358 | blockedChannels: Array.from(blockedChannelsSet), 359 | blockedChannelsRegExp, 360 | blockedComments, 361 | blockedVideoTitles, 362 | excludedChannels: Array.from(excludedChannels), 363 | settings: settingsStorageObject.settings, 364 | }) 365 | .catch((error) => { 366 | console.error(error); 367 | }) 368 | .then(() => { 369 | // remove old storage 370 | chrome.storage.local.remove(["0", "1", "2", "3", "4", "content_ui", "settings_ui"]); 371 | }); 372 | }); 373 | } 374 | -------------------------------------------------------------------------------- /service-worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES6", 5 | "outDir": "../dist/service-worker", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["./**/*.ts"], 12 | "exclude": ["../dist"] 13 | } 14 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ./content-scripts 4 | npm i 5 | 6 | cd ../service-worker 7 | npm i 8 | 9 | cd ../ui 10 | npm i -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "channel-blocker-ui", 3 | "version": "1.0.0", 4 | "description": "The ui for the channel blocker extension.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-sass": "node-sass --include-path sass ./settings/style --output ../dist/ui/settings/style", 8 | "build": "tsc && npm run build-sass" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Time-Machine-Development/Channel-Blocker.git" 13 | }, 14 | "author": "Marc Beyer", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/Time-Machine-Development/Channel-Blocker/issues" 18 | }, 19 | "homepage": "https://github.com/Time-Machine-Development/Channel-Blocker#readme", 20 | "devDependencies": { 21 | "@types/chrome": "^0.0.254", 22 | "chrome-types": "^0.1.246", 23 | "node-sass": "^9.0.0", 24 | "typescript": "^5.3.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/popup/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8"> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 | <title>Document 8 | 9 | 10 | 11 |

Test

12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/popup/scripts/index.ts: -------------------------------------------------------------------------------- 1 | console.log("Start"); 2 | -------------------------------------------------------------------------------- /ui/settings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Settings 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | CB_icon 17 | 20 |

Settings

21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |

Choose your donation method

29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | 52 |
53 |

Blocked users/channels

54 |
55 |
56 | 57 | 58 |
59 |
60 | 61 | 65 |
66 | 70 |
71 | 72 |
73 |
74 |
75 |
76 |

Design

77 |
78 |
79 | 80 | 85 |
86 |
87 |
88 | 89 | 90 |
91 |
92 |
93 | 94 | 95 |
96 |
97 |
98 | 99 | 103 |
104 |
105 |

Visibility

106 |
107 |
108 | 109 | 113 |
114 |
115 |
116 | 117 | 119 |
120 |
121 |
122 | 123 |
124 |
125 |
126 |

Import/Export Your Configuration

127 |
128 |
129 | 130 |
131 |
132 |
133 | 134 |
135 | 136 |
137 |
138 |
139 |

About Channel Blocker

140 |
141 |
142 |

143 | Channel Blocker empowers you to block channels effortlessly with just one 144 | click. 145 | It also supports regular expressions for unparalleled customization, allowing you to tailor your 146 | YouTube™ experience to your preferences. 147 | And another thing: Our commitment to privacy means no user information is collected. 148 |

149 |
150 |
151 |
152 |

Support Us and Our Work:

153 |

154 | Do you like what we do? Do you want to support us and our work? A subscription on Patreon would 155 | help 156 | a lot at developing this tool as well as realizing our future projects. 157 |

158 |
159 |
160 |
161 |

Email: Contact us at yt-cleaner@protonmail.com

162 |

163 | GitHub: Find the source code on 164 | 165 | https://github.com/Time-Machine-Development/Channel-Blocker-Manifest-v3 166 | 167 |

168 |
169 |
170 |
171 |
172 |

FAQ

173 |
174 | 175 |
176 |
177 |

Why does this add-on require permissions?

178 |
179 | 180 | 181 |
182 |
183 |
184 |
185 |
186 |

Access to Your Data on www.youtube.com:

187 |

188 | - This permission is necessary for functionalities such as removing users/channels 189 | you 190 | have 191 | blocked 192 | on YouTube™ and adding the 'x'-button. 193 | It allows the add-on to interact with and modify your YouTube™ data as needed. 194 |

195 |

Access to Browser Tabs:

196 |

197 | - This permission is essential for keeping track of your active YouTube™ tabs and 198 | managing 199 | the 200 | Add-on setting page tab. 201 |

202 |

Storage of Unlimited Client-Side Data:

203 |

204 | - This permission is required to store an unlimited amount of client-side data, 205 | enabling 206 | the 207 | add-on 208 | to effectively block any number of users or channels. 209 |

210 |
211 |
212 |
213 |
214 | 215 |
216 |
217 |

Encountered a Bug, Have a Feature Idea, or Need Assistance?

218 |
219 | 220 | 221 |
222 |
223 |
224 |
225 |
226 |

227 | If you've encountered a bug, have a feature idea, or need assistance with our 228 | product, we're here to help! Please don't hesitate to reach out to us through our 229 | 231 | GitHub Issues page 232 | . 233 |
234 | Before submitting a new issue, we recommend checking if a 235 | similar issue has already been reported to avoid duplicates. 236 |

237 |

How to Get in Touch:

238 |

For direct assistance, you can also contact us via email at 239 | yt-cleaner@protonmail.com 240 |

241 |
242 |
243 |
244 |
245 | 246 |
247 |
248 |

Do you collect any data?

249 |
250 | 251 | 252 |
253 |
254 |
255 |
256 |
257 |

258 | We want to assure you that we do not collect any data 259 | from 260 | you 261 | in 262 | any form. 263 | Your privacy is important to us, and our commitment is to ensure that your usage 264 | remains 265 | secure 266 | and confidential. 267 |

268 |
269 |
270 |
271 |
272 | 273 |
274 |
275 |

Troubleshooting Guide: Add-on Not Functioning

276 |
277 | 278 | 279 |
280 |
281 |
282 |
283 |
284 |

Check for Updates

285 |

286 | Ensure that you have the latest version of Channel Blocker installed. If not, update 287 | to 288 | the 289 | newest version for optimal performance. 290 |

291 | 292 |

Mobile Compatibility

293 |

294 | If you are using a phone, we are sorry but this add-on doesn't work on mobile 295 | browsers. 296 |

297 | 298 |

Reporting a Bug

299 |

300 | If you encounter any issues, even after updating to the latest version, please help 301 | us improve by reporting them. You can submit bug reports through our 302 | 304 | GitHub Issues page 305 | . 306 |
307 | Before opening a new issue, we kindly ask you to check if a similar issue has 308 | already been reported. Duplicate issues can clutter the tracker and delay 309 | resolution. 310 |

311 | 312 |
313 |
314 |
315 |
316 |
317 |
318 | 319 | 320 | -------------------------------------------------------------------------------- /ui/settings/scripts/donate.ts: -------------------------------------------------------------------------------- 1 | const donateButton = document.getElementById("donate-btn") as HTMLButtonElement; 2 | const donateButtonPaypal = document.getElementById("donate-btn-paypal") as HTMLButtonElement; 3 | const donateButtonPatreon = document.getElementById("donate-btn-patreon") as HTMLButtonElement; 4 | const closeDonationButton = document.getElementById("close-donation-btn") as HTMLButtonElement; 5 | const donationDialog = document.getElementById("donation-dialog") as HTMLDialogElement; 6 | 7 | const paypalDonationUrl = "https://www.paypal.com/donate/?hosted_button_id=KYJWDCH3ZJ4U2"; 8 | const patreonUrl = "https://www.patreon.com/time_machine_development"; 9 | 10 | export function initDonation() { 11 | donateButton.addEventListener("click", () => { 12 | donationDialog.showModal(); 13 | //donationContainer.classList.add("open"); 14 | }); 15 | closeDonationButton.addEventListener("click", () => { 16 | donationDialog.close(); 17 | //donationDialog.classList.remove("open"); 18 | }); 19 | donateButtonPaypal.addEventListener("click", () => { 20 | window.open(paypalDonationUrl, "_blank"); 21 | }); 22 | donateButtonPatreon.addEventListener("click", () => { 23 | window.open(patreonUrl, "_blank"); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /ui/settings/scripts/enums.ts: -------------------------------------------------------------------------------- 1 | export enum MessageType { 2 | ADD_BLOCKING_RULE, 3 | REMOVE_BLOCKING_RULE, 4 | IS_BLOCKED, 5 | STORAGE_CHANGED, 6 | REQUEST_SETTINGS, 7 | SETTINGS_CHANGED, 8 | } 9 | 10 | export enum CommunicationRole { 11 | SERVICE_WORKER, 12 | CONTENT_SCRIPT, 13 | SETTINGS, 14 | } 15 | 16 | export enum SettingsDesign { 17 | DETECT, 18 | DARK, 19 | LICHT, 20 | } 21 | 22 | export enum SettingsState { 23 | BLOCKED_CHANNELS, 24 | BLOCKED_TITLES, 25 | BLOCKED_NAMES, 26 | BLOCKED_COMMENTS, 27 | EXCLUDED_CHANNELS, 28 | APPEARANCE, 29 | IMPORT_EXPORT, 30 | ABOUT, 31 | FAQ, 32 | } 33 | -------------------------------------------------------------------------------- /ui/settings/scripts/faq.ts: -------------------------------------------------------------------------------- 1 | export function initFaq() { 2 | const faqPermissionHeading = document.getElementById("faq-permission-heading") as HTMLDivElement; 3 | const faqContactHeading = document.getElementById("faq-contact-heading") as HTMLDivElement; 4 | const faqPrivacyHeading = document.getElementById("faq-privacy-heading") as HTMLDivElement; 5 | const faqTroubleshootingHeading = document.getElementById("faq-troubleshooting-heading") as HTMLDivElement; 6 | 7 | const faqPermissionSection = document.getElementById("faq-permission-section") as HTMLDivElement; 8 | const faqContactSection = document.getElementById("faq-contact-section") as HTMLDivElement; 9 | const faqPrivacySection = document.getElementById("faq-privacy-section") as HTMLDivElement; 10 | const faqTroubleshootingSection = document.getElementById("faq-troubleshooting-section") as HTMLDivElement; 11 | 12 | faqPermissionHeading.addEventListener("click", () => { 13 | faqPermissionSection.classList.toggle("active"); 14 | faqContactSection.classList.toggle("active", false); 15 | faqPrivacySection.classList.toggle("active", false); 16 | faqTroubleshootingSection.classList.toggle("active", false); 17 | }); 18 | faqContactHeading.addEventListener("click", () => { 19 | faqPermissionSection.classList.toggle("active", false); 20 | faqContactSection.classList.toggle("active"); 21 | faqPrivacySection.classList.toggle("active", false); 22 | faqTroubleshootingSection.classList.toggle("active", false); 23 | }); 24 | faqPrivacyHeading.addEventListener("click", () => { 25 | faqPermissionSection.classList.toggle("active", false); 26 | faqContactSection.classList.toggle("active", false); 27 | faqPrivacySection.classList.toggle("active"); 28 | faqTroubleshootingSection.classList.toggle("active", false); 29 | }); 30 | faqTroubleshootingHeading.addEventListener("click", () => { 31 | faqPermissionSection.classList.toggle("active", false); 32 | faqContactSection.classList.toggle("active", false); 33 | faqPrivacySection.classList.toggle("active", false); 34 | faqTroubleshootingSection.classList.toggle("active"); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /ui/settings/scripts/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Limits a value to a range between a minimum and a maximum value. 3 | * @param min The minimum value. 4 | * @param max The maximum value. 5 | * @param value The value to be clamped. 6 | * @returns 7 | */ 8 | export function clamp(min: number, max: number, value: number): number { 9 | return Math.max(min, Math.min(max, value)); 10 | } 11 | -------------------------------------------------------------------------------- /ui/settings/scripts/importExport.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType, SettingsDesign, SettingsState } from "./enums.js"; 2 | import { clamp } from "./helper.js"; 3 | import { loadDataFromStorage, setSettingsState } from "./index.js"; 4 | import { StorageChangedMessage } from "./interfaces/interfaces.js"; 5 | import { CombinedStorageObject, OldStorageObject } from "./interfaces/storage.js"; 6 | import { loadSettingsDataFromStorage } from "./settings.js"; 7 | 8 | const STORAGE_VERSION = "1.0"; 9 | 10 | /** 11 | * Initialize the import and export settings section. 12 | */ 13 | export function initImportExport() { 14 | const importBtn = document.getElementById("import-btn") as HTMLButtonElement; 15 | const exportBtn = document.getElementById("export-btn") as HTMLButtonElement; 16 | const fileLoaderInput = document.getElementById("file-loader-input") as HTMLInputElement; 17 | 18 | fileLoaderInput.addEventListener("change", (event) => { 19 | let file = fileLoaderInput?.files?.item(0); 20 | if (file) { 21 | const fileReader = new FileReader(); 22 | fileReader.addEventListener( 23 | "load", 24 | () => { 25 | console.log("FileReader loaded"); 26 | 27 | try { 28 | const fileContent = fileReader.result; 29 | if (typeof fileContent === "string") { 30 | const fileContentJson = JSON.parse(fileContent); 31 | if (fileContentJson.version === undefined) { 32 | loadOldFormat(fileContentJson); 33 | } else { 34 | loadNewFormat(fileContentJson); 35 | } 36 | sendStorageChangeMsg(); 37 | } else { 38 | throw new Error("Could not read file."); 39 | } 40 | } catch (error) { 41 | console.error(`Error: ${error}`); 42 | 43 | alert(`Error: ${error}`); 44 | } 45 | }, 46 | false 47 | ); 48 | fileReader.readAsText(file, "UTF-8"); 49 | } 50 | }); 51 | 52 | importBtn.addEventListener( 53 | "click", 54 | () => { 55 | // Check if the file-APIs are supported. 56 | if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { 57 | // The file-APIs are not supported. 58 | alert("The file-APIs are not supported. You are not able to import."); 59 | return; 60 | } 61 | fileLoaderInput.click(); 62 | }, 63 | false 64 | ); 65 | 66 | exportBtn.addEventListener("click", () => { 67 | const defaultCombinedStorageObject: CombinedStorageObject = { 68 | version: STORAGE_VERSION, 69 | settings: { 70 | design: SettingsDesign.DETECT, 71 | advancedView: false, 72 | openPopup: false, 73 | buttonVisible: true, 74 | buttonColor: "#717171", 75 | buttonSize: 142, 76 | animationSpeed: 200, 77 | }, 78 | blockedChannels: [], 79 | excludedChannels: [], 80 | blockedVideoTitles: {}, 81 | blockedChannelsRegExp: {}, 82 | blockedComments: {}, 83 | }; 84 | 85 | chrome.storage.local.get(defaultCombinedStorageObject).then((result) => { 86 | const combinedStorageObject = result as CombinedStorageObject; 87 | const currentDate = new Date(); 88 | download( 89 | JSON.stringify(combinedStorageObject), 90 | `ChannelBlocker ${currentDate.getFullYear()}-${currentDate.getMonth() + 1}-${currentDate.getDate()}.save`, 91 | ".save" 92 | ); 93 | }); 94 | }); 95 | } 96 | 97 | /** 98 | * Load the date from the old format and add them to currently saved data. 99 | * @param oldStorageObject The loaded JSON data. 100 | */ 101 | function loadOldFormat(oldStorageObject: OldStorageObject) { 102 | const defaultCombinedStorageObject: CombinedStorageObject = { 103 | version: STORAGE_VERSION, 104 | settings: { 105 | design: SettingsDesign.DETECT, 106 | advancedView: false, 107 | openPopup: false, 108 | buttonVisible: true, 109 | buttonColor: "#717171", 110 | buttonSize: 142, 111 | animationSpeed: 200, 112 | }, 113 | blockedChannels: [], 114 | excludedChannels: [], 115 | blockedVideoTitles: {}, 116 | blockedChannelsRegExp: {}, 117 | blockedComments: {}, 118 | }; 119 | 120 | chrome.storage.local.get(defaultCombinedStorageObject).then((result) => { 121 | const combinedStorageObject = result as CombinedStorageObject; 122 | 123 | // Load blocking rules 124 | if (oldStorageObject[0] !== undefined) { 125 | combinedStorageObject.blockedChannels.push(...Object.keys(oldStorageObject[0])); 126 | } 127 | if (oldStorageObject[1] !== undefined) { 128 | for (const key in oldStorageObject[1]) { 129 | if (Object.prototype.hasOwnProperty.call(oldStorageObject[1], key)) { 130 | combinedStorageObject.blockedVideoTitles[key] = oldStorageObject[1][key] === 0 ? "i" : ""; 131 | } 132 | } 133 | } 134 | if (oldStorageObject[2] !== undefined) { 135 | for (const key in oldStorageObject[2]) { 136 | if (Object.prototype.hasOwnProperty.call(oldStorageObject[2], key)) { 137 | combinedStorageObject.blockedChannelsRegExp[key] = oldStorageObject[2][key] === 0 ? "i" : ""; 138 | } 139 | } 140 | } 141 | if (oldStorageObject[3] !== undefined) { 142 | for (const key in oldStorageObject[3]) { 143 | if (Object.prototype.hasOwnProperty.call(oldStorageObject[3], key)) { 144 | combinedStorageObject.blockedComments[key] = oldStorageObject[3][key] === 0 ? "i" : ""; 145 | } 146 | } 147 | } 148 | if (oldStorageObject[4] !== undefined) { 149 | combinedStorageObject.excludedChannels.push(...Object.keys(oldStorageObject[4])); 150 | } 151 | 152 | // Load settings 153 | if (oldStorageObject?.settings_ui?.[0] !== undefined) { 154 | // The old format only had two designs. Dark: 0 and Light: 1. 155 | // Currently Device: 0 is the default, therefore adding 1 adjusts this. 156 | combinedStorageObject.settings.design = clamp(0, 2, oldStorageObject.settings_ui[0] + 1); 157 | } 158 | if (oldStorageObject?.settings_ui?.[1] !== undefined) { 159 | // No longer in use 160 | combinedStorageObject.settings.advancedView = oldStorageObject.settings_ui[1]; 161 | } 162 | if (oldStorageObject?.settings_ui?.[2] !== undefined) { 163 | combinedStorageObject.settings.openPopup = oldStorageObject.settings_ui[2]; 164 | } 165 | if (oldStorageObject?.content_ui?.[0] !== undefined) { 166 | combinedStorageObject.settings.buttonVisible = oldStorageObject.content_ui[0]; 167 | } 168 | if (oldStorageObject?.content_ui?.[1] !== undefined) { 169 | combinedStorageObject.settings.buttonColor = oldStorageObject.content_ui[1]; 170 | } 171 | if (oldStorageObject?.content_ui?.[2] !== undefined) { 172 | // The old default was 106, but in the new implementation this is pretty small so add 36 to adjust. 173 | combinedStorageObject.settings.buttonSize = clamp(100, 200, oldStorageObject.content_ui[2] + 36); 174 | } 175 | if (oldStorageObject?.content_ui?.[3] !== undefined) { 176 | combinedStorageObject.settings.animationSpeed = clamp(100, 200, oldStorageObject.content_ui[3]); 177 | } 178 | 179 | // Save data 180 | chrome.storage.local.set(combinedStorageObject); 181 | setSettingsState(SettingsState.BLOCKED_CHANNELS); 182 | loadDataFromStorage(); 183 | loadSettingsDataFromStorage(); 184 | }); 185 | } 186 | 187 | /** 188 | * Load the date from the new format and add them to currently saved data. 189 | * @param oldStorageObject The loaded JSON data. 190 | */ 191 | function loadNewFormat(loadedStorageObject: CombinedStorageObject) { 192 | const defaultCombinedStorageObject: CombinedStorageObject = { 193 | version: STORAGE_VERSION, 194 | settings: { 195 | design: SettingsDesign.DETECT, 196 | advancedView: false, 197 | openPopup: false, 198 | buttonVisible: true, 199 | buttonColor: "#717171", 200 | buttonSize: 142, 201 | animationSpeed: 200, 202 | }, 203 | blockedChannels: [], 204 | excludedChannels: [], 205 | blockedVideoTitles: {}, 206 | blockedChannelsRegExp: {}, 207 | blockedComments: {}, 208 | }; 209 | 210 | chrome.storage.local.get(defaultCombinedStorageObject).then((result) => { 211 | const storageObject = result as CombinedStorageObject; 212 | 213 | console.log("NEW FORMAT", storageObject); 214 | console.log("NEW FORMAT", loadedStorageObject); 215 | console.log( 216 | "filter", 217 | loadedStorageObject.blockedChannels.filter((channel) => { 218 | return !storageObject.blockedChannels.includes(channel); 219 | }) 220 | ); 221 | 222 | storageObject.blockedChannels.push( 223 | ...loadedStorageObject.blockedChannels.filter((channel) => { 224 | return !storageObject.blockedChannels.includes(channel); 225 | }) 226 | ); 227 | storageObject.blockedVideoTitles = { ...storageObject.blockedVideoTitles, ...loadedStorageObject.blockedVideoTitles }; 228 | storageObject.blockedChannelsRegExp = { ...storageObject.blockedChannelsRegExp, ...loadedStorageObject.blockedChannelsRegExp }; 229 | storageObject.blockedComments = { ...storageObject.blockedComments, ...loadedStorageObject.blockedComments }; 230 | storageObject.excludedChannels.push( 231 | ...loadedStorageObject.excludedChannels.filter((channel) => { 232 | return !storageObject.excludedChannels.includes(channel); 233 | }) 234 | ); 235 | storageObject.settings = { ...storageObject.settings, ...loadedStorageObject.settings }; 236 | 237 | // Save data 238 | chrome.storage.local.set(storageObject); 239 | setSettingsState(SettingsState.BLOCKED_CHANNELS); 240 | loadDataFromStorage(); 241 | loadSettingsDataFromStorage(); 242 | }); 243 | } 244 | 245 | /** 246 | * Creates a file with the given data and downloads it. 247 | * @param data The content of the file. 248 | * @param filename The name of the file. 249 | * @param type The type of the file. 250 | */ 251 | function download(data: BlobPart, filename: string, type: string) { 252 | const file = new Blob([data], { type: type }); 253 | const a = document.createElement("a"); 254 | const url = URL.createObjectURL(file); 255 | 256 | a.href = url; 257 | a.download = filename; 258 | document.body.appendChild(a); 259 | a.click(); 260 | setTimeout(() => { 261 | document.body.removeChild(a); 262 | window.URL.revokeObjectURL(url); 263 | }, 0); 264 | } 265 | 266 | /** 267 | * Send a message to the service worker, that informs them that the storage was changed 268 | */ 269 | function sendStorageChangeMsg() { 270 | const message: StorageChangedMessage = { 271 | sender: CommunicationRole.SETTINGS, 272 | receiver: CommunicationRole.SERVICE_WORKER, 273 | type: MessageType.STORAGE_CHANGED, 274 | content: undefined, 275 | }; 276 | chrome.runtime.sendMessage(message); 277 | console.log("sendStorageChangeMsg"); 278 | } 279 | -------------------------------------------------------------------------------- /ui/settings/scripts/index.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType, SettingsState } from "./enums.js"; 2 | import { AddBlockingRuleMessage, Message, RemoveBlockingRuleMessage } from "./interfaces/interfaces.js"; 3 | import { KeyValueMap, StorageObject } from "./interfaces/storage.js"; 4 | import { initFaq } from "./faq.js"; 5 | import { initNavigation } from "./navigation.js"; 6 | import { initAppearanceUI } from "./settings.js"; 7 | import { initImportExport } from "./importExport.js"; 8 | import { initDonation } from "./donate.js"; 9 | 10 | const blockedChannelsSelect: HTMLSelectElement = document.getElementById("blocked-channels") as HTMLSelectElement; 11 | const blockedChannelsInput: HTMLInputElement = document.getElementById("blocked-channels-input") as HTMLInputElement; 12 | const blockedChannelsAddBtn: HTMLButtonElement = document.getElementById("blocked-channels-add-btn") as HTMLButtonElement; 13 | const blockedChannelsRemoveBtn: HTMLButtonElement = document.getElementById("blocked-channels-remove-btn") as HTMLButtonElement; 14 | const caseInsensitiveRow = document.getElementById("case-insensitive-row") as HTMLDivElement; 15 | const caseInsensitiveCheckbox = document.getElementById("case-insensitive-checkbox") as HTMLInputElement; 16 | const nav = document.getElementById("main-nav") as HTMLElement; 17 | 18 | const headingElement: HTMLHeadingElement = document.getElementById("heading") as HTMLHeadingElement; 19 | const rulesSection: HTMLDivElement = document.getElementById("rules-section") as HTMLDivElement; 20 | const appearanceSection: HTMLDivElement = document.getElementById("appearance-section") as HTMLDivElement; 21 | const importExportSection: HTMLDivElement = document.getElementById("import-export-section") as HTMLDivElement; 22 | const aboutSection: HTMLDivElement = document.getElementById("about-section") as HTMLDivElement; 23 | const faqSection: HTMLDivElement = document.getElementById("faq-section") as HTMLDivElement; 24 | 25 | const blockedChannelsNav: HTMLLIElement = document.getElementById("blocked-channels-nav") as HTMLLIElement; 26 | const blockedTitlesNav: HTMLLIElement = document.getElementById("blocked-titles-nav") as HTMLLIElement; 27 | const blockedNamesNav: HTMLLIElement = document.getElementById("blocked-names-nav") as HTMLLIElement; 28 | const blockedCommentsNav: HTMLLIElement = document.getElementById("blocked-comments-nav") as HTMLLIElement; 29 | const excludedChannelsNav: HTMLLIElement = document.getElementById("excluded-channels-nav") as HTMLLIElement; 30 | const appearanceNav: HTMLLIElement = document.getElementById("appearance-nav") as HTMLLIElement; 31 | const importExportNav: HTMLLIElement = document.getElementById("import-export-nav") as HTMLLIElement; 32 | const aboutNav: HTMLLIElement = document.getElementById("about-nav") as HTMLLIElement; 33 | const faqNav: HTMLLIElement = document.getElementById("faq-nav") as HTMLLIElement; 34 | 35 | const STORAGE_VERSION = "1.0"; 36 | 37 | let settingsState: SettingsState = SettingsState.BLOCKED_CHANNELS; 38 | 39 | let defaultStorage: StorageObject = { 40 | version: STORAGE_VERSION, 41 | blockedChannels: [], 42 | blockedChannelsRegExp: {}, 43 | blockedComments: {}, 44 | blockedVideoTitles: {}, 45 | excludedChannels: [], 46 | }; 47 | 48 | let blockedChannelsSet = new Set(); 49 | let excludedChannels = new Set(); 50 | 51 | let blockedChannelsRegExp: KeyValueMap = {}; 52 | let blockedComments: KeyValueMap = {}; 53 | let blockedVideoTitles: KeyValueMap = {}; 54 | 55 | loadDataFromStorage(); 56 | 57 | export function loadDataFromStorage() { 58 | chrome.storage.local.get(defaultStorage).then((result) => { 59 | const storageObject = result as StorageObject; 60 | console.log("Loaded stored data", storageObject); 61 | 62 | blockedChannelsSet = new Set(); 63 | excludedChannels = new Set(); 64 | 65 | blockedChannelsRegExp = {}; 66 | blockedComments = {}; 67 | blockedVideoTitles = {}; 68 | 69 | if (storageObject.version === "0") { 70 | // This should not happen, because the service worker is converting the old storage / filling it with default values. 71 | } else { 72 | for (let index = 0; index < storageObject.blockedChannels.length; index++) { 73 | blockedChannelsSet.add(storageObject.blockedChannels[index]); 74 | } 75 | for (let index = 0; index < storageObject.excludedChannels.length; index++) { 76 | excludedChannels.add(storageObject.excludedChannels[index]); 77 | } 78 | 79 | blockedChannelsRegExp = storageObject.blockedChannelsRegExp; 80 | blockedComments = storageObject.blockedComments; 81 | blockedVideoTitles = storageObject.blockedVideoTitles; 82 | } 83 | 84 | updateUI(); 85 | }); 86 | } 87 | 88 | export function setSettingsState(pSettingsState: SettingsState) { 89 | settingsState = pSettingsState; 90 | blockedChannelsRemoveBtn.classList.add("outlined"); 91 | } 92 | 93 | function updateUI() { 94 | nav.classList.remove("open"); 95 | const showRulesSection = 96 | settingsState === SettingsState.BLOCKED_CHANNELS || 97 | settingsState === SettingsState.BLOCKED_TITLES || 98 | settingsState === SettingsState.BLOCKED_NAMES || 99 | settingsState === SettingsState.BLOCKED_COMMENTS || 100 | settingsState === SettingsState.EXCLUDED_CHANNELS; 101 | rulesSection.style.display = showRulesSection ? "" : "none"; 102 | blockedChannelsNav.classList.remove("active"); 103 | blockedTitlesNav.classList.remove("active"); 104 | blockedNamesNav.classList.remove("active"); 105 | blockedCommentsNav.classList.remove("active"); 106 | excludedChannelsNav.classList.remove("active"); 107 | if (showRulesSection) { 108 | updateRulesUI(); 109 | } 110 | 111 | const showAppearanceSection = settingsState === SettingsState.APPEARANCE; 112 | appearanceSection.style.display = showAppearanceSection ? "" : "none"; 113 | appearanceNav.classList.toggle("active", showAppearanceSection); 114 | 115 | const showImportExportSection = settingsState === SettingsState.IMPORT_EXPORT; 116 | importExportSection.style.display = showImportExportSection ? "" : "none"; 117 | importExportNav.classList.toggle("active", showImportExportSection); 118 | 119 | const showAboutSection = settingsState === SettingsState.ABOUT; 120 | aboutSection.style.display = showAboutSection ? "" : "none"; 121 | aboutNav.classList.toggle("active", showAboutSection); 122 | 123 | const showFaqSection = settingsState === SettingsState.FAQ; 124 | faqSection.style.display = showFaqSection ? "" : "none"; 125 | faqNav.classList.toggle("active", showFaqSection); 126 | } 127 | 128 | function updateRulesUI() { 129 | while (blockedChannelsSelect.firstChild !== null) { 130 | blockedChannelsSelect.removeChild(blockedChannelsSelect.firstChild); 131 | } 132 | 133 | switch (settingsState) { 134 | case SettingsState.BLOCKED_CHANNELS: 135 | blockedChannelsNav.classList.add("active"); 136 | caseInsensitiveRow.style.display = "none"; 137 | headingElement.innerText = "Blocked Users/Channels"; 138 | blockedChannelsInput.placeholder = "User/Channel Name"; 139 | blockedChannelsSet.forEach((channelName) => { 140 | insertOption(channelName); 141 | }); 142 | break; 143 | case SettingsState.BLOCKED_TITLES: 144 | blockedTitlesNav.classList.add("active"); 145 | caseInsensitiveRow.style.display = ""; 146 | headingElement.innerText = "Blocked Video Titles by Regular Expressions"; 147 | blockedChannelsInput.placeholder = "Video Title Regular Expression"; 148 | for (const key in blockedVideoTitles) { 149 | if (Object.prototype.hasOwnProperty.call(blockedVideoTitles, key)) { 150 | insertOption(key, blockedVideoTitles[key] !== ""); 151 | } 152 | } 153 | break; 154 | case SettingsState.BLOCKED_NAMES: 155 | blockedNamesNav.classList.add("active"); 156 | caseInsensitiveRow.style.display = ""; 157 | headingElement.innerText = "Blocked User/Channel Names by Regular Expressions"; 158 | blockedChannelsInput.placeholder = "User/Channel Name Regular Expression"; 159 | for (const key in blockedChannelsRegExp) { 160 | if (Object.prototype.hasOwnProperty.call(blockedChannelsRegExp, key)) { 161 | insertOption(key, blockedChannelsRegExp[key] !== ""); 162 | } 163 | } 164 | break; 165 | case SettingsState.BLOCKED_COMMENTS: 166 | blockedCommentsNav.classList.add("active"); 167 | caseInsensitiveRow.style.display = ""; 168 | headingElement.innerText = "Blocked Comments by Regular Expressions"; 169 | blockedChannelsInput.placeholder = "Comment Regular Expression"; 170 | for (const key in blockedComments) { 171 | if (Object.prototype.hasOwnProperty.call(blockedComments, key)) { 172 | insertOption(key, blockedComments[key] !== ""); 173 | } 174 | } 175 | break; 176 | case SettingsState.EXCLUDED_CHANNELS: 177 | excludedChannelsNav.classList.add("active"); 178 | caseInsensitiveRow.style.display = "none"; 179 | headingElement.innerText = "Excluded Users/Channels from Regular Expressions"; 180 | blockedChannelsInput.placeholder = "User/Channel Name"; 181 | excludedChannels.forEach((channelName) => { 182 | insertOption(channelName); 183 | }); 184 | break; 185 | } 186 | 187 | blockedChannelsSelect.classList.toggle("larger", blockedChannelsSelect.childElementCount > 4); 188 | blockedChannelsSelect.classList.toggle("largest", blockedChannelsSelect.childElementCount > 8); 189 | } 190 | 191 | function insertOption(value: string, isCaseInsensitive: boolean = false) { 192 | let option = document.createElement("option"); 193 | option.value = value; 194 | option.innerText = value; 195 | option.classList.toggle("case-insensitive", isCaseInsensitive); 196 | blockedChannelsSelect.insertAdjacentElement("afterbegin", option); 197 | } 198 | 199 | function addNewRule() { 200 | const rule = blockedChannelsInput.value; 201 | if (rule.trim().length === 0) return; 202 | 203 | const message: AddBlockingRuleMessage = { 204 | sender: CommunicationRole.SETTINGS, 205 | receiver: CommunicationRole.SERVICE_WORKER, 206 | type: MessageType.ADD_BLOCKING_RULE, 207 | content: { 208 | blockedChannel: settingsState === SettingsState.BLOCKED_CHANNELS ? rule : undefined, 209 | blockingVideoTitleRegExp: settingsState === SettingsState.BLOCKED_TITLES ? rule : undefined, 210 | blockingChannelRegExp: settingsState === SettingsState.BLOCKED_NAMES ? rule : undefined, 211 | blockingCommentRegExp: settingsState === SettingsState.BLOCKED_COMMENTS ? rule : undefined, 212 | excludedChannel: settingsState === SettingsState.EXCLUDED_CHANNELS ? rule : undefined, 213 | caseInsensitive: caseInsensitiveCheckbox.checked, 214 | }, 215 | }; 216 | chrome.runtime.sendMessage(message); 217 | blockedChannelsInput.value = ""; 218 | } 219 | function removeRule() { 220 | let selectedOptions = []; 221 | for (let index = 0; index < blockedChannelsSelect.options.length; index++) { 222 | const option = blockedChannelsSelect.options[index]; 223 | if (option.selected) selectedOptions.push(option.value); 224 | } 225 | const message: RemoveBlockingRuleMessage = { 226 | sender: CommunicationRole.SETTINGS, 227 | receiver: CommunicationRole.SERVICE_WORKER, 228 | type: MessageType.REMOVE_BLOCKING_RULE, 229 | content: { 230 | blockedChannel: settingsState === SettingsState.BLOCKED_CHANNELS ? selectedOptions : undefined, 231 | blockingVideoTitleRegExp: settingsState === SettingsState.BLOCKED_TITLES ? selectedOptions : undefined, 232 | blockingChannelRegExp: settingsState === SettingsState.BLOCKED_NAMES ? selectedOptions : undefined, 233 | blockingCommentRegExp: settingsState === SettingsState.BLOCKED_COMMENTS ? selectedOptions : undefined, 234 | excludedChannel: settingsState === SettingsState.EXCLUDED_CHANNELS ? selectedOptions : undefined, 235 | }, 236 | }; 237 | chrome.runtime.sendMessage(message); 238 | } 239 | 240 | (function initUI() { 241 | initFaq(); 242 | initNavigation(); 243 | initAppearanceUI(); 244 | initImportExport(); 245 | initDonation(); 246 | 247 | blockedChannelsSelect.addEventListener("change", (event) => { 248 | blockedChannelsRemoveBtn.classList.toggle("outlined", blockedChannelsSelect.value === ""); 249 | }); 250 | 251 | blockedChannelsAddBtn.addEventListener("click", addNewRule); 252 | blockedChannelsInput.addEventListener("keydown", (event) => { 253 | if (event.key == "Enter") addNewRule(); 254 | }); 255 | 256 | blockedChannelsRemoveBtn.addEventListener("click", removeRule); 257 | 258 | blockedChannelsNav.addEventListener("click", () => { 259 | setSettingsState(SettingsState.BLOCKED_CHANNELS); 260 | updateUI(); 261 | }); 262 | blockedTitlesNav.addEventListener("click", () => { 263 | setSettingsState(SettingsState.BLOCKED_TITLES); 264 | updateUI(); 265 | }); 266 | blockedNamesNav.addEventListener("click", () => { 267 | setSettingsState(SettingsState.BLOCKED_NAMES); 268 | updateUI(); 269 | }); 270 | blockedCommentsNav.addEventListener("click", () => { 271 | setSettingsState(SettingsState.BLOCKED_COMMENTS); 272 | updateUI(); 273 | }); 274 | excludedChannelsNav.addEventListener("click", () => { 275 | setSettingsState(SettingsState.EXCLUDED_CHANNELS); 276 | updateUI(); 277 | }); 278 | appearanceNav.addEventListener("click", () => { 279 | setSettingsState(SettingsState.APPEARANCE); 280 | updateUI(); 281 | }); 282 | importExportNav.addEventListener("click", () => { 283 | setSettingsState(SettingsState.IMPORT_EXPORT); 284 | updateUI(); 285 | }); 286 | aboutNav.addEventListener("click", () => { 287 | setSettingsState(SettingsState.ABOUT); 288 | updateUI(); 289 | }); 290 | faqNav.addEventListener("click", () => { 291 | setSettingsState(SettingsState.FAQ); 292 | updateUI(); 293 | }); 294 | })(); 295 | 296 | chrome.runtime.onMessage.addListener((message: Message, sender: chrome.runtime.MessageSender) => { 297 | if (message.receiver !== CommunicationRole.SETTINGS) return; 298 | 299 | switch (message.type) { 300 | case MessageType.STORAGE_CHANGED: 301 | loadDataFromStorage(); 302 | break; 303 | 304 | default: 305 | break; 306 | } 307 | }); 308 | -------------------------------------------------------------------------------- /ui/settings/scripts/interfaces/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType } from "../enums.js"; 2 | 3 | export interface Message { 4 | sender: CommunicationRole; 5 | receiver: CommunicationRole; 6 | type: MessageType; 7 | content: any; 8 | } 9 | 10 | export interface AddBlockingRuleMessage extends Message { 11 | content: { 12 | blockedChannel?: string; 13 | excludedChannel?: string; 14 | blockingChannelRegExp?: string; 15 | blockingCommentRegExp?: string; 16 | blockingVideoTitleRegExp?: string; 17 | caseInsensitive?: boolean; 18 | }; 19 | } 20 | 21 | export interface RemoveBlockingRuleMessage extends Message { 22 | content: { 23 | blockedChannel?: string[]; 24 | excludedChannel?: string[]; 25 | blockingChannelRegExp?: string[]; 26 | blockingCommentRegExp?: string[]; 27 | blockingVideoTitleRegExp?: string[]; 28 | }; 29 | } 30 | 31 | export interface RequestSettingsMessage extends Message { 32 | content: undefined; 33 | } 34 | 35 | export interface SettingsChangedMessage extends Message { 36 | content: { 37 | buttonVisible: boolean; 38 | buttonColor: string; 39 | buttonSize: number; 40 | animationSpeed: number; 41 | }; 42 | } 43 | 44 | export interface IsBlockedMessage extends Message { 45 | content: { 46 | videoTitle?: string; 47 | userChannelName?: string; 48 | commentContent?: string; 49 | }; 50 | } 51 | 52 | export interface StorageChangedMessage extends Message { 53 | content: undefined; 54 | } 55 | -------------------------------------------------------------------------------- /ui/settings/scripts/interfaces/storage.ts: -------------------------------------------------------------------------------- 1 | import { SettingsDesign } from "../enums.js"; 2 | 3 | export interface OldStorageObject { 4 | "0"?: { [key: string]: number }; // blocked channels 5 | "1"?: { [key: string]: number }; // video title RegExp 6 | "2"?: { [key: string]: number }; // channel name RegExp 7 | "3"?: { [key: string]: number }; // comment RegExp 8 | "4"?: { [key: string]: number }; // excluded channels 9 | content_ui: { 10 | "0": boolean; // button_visible 11 | "1": string; // button_color 12 | "2": number; // button_size 13 | "3": number; // animation_speed 14 | }; 15 | settings_ui: { 16 | "0": number; // design 17 | "1": boolean; // advanced_view 18 | "2": boolean; // open_popup 19 | }; 20 | } 21 | 22 | export interface KeyValueMap { 23 | [key: string]: string; 24 | } 25 | 26 | export interface StorageObject { 27 | version: string; 28 | 29 | blockedChannels: string[]; 30 | excludedChannels: string[]; 31 | 32 | blockedVideoTitles: KeyValueMap; 33 | blockedChannelsRegExp: KeyValueMap; 34 | blockedComments: KeyValueMap; 35 | } 36 | 37 | export interface SettingsStorageObject { 38 | version: string; 39 | 40 | settings: { 41 | design: SettingsDesign; 42 | advancedView: boolean; 43 | openPopup: boolean; 44 | buttonVisible: boolean; 45 | buttonColor: string; 46 | buttonSize: number; 47 | animationSpeed: number; 48 | }; 49 | } 50 | 51 | export interface CombinedStorageObject extends SettingsStorageObject, StorageObject {} 52 | -------------------------------------------------------------------------------- /ui/settings/scripts/navigation.ts: -------------------------------------------------------------------------------- 1 | export function initNavigation() { 2 | const burgerMenu = document.getElementById("burger-menu") as HTMLButtonElement; 3 | const nav = document.getElementById("main-nav") as HTMLElement; 4 | 5 | console.log("burgerMenu", burgerMenu); 6 | 7 | burgerMenu.addEventListener("click", () => { 8 | nav.classList.toggle("open"); 9 | console.log("toggle nav"); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /ui/settings/scripts/settings.ts: -------------------------------------------------------------------------------- 1 | import { CommunicationRole, MessageType, SettingsDesign } from "./enums.js"; 2 | import { SettingsChangedMessage } from "./interfaces/interfaces.js"; 3 | import { SettingsStorageObject } from "./interfaces/storage.js"; 4 | 5 | const modeDropdown = document.getElementById("mode-dropdown") as HTMLSelectElement; 6 | const btnColorInput = document.getElementById("block-btn-color-picker") as HTMLInputElement; 7 | const btnSizeSlider = document.getElementById("btn-size-slider") as HTMLInputElement; 8 | const popupCheckbox = document.getElementById("popup-checkbox") as HTMLInputElement; 9 | const showBtnCheckbox = document.getElementById("show-btn-checkbox") as HTMLInputElement; 10 | const animationSpeedSlider = document.getElementById("animation-speed-slider") as HTMLInputElement; 11 | const resetBtn = document.getElementById("reset-appearance-btn") as HTMLButtonElement; 12 | 13 | let defaultStorage: SettingsStorageObject = { 14 | version: "0", 15 | settings: { 16 | design: SettingsDesign.DETECT, 17 | advancedView: false, 18 | openPopup: false, 19 | buttonVisible: true, 20 | buttonColor: "#717171", 21 | buttonSize: 142, 22 | animationSpeed: 200, 23 | }, 24 | }; 25 | let settings = { ...defaultStorage.settings }; 26 | 27 | loadSettingsDataFromStorage(); 28 | 29 | export function loadSettingsDataFromStorage() { 30 | chrome.storage.local.get(defaultStorage).then((result) => { 31 | const storageObject = result as SettingsStorageObject; 32 | console.log("Loaded stored data", storageObject); 33 | 34 | if (storageObject.version === "0") { 35 | // Should not be possible because the service worker converts / fill the storage 36 | } else { 37 | settings = storageObject.settings; 38 | } 39 | updateUI(); 40 | }); 41 | } 42 | 43 | function updateUI() { 44 | updateColorScheme(); 45 | updateBtnColor(); 46 | updateBtnSize(); 47 | updatePopup(); 48 | updateShowBtn(); 49 | updateAnimationSpeed(); 50 | } 51 | 52 | function updateColorScheme() { 53 | document.body.classList.toggle("detect-scheme", settings.design === SettingsDesign.DETECT); 54 | document.body.classList.toggle("dark-scheme", settings.design === SettingsDesign.DARK); 55 | modeDropdown.value = `${settings.design}`; 56 | } 57 | 58 | function updateBtnColor() { 59 | btnColorInput.value = settings.buttonColor; 60 | } 61 | 62 | function updateBtnSize() { 63 | btnSizeSlider.value = `${settings.buttonSize}`; 64 | } 65 | 66 | function updatePopup() { 67 | popupCheckbox.checked = settings.openPopup; 68 | } 69 | 70 | function updateShowBtn() { 71 | showBtnCheckbox.checked = settings.buttonVisible; 72 | } 73 | 74 | function updateAnimationSpeed() { 75 | animationSpeedSlider.value = `${settings.animationSpeed}`; 76 | } 77 | 78 | export function initAppearanceUI() { 79 | modeDropdown.addEventListener("change", () => { 80 | settings.design = Number(modeDropdown.value); 81 | chrome.storage.local.set({ settings }); 82 | updateColorScheme(); 83 | }); 84 | 85 | btnColorInput.addEventListener("change", () => { 86 | settings.buttonColor = btnColorInput.value; 87 | chrome.storage.local.set({ settings }); 88 | updateBtnColor(); 89 | sendSettingChangedMessage(); 90 | }); 91 | 92 | btnSizeSlider.addEventListener("change", () => { 93 | settings.buttonSize = Number(btnSizeSlider.value); 94 | chrome.storage.local.set({ settings }); 95 | updateBtnSize(); 96 | sendSettingChangedMessage(); 97 | }); 98 | 99 | popupCheckbox.addEventListener("change", () => { 100 | settings.openPopup = popupCheckbox.checked; 101 | chrome.storage.local.set({ settings }); 102 | updatePopup(); 103 | }); 104 | 105 | showBtnCheckbox.addEventListener("change", () => { 106 | settings.buttonVisible = showBtnCheckbox.checked; 107 | chrome.storage.local.set({ settings }); 108 | updateShowBtn(); 109 | sendSettingChangedMessage(); 110 | }); 111 | 112 | animationSpeedSlider.addEventListener("change", () => { 113 | settings.animationSpeed = Number(animationSpeedSlider.value); 114 | chrome.storage.local.set({ settings }); 115 | updateAnimationSpeed(); 116 | sendSettingChangedMessage(); 117 | }); 118 | 119 | resetBtn.addEventListener("click", () => { 120 | settings = { ...defaultStorage.settings }; 121 | chrome.storage.local.set({ settings }); 122 | updateUI(); 123 | sendSettingChangedMessage(); 124 | }); 125 | } 126 | 127 | function sendSettingChangedMessage() { 128 | const message: SettingsChangedMessage = { 129 | sender: CommunicationRole.SETTINGS, 130 | receiver: CommunicationRole.SERVICE_WORKER, 131 | type: MessageType.SETTINGS_CHANGED, 132 | content: { 133 | buttonVisible: settings.buttonVisible, 134 | buttonColor: settings.buttonColor, 135 | buttonSize: settings.buttonSize, 136 | animationSpeed: settings.animationSpeed, 137 | }, 138 | }; 139 | chrome.runtime.sendMessage(message); 140 | } 141 | -------------------------------------------------------------------------------- /ui/settings/style/main.scss: -------------------------------------------------------------------------------- 1 | @import "modules/variables"; 2 | @import "modules/index"; 3 | @import "modules/sections"; 4 | @import "modules/navigation"; 5 | @import "modules/header"; 6 | @import "modules/switch"; 7 | @import "modules/donate"; 8 | @import "modules/faq"; 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | margin: 0; 14 | padding: 0; 15 | box-sizing: border-box; 16 | } 17 | 18 | body { 19 | width: 100%; 20 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 21 | } 22 | 23 | svg:not([fill]) { 24 | fill: currentColor; 25 | } 26 | 27 | hr { 28 | color: currentColor; 29 | width: 100%; 30 | border: inherit; 31 | border-bottom: none; 32 | } 33 | 34 | [aria-busy="true"] { 35 | cursor: progress; 36 | } 37 | 38 | [aria-controls], 39 | button { 40 | cursor: pointer; 41 | } 42 | 43 | [aria-disabled="true"], 44 | [disabled] { 45 | cursor: default; 46 | } 47 | 48 | @media (prefers-reduced-motion: reduce) { 49 | *, 50 | ::before, 51 | ::after { 52 | animation-delay: -1ms !important; 53 | animation-duration: 1ms !important; 54 | animation-iteration-count: 1 !important; 55 | background-attachment: initial !important; 56 | scroll-behavior: auto !important; 57 | transition-delay: 0s !important; 58 | transition-duration: 0s !important; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_donate.scss: -------------------------------------------------------------------------------- 1 | #donation-dialog { 2 | width: 400px; 3 | 4 | background-color: var(--surface-color); 5 | border-radius: 8px; 6 | border: none; 7 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 8 | box-shadow: color-mix(in srgb, rgb(0, 0, 0) 30%, transparent) 0 1px 2px 0, 9 | color-mix(in srgb, rgb(0, 0, 0) 15%, transparent) 0 2px 6px 2px; 10 | 11 | padding: 0; 12 | margin: 100px auto; 13 | color: var(--font-color); 14 | 15 | .head { 16 | width: 100%; 17 | display: flex; 18 | flex-direction: row; 19 | justify-content: center; 20 | align-items: center; 21 | 22 | h1 { 23 | margin: 8px 16px; 24 | flex: 1; 25 | 26 | font-size: 1.25rem; 27 | font-weight: 500; 28 | } 29 | 30 | button { 31 | min-width: auto; 32 | background: none; 33 | padding: 0 16px; 34 | color: var(--font-color); 35 | } 36 | } 37 | 38 | .main-section { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: space-around; 42 | align-items: center; 43 | 44 | width: 100%; 45 | flex: 1; 46 | padding-bottom: 16px; 47 | 48 | button { 49 | padding: 8px 16px; 50 | margin-top: 16px; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_faq.scss: -------------------------------------------------------------------------------- 1 | #faq-card { 2 | padding: 0; 3 | overflow: hidden; 4 | 5 | .faq-section { 6 | margin: 0; 7 | padding: 0; 8 | 9 | .faq-heading { 10 | cursor: pointer; 11 | padding: 16px; 12 | margin: 0; 13 | 14 | border-top: 2px solid var(--hr-color); 15 | 16 | .arrow { 17 | .open-arrow { 18 | display: block; 19 | } 20 | .close-arrow { 21 | display: none; 22 | } 23 | } 24 | } 25 | 26 | .faq-content { 27 | display: grid; 28 | grid-template-rows: 0fr; 29 | transition: grid-template-rows 0.3s ease-out; 30 | 31 | margin: 0; 32 | padding: 0; 33 | 34 | .faq-content-inner { 35 | overflow: hidden; 36 | 37 | margin: 0; 38 | padding: 0; 39 | 40 | .inner { 41 | margin: 0; 42 | padding: 16px; 43 | border-top: 2px solid var(--hr-color); 44 | } 45 | } 46 | } 47 | 48 | &:first-of-type { 49 | .faq-heading { 50 | border-top: none; 51 | } 52 | } 53 | 54 | &.active { 55 | .faq-heading { 56 | background-color: var(--primary-color); 57 | color: var(--on-primary-color); 58 | 59 | .arrow { 60 | .open-arrow { 61 | display: none; 62 | } 63 | .close-arrow { 64 | display: block; 65 | } 66 | } 67 | } 68 | 69 | .faq-content { 70 | grid-template-rows: 1fr; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_header.scss: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | flex-direction: row; 4 | justify-content: space-between; 5 | 6 | div { 7 | display: flex; 8 | flex-direction: row; 9 | } 10 | 11 | img.icon { 12 | width: 32px; 13 | height: 32px; 14 | 15 | margin: 0 16px 0 0; 16 | } 17 | 18 | #burger-menu { 19 | display: none; 20 | 21 | margin: 0; 22 | padding: 0; 23 | border: none; 24 | border-radius: 0; 25 | width: fit-content; 26 | min-width: auto; 27 | background: none; 28 | 29 | .icon { 30 | width: 16px; 31 | height: 16px; 32 | } 33 | } 34 | } 35 | 36 | @media (max-width: 950px) { 37 | header { 38 | #burger-menu { 39 | display: block; 40 | } 41 | #icon { 42 | display: none; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Arial, sans-serif; 4 | 5 | background-color: var(--background-color); 6 | color: var(--font-color); 7 | } 8 | 9 | a { 10 | color: var(--primary-highlight-color); 11 | } 12 | 13 | .strong { 14 | font-weight: bolder; 15 | } 16 | 17 | .card { 18 | display: flex; 19 | flex-direction: column; 20 | 21 | background-color: var(--surface-color); 22 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 23 | box-shadow: color-mix(in srgb, rgb(0, 0, 0) 30%, transparent) 0 1px 2px 0, 24 | color-mix(in srgb, rgb(0, 0, 0) 15%, transparent) 0 2px 6px 2px; 25 | border-radius: 8px; 26 | padding: 16px 0; 27 | margin: 8px; 28 | width: 100%; 29 | min-width: 550px; 30 | max-width: 680px; 31 | 32 | div { 33 | margin-left: 16px; 34 | margin-right: 16px; 35 | } 36 | } 37 | 38 | .textfield { 39 | display: flex; 40 | height: 36px; 41 | padding: 0 0 0 16px; 42 | 43 | background-color: var(--input-color); 44 | border: 1px solid var(--border-color); 45 | border-radius: 1rem; 46 | 47 | input[type="text"] { 48 | width: 100%; 49 | 50 | border: none; 51 | background-color: var(--input-color); 52 | color: var(--font-color); 53 | 54 | &:focus { 55 | outline: none; 56 | } 57 | } 58 | 59 | button { 60 | background-color: var(--surface-color); 61 | color: var(--primary-highlight-color); 62 | margin: 0 0 0 8px; 63 | border: none; 64 | 65 | &:hover { 66 | background-color: var(--highlight-color); 67 | } 68 | } 69 | 70 | &:focus-within { 71 | border: 2px solid var(--border-color-active); 72 | } 73 | } 74 | 75 | button { 76 | background-color: var(--primary-color); 77 | color: var(--on-primary-color); 78 | padding: var(--size-05); 79 | border-radius: var(--size-1); 80 | border: none; 81 | min-width: 100px; 82 | 83 | &:hover { 84 | background-color: var(--primary-highlight-color); 85 | } 86 | 87 | &.outlined { 88 | background: none; 89 | border: 2px solid var(--primary-highlight-color); 90 | color: var(--primary-highlight-color); 91 | 92 | &:hover { 93 | background-color: var(--highlight-color); 94 | } 95 | } 96 | } 97 | 98 | hr { 99 | height: 2px; 100 | width: 100%; 101 | margin: 16px 0; 102 | background-color: var(--hr-color); 103 | } 104 | 105 | select[multiple="multiple"] { 106 | background-color: var(--background-color); 107 | color: var(--font-color); 108 | margin: 16px; 109 | 110 | option { 111 | padding: var(--size-05); 112 | 113 | &.case-insensitive { 114 | &::after { 115 | content: " (case-insensitive)"; 116 | color: var(--primary-color); 117 | font-style: italic; 118 | } 119 | } 120 | 121 | &:checked { 122 | background-color: var(--primary-color); 123 | color: var(--on-primary-color); 124 | } 125 | 126 | &:hover { 127 | background-color: var(--highlight-color); 128 | color: var(--font-color); 129 | } 130 | 131 | &:hover &:checked { 132 | background-color: red; 133 | color: var(--font-color); 134 | } 135 | } 136 | 137 | &::-webkit-scrollbar { 138 | width: 8px; 139 | } 140 | &::-webkit-scrollbar-thumb { 141 | background-color: var(--primary-color); 142 | border-radius: 5px; 143 | margin: 1px; 144 | } 145 | &::-webkit-scrollbar-track { 146 | background-color: var(--highlight-color); 147 | border-radius: 5px; 148 | } 149 | 150 | &.larger { 151 | height: 250px; 152 | } 153 | 154 | &.largest { 155 | height: 375px; 156 | } 157 | } 158 | 159 | .align-right { 160 | display: flex; 161 | justify-content: flex-end; 162 | } 163 | 164 | .card-like { 165 | width: 100%; 166 | min-width: 550px; 167 | max-width: 680px; 168 | 169 | margin: 8px; 170 | } 171 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_navigation.scss: -------------------------------------------------------------------------------- 1 | header { 2 | padding: 16px; 3 | 4 | h1 { 5 | font-size: 1.5rem; 6 | font-weight: 500; 7 | } 8 | } 9 | 10 | nav { 11 | position: absolute; 12 | max-width: 200px; 13 | flex-shrink: 0; 14 | 15 | overflow: hidden; 16 | transition: max-width 0.3s ease-out; 17 | 18 | ul { 19 | li { 20 | padding: 8px 16px; 21 | font-size: 14px; 22 | font-weight: 500; 23 | 24 | text-wrap: nowrap; 25 | 26 | &.active, 27 | &:hover { 28 | border-radius: 0 var(--size-1) var(--size-1) 0; 29 | border: none; 30 | min-width: 100px; 31 | } 32 | 33 | &:hover { 34 | background-color: var(--highlight-color); 35 | color: var(--font-color); 36 | } 37 | 38 | &.active { 39 | background-color: var(--primary-color); 40 | color: var(--on-primary-color); 41 | } 42 | 43 | cursor: pointer; 44 | } 45 | } 46 | } 47 | 48 | @media (max-width: 1400px) { 49 | nav { 50 | position: initial; 51 | } 52 | } 53 | 54 | @media (max-width: 950px) { 55 | nav { 56 | max-width: 0; 57 | 58 | &.open { 59 | max-width: 200px; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_sections.scss: -------------------------------------------------------------------------------- 1 | #main-container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: start; 5 | } 6 | 7 | .section { 8 | display: flex; 9 | flex-direction: column; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | flex-shrink: 1; 14 | 15 | width: 100%; 16 | padding: 0 32px 32px 32px; 17 | 18 | h2 { 19 | width: 100%; 20 | min-width: 550px; 21 | max-width: 680px; 22 | 23 | font-size: 14px; 24 | font-weight: 500; 25 | } 26 | 27 | h3 { 28 | font-size: 16px; 29 | font-weight: 500; 30 | } 31 | 32 | h4 { 33 | font-size: 14px; 34 | font-weight: 500; 35 | margin-top: 8px; 36 | margin-bottom: 8px; 37 | } 38 | 39 | p { 40 | font-size: 14px; 41 | margin-top: 8px; 42 | margin-bottom: 8px; 43 | } 44 | } 45 | 46 | .settings-row { 47 | display: flex; 48 | justify-content: space-between; 49 | 50 | label { 51 | display: flex; 52 | justify-content: center; 53 | flex-direction: column; 54 | font-size: 1rem; 55 | } 56 | 57 | input, 58 | select { 59 | width: 25%; 60 | } 61 | 62 | select { 63 | background-color: var(--background-color); 64 | color: var(--font-color); 65 | padding: 8px; 66 | border-radius: 8px; 67 | cursor: pointer; 68 | } 69 | 70 | input[type="color"] { 71 | border: none; 72 | background: none; 73 | height: 32px; 74 | padding: 0; 75 | } 76 | 77 | input[type="range"] { 78 | &::-webkit-slider-thumb { 79 | background: var(--primary-color); 80 | } 81 | } 82 | 83 | &.space-top { 84 | margin-top: 16px; 85 | } 86 | 87 | &.disabled { 88 | cursor: not-allowed; 89 | 90 | label, 91 | select, 92 | input, 93 | span { 94 | cursor: not-allowed; 95 | } 96 | 97 | label { 98 | color: var(--font-color-disabled); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_switch.scss: -------------------------------------------------------------------------------- 1 | /* TOGGLE-SWITCH */ 2 | /* The switch - the box around the slider */ 3 | 4 | .switch { 5 | position: relative; 6 | display: inline-block; 7 | width: 32px; 8 | height: 20px; 9 | } 10 | 11 | /* Hide default HTML checkbox */ 12 | .switch input { 13 | opacity: 0; 14 | width: 0; 15 | height: 0; 16 | } 17 | 18 | /* The slider */ 19 | .slider { 20 | position: absolute; 21 | cursor: pointer; 22 | top: 0; 23 | left: 0; 24 | right: 0; 25 | bottom: 0; 26 | background: var(--surface-color); 27 | border: 2px solid var(--primary-color); 28 | -webkit-transition: 0.4s; 29 | transition: 0.4s; 30 | } 31 | 32 | .slider:before { 33 | position: absolute; 34 | content: ""; 35 | height: 12px; 36 | width: 12px; 37 | left: 2px; 38 | bottom: 2px; 39 | background-color: var(--primary-color); 40 | -webkit-transition: 0.4s; 41 | transition: 0.4s; 42 | } 43 | 44 | input:checked + .slider { 45 | background-color: var(--primary-color); 46 | } 47 | 48 | input:focus + .slider { 49 | box-shadow: 0 0 1px #2196f3; 50 | } 51 | 52 | input:checked + .slider:before { 53 | transform: translateX(12px); 54 | background-color: var(--surface-color); 55 | } 56 | 57 | /* Rounded sliders */ 58 | .slider.round { 59 | border-radius: 16px; 60 | } 61 | 62 | .slider.round:before { 63 | border-radius: 50%; 64 | } 65 | -------------------------------------------------------------------------------- /ui/settings/style/modules/_variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #0078d4; 2 | $primary-color-darken: darken($primary-color, 10%); 3 | $primary-color-lighten: lighten($primary-color, 20%); 4 | 5 | $primary-color-dark-mode: #0060aa; 6 | $primary-color-darken-dark-mode: darken($primary-color-dark-mode, 10%); 7 | $primary-color-lighten-dark-mode: lighten($primary-color-dark-mode, 20%); 8 | 9 | body { 10 | font-size: larger; 11 | --size-05: 0.5rem; 12 | --size-1: 1rem; 13 | --size-2: 1.2rem; 14 | --size-3: 1.5rem; 15 | 16 | --background-color: #ffffff; 17 | --surface-color: #ffffff; 18 | --input-color: #edf1fa; 19 | 20 | --font-color: #141414; 21 | --font-color-disabled: #707070; 22 | 23 | --primary-color: #0078d4; 24 | --on-primary-color: #ffffff; 25 | 26 | --hr-color: #f0f0f0; 27 | 28 | --border-color: #c7c7c7; 29 | --border-color-active: #0078d4; 30 | 31 | --highlight-color: #ededed; 32 | --primary-highlight-color: #005ea7; 33 | } 34 | 35 | body.dark-scheme { 36 | --background-color: #202124; 37 | --surface-color: #292a2d; 38 | --input-color: #282828; 39 | 40 | --font-color: #e3e3e3; 41 | --font-color-disabled: #707070; 42 | 43 | --primary-color: #005ea7; 44 | --on-primary-color: #e3e3e3; 45 | 46 | --hr-color: #3f4042; 47 | 48 | --border-color: #757575; 49 | --border-color-active: #acc6f9; 50 | 51 | --highlight-color: #38393b; 52 | --primary-highlight-color: #0078d4; 53 | } 54 | 55 | @media (prefers-color-scheme: dark) { 56 | body.detect-scheme { 57 | --background-color: #202124; 58 | --surface-color: #292a2d; 59 | --input-color: #282828; 60 | 61 | --font-color: #e3e3e3; 62 | --font-color-disabled: #707070; 63 | 64 | --primary-color: #005ea7; 65 | --on-primary-color: #e3e3e3; 66 | 67 | --hr-color: #3f4042; 68 | 69 | --border-color: #757575; 70 | --border-color-active: #acc6f9; 71 | 72 | --highlight-color: #38393b; 73 | --primary-highlight-color: #0078d4; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES6", 5 | "outDir": "../dist/ui", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["./**/*.ts"], 12 | "exclude": ["../dist"] 13 | } 14 | --------------------------------------------------------------------------------