├── src ├── typescript │ ├── global.d.ts │ ├── util │ │ ├── cleanURL.ts │ │ └── constants.ts │ ├── action │ │ ├── index.ts │ │ ├── desktop.ts │ │ └── settings.ts │ └── background │ │ ├── redirectShortsPage.ts │ │ ├── handlePageUpdate.ts │ │ ├── modifyGeneralPage.ts │ │ ├── modifyYouTubePage.ts │ │ └── index.ts ├── vendors │ ├── common │ │ ├── assets │ │ │ ├── icon16.png │ │ │ ├── icon48.png │ │ │ ├── icon128.png │ │ │ └── icon.svg │ │ ├── netRequestRule.json │ │ ├── _locales │ │ │ ├── en │ │ │ │ └── messages.json │ │ │ └── fr │ │ │ │ └── messages.json │ │ └── index.html │ ├── firefox │ │ ├── background.html │ │ └── manifest.json │ └── chromium │ │ └── manifest.json ├── assets │ ├── Shorts Deflector Icon 128.png │ ├── Shorts Deflector Icon 16.png │ ├── Shorts Deflector Icon 48.png │ └── Shorts Deflector Icon.svg └── resources │ └── source.css ├── promo ├── Shorts Deflector Demo.pdn ├── Shorts Deflector Dark en.png ├── Shorts Deflector Demo en.png ├── Shorts Deflector Light en.png ├── Shorts Deflector Promo Tile 1400 560.pdn ├── Shorts Deflector Promo Tile 1400 560.png ├── Shorts Deflector Promo Tile 440 280.pdn ├── Shorts Deflector Promo Tile 440 280.png ├── Shorts Deflector Promo Tile 920 680.pdn └── Shorts Deflector Promo Tile 920 680.png ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── Release.yml ├── tsconfig.json ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── .eslintrc.json ├── README.md ├── package.json └── tailwind.config.js /src/typescript/global.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | const browser: typeof chrome; 5 | } 6 | -------------------------------------------------------------------------------- /promo/Shorts Deflector Demo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Demo.pdn -------------------------------------------------------------------------------- /promo/Shorts Deflector Dark en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Dark en.png -------------------------------------------------------------------------------- /promo/Shorts Deflector Demo en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Demo en.png -------------------------------------------------------------------------------- /promo/Shorts Deflector Light en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Light en.png -------------------------------------------------------------------------------- /src/typescript/util/cleanURL.ts: -------------------------------------------------------------------------------- 1 | export function cleanURL(url = '') { 2 | return url.replace('shorts/', 'watch?v='); 3 | } 4 | -------------------------------------------------------------------------------- /src/vendors/common/assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/src/vendors/common/assets/icon16.png -------------------------------------------------------------------------------- /src/vendors/common/assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/src/vendors/common/assets/icon48.png -------------------------------------------------------------------------------- /src/vendors/common/assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/src/vendors/common/assets/icon128.png -------------------------------------------------------------------------------- /src/assets/Shorts Deflector Icon 128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/src/assets/Shorts Deflector Icon 128.png -------------------------------------------------------------------------------- /src/assets/Shorts Deflector Icon 16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/src/assets/Shorts Deflector Icon 16.png -------------------------------------------------------------------------------- /src/assets/Shorts Deflector Icon 48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/src/assets/Shorts Deflector Icon 48.png -------------------------------------------------------------------------------- /promo/Shorts Deflector Promo Tile 1400 560.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Promo Tile 1400 560.pdn -------------------------------------------------------------------------------- /promo/Shorts Deflector Promo Tile 1400 560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Promo Tile 1400 560.png -------------------------------------------------------------------------------- /promo/Shorts Deflector Promo Tile 440 280.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Promo Tile 440 280.pdn -------------------------------------------------------------------------------- /promo/Shorts Deflector Promo Tile 440 280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Promo Tile 440 280.png -------------------------------------------------------------------------------- /promo/Shorts Deflector Promo Tile 920 680.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Promo Tile 920 680.pdn -------------------------------------------------------------------------------- /promo/Shorts Deflector Promo Tile 920 680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evenevan/shorts-deflector/HEAD/promo/Shorts Deflector Promo Tile 920 680.png -------------------------------------------------------------------------------- /src/vendors/firefox/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask any questions you have 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question** 11 | 12 | **Additional context** 13 | Add any other context or screenshots about your question(s) here. 14 | -------------------------------------------------------------------------------- /src/typescript/action/index.ts: -------------------------------------------------------------------------------- 1 | import './desktop.js'; 2 | import './settings.js'; 3 | import { i18nKeys, runtime } from '../util/constants.js'; 4 | 5 | i18nKeys.forEach(([htmlKey, localeKey]) => { 6 | const element = document.getElementById(htmlKey); 7 | element!.textContent = runtime.i18n.getMessage(localeKey); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/strictest/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "ES2022", 6 | "allowJs": false, 7 | "checkJs": false, 8 | "exactOptionalPropertyTypes": false, 9 | "noPropertyAccessFromIndexSignature": false, 10 | "types": [ 11 | "chrome", 12 | ] 13 | } 14 | } -------------------------------------------------------------------------------- /src/typescript/background/redirectShortsPage.ts: -------------------------------------------------------------------------------- 1 | // This method is better than tabs.update because it doesn't leave the Shorts page in your history 2 | // Using tabs.goBack and then tabs.update, which does solve the history issue, is much slower 3 | 4 | export function redirectShortsPage() { 5 | const cleanURL = window.location.toString().replace('shorts/', 'watch?v='); 6 | window.location.replace(cleanURL); 7 | } 8 | -------------------------------------------------------------------------------- /src/vendors/common/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/Shorts Deflector Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/vendors/common/netRequestRule.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "priority": 1, 5 | "action": { 6 | "type": "redirect", 7 | "redirect": { 8 | "regexSubstitution": "https://www.youtube.com/watch?v=\\2" 9 | } 10 | }, 11 | "condition": { 12 | "regexFilter": "^https://(www\\.)?youtube\\.com/shorts/(.+)", 13 | "resourceTypes": [ 14 | "main_frame" 15 | ] 16 | } 17 | } 18 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | tmp 4 | src/vendors/chromium/_locales 5 | src/vendors/chromium/assets 6 | src/vendors/chromium/index.html 7 | src/vendors/chromium/netRequestRule.json 8 | src/vendors/chromium/resources 9 | src/vendors/chromium/scripts 10 | src/vendors/common/resources 11 | src/vendors/common/scripts 12 | src/vendors/firefox/_locales 13 | src/vendors/firefox/assets 14 | src/vendors/firefox/index.html 15 | src/vendors/firefox/netRequestRule.json 16 | src/vendors/firefox/resources 17 | src/vendors/firefox/scripts -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 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 alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": false, 4 | "bracketSpacing": true, 5 | "embeddedLanguageFormatting": "auto", 6 | "endOfLine": "crlf", 7 | "htmlWhitespaceSensitivity": "strict", 8 | "insertPragma": false, 9 | "printWidth": 100, 10 | "proseWrap": "always", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": true, 14 | "singleAttributePerLine": false, 15 | "singleQuote": true, 16 | "rangeStart": 0, 17 | "rangeEnd": 2147483647, 18 | "tabWidth": 4, 19 | "trailingComma": "all", 20 | "useTabs": false 21 | } -------------------------------------------------------------------------------- /src/vendors/common/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Seamlessly play YouTube Shorts with the normal desktop interface" 4 | }, 5 | "desktopTitle": { 6 | "message": "Switch to Desktop Interface" 7 | }, 8 | "desktopTooltip": { 9 | "message": "Available on YouTube Shorts pages" 10 | }, 11 | "automaticTitle": { 12 | "message": "Automatic Switching" 13 | }, 14 | "automaticDescription": { 15 | "message": "Automatically switch YouTube Shorts to the desktop UI." 16 | }, 17 | "changeLinksTitle": { 18 | "message": "Change Links on Pages" 19 | }, 20 | "changeLinksDescription": { 21 | "message": "Change YouTube Shorts hyperlinks directly on pages. Requires extra permissions and has a performance impact." 22 | } 23 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | **Describe the bug** 9 | A clear and concise description of what the bug is. 10 | 11 | **Expected behavior** 12 | A clear and concise description of what you expected to happen. 13 | 14 | **Version** 15 | State the version of the extension you experienced the bug with. 16 | 17 | **Browser** 18 | State the browser version that you experienced the bug with. 19 | 20 | **Reproduction** 21 | Steps to reproduce the behavior: 22 | 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Screenshots** 29 | If applicable, include screenshots to help explain your problem. 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /src/vendors/common/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "message": "Lisez les YouTube Shorts en toute transparence avec l'interface de bureau normale." 4 | }, 5 | "desktopTitle": { 6 | "message": "Passer à l'Interface du bureau" 7 | }, 8 | "desktopTooltip": { 9 | "message": "Disponible sur les pages des YouTube Shorts" 10 | }, 11 | "automaticTitle": { 12 | "message": "Changement automatique" 13 | }, 14 | "automaticDescription": { 15 | "message": "Changement automatiquement les YouTube Shorts à l'interface de bureau." 16 | }, 17 | "changeLinksTitle": { 18 | "message": "Modifier les hyperliens sur les pages web" 19 | }, 20 | "changeLinksDescription": { 21 | "message": "Modifiez les liens hypertextes des YouTube Shorts directement sur les pages web. Nécessite des autorisations supplémentaires et a un impact sur les performances." 22 | } 23 | } -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version" 8 | required: true 9 | type: string 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check Environment Variables 19 | run: env 20 | 21 | - name: Checkout Repository 22 | uses: actions/checkout@v4 23 | with: 24 | submodules: true 25 | 26 | - name: Use Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | 31 | - name: Install Dependencies 32 | run: npm ci 33 | 34 | - name: Build 35 | run: npm run build 36 | 37 | - name: Release 38 | uses: softprops/action-gh-release@v2 39 | with: 40 | files: ./dist/*.zip 41 | fail_on_unmatched_files: true 42 | tag_name: "${{ inputs.version }}" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Evan F 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/resources/source.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .h1 { 6 | @apply text-base font-semibold; 7 | } 8 | 9 | .h2 { 10 | @apply text-sm font-normal; 11 | } 12 | 13 | .h3 { 14 | @apply text-xs font-normal; 15 | } 16 | 17 | .setting-title { 18 | @apply h1 truncate text-base font-semibold; 19 | } 20 | 21 | .description { 22 | @apply h2 text-light-2 dark:text-dark-2; 23 | } 24 | 25 | .toggle-container { 26 | @apply flex h-4 w-8 items-center justify-start; 27 | } 28 | 29 | .toggle-checkbox { 30 | @apply absolute z-40 h-4 w-8 appearance-none hover:cursor-pointer disabled:hover:cursor-not-allowed; 31 | } 32 | 33 | .toggle-background { 34 | @apply z-10 h-2 w-8 rounded-full bg-light-5 dark:bg-dark-5 peer-disabled:bg-light-4 peer-disabled:dark:bg-dark-3; 35 | } 36 | 37 | .toggle-foreground { 38 | @apply absolute z-20 h-2 w-2 rounded-full bg-youtube-red ease-in-out peer-checked:w-6 peer-hover:duration-150 peer-disabled:bg-youtube-red-disabled; 39 | } 40 | 41 | .toggle-handle { 42 | @apply absolute z-30 h-4 w-4 rounded-full bg-light-4 ease-in-out peer-checked:translate-x-4 peer-hover:duration-150 peer-disabled:bg-light-3 dark:bg-dark-6 peer-disabled:dark:bg-dark-4; 43 | } 44 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 2022, 8 | "sourceType": "module", 9 | "project": "./tsconfig.json" 10 | }, 11 | "extends": [ 12 | "airbnb-base", 13 | "airbnb-typescript/base" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/indent": ["error", 4], 17 | "@typescript-eslint/no-use-before-define": [ 18 | "error", { 19 | "functions": false 20 | } 21 | ], 22 | "class-methods-use-this": ["off"], 23 | "eol-last": ["error", "always"], 24 | "function-paren-newline": ["error", "consistent"], 25 | "import/prefer-default-export": ["off"], 26 | "indent": "off", 27 | "linebreak-style": ["error", "windows"], 28 | "new-cap": [ 29 | "error", { 30 | "newIsCapExceptions": ["i18n"] 31 | } 32 | ], 33 | "no-console": ["off"], 34 | "object-curly-newline": ["error", { 35 | "consistent": true 36 | }], 37 | "object-shorthand": ["error", "never"], 38 | "sort-imports": ["error", { 39 | "ignoreCase": true, 40 | "ignoreDeclarationSort": true 41 | }] 42 | } 43 | } -------------------------------------------------------------------------------- /src/vendors/chromium/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "default_popup": "./index.html", 4 | "default_icon": { 5 | "16": "./assets/icon16.png", 6 | "48": "./assets/icon48.png", 7 | "128": "./assets/icon128.png" 8 | } 9 | }, 10 | "author": "Attituding", 11 | "background": { 12 | "service_worker": "./scripts/background/index.js", 13 | "type": "module" 14 | }, 15 | "declarative_net_request": { 16 | "rule_resources": [ 17 | { 18 | "id": "shorts", 19 | "enabled": true, 20 | "path": "./netRequestRule.json" 21 | } 22 | ] 23 | }, 24 | "default_locale": "en", 25 | "description": "__MSG_extensionDescription__", 26 | "host_permissions": [ 27 | "https://www.youtube.com/*" 28 | ], 29 | "icons": { 30 | "16": "./assets/icon16.png", 31 | "48": "./assets/icon48.png", 32 | "128": "./assets/icon128.png" 33 | }, 34 | "manifest_version": 3, 35 | "minimum_chrome_version": "102", 36 | "name": "Shorts Deflector", 37 | "optional_host_permissions": [ 38 | "*://*/*" 39 | ], 40 | "permissions": [ 41 | "declarativeNetRequestWithHostAccess", 42 | "scripting", 43 | "storage", 44 | "webNavigation" 45 | ], 46 | "version": "1.17.0" 47 | } -------------------------------------------------------------------------------- /src/typescript/util/constants.ts: -------------------------------------------------------------------------------- 1 | // HTML Ids 2 | export const automaticHTMLKey = 'automatic'; 3 | export const desktopHTMLKey = 'desktop'; 4 | export const dynamicHTMLKey = 'dynamic'; 5 | export const improvePerformanceHTMLKey = 'improve-performance'; 6 | export const linkHTMLKey = 'link'; 7 | 8 | export const i18nKeys: [string, string][] = [ 9 | ['desktop-tooltip', 'desktopTooltip'], 10 | ['desktop-title', 'desktopTitle'], 11 | ['automatic-title', 'automaticTitle'], 12 | ['automatic-description', 'automaticDescription'], 13 | ['change-links-title', 'changeLinksTitle'], 14 | ['change-links-description', 'changeLinksDescription'], 15 | ]; 16 | 17 | // browser.storage Keys 18 | export const automaticStorageKey = 'automatic'; 19 | // improvePerformance is the legacy name for changeLinks 20 | export const changeLinksStorageKey = 'improvePerformance'; 21 | 22 | // Regex 23 | export const youTubeRegex = /^http(s)?:\/\/www\.youtube\.com/; 24 | export const youTubeShortsRegex = /^http(s)?:\/\/www\.youtube\.com\/shorts\/(.+)$/; 25 | 26 | // Rulesets 27 | export const shortsRuleset = 'shorts'; 28 | 29 | // Hostnames 30 | export const youTubeHostname = 'https://www.youtube.com/*'; 31 | export const allHostname = '*://*/*'; 32 | 33 | // Runtime selection 34 | // eslint-disable-next-line import/no-mutable-exports 35 | export let runtime = chrome; 36 | 37 | try { 38 | runtime = browser; 39 | // eslint-disable-next-line no-empty 40 | } catch { } 41 | -------------------------------------------------------------------------------- /src/vendors/firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "default_popup": "./index.html", 4 | "default_icon": { 5 | "16": "./assets/icon.svg", 6 | "48": "./assets/icon.svg", 7 | "128": "./assets/icon.svg" 8 | } 9 | }, 10 | "author": "Attituding", 11 | "background": { 12 | "page": "./background.html" 13 | }, 14 | "browser_specific_settings": { 15 | "gecko": { 16 | "id": "shortsdeflector@addons.com", 17 | "strict_min_version": "113.0" 18 | } 19 | }, 20 | "declarative_net_request": { 21 | "rule_resources": [ 22 | { 23 | "id": "shorts", 24 | "enabled": true, 25 | "path": "./netRequestRule.json" 26 | } 27 | ] 28 | }, 29 | "default_locale": "en", 30 | "description": "__MSG_extensionDescription__", 31 | "host_permissions": [ 32 | "https://www.youtube.com/*" 33 | ], 34 | "icons": { 35 | "16": "./assets/icon.svg", 36 | "48": "./assets/icon.svg", 37 | "128": "./assets/icon.svg" 38 | }, 39 | "manifest_version": 3, 40 | "name": "Shorts Deflector", 41 | "optional_permissions": [ 42 | "*://*/*" 43 | ], 44 | "permissions": [ 45 | "declarativeNetRequestWithHostAccess", 46 | "scripting", 47 | "storage", 48 | "webNavigation" 49 | ], 50 | "version": "1.17.0" 51 | } -------------------------------------------------------------------------------- /src/typescript/background/handlePageUpdate.ts: -------------------------------------------------------------------------------- 1 | import { modifyGeneralPage } from './modifyGeneralPage.js'; 2 | import { modifyYouTubePage } from './modifyYouTubePage.js'; 3 | import { redirectShortsPage } from './redirectShortsPage.js'; 4 | import { 5 | automaticStorageKey, 6 | changeLinksStorageKey, 7 | runtime, 8 | youTubeRegex, 9 | youTubeShortsRegex, 10 | } from '../util/constants.js'; 11 | 12 | export async function handlePageUpdate(tabId: number, tab: chrome.tabs.Tab) { 13 | const { 14 | [automaticStorageKey]: automatic, 15 | [changeLinksStorageKey]: improvePerformance, 16 | } = await runtime.storage.sync.get([ 17 | automaticStorageKey, 18 | changeLinksStorageKey, 19 | ]); 20 | 21 | if ( 22 | automatic === false 23 | || (improvePerformance === false && youTubeRegex.test(tab.url!) === false) 24 | ) { 25 | return; 26 | } 27 | 28 | const url = youTubeShortsRegex.test(tab.url!); 29 | 30 | if (url) { 31 | // Redirecting 32 | 33 | await runtime.scripting.executeScript({ 34 | // @ts-ignore 35 | injectImmediately: true, 36 | target: { 37 | tabId: tabId, 38 | }, 39 | func: redirectShortsPage, 40 | }); 41 | } else { 42 | // URL Updating 43 | 44 | const script = youTubeRegex.test(tab.url!) ? modifyYouTubePage : modifyGeneralPage; 45 | 46 | await runtime.scripting.executeScript({ 47 | target: { 48 | tabId: tabId, 49 | }, 50 | func: script, 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Why does clicking on a YouTube Short on a desktop bring up the mobile-optimized interface? With no video scrubbing, harder to access comments, less accessible view count, and more, it makes no sense to use on a desktop. 4 | 5 | Shorts Deflector is an extension that allows you to seamlessly watch YouTube Shorts with the normal desktop interface. By using several techniques, this extension is super fast; you will barely notice the switch to the desktop viewer. Install now and try it out for yourself! 6 | 7 | Upon install, the "Read your browsing history" permission may seem alarming, but this permission is only used to detect page updates. 8 | 9 | Read more at https://evenevan.github.io/shorts-deflector/. 10 | 11 | # Home Page 12 | 13 | [https://evenevan.github.io/shorts-deflector/](https://evenevan.github.io/shorts-deflector/) 14 | 15 | # Install 16 | 17 | [![](https://user-images.githubusercontent.com/585534/107280622-91a8ea80-6a26-11eb-8d07-77c548b28665.png)](https://chrome.google.com/webstore/detail/shorts-deflector/gilmponliddppjjcfjmanmmfgiilikhg) [![](https://user-images.githubusercontent.com/585534/107280546-7b9b2a00-6a26-11eb-8f9f-f95932f4bfec.png)](https://addons.mozilla.org/firefox/addon/shorts-deflector/) 18 | 19 | # Contribution 20 | 21 | Want to help improve this extension? 22 | 23 | 1) Fork this project 24 | 2) Make your changes and commit/push to your fork 25 | 3) [Build](#build) 26 | 4) Test out your changes 27 | 5) Make a pull request 28 | 29 | # Build 30 | 31 | ## Prerequisites (available via cli): 32 | 33 | - Version of Node.js >= v18 w/ NPM 34 | - Bash 35 | - 7zip 36 | 37 | ## Steps: 38 | 39 | 1) Open terminal with this directory 40 | 2) Run "npm i" 41 | 3) Run "npm run build" 42 | 4) Output zips will appear in /dist folder 43 | 44 | # Develop 45 | 46 | ## Firefox 47 | 48 | Use 49 | 50 | ``` 51 | npm run firefox 52 | ``` 53 | -------------------------------------------------------------------------------- /src/typescript/action/desktop.ts: -------------------------------------------------------------------------------- 1 | import { cleanURL } from '../util/cleanURL.js'; 2 | import { 3 | desktopHTMLKey, 4 | dynamicHTMLKey, 5 | linkHTMLKey, 6 | runtime, 7 | youTubeShortsRegex, 8 | } from '../util/constants.js'; 9 | 10 | const desktopButton = document.getElementById(desktopHTMLKey) as HTMLButtonElement; 11 | const linkAnchor = document.getElementById(linkHTMLKey) as HTMLAnchorElement; 12 | const dynamicDiv = document.getElementById(dynamicHTMLKey) as HTMLDivElement; 13 | 14 | let [tab] = await runtime.tabs.query({ 15 | active: true, 16 | currentWindow: true, 17 | }); 18 | 19 | update(); 20 | 21 | runtime.tabs.onUpdated.addListener((_id, _changes, newTab) => { 22 | if (newTab.id === tab?.id) { 23 | tab = newTab; 24 | 25 | update(); 26 | } 27 | }); 28 | 29 | desktopButton.addEventListener('click', async () => { 30 | loading(); 31 | 32 | await runtime.tabs.update((tab!.id!), { 33 | url: cleanURL(tab?.url), 34 | }); 35 | }); 36 | 37 | function update() { 38 | if (tab?.status === 'complete') { 39 | loaded(); 40 | } else { 41 | loading(); 42 | } 43 | } 44 | 45 | function loading() { 46 | desktopButton.disabled = true; 47 | dynamicDiv.dataset.state = 'loading'; 48 | linkAnchor.removeAttribute('href'); 49 | linkAnchor.setAttribute('aria-disabled', 'true'); 50 | } 51 | 52 | function loaded() { 53 | const isNotYouTubeShortsPage = !tab?.url?.match(youTubeShortsRegex); 54 | 55 | desktopButton.disabled = isNotYouTubeShortsPage; 56 | dynamicDiv.dataset.state = 'link'; 57 | linkAnchor.setAttribute('aria-disabled', isNotYouTubeShortsPage.toString()); 58 | 59 | if (isNotYouTubeShortsPage) { 60 | linkAnchor.removeAttribute('href'); 61 | linkAnchor.setAttribute('tabIndex', '-1'); 62 | } else { 63 | linkAnchor.setAttribute('href', cleanURL(tab?.url)); 64 | linkAnchor.removeAttribute('tabIndex'); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shorts-deflector", 3 | "description": "Seamlessly play YouTube Shorts with the normal desktop interface.", 4 | "license": "MIT", 5 | "author": "Evan F ", 6 | "devDependencies": { 7 | "@tsconfig/strictest": "^2.0.5", 8 | "@types/chrome": "^0.0.270", 9 | "@typescript-eslint/eslint-plugin": "^5.62.0", 10 | "@typescript-eslint/parser": "^5.62.0", 11 | "eslint": "^8.56.0", 12 | "eslint-config-airbnb-base": "^15.0.0", 13 | "eslint-config-airbnb-typescript": "^17.1.0", 14 | "eslint-plugin-import": "^2.29.1", 15 | "prettier-plugin-tailwindcss": "^0.6.6", 16 | "tailwindcss": "^3.4.10", 17 | "typescript": "^5.5.4", 18 | "web-ext": "^8.2.0" 19 | }, 20 | "scripts": { 21 | "build": "npm run clear && npm run tailwindcss && npm run typescript && npm run copy && npm run zip", 22 | "clear": "bash -c \"echo './dist ./src/vendors/chromium/_locales ./src/vendors/chromium/assets ./src/vendors/chromium/index.html ./src/vendors/chromium/netRequestRule.json ./src/vendors/chromium/resources ./src/vendors/chromium/scripts ./src/vendors/common/resources ./src/vendors/common/scripts ./src/vendors/firefox/_locales ./src/vendors/firefox/assets ./src/vendors/firefox/index.html ./src/vendors/firefox/netRequestRule.json ./src/vendors/firefox/resources ./src/vendors/firefox/scripts' | xargs rm -rf\"", 23 | "copy": "bash -c \"cp -r ./src/vendors/common/* src/vendors/chromium && cp -r ./src/vendors/common/* src/vendors/firefox/\"", 24 | "firefox": "web-ext run --source-dir ./src/vendors/firefox", 25 | "format": "prettier --write \"**/*.ts\" && eslint --fix \"**/*.ts\"", 26 | "tailwindcss": "npx tailwindcss -i src/resources/source.css -o src/vendors/common/resources/tailwind.css", 27 | "typescript": "tsc --outDir src/vendors/common/scripts", 28 | "zip": "7z a -tzip ./dist/chromium.zip ./src/vendors/chromium/* && 7z a -tzip ./dist/firefox.zip ./src/vendors/firefox/*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/typescript/action/settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | allHostname, 3 | automaticHTMLKey, 4 | automaticStorageKey, 5 | changeLinksStorageKey, 6 | improvePerformanceHTMLKey, 7 | runtime, 8 | shortsRuleset, 9 | youTubeHostname, 10 | } from '../util/constants.js'; 11 | 12 | const automaticSwitch = document.getElementById(automaticHTMLKey) as HTMLInputElement; 13 | 14 | const improvePerformanceSwitch = document.getElementById( 15 | improvePerformanceHTMLKey, 16 | ) as HTMLInputElement; 17 | 18 | const storageKeys = await runtime.storage.sync.get([ 19 | automaticStorageKey, 20 | changeLinksStorageKey, 21 | ]); 22 | 23 | automaticSwitch.checked = storageKeys[automaticStorageKey]; 24 | improvePerformanceSwitch.checked = storageKeys[changeLinksStorageKey]; 25 | improvePerformanceSwitch.disabled = automaticSwitch.checked === false; 26 | 27 | automaticSwitch.addEventListener('click', async () => { 28 | if (automaticSwitch.checked) { 29 | const granted = (await runtime.permissions.request({ 30 | origins: [youTubeHostname], 31 | })) as unknown as boolean; 32 | 33 | if (granted === false) { 34 | automaticSwitch.checked = false; 35 | 36 | return; 37 | } 38 | } 39 | 40 | improvePerformanceSwitch.disabled = automaticSwitch.checked === false; 41 | 42 | await runtime.storage.sync.set({ 43 | [automaticStorageKey]: automaticSwitch.checked, 44 | }); 45 | 46 | const declarativeNetRequestKey = automaticSwitch.checked 47 | ? 'enableRulesetIds' 48 | : 'disableRulesetIds'; 49 | 50 | await runtime.declarativeNetRequest.updateEnabledRulesets({ 51 | [declarativeNetRequestKey]: [shortsRuleset], 52 | }); 53 | }); 54 | 55 | improvePerformanceSwitch.addEventListener('click', async () => { 56 | if (improvePerformanceSwitch.checked) { 57 | const granted = (await runtime.permissions.request({ 58 | origins: [allHostname], 59 | })) as unknown as boolean; 60 | 61 | if (granted === false) { 62 | improvePerformanceSwitch.checked = false; 63 | 64 | return; 65 | } 66 | } 67 | 68 | await runtime.storage.sync.set({ 69 | [changeLinksStorageKey]: improvePerformanceSwitch.checked, 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/typescript/background/modifyGeneralPage.ts: -------------------------------------------------------------------------------- 1 | export function modifyGeneralPage() { 2 | // While we don't have the host permissions for non www, we can still change them 3 | // as YouTube uses non www for the share link for Shorts 4 | const youTubeShortsRegex = /^http(s)?:\/\/(www.)?youtube\.com\/shorts\/(.+)$/; 5 | 6 | const observer = new MutationObserver((mutationRecords) => { 7 | mutationRecords.forEach((mutation) => { 8 | if (mutation.type === 'attributes') { 9 | // If the type of attributes, then the mutation.target has changed href 10 | patchAnchor(mutation.target as HTMLAnchorElement); 11 | } else if (mutation.type === 'childList') { 12 | // If the type is childList, one of the target's children has changed 13 | const addedElements = Array.from(mutation.addedNodes) 14 | .filter((addedNode) => addedNode instanceof HTMLElement); 15 | 16 | // Get all new children that have been added to the target 17 | addedElements 18 | .filter((addedElement) => addedElement instanceof HTMLAnchorElement) 19 | .forEach((anchor) => patchAnchor(anchor)); 20 | 21 | // This is needed because if a newly added element has children, 22 | // those won't be in mutation.addedNodes 23 | addedElements 24 | .map((addedElement) => Array.from(addedElement.getElementsByTagName('a'))) 25 | .flat(1) 26 | .forEach((anchor) => patchAnchor(anchor)); 27 | } 28 | }); 29 | }); 30 | 31 | observer.observe(document.body, { 32 | attributes: true, 33 | attributeFilter: ['href'], 34 | childList: true, 35 | subtree: true, 36 | }); 37 | 38 | // eslint-disable-next-line no-restricted-syntax 39 | for (const anchor of document.getElementsByTagName('a')) { 40 | patchAnchor(anchor); 41 | } 42 | 43 | // If patchAnchor modifies the href, that will indeed cause the observer 44 | // to fire an event. I don't know a fix and the overhead should be minimal 45 | // but this is something to keep in mind 46 | function patchAnchor(anchor: HTMLAnchorElement) { 47 | if (anchor.href.match(youTubeShortsRegex)) { 48 | // eslint-disable-next-line no-param-reassign 49 | anchor.href = anchor.href.replace('shorts/', 'watch?v='); 50 | } 51 | 52 | /* 53 | This probably breaks things more than it helps 54 | 55 | anchor.addEventListener( 56 | 'click', 57 | (event) => { 58 | event.stopImmediatePropagation(); 59 | }, 60 | true, 61 | ); 62 | */ 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/typescript/background/modifyYouTubePage.ts: -------------------------------------------------------------------------------- 1 | // Special optimized algorithm for YouTube 2 | 3 | export function modifyYouTubePage() { 4 | // While we don't have the host permissions for non www, we can still change them 5 | // as YouTube uses non www for the share link for Shorts 6 | const youTubeShortsRegex = /^http(s)?:\/\/(www.)?youtube\.com\/shorts\/(.+)$/; 7 | 8 | const observer = new MutationObserver((mutationRecords) => { 9 | const elements = mutationRecords 10 | .filter( 11 | (mutationRecord) => mutationRecord.type === 'attributes' && mutationRecord.target.nodeType === 1, 12 | ) 13 | .map((mutationRecord) => mutationRecord.target) as HTMLElement[]; 14 | 15 | elements.forEach((element) => { 16 | switch (element.tagName) { 17 | case 'A': 18 | { 19 | const anchor = element as HTMLAnchorElement; 20 | patchAnchor(anchor); 21 | } 22 | break; 23 | case 'YTD-THUMBNAIL-OVERLAY-TIME-STATUS-RENDERER': { 24 | const attribute = element.attributes.getNamedItem('overlay-style'); 25 | 26 | if (attribute?.value === 'SHORTS') { 27 | attribute.value = 'DEFAULT'; 28 | 29 | // eslint-disable-next-line no-param-reassign 30 | element.innerHTML = ''; 31 | } 32 | } 33 | // no default 34 | } 35 | }); 36 | }); 37 | 38 | observer.observe(document.body, { 39 | attributes: true, 40 | attributeFilter: ['has-icon', 'href'], 41 | childList: true, 42 | subtree: true, 43 | }); 44 | 45 | // eslint-disable-next-line no-restricted-syntax 46 | for (const anchor of document.getElementsByTagName('a')) { 47 | patchAnchor(anchor); 48 | } 49 | 50 | // If patchAnchor modifies the href, that will indeed cause the observer 51 | // to fire an event. I don't know a fix and the overhead should be minimal 52 | // but this is something to keep in mind 53 | function patchAnchor(anchor: HTMLAnchorElement) { 54 | if (anchor.href.match(youTubeShortsRegex)) { 55 | // eslint-disable-next-line no-param-reassign 56 | anchor.href = anchor.href.replace('shorts/', 'watch?v='); 57 | } 58 | 59 | /* 60 | Removed to fix the "Watch Later" and "Add to Queue" buttons, which rely on this event listener 61 | 62 | anchor.addEventListener( 63 | 'click', 64 | (event) => { 65 | event.stopImmediatePropagation(); 66 | }, 67 | true, 68 | ); 69 | */ 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const colors = require('tailwindcss/colors') 2 | 3 | module.exports = { 4 | content: ['src/vendors/**/*.html'], 5 | theme: { 6 | extend: { 7 | colors: { 8 | 'youtube-red': '#f40407', 9 | 'youtube-red-disabled': '#8e0203', 10 | }, 11 | backgroundColor: { 12 | 'dark-1': colors.neutral['900'], 13 | 'dark-2': colors.neutral['800'], 14 | 'dark-3': colors.neutral['700'], 15 | 'dark-4': colors.neutral['600'], 16 | 'dark-5': colors.neutral['500'], 17 | 'dark-6': colors.neutral['400'], 18 | 'light-1': colors.white, 19 | 'light-2': colors.neutral['100'], 20 | 'light-3': colors.neutral['200'], 21 | 'light-4': colors.neutral['300'], 22 | 'light-5': colors.neutral['400'], 23 | 'light-6': colors.neutral['500'], 24 | 'dark-tooltip': colors.neutral['600'], 25 | 'light-tooltip': colors.neutral['600'], 26 | }, 27 | borderColor: { 28 | 'dark-1': colors.neutral['900'], 29 | 'dark-2': colors.neutral['800'], 30 | 'dark-3': colors.neutral['700'], 31 | 'dark-4': colors.neutral['600'], 32 | 'dark-5': colors.neutral['500'], 33 | 'dark-6': colors.neutral['400'], 34 | 'light-1': colors.white, 35 | 'light-2': colors.neutral['100'], 36 | 'light-3': colors.neutral['200'], 37 | 'light-4': colors.neutral['300'], 38 | 'light-5': colors.neutral['400'], 39 | 'light-6': colors.neutral['500'], 40 | }, 41 | fill: { 42 | 'dark-1': colors.white, 43 | 'dark-2': colors.neutral['200'], 44 | 'dark-3': colors.neutral['400'], 45 | 'dark-4': colors.neutral['600'], 46 | 'dark-5': colors.neutral['800'], 47 | 'light-1': colors.black, 48 | 'light-2': colors.neutral['900'], 49 | 'light-3': colors.neutral['800'], 50 | 'light-4': colors.neutral['700'], 51 | 'light-5': colors.neutral['600'], 52 | }, 53 | textColor: { 54 | 'dark-1': colors.white, 55 | 'dark-2': colors.neutral['200'], 56 | 'dark-3': colors.neutral['400'], 57 | 'dark-4': colors.neutral['600'], 58 | 'dark-5': colors.neutral['800'], 59 | 'light-1': colors.black, 60 | 'light-2': colors.neutral['900'], 61 | 'light-3': colors.neutral['800'], 62 | 'light-4': colors.neutral['700'], 63 | 'light-5': colors.neutral['600'], 64 | 'dark-tooltip': colors.white, 65 | 'light-tooltip': colors.white, 66 | }, 67 | width: { 68 | body: '22rem', 69 | }, 70 | }, 71 | }, 72 | plugins: [], 73 | }; 74 | -------------------------------------------------------------------------------- /src/typescript/background/index.ts: -------------------------------------------------------------------------------- 1 | import { handlePageUpdate } from './handlePageUpdate.js'; 2 | import { 3 | allHostname, 4 | automaticStorageKey, 5 | changeLinksStorageKey, 6 | runtime, 7 | youTubeHostname, 8 | } from '../util/constants.js'; 9 | 10 | // Install/Update Handling 11 | runtime.runtime.onInstalled.addListener(async (details) => { 12 | if ( 13 | details.reason === runtime.runtime.OnInstalledReason.INSTALL 14 | || details.reason === runtime.runtime.OnInstalledReason.UPDATE 15 | ) { 16 | const keys = await runtime.storage.sync.get(null); 17 | 18 | // @ts-ignore 19 | const automaticPermission = await runtime.permissions.contains({ 20 | origins: [youTubeHostname], 21 | }); 22 | 23 | // @ts-ignore 24 | const improvePerformancePermission = await runtime.permissions.contains({ 25 | origins: [allHostname], 26 | }); 27 | 28 | const newKeys = { 29 | [automaticStorageKey]: 30 | typeof keys[automaticStorageKey] !== 'undefined' 31 | ? keys[automaticStorageKey] && automaticPermission 32 | : automaticPermission, 33 | [changeLinksStorageKey]: 34 | typeof keys[changeLinksStorageKey] !== 'undefined' 35 | ? keys[changeLinksStorageKey] && improvePerformancePermission 36 | : improvePerformancePermission, 37 | }; 38 | 39 | await runtime.storage.sync.set(newKeys); 40 | 41 | console.log('Set settings', newKeys); 42 | } 43 | }); 44 | 45 | // Handle Permission Removal 46 | runtime.permissions.onRemoved.addListener(async () => { 47 | // @ts-ignore 48 | const automaticPermission = (await runtime.permissions.contains({ 49 | origins: [youTubeHostname], 50 | })) as unknown as boolean; 51 | 52 | // @ts-ignore 53 | const improvePerformancePermission = (await runtime.permissions.contains({ 54 | origins: [allHostname], 55 | })) as unknown as boolean; 56 | 57 | if (automaticPermission === false) { 58 | await runtime.declarativeNetRequest.updateEnabledRulesets({ 59 | disableRulesetIds: ['shorts'], 60 | }); 61 | } 62 | 63 | await runtime.storage.sync.set({ 64 | [automaticStorageKey]: automaticPermission, 65 | [changeLinksStorageKey]: improvePerformancePermission, 66 | }); 67 | }); 68 | 69 | // Listener for new pages with the same URL 70 | runtime.webNavigation.onCommitted.addListener( 71 | async (details) => { 72 | if (details.frameId === 0) { 73 | const tab = await runtime.tabs.get(details.tabId); 74 | 75 | if (details.url === tab.url) { 76 | await handlePageUpdate(details.tabId, tab); 77 | } 78 | } 79 | }, 80 | { 81 | url: [ 82 | { 83 | schemes: ['http', 'https'], 84 | }, 85 | ], 86 | }, 87 | ); 88 | 89 | // Listener for new pages with new URLs 90 | runtime.webNavigation.onHistoryStateUpdated.addListener( 91 | async (details) => { 92 | if (details.frameId === 0) { 93 | const tab = await runtime.tabs.get(details.tabId); 94 | 95 | if (details.url === tab.url) { 96 | await handlePageUpdate(details.tabId, tab); 97 | } 98 | } 99 | }, 100 | { 101 | url: [ 102 | { 103 | schemes: ['http', 'https'], 104 | }, 105 | ], 106 | }, 107 | ); 108 | -------------------------------------------------------------------------------- /src/vendors/common/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 |
14 |
15 | 20 | 21 | 22 |
23 | 33 |
36 |
37 |
38 | 69 |
72 | 76 |
77 |
78 |
79 |
80 |
81 | 82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 |
95 | 100 |
101 |
102 |
103 |
104 |
105 | 106 |
107 |
108 | 109 | 110 | --------------------------------------------------------------------------------