├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── release.yml │ ├── take-action.yml │ ├── tests.yml │ ├── update-oss-attribution.yml │ └── updateInvidous.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── LICENSE ├── LICENSE-APPSTORE.txt ├── LICENSE-HISTORY.txt ├── README.md ├── ci ├── generateList.ts ├── invidiousCI.ts ├── invidiousType.ts ├── invidiouslist.json ├── pipedCI.ts └── prettify.ts ├── config.json.example ├── crowdin.yml ├── jest.config.js ├── manifest ├── beta-manifest-extra.json ├── chrome-manifest-extra.json ├── firefox-beta-manifest-extra.json ├── firefox-manifest-extra.json ├── manifest-v2-extra.json ├── manifest.json └── safari-manifest-extra.json ├── oss-attribution └── licenseInfos.json ├── package-lock.json ├── package.json ├── public ├── content.css ├── help │ ├── images │ │ ├── popup.png │ │ └── voting on notice.gif │ ├── index.html │ └── styles.css ├── icons │ ├── IconSponsorBlocker1024px.png │ ├── IconSponsorBlocker128px.png │ ├── IconSponsorBlocker16px.png │ ├── IconSponsorBlocker256px.png │ ├── IconSponsorBlocker32px.png │ ├── IconSponsorBlocker512px.png │ ├── IconSponsorBlocker64px.png │ ├── LogoSponsorBlocker1024px.png │ ├── LogoSponsorBlocker128px.png │ ├── LogoSponsorBlocker256px.png │ ├── LogoSponsorBlocker512px.png │ ├── LogoSponsorBlocker64px.png │ ├── PlayerCancelSegmentIconSponsorBlocker.svg │ ├── PlayerDeleteIconSponsorBlocker.svg │ ├── PlayerInfoIconSponsorBlocker.svg │ ├── PlayerStartIconSponsorBlocker.svg │ ├── PlayerStopIconSponsorBlocker.svg │ ├── PlayerUploadFailedIconSponsorBlocker.svg │ ├── PlayerUploadIconSponsorBlocker.svg │ ├── SafariIconSponsorBlocker128px.png │ ├── SafariIconSponsorBlocker16px.png │ ├── SafariIconSponsorBlocker32px.png │ ├── SafariIconSponsorBlocker64px.png │ ├── beep.oga │ ├── bolt.svg │ ├── campaign.svg │ ├── check-smaller.svg │ ├── check.svg │ ├── clipboard.svg │ ├── close-smaller.svg │ ├── close.png │ ├── dearrow.svg │ ├── downvote.png │ ├── export.svg │ ├── heart.svg │ ├── help.svg │ ├── import.svg │ ├── lightbulb.svg │ ├── loop.svg │ ├── looped.svg │ ├── money.svg │ ├── music-note.svg │ ├── newprofilepic.jpg │ ├── not_visible.svg │ ├── pause.svg │ ├── pencil.svg │ ├── refresh.svg │ ├── report.png │ ├── right-arrow.svg │ ├── segway.png │ ├── settings.svg │ ├── skip.svg │ ├── skipIcon.svg │ ├── sort.svg │ ├── star.svg │ ├── stop.svg │ ├── stopwatch.svg │ ├── thumb.svg │ ├── thumbs_down.svg │ ├── thumbs_down_locked.svg │ ├── thumbs_up.svg │ ├── upvote.png │ ├── upvote.svg │ └── visible.svg ├── libs │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBduz8A.woff2 │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2 │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBduz8A.woff2 │ ├── 6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRduz8A.woff2 │ └── Source+Sans+Pro.css ├── options │ ├── options.css │ └── options.html ├── oss-attribution │ └── attribution.txt ├── permissions │ ├── index.html │ └── styles.css ├── popup-old.html ├── popup.css ├── popup.html ├── res │ └── countries.json └── shared.css ├── src ├── background.ts ├── components │ ├── CategoryPillComponent.tsx │ ├── ChapterVoteComponent.tsx │ ├── NoticeComponent.tsx │ ├── NoticeTextSectionComponent.tsx │ ├── SelectorComponent.tsx │ ├── SkipNoticeComponent.tsx │ ├── SponsorTimeEditComponent.tsx │ ├── SubmissionNoticeComponent.tsx │ └── options │ │ ├── CategoryChooserComponent.tsx │ │ ├── CategorySkipOptionsComponent.tsx │ │ ├── KeybindComponent.tsx │ │ ├── KeybindDialogComponent.tsx │ │ ├── ToggleOptionComponent.tsx │ │ ├── UnsubmittedVideoListComponent.tsx │ │ ├── UnsubmittedVideoListItem.tsx │ │ └── UnsubmittedVideosComponent.tsx ├── config.ts ├── content.ts ├── dearrowPromotion.ts ├── document.ts ├── globals.d.ts ├── help.ts ├── js-components │ ├── previewBar.ts │ └── skipButtonControlBar.ts ├── messageTypes.ts ├── options.ts ├── permissions.ts ├── popup.ts ├── popup │ ├── PopupComponent.tsx │ ├── SegmentListComponent.tsx │ ├── SegmentSubmissionComponent.tsx │ ├── YourWorkComponent.tsx │ ├── popup.tsx │ └── popupUtils.ts ├── render │ ├── CategoryChooser.tsx │ ├── CategoryPill.tsx │ ├── ChapterVote.tsx │ ├── GenericNotice.tsx │ ├── RectangleTooltip.tsx │ ├── SkipNotice.tsx │ ├── SubmissionNotice.tsx │ ├── Tooltip.tsx │ ├── UnsubmittedVideos.tsx │ └── UpcomingNotice.tsx ├── svg-icons │ ├── checkIcon.tsx │ ├── clipboardIcon.tsx │ ├── lock_svg.tsx │ ├── pencilIcon.tsx │ ├── pencil_svg.tsx │ ├── sb_svg.tsx │ ├── thumbs_down_svg.tsx │ └── thumbs_up_svg.tsx ├── types.ts ├── utils.ts └── utils │ ├── arrayUtils.ts │ ├── categoryUtils.ts │ ├── compatibility.ts │ ├── configUtils.ts │ ├── constants.ts │ ├── crossExtension.ts │ ├── exporter.ts │ ├── genericUtils.ts │ ├── logger.ts │ ├── mobileUtils.ts │ ├── noticeUtils.ts │ ├── pageCleaner.ts │ ├── pageUtils.ts │ ├── requests.ts │ ├── segmentData.ts │ ├── thumbnails.ts │ ├── urlParser.ts │ ├── videoLabels.ts │ └── warnings.ts ├── test ├── exporter.test.ts ├── previewBar.test.ts ├── selenium.test.ts └── urlParser.test.ts ├── tsconfig-production.json ├── tsconfig.json └── webpack ├── configDiffPlugin.js ├── webpack.common.js ├── webpack.dev.js ├── webpack.manifest.js └── webpack.prod.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.{js,json,ts,tsx}] 12 | charset = utf-8 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [package.json] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": 12, 19 | "sourceType": "module" 20 | }, 21 | "plugins": ["react", "@typescript-eslint"], 22 | "rules": { 23 | "@typescript-eslint/no-unused-vars": "error", 24 | "no-self-assign": "off", 25 | "@typescript-eslint/no-empty-interface": "off", 26 | "react/prop-types": [2, { "ignore": ["children"] }], 27 | "@typescript-eslint/member-delimiter-style": "warn", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/ban-ts-comment": "off", 30 | "@typescript-eslint/no-this-alias": "off" 31 | }, 32 | "settings": { 33 | "react": { 34 | "version": "detect" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ajayyy-org 2 | patreon: ajayyy 3 | custom: [sponsor.ajay.app/donate] 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | - [ ] I agree to license my contribution under GPL-3.0 and agree to allow distribution on app stores as outlined in [LICENSE-APPSTORE](https://github.com/ajayyy/SponsorBlock/blob/master/LICENSE-APPSTORE.txt) 2 | 3 | To test this pull request, follow the [instructions in the wiki](https://github.com/ajayyy/SponsorBlock/wiki/Testing-a-Pull-Request). 4 | 5 | *** 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | name: Create artifacts 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | # Initialization 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: recursive 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '18' 19 | - run: npm ci 20 | - name: Copy configuration 21 | run: cp config.json.example config.json 22 | 23 | # Run linter 24 | - name: Lint 25 | run: npm run lint 26 | 27 | # Create Chrome artifacts 28 | - name: Create Chrome artifacts 29 | run: npm run build:chrome 30 | - uses: actions/upload-artifact@v4 31 | with: 32 | name: ChromeExtension 33 | path: dist 34 | - run: mkdir ./builds 35 | - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 36 | with: 37 | args: zip -qq -r ./builds/ChromeExtension.zip ./dist 38 | 39 | # Create Firefox artifacts 40 | - name: Create Firefox artifacts 41 | run: npm run build:firefox 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: FirefoxExtension 45 | path: dist 46 | - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 47 | with: 48 | args: zip -qq -r ./builds/FirefoxExtension.zip ./dist 49 | 50 | # Create Beta artifacts (Builds with the name changed to beta) 51 | - name: Create Chrome Beta artifacts 52 | run: npm run build:chrome -- --env stream=beta 53 | - uses: actions/upload-artifact@v4 54 | with: 55 | name: ChromeExtensionBeta 56 | path: dist 57 | - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 58 | with: 59 | args: zip -qq -r ./builds/ChromeExtensionBeta.zip ./dist 60 | 61 | - name: Create Firefox Beta artifacts 62 | run: npm run build:firefox -- --env stream=beta 63 | - uses: actions/upload-artifact@v4 64 | with: 65 | name: FirefoxExtensionBeta 66 | path: dist 67 | - uses: montudor/action-zip@0852c26906e00f8a315c704958823928d8018b28 68 | with: 69 | args: zip -qq -r ./builds/FirefoxExtensionBeta.zip ./dist 70 | 71 | -------------------------------------------------------------------------------- /.github/workflows/take-action.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/take.yml 2 | name: Assign issue to contributor 3 | on: 4 | issue_comment: 5 | 6 | jobs: 7 | assign: 8 | name: Take an issue 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: take the issue 12 | uses: bdougie/take-action@28b86cd8d25593f037406ecbf96082db2836e928 13 | env: 14 | GITHUB_TOKEN: ${{ github.token }} 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run tests 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | # Initialization 12 | - uses: actions/checkout@v4 13 | with: 14 | submodules: recursive 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: '18' 18 | - run: npm ci 19 | - run: sudo apt-get install chromium-chromedriver 20 | 21 | - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce 22 | with: 23 | chrome-version: 135 24 | install-dependencies: true 25 | install-chromedriver: true 26 | 27 | - name: Copy configuration 28 | run: cp config.json.example config.json 29 | 30 | - name: Set up WireGuard Connection 31 | uses: niklaskeerl/easy-wireguard-action@50341d5f4b8245ff3a90e278aca67b2d283c78d0 32 | with: 33 | WG_CONFIG_FILE: ${{ secrets.WG_CONFIG_FILE }} 34 | 35 | - name: Run tests 36 | run: npm run test 37 | 38 | - name: Upload results on fail 39 | if: ${{ failure() }} 40 | uses: actions/upload-artifact@v4 41 | with: 42 | name: Test Results 43 | path: ./test-results -------------------------------------------------------------------------------- /.github/workflows/update-oss-attribution.yml: -------------------------------------------------------------------------------- 1 | name: update oss attributions 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'package.json' 8 | - 'package-lock.json' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-oss: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: '18' 21 | - name: Install and generate attribution 22 | run: | 23 | npm ci 24 | npm i -g oss-attribution-generator 25 | generate-attribution 26 | mv ./oss-attribution/attribution.txt ./public/oss-attribution/attribution.txt 27 | - name: Prettify attributions 28 | run: | 29 | cd ci && npx ts-node prettify.ts 30 | 31 | - name: Create pull request to update list 32 | uses: peter-evans/create-pull-request@v7 33 | # v4.2.3 34 | with: 35 | commit-message: Update OSS Attribution 36 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 37 | branch: ci/oss_attribution 38 | title: Update OSS Attribution 39 | body: Automated OSS Attribution update 40 | -------------------------------------------------------------------------------- /.github/workflows/updateInvidous.yml: -------------------------------------------------------------------------------- 1 | name: update invidious 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 1 * *' # check every month 6 | 7 | jobs: 8 | check-list: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | submodules: recursive 14 | - name: Download instance lists 15 | run: | 16 | wget https://api.invidious.io/instances.json -O ci/invidious_instances.json 17 | wget https://github.com/TeamPiped/piped-uptime/raw/master/history/summary.json -O ci/piped_instances.json 18 | - name: Install dependencies 19 | run: npm ci 20 | - name: "Run CI" 21 | run: npm run ci:invidious 22 | 23 | - name: Create pull request to update list 24 | uses: peter-evans/create-pull-request@v7 25 | # v4.2.3 26 | with: 27 | commit-message: Update Invidious List 28 | author: github-actions[bot] 29 | branch: ci/update_invidious_list 30 | title: Update Invidious List 31 | body: Automated Invidious list update -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | ignored 3 | .idea/ 4 | node_modules 5 | web-ext-artifacts 6 | .vscode/ 7 | dist/ 8 | tmp/ 9 | .DS_Store 10 | ci/invidious_instances.json 11 | ci/piped_instances.json 12 | test-results -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "public/_locales"] 2 | path = public/_locales 3 | url = https://github.com/ajayyy/ExtensionTranslations 4 | [submodule "maze-utils"] 5 | path = maze-utils 6 | url = https://github.com/ajayyy/maze-utils 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | If you make any contributions to SponsorBlock after this file was created, you are agreeing that any code you have contributed will be licensed under GPL-3.0 and agree to allow distribution on app stores as outlined in LICENSE-APPSTORE. 2 | 3 | # Translations 4 | https://crowdin.com/project/sponsorblock 5 | 6 | # Building 7 | ## Building locally 8 | 0. You must have [Node.js 16 or later](https://nodejs.org/) and npm installed. Works best on Linux 9 | 1. Clone with submodules 10 | ```bash 11 | git clone --recursive https://github.com/ajayyy/SponsorBlock 12 | ``` 13 | Or if you already cloned it, pull submodules with 14 | ```bash 15 | git submodule update --init --recursive 16 | ``` 17 | 2. Copy the file `config.json.example` to `config.json` and adjust configuration as desired. 18 | - Comments are invalid in JSON, make sure they are all removed. 19 | - You will need to repeat this step in the future if you get build errors related to `CompileConfig` or `property does not exist on type ConfigClass`. This can happen for example when a new category is added. 20 | 3. Run `npm ci` in the repository to install dependencies. 21 | 4. Run `npm run build:dev` (for Chrome) or `npm run build:dev:firefox` (for Firefox) to generate a development version of the extension with source maps. 22 | - You can also run `npm run build` (for Chrome) or `npm run build:firefox` (for Firefox) to generate a production build. 23 | 5. The built extension is now in `dist/`. You can load this folder directly in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest), or convert it to a zip file to load it as a [temporary extension](https://developer.mozilla.org/docs/Tools/about:debugging#loading_a_temporary_extension) in Firefox. 24 | 25 | ## Developing with a clean profile and hot reloading 26 | Run `npm run dev` (for Chrome) or `npm run dev:firefox` (for Firefox) to run the extension using a clean browser profile with hot reloading. This uses [`web-ext run`](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#commands). 27 | 28 | Known chromium bug: Extension is not loaded properly on first start. Visit `chrome://extensions/` and reload the extension. 29 | 30 | For Firefox for Android, use `npm run dev:firefox-android -- --adb-device `. See the [Firefox documentation](https://extensionworkshop.com/documentation/develop/developing-extensions-for-firefox-for-android/#debug-your-extension) for more information. You may need to edit package.json and add the parameters directly there. 31 | 32 | -------------------------------------------------------------------------------- /LICENSE-APPSTORE.txt: -------------------------------------------------------------------------------- 1 | The developers are aware that the terms of service that 2 | apply to apps distributed via Apple's App Store services and similar app stores may conflict 3 | with rights granted under the SponsorBlock license, the GNU General 4 | Public License, version 3. The copyright holders of the SponsorBlock 5 | project do not wish this conflict to prevent the otherwise-compliant 6 | distribution of derived apps via the App Store and similar app stores. 7 | Therefore, we have committed not to pursue any license 8 | violation that results solely from the conflict between the GNU GPLv3 9 | and the Apple App Store terms of service or similar app stores. In 10 | other words, as long as you comply with the GPL in all other respects, 11 | including its requirements to provide users with source code and the 12 | text of the license, we will not object to your distribution of the 13 | SponsorBlock project through the App Store. -------------------------------------------------------------------------------- /ci/generateList.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file is only ran by GitHub Actions in order to populate the Invidious instances list 3 | 4 | This file should not be shipped with the extension 5 | */ 6 | 7 | /* 8 | Criteria for inclusion: 9 | Invidious 10 | - uptime >= 80% 11 | - must have been up for at least 90 days 12 | - HTTPS only 13 | - url includes name (this is to avoid redirects) 14 | 15 | Piped 16 | - 30d uptime >= 90% 17 | - available for at least 80/90 days 18 | - must have been up for at least 90 days 19 | - must not be a wildcard redirect to piped.video 20 | - must be currently up 21 | - must have a functioning frontend 22 | - must have a functioning API 23 | */ 24 | 25 | import { writeFile, existsSync } from "fs" 26 | import { join } from "path" 27 | import { getInvidiousList } from "./invidiousCI"; 28 | // import { getPipedList } from "./pipedCI"; 29 | 30 | const checkPath = (path: string) => existsSync(path); 31 | const fixArray = (arr: string[]) => [...new Set(arr)].sort() 32 | 33 | async function generateList() { 34 | // import file from https://api.invidious.io/instances.json 35 | const invidiousPath = join(__dirname, "invidious_instances.json"); 36 | // import file from https://github.com/TeamPiped/piped-uptime/raw/master/history/summary.json 37 | const pipedPath = join(__dirname, "piped_instances.json"); 38 | 39 | // check if files exist 40 | if (!checkPath(invidiousPath) || !checkPath(pipedPath)) { 41 | console.log("Missing files") 42 | process.exit(1); 43 | } 44 | 45 | // static non-invidious instances 46 | const staticInstances = ["www.youtubekids.com"]; 47 | // invidious instances 48 | const invidiousList = fixArray(getInvidiousList()) 49 | // piped instnaces 50 | // const pipedList = fixArray(await getPipedList()) 51 | 52 | console.log([...staticInstances, ...invidiousList]) 53 | 54 | writeFile( 55 | join(__dirname, "./invidiouslist.json"), 56 | JSON.stringify([...staticInstances, ...invidiousList]), 57 | (err) => { 58 | if (err) return console.log(err); 59 | } 60 | ); 61 | } 62 | generateList() 63 | -------------------------------------------------------------------------------- /ci/invidiousCI.ts: -------------------------------------------------------------------------------- 1 | import { InvidiousInstance, monitor } from "./invidiousType" 2 | 3 | import * as data from "../ci/invidious_instances.json"; 4 | 5 | // only https servers 6 | const mapped = (data as InvidiousInstance[]) 7 | .filter((i) => 8 | i[1]?.type === "https" 9 | && i[1]?.monitor?.enabled 10 | ) 11 | .map((instance) => { 12 | const monitor = instance[1].monitor as monitor; 13 | return { 14 | name: instance[0], 15 | url: instance[1].uri, 16 | uptime: monitor.uptime || 0, 17 | down: monitor.down ?? false, 18 | created_at: monitor.created_at, 19 | } 20 | }); 21 | 22 | // reliability and sanity checks 23 | const reliableCheck = mapped 24 | .filter(instance => { 25 | const uptime = instance.uptime > 80 && !instance.down; 26 | const nameIncluded = instance.url.includes(instance.name); 27 | const ninetyDays = 90 * 24 * 60 * 60 * 1000; 28 | const ninetyDaysAgo = new Date(Date.now() - ninetyDays); 29 | const createdAt = new Date(instance.created_at).getTime() < ninetyDaysAgo.getTime(); 30 | return uptime && nameIncluded && createdAt; 31 | }) 32 | 33 | export const getInvidiousList = (): string[] => 34 | reliableCheck.map(instance => instance.name).sort() -------------------------------------------------------------------------------- /ci/invidiousType.ts: -------------------------------------------------------------------------------- 1 | export type InvidiousInstance = [ 2 | string, 3 | { 4 | flag: string; 5 | region: string; 6 | stats: null | ivStats; 7 | cors: null | boolean; 8 | api: null | boolean; 9 | type: "https" | "http" | "onion" | "i2p"; 10 | uri: string; 11 | monitor: null | monitor; 12 | } 13 | ] 14 | 15 | export type monitor = { 16 | token: string; 17 | url: string; 18 | alias: string; 19 | last_status: number; 20 | uptime: number; 21 | down: boolean; 22 | down_since: null | string; 23 | up_since: null | string; 24 | error: null | string; 25 | period: number; 26 | apdex_t: number; 27 | string_match: string; 28 | enabled: boolean; 29 | published: boolean; 30 | disabled_locations: string[]; 31 | recipients: string[]; 32 | last_check_at: string; 33 | next_check_at: string; 34 | created_at: string; 35 | mute_until: null | string; 36 | favicon_url: string; 37 | custom_headers: Record; 38 | http_verb: string; 39 | http_body: string; 40 | ssl: { 41 | tested_at: string; 42 | expires_at: string; 43 | valid: boolean; 44 | error: null | string; 45 | }; 46 | } 47 | 48 | export type ivStats = { 49 | version: string; 50 | software: { 51 | name: "invidious" | string; 52 | version: string; 53 | branch: "master" | string; 54 | }; 55 | openRegistrations: boolean; 56 | usage: { 57 | users: { 58 | total: number; 59 | activeHalfyear: number; 60 | activeMonth: number; 61 | }; 62 | }; 63 | metadata: { 64 | updatedAt: number; 65 | lastChannelRefreshedAt: number; 66 | }; 67 | playback: { 68 | totalRequests: number; 69 | successfulRequests: number; 70 | ratio: number; 71 | }; 72 | } -------------------------------------------------------------------------------- /ci/invidiouslist.json: -------------------------------------------------------------------------------- 1 | ["www.youtubekids.com","inv.nadeko.net","inv.tux.pizza","invidious.adminforge.de","invidious.jing.rocks","invidious.nerdvpn.de","invidious.perennialte.ch","invidious.privacyredirect.com","invidious.reallyaweso.me","invidious.yourdevice.ch","iv.ggtyler.dev","iv.nboeck.de","yewtu.be"] -------------------------------------------------------------------------------- /ci/pipedCI.ts: -------------------------------------------------------------------------------- 1 | import * as data from "../ci/piped_instances.json"; 2 | 3 | type percent = string 4 | type dailyMinutesDown = Record 5 | 6 | type PipedInstance = { 7 | name: string; 8 | url: string; 9 | icon: string; 10 | slug: string; 11 | status: string; 12 | uptime: percent; 13 | uptimeDay: percent; 14 | uptimeWeek: percent; 15 | uptimeMonth: percent; 16 | uptimeYear: percent; 17 | time: number; 18 | timeDay: number; 19 | timeWeek: number; 20 | timeMonth: number; 21 | timeYear: number; 22 | dailyMinutesDown: dailyMinutesDown 23 | } 24 | 25 | const percentNumber = (percent: percent) => Number(percent.replace("%", "")) 26 | const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) 27 | 28 | function dailyMinuteFilter (dailyMinutesDown: dailyMinutesDown) { 29 | let daysDown = 0 30 | for (const [date, minsDown] of Object.entries(dailyMinutesDown)) { 31 | if (new Date(date) >= ninetyDaysAgo && minsDown > 1000) { // if within 90 days and down for more than 1000 minutes 32 | daysDown++ 33 | } 34 | } 35 | // return true f less than 10 days down 36 | return daysDown < 10 37 | } 38 | 39 | const getHost = (url: string) => new URL(url).host 40 | 41 | const getWatchPage = async (instance: PipedInstance) => 42 | fetch(`https://${getHost(instance.url)}`, { redirect: "manual" }) 43 | .then(res => res.headers.get("Location")) 44 | .catch(e => { console.log (e); return null }) 45 | 46 | const siteOK = async (instance) => { 47 | // check if entire site is redirect 48 | const notRedirect = await fetch(instance.url, { redirect: "manual" }) 49 | .then(res => res.status == 200) 50 | // only allow kavin to return piped.video 51 | // if (instance.url.startsWith("https://piped.video") && instance.slug !== "kavin-rocks-official") return false 52 | // check if frontend is OK 53 | const watchPageStatus = await fetch(instance.frontendUrl) 54 | .then(res => res.ok) 55 | // test API - stream returns ok result 56 | const streamStatus = await fetch(`${instance.apiUrl}/streams/BaW_jenozKc`) 57 | .then(res => res.ok) 58 | // get startTime of monitor 59 | const age = await fetch(instance.historyUrl) 60 | .then(res => res.text()) 61 | .then(text => { // startTime greater than 90 days ago 62 | const date = text.match(/startTime: (.+)/)[1] 63 | return Date.parse(date) < ninetyDaysAgo.valueOf() 64 | }) 65 | // console.log(notRedirect, watchPageStatus, streamStatus, age, instance.frontendUrl, instance.apiUrl) 66 | return notRedirect && watchPageStatus && streamStatus && age 67 | } 68 | 69 | const staticFilters = (data as PipedInstance[]) 70 | .filter(instance => { 71 | const isup = instance.status === "up" 72 | const monthCheck = percentNumber(instance.uptimeMonth) >= 90 73 | const dailyMinuteCheck = dailyMinuteFilter(instance.dailyMinutesDown) 74 | return isup && monthCheck && dailyMinuteCheck 75 | }) 76 | .map(async instance => { 77 | // get frontend url 78 | const frontendUrl = await getWatchPage(instance) 79 | if (!frontendUrl) return null // return false if frontend doesn't resolve 80 | // get api base 81 | const apiUrl = instance.url.replace("/healthcheck", "") 82 | const historyUrl = `https://raw.githubusercontent.com/TeamPiped/piped-uptime/master/history/${instance.slug}.yml` 83 | const pass = await siteOK({ apiUrl, historyUrl, frontendUrl, url: instance.url }) 84 | const frontendHost = getHost(frontendUrl) 85 | return pass ? frontendHost : null 86 | }) 87 | 88 | export async function getPipedList(): Promise { 89 | const instances = await Promise.all(staticFilters) 90 | .then(arr => arr.filter(i => i !== null)) 91 | return instances 92 | } 93 | -------------------------------------------------------------------------------- /ci/prettify.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs'; 2 | 3 | import * as license from "../oss-attribution/licenseInfos.json"; 4 | 5 | const result = JSON.stringify(license, null, 2); 6 | writeFile("../oss-attribution/licenseInfos.json", result, err => { if (err) return console.log(err) } ); -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "serverAddress": "https://sponsor.ajay.app", 3 | "testingServerAddress": "https://sponsor.ajay.app/test", 4 | "serverAddressComment": "This specifies the default SponsorBlock server to connect to", 5 | "categoryList": ["sponsor", "selfpromo", "exclusive_access", "interaction", "poi_highlight", "intro", "outro", "preview", "filler", "chapter", "music_offtopic"], 6 | "categorySupport": { 7 | "sponsor": ["skip", "mute", "full"], 8 | "selfpromo": ["skip", "mute", "full"], 9 | "exclusive_access": ["full"], 10 | "interaction": ["skip", "mute"], 11 | "intro": ["skip", "mute"], 12 | "outro": ["skip", "mute"], 13 | "preview": ["skip", "mute"], 14 | "filler": ["skip", "mute"], 15 | "music_offtopic": ["skip"], 16 | "poi_highlight": ["poi"], 17 | "chapter": ["chapter"] 18 | }, 19 | "wikiLinks": { 20 | "sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor", 21 | "selfpromo": "https://wiki.sponsor.ajay.app/w/Unpaid/Self_Promotion", 22 | "exclusive_access": "https://wiki.sponsor.ajay.app/w/Exclusive_Access", 23 | "interaction": "https://wiki.sponsor.ajay.app/w/Interaction_Reminder_(Subscribe)", 24 | "intro": "https://wiki.sponsor.ajay.app/w/Intermission/Intro_Animation", 25 | "outro": "https://wiki.sponsor.ajay.app/w/Endcards/Credits", 26 | "preview": "https://wiki.sponsor.ajay.app/w/Preview/Recap", 27 | "filler": "https://wiki.sponsor.ajay.app/w/Tangents/Jokes", 28 | "music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section", 29 | "poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight", 30 | "guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines", 31 | "mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment", 32 | "chapter": "https://wiki.sponsor.ajay.app/w/Chapter" 33 | }, 34 | "extensionImportList": { 35 | "chromium": [ 36 | "enamippconapkdmgfgjchkhakpfinmaj" 37 | ], 38 | "firefox": [ 39 | "deArrow@ajay.app", 40 | "deArrowBETA@ajay.app" 41 | ], 42 | "safari": [ 43 | "app.ajay.dearrow.extension" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /public/_locales/en/* 3 | translation: /public/_locales/%two_letters_code%/%original_file_name% 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "test" 4 | ], 5 | "transform": { 6 | "^.+\\.ts$": "ts-jest" 7 | }, 8 | "reporters": ["default", "github-actions"] 9 | }; 10 | -------------------------------------------------------------------------------- /manifest/beta-manifest-extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BETA - SponsorBlock" 3 | } 4 | -------------------------------------------------------------------------------- /manifest/firefox-beta-manifest-extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser_specific_settings": { 3 | "gecko": { 4 | "id": "sponsorBlockerBETA@ajay.app" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /manifest/firefox-manifest-extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser_specific_settings": { 3 | "gecko": { 4 | "id": "sponsorBlocker@ajay.app", 5 | "strict_min_version": "102.0" 6 | }, 7 | "gecko_android": { 8 | "strict_min_version": "113.0" 9 | } 10 | }, 11 | "background": { 12 | "persistent": false 13 | }, 14 | "browser_action": { 15 | "default_area": "navbar" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /manifest/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_fullName__", 3 | "short_name": "SponsorBlock", 4 | "version": "5.12.4", 5 | "default_locale": "en", 6 | "description": "__MSG_Description__", 7 | "homepage_url": "https://sponsor.ajay.app", 8 | "icons": { 9 | "16": "icons/IconSponsorBlocker16px.png", 10 | "32": "icons/IconSponsorBlocker32px.png", 11 | "64": "icons/IconSponsorBlocker64px.png", 12 | "128": "icons/IconSponsorBlocker128px.png", 13 | "256": "icons/IconSponsorBlocker256px.png", 14 | "512": "icons/IconSponsorBlocker512px.png", 15 | "1024": "icons/IconSponsorBlocker1024px.png" 16 | }, 17 | "permissions": [ 18 | "storage", 19 | "scripting" 20 | ], 21 | "options_ui": { 22 | "page": "options/options.html", 23 | "open_in_tab": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /manifest/safari-manifest-extra.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "persistent": false 4 | }, 5 | "optional_permissions": [ 6 | "webNavigation" 7 | ], 8 | "browser_action": { 9 | "default_icon": { 10 | "16": "icons/SafariIconSponsorBlocker16px.png", 11 | "32": "icons/SafariIconSponsorBlocker32px.png", 12 | "64": "icons/SafariIconSponsorBlocker64px.png", 13 | "128": "icons/SafariIconSponsorBlocker128px.png" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sponsorblock", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "background.js", 6 | "dependencies": { 7 | "content-scripts-register-polyfill": "^4.0.2", 8 | "react": "^18.2.0", 9 | "react-dom": "^18.2.0" 10 | }, 11 | "overrides": { 12 | "content-scripts-register-polyfill": { 13 | "webext-content-scripts": "v2.5.5" 14 | } 15 | }, 16 | "devDependencies": { 17 | "@types/chrome": "^0.0.220", 18 | "@types/firefox-webext-browser": "^111.0.0", 19 | "@types/jest": "^29.4.0", 20 | "@types/react": "^18.0.28", 21 | "@types/react-dom": "^18.0.11", 22 | "@types/selenium-webdriver": "^4.1.13", 23 | "@types/wicg-mediasession": "^1.1.4", 24 | "@typescript-eslint/eslint-plugin": "^5.54.1", 25 | "@typescript-eslint/parser": "^5.54.1", 26 | "chromedriver": "^135.0.0", 27 | "concurrently": "^7.6.0", 28 | "copy-webpack-plugin": "^11.0.0", 29 | "eslint": "^8.35.0", 30 | "eslint-plugin-react": "^7.32.2", 31 | "fork-ts-checker-webpack-plugin": "^7.3.0", 32 | "jest": "^29.5.0", 33 | "jest-environment-jsdom": "^29.5.0", 34 | "rimraf": "^4.3.1", 35 | "schema-utils": "^4.0.0", 36 | "selenium-webdriver": "^4.8.1", 37 | "ts-jest": "^29.0.5", 38 | "ts-loader": "^9.4.2", 39 | "ts-node": "^10.9.1", 40 | "typescript": "4.9", 41 | "web-ext": "^8.2.0", 42 | "webpack": "^5.94.0", 43 | "webpack-cli": "^4.10.0", 44 | "webpack-merge": "^5.8.0" 45 | }, 46 | "scripts": { 47 | "web-run": "npm run web-run:chrome", 48 | "web-sign": "web-ext sign --channel unlisted -s dist", 49 | "web-run:firefox": "cd dist && web-ext run --start-url https://addons.mozilla.org/firefox/addon/ublock-origin/", 50 | "web-run:firefox-android": "cd dist && web-ext run -t firefox-android --firefox-apk org.mozilla.fenix", 51 | "web-run:chrome": "cd dist && web-ext run --start-url https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm -t chromium", 52 | "build": "npm run build:chrome", 53 | "build:chrome": "webpack --env browser=chrome --config webpack/webpack.prod.js", 54 | "build:firefox": "webpack --env browser=firefox --config webpack/webpack.prod.js", 55 | "build:safari": "webpack --env browser=safari --config webpack/webpack.prod.js", 56 | "build:edge": "webpack --env browser=edge --config webpack/webpack.prod.js", 57 | "build:dev": "npm run build:dev:chrome", 58 | "build:dev:chrome": "webpack --env browser=chrome --config webpack/webpack.dev.js", 59 | "build:dev:firefox": "webpack --env browser=firefox --config webpack/webpack.dev.js", 60 | "build:watch": "npm run build:watch:chrome", 61 | "build:watch:chrome": "webpack --env browser=chrome --config webpack/webpack.dev.js --watch", 62 | "build:watch:firefox": "webpack --env browser=firefox --config webpack/webpack.dev.js --watch", 63 | "ci:invidious": "ts-node ci/generateList.ts", 64 | "dev": "npm run build:dev && concurrently \"npm run web-run\" \"npm run build:watch\"", 65 | "dev:firefox": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox\" \"npm run build:watch:firefox\"", 66 | "dev:firefox-android": "npm run build:dev:firefox && concurrently \"npm run web-run:firefox-android\" \"npm run build:watch:firefox\"", 67 | "clean": "rimraf dist", 68 | "test": "npm run build:chrome && npx jest", 69 | "test-without-building": "npx jest", 70 | "lint": "eslint src", 71 | "lint:fix": "eslint src --fix" 72 | }, 73 | "engines": { 74 | "node": ">=16" 75 | }, 76 | "funding": [ 77 | { 78 | "type": "individual", 79 | "url": "https://sponsor.ajay.app/donate" 80 | }, 81 | { 82 | "type": "github", 83 | "url": "https://github.com/sponsors/ajayyy-org" 84 | }, 85 | { 86 | "type": "patreon", 87 | "url": "https://www.patreon.com/ajayyy" 88 | }, 89 | { 90 | "type": "individual", 91 | "url": "https://paypal.me/ajayyy" 92 | } 93 | ], 94 | "repository": { 95 | "type": "git", 96 | "url": "git+https://github.com/ajayyy/SponsorBlock.git" 97 | }, 98 | "author": "Ajay Ramachandran", 99 | "license": "GPL-3.0", 100 | "private": true 101 | } 102 | -------------------------------------------------------------------------------- /public/help/images/popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/help/images/popup.png -------------------------------------------------------------------------------- /public/help/images/voting on notice.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/help/images/voting on notice.gif -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker1024px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker1024px.png -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker128px.png -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker16px.png -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker256px.png -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker32px.png -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker512px.png -------------------------------------------------------------------------------- /public/icons/IconSponsorBlocker64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/IconSponsorBlocker64px.png -------------------------------------------------------------------------------- /public/icons/LogoSponsorBlocker1024px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/LogoSponsorBlocker1024px.png -------------------------------------------------------------------------------- /public/icons/LogoSponsorBlocker128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/LogoSponsorBlocker128px.png -------------------------------------------------------------------------------- /public/icons/LogoSponsorBlocker256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/LogoSponsorBlocker256px.png -------------------------------------------------------------------------------- /public/icons/LogoSponsorBlocker512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/LogoSponsorBlocker512px.png -------------------------------------------------------------------------------- /public/icons/LogoSponsorBlocker64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/LogoSponsorBlocker64px.png -------------------------------------------------------------------------------- /public/icons/PlayerCancelSegmentIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | LogoSponsorBlocker2 27 | 28 | 29 | 30 | 51 | 53 | 55 | 56 | LogoSponsorBlocker2 58 | 64 | 68 | 69 | -------------------------------------------------------------------------------- /public/icons/PlayerDeleteIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | LogoSponsorBlocker2 27 | 28 | 29 | 30 | 50 | 52 | 54 | 55 | LogoSponsorBlocker2 57 | 63 | 67 | 71 | 74 | 78 | 82 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /public/icons/PlayerInfoIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/icons/PlayerStartIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 46 | 48 | 50 | 51 | LogoSponsorBlocker2 53 | 56 | 60 | 65 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/PlayerStopIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | LogoSponsorBlocker2 27 | 28 | 29 | 30 | 50 | 52 | 54 | 55 | LogoSponsorBlocker2 57 | 63 | 67 | 68 | -------------------------------------------------------------------------------- /public/icons/PlayerUploadFailedIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | LogoSponsorBlocker2 27 | 28 | 29 | 30 | 50 | 52 | 54 | 55 | LogoSponsorBlocker2 57 | 63 | 67 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/PlayerUploadIconSponsorBlocker.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | LogoSponsorBlocker2 27 | 28 | 29 | 30 | 50 | 52 | 54 | 55 | LogoSponsorBlocker2 57 | 63 | 67 | 68 | -------------------------------------------------------------------------------- /public/icons/SafariIconSponsorBlocker128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/SafariIconSponsorBlocker128px.png -------------------------------------------------------------------------------- /public/icons/SafariIconSponsorBlocker16px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/SafariIconSponsorBlocker16px.png -------------------------------------------------------------------------------- /public/icons/SafariIconSponsorBlocker32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/SafariIconSponsorBlocker32px.png -------------------------------------------------------------------------------- /public/icons/SafariIconSponsorBlocker64px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/SafariIconSponsorBlocker64px.png -------------------------------------------------------------------------------- /public/icons/beep.oga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/beep.oga -------------------------------------------------------------------------------- /public/icons/bolt.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/campaign.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/check-smaller.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 38 | 39 | -------------------------------------------------------------------------------- /public/icons/check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/close-smaller.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/close.png -------------------------------------------------------------------------------- /public/icons/dearrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 55 | -------------------------------------------------------------------------------- /public/icons/downvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/downvote.png -------------------------------------------------------------------------------- /public/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 36 | 40 | 44 | 48 | 49 | 53 | 54 | 58 | 59 | 63 | 64 | 68 | 69 | 73 | 74 | 78 | 79 | 83 | 84 | 88 | 89 | 93 | 94 | 98 | 99 | 103 | 104 | 108 | 109 | 113 | 114 | 118 | 119 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /public/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 35 | 39 | 43 | 44 | -------------------------------------------------------------------------------- /public/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 54 | 58 | 59 | -------------------------------------------------------------------------------- /public/icons/import.svg: -------------------------------------------------------------------------------- 1 | 2 | 36 | 38 | 42 | 47 | 48 | 50 | 51 | 53 | 54 | 56 | 57 | 59 | 60 | 62 | 63 | 65 | 66 | 68 | 69 | 71 | 72 | 74 | 75 | 77 | 78 | 80 | 81 | 83 | 84 | 86 | 87 | 89 | 90 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /public/icons/lightbulb.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/loop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/icons/looped.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/money.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/music-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/newprofilepic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/newprofilepic.jpg -------------------------------------------------------------------------------- /public/icons/not_visible.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 35 | 39 | 40 | -------------------------------------------------------------------------------- /public/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 31 | 51 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /public/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/report.png -------------------------------------------------------------------------------- /public/icons/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/segway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/segway.png -------------------------------------------------------------------------------- /public/icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/skip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/skipIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | LogoSponsorBlocker2 27 | 28 | 29 | 30 | 51 | 53 | 55 | 56 | LogoSponsorBlocker2 58 | 64 | 68 | 71 | 72 | -------------------------------------------------------------------------------- /public/icons/sort.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 31 | 51 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /public/icons/stopwatch.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 33 | 37 | 38 | -------------------------------------------------------------------------------- /public/icons/thumb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/thumbs_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 54 | 58 | 59 | -------------------------------------------------------------------------------- /public/icons/thumbs_down_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 54 | 58 | 59 | -------------------------------------------------------------------------------- /public/icons/thumbs_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 31 | 51 | 55 | 59 | 60 | -------------------------------------------------------------------------------- /public/icons/upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/icons/upvote.png -------------------------------------------------------------------------------- /public/icons/upvote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml 47 | 49 | 52 | 53 | 55 | 56 | 58 | 59 | 61 | 62 | 64 | 65 | 67 | 68 | 70 | 71 | 73 | 74 | 76 | 77 | 79 | 80 | 82 | 83 | 85 | 86 | 88 | 89 | 91 | 92 | 94 | 95 | 97 | 98 | -------------------------------------------------------------------------------- /public/icons/visible.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBduz8A.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlBduz8A.woff2 -------------------------------------------------------------------------------- /public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwlxdu.woff2 -------------------------------------------------------------------------------- /public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBduz8A.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmBduz8A.woff2 -------------------------------------------------------------------------------- /public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRduz8A.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajayyy/SponsorBlock/3734b61c81b11713130adf5d016028727f43a155/public/libs/6xKydSBYKcSV-LCoeQqfX1RYOo3ig4vwmRduz8A.woff2 -------------------------------------------------------------------------------- /public/permissions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Permissions - SponsorBlock 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | SponsorBlock 17 |
18 | 19 |
20 | 21 |
22 | __MSG_invidiousPermissionRefresh__ 23 |
24 | 25 |
26 | 27 |
28 |
29 | __MSG_acceptPermission__ 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/res/countries.json: -------------------------------------------------------------------------------- 1 | {"Albania":{"allowed":true},"Algeria":{"allowed":true},"Angola":{"allowed":true},"Argentina":{"allowed":true},"Armenia":{"allowed":true},"Australia":{"allowed":false},"Austria":{"allowed":false},"Azerbaijan":{"allowed":true},"Bangladesh":{"allowed":true},"Belarus":{"allowed":true},"Belgium":{"allowed":false},"Belize":{"allowed":true},"Benin":{"allowed":true},"Bhutan":{"allowed":true},"Bolivia":{"allowed":true},"Bosnia and Herzegovina":{"allowed":true},"Botswana":{"allowed":true},"Brazil":{"allowed":true},"Bulgaria":{"allowed":true},"Burkina Faso":{"allowed":true},"Burundi":{"allowed":true},"Cameroon":{"allowed":true},"Canada":{"allowed":false},"Central African Republic":{"allowed":true},"Chad":{"allowed":true},"Chile":{"allowed":true},"China":{"allowed":true},"Colombia":{"allowed":true},"Comoros":{"allowed":true},"Costa Rica":{"allowed":true},"Croatia":{"allowed":true},"Cyprus":{"allowed":false},"Czech Republic":{"allowed":false},"Denmark":{"allowed":false},"Djibouti":{"allowed":true},"Dominican Republic":{"allowed":true},"DR Congo":{"allowed":true},"Ecuador":{"allowed":true},"Egypt":{"allowed":true},"El Salvador":{"allowed":true},"Estonia":{"allowed":false},"Eswatini":{"allowed":true},"Ethiopia":{"allowed":true},"Fiji":{"allowed":true},"Finland":{"allowed":false},"France":{"allowed":false},"Gabon":{"allowed":true},"Gambia":{"allowed":true},"Georgia":{"allowed":true},"Germany":{"allowed":false},"Ghana":{"allowed":true},"Greece":{"allowed":true},"Guatemala":{"allowed":true},"Guinea":{"allowed":true},"Guinea-Bissau":{"allowed":true},"Guyana":{"allowed":true},"Haiti":{"allowed":true},"Honduras":{"allowed":true},"Hungary":{"allowed":true},"Iceland":{"allowed":false},"India":{"allowed":true},"Iran":{"allowed":true},"Iraq":{"allowed":true},"Ireland":{"allowed":false},"Israel":{"allowed":false},"Italy":{"allowed":false},"Ivory Coast":{"allowed":true},"Jamaica":{"allowed":true},"Japan":{"allowed":false},"Jordan":{"allowed":true},"Kazakhstan":{"allowed":true},"Kenya":{"allowed":true},"Kiribati":{"allowed":true},"Kyrgyzstan":{"allowed":true},"Laos":{"allowed":true},"Latvia":{"allowed":true},"Lebanon":{"allowed":true},"Lesotho":{"allowed":true},"Liberia":{"allowed":true},"Lithuania":{"allowed":true},"Luxembourg":{"allowed":false},"Madagascar":{"allowed":true},"Malawi":{"allowed":true},"Malaysia":{"allowed":true},"Maldives":{"allowed":true},"Mali":{"allowed":true},"Malta":{"allowed":false},"Mauritania":{"allowed":true},"Mauritius":{"allowed":true},"Mexico":{"allowed":true},"Micronesia":{"allowed":true},"Moldova":{"allowed":true},"Mongolia":{"allowed":true},"Montenegro":{"allowed":true},"Morocco":{"allowed":true},"Mozambique":{"allowed":true},"Myanmar":{"allowed":true},"Namibia":{"allowed":true},"Nepal":{"allowed":true},"Netherlands":{"allowed":false},"Nicaragua":{"allowed":true},"Niger":{"allowed":true},"Nigeria":{"allowed":true},"North Macedonia":{"allowed":true},"Norway":{"allowed":false},"Pakistan":{"allowed":true},"Panama":{"allowed":true},"Papua New Guinea":{"allowed":true},"Paraguay":{"allowed":true},"Peru":{"allowed":true},"Philippines":{"allowed":true},"Poland":{"allowed":true},"Portugal":{"allowed":true},"Republic of the Congo":{"allowed":true},"Romania":{"allowed":true},"Russia":{"allowed":true},"Rwanda":{"allowed":true},"Saint Lucia":{"allowed":true},"Samoa":{"allowed":true},"Sao Tome and Principe":{"allowed":true},"Senegal":{"allowed":true},"Serbia":{"allowed":true},"Seychelles":{"allowed":true},"Sierra Leone":{"allowed":true},"Slovakia":{"allowed":true},"Slovenia":{"allowed":false},"Solomon Islands":{"allowed":true},"South Africa":{"allowed":true},"South Korea":{"allowed":false},"South Sudan":{"allowed":true},"Spain":{"allowed":false},"Sri Lanka":{"allowed":true},"Sudan":{"allowed":true},"Suriname":{"allowed":true},"Sweden":{"allowed":false},"Switzerland":{"allowed":false},"Syria":{"allowed":true},"Taiwan":{"allowed":false},"Tajikistan":{"allowed":true},"Tanzania":{"allowed":true},"Thailand":{"allowed":true},"Timor-Leste":{"allowed":true},"Togo":{"allowed":true},"Tonga":{"allowed":true},"Trinidad and Tobago":{"allowed":true},"Tunisia":{"allowed":true},"Turkey":{"allowed":true},"Turkmenistan":{"allowed":true},"Tuvalu":{"allowed":true},"Uganda":{"allowed":true},"Ukraine":{"allowed":true},"United Arab Emirates":{"allowed":false},"United Kingdom":{"allowed":false},"United States":{"allowed":false},"Uruguay":{"allowed":true},"Uzbekistan":{"allowed":true},"Vanuatu":{"allowed":true},"Venezuela":{"allowed":true},"Vietnam":{"allowed":true},"Yemen":{"allowed":true},"Zambia":{"allowed":true},"Zimbabwe":{"allowed":true}} -------------------------------------------------------------------------------- /src/components/NoticeTextSectionComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface NoticeTextSelectionProps { 4 | icon?: string; 5 | text: string; 6 | idSuffix: string; 7 | onClick?: (event: React.MouseEvent) => unknown; 8 | children?: React.ReactNode; 9 | } 10 | 11 | export interface NoticeTextSelectionState { 12 | 13 | } 14 | 15 | class NoticeTextSelectionComponent extends React.Component { 16 | 17 | constructor(props: NoticeTextSelectionProps) { 18 | super(props); 19 | } 20 | 21 | render(): React.ReactElement { 22 | const style: React.CSSProperties = {}; 23 | if (this.props.onClick) { 24 | style.cursor = "pointer"; 25 | style.textDecoration = "underline" 26 | } 27 | 28 | return ( 29 | 33 | 34 | 35 | {this.props.icon ? 36 | 37 | : null} 38 | 39 | 40 | {this.getTextElements(this.props.text)} 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | private getTextElements(text: string): Array { 48 | const elements: Array = []; 49 | const textParts = text.split(/(?=\s+)/); 50 | for (const textPart of textParts) { 51 | if (textPart.match(/^\s*http/)) { 52 | elements.push( 53 | 54 | {textPart} 55 | 56 | ); 57 | } else { 58 | elements.push(textPart); 59 | } 60 | 61 | } 62 | 63 | return elements; 64 | } 65 | } 66 | 67 | export default NoticeTextSelectionComponent; -------------------------------------------------------------------------------- /src/components/SelectorComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface SelectorOption { 4 | label: string; 5 | } 6 | 7 | export interface SelectorProps { 8 | id: string; 9 | options: SelectorOption[]; 10 | onChange: (value: string) => void; 11 | onMouseEnter?: () => void; 12 | onMouseLeave?: () => void; 13 | } 14 | 15 | export interface SelectorState { 16 | 17 | } 18 | 19 | class SelectorComponent extends React.Component { 20 | 21 | constructor(props: SelectorProps) { 22 | super(props); 23 | 24 | // Setup state 25 | this.state = { 26 | 27 | } 28 | } 29 | 30 | render(): React.ReactElement { 31 | return ( 32 |
0 ? "inherit" : "none"}} 34 | className="sbSelector"> 35 |
38 | {this.getOptions()} 39 |
40 |
41 | ); 42 | } 43 | 44 | getOptions(): React.ReactElement[] { 45 | const result: React.ReactElement[] = []; 46 | for (const option of this.props.options) { 47 | result.push( 48 |
{ 50 | e.stopPropagation(); 51 | this.props.onChange(option.label); 52 | }} 53 | key={option.label}> 54 | {option.label} 55 |
56 | ); 57 | } 58 | 59 | return result; 60 | } 61 | } 62 | 63 | export default SelectorComponent; -------------------------------------------------------------------------------- /src/components/options/CategoryChooserComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import * as CompileConfig from "../../../config.json"; 4 | import { Category } from "../../types"; 5 | import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent"; 6 | 7 | export interface CategoryChooserProps { 8 | 9 | } 10 | 11 | export interface CategoryChooserState { 12 | 13 | } 14 | 15 | class CategoryChooserComponent extends React.Component { 16 | 17 | constructor(props: CategoryChooserProps) { 18 | super(props); 19 | 20 | // Setup state 21 | this.state = { 22 | 23 | } 24 | } 25 | 26 | render(): React.ReactElement { 27 | return ( 28 | 30 | 31 | {/* Headers */} 32 | 34 | 37 | 38 | 42 | 43 | 47 | 48 | 52 | 53 | 54 | {this.getCategorySkipOptions()} 55 | 56 |
35 | {chrome.i18n.getMessage("category")} 36 | 40 | {chrome.i18n.getMessage("skipOption")} 41 | 45 | {chrome.i18n.getMessage("seekBarColor")} 46 | 50 | {chrome.i18n.getMessage("previewColor")} 51 |
57 | ); 58 | } 59 | 60 | getCategorySkipOptions(): JSX.Element[] { 61 | const elements: JSX.Element[] = []; 62 | 63 | for (const category of CompileConfig.categoryList) { 64 | elements.push( 65 | 67 | 68 | ); 69 | } 70 | 71 | return elements; 72 | } 73 | } 74 | 75 | export default CategoryChooserComponent; -------------------------------------------------------------------------------- /src/components/options/KeybindComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, Root } from 'react-dom/client'; 3 | import Config from "../../config"; 4 | import KeybindDialogComponent from "./KeybindDialogComponent"; 5 | import { formatKey, Keybind, keybindEquals, keybindToString } from "../../../maze-utils/src/config"; 6 | 7 | export interface KeybindProps { 8 | option: string; 9 | } 10 | 11 | export interface KeybindState { 12 | keybind: Keybind; 13 | } 14 | 15 | let dialog; 16 | let root: Root; 17 | 18 | class KeybindComponent extends React.Component { 19 | constructor(props: KeybindProps) { 20 | super(props); 21 | this.state = {keybind: Config.config[this.props.option]}; 22 | } 23 | 24 | render(): React.ReactElement { 25 | return( 26 | <> 27 |
this.openEditDialog()}> 28 | {this.state.keybind?.ctrl &&
Ctrl
} 29 | {this.state.keybind?.ctrl && +} 30 | {this.state.keybind?.alt &&
Alt
} 31 | {this.state.keybind?.alt && +} 32 | {this.state.keybind?.shift &&
Shift
} 33 | {this.state.keybind?.shift && +} 34 | {this.state.keybind?.key != null &&
{formatKey(this.state.keybind.key)}
} 35 | {this.state.keybind == null && {chrome.i18n.getMessage("notSet")}} 36 |
37 | 38 | {this.state.keybind != null && 39 |
this.unbind()}> 40 | {chrome.i18n.getMessage("unbind")} 41 |
42 | } 43 | 44 | ); 45 | } 46 | 47 | equals(other: Keybind): boolean { 48 | return keybindEquals(this.state.keybind, other); 49 | } 50 | 51 | toString(): string { 52 | return keybindToString(this.state.keybind); 53 | } 54 | 55 | openEditDialog(): void { 56 | dialog = parent.document.createElement("div"); 57 | dialog.id = "keybind-dialog"; 58 | parent.document.body.prepend(dialog); 59 | root = createRoot(dialog); 60 | root.render( this.closeEditDialog(updateWith)} />); 61 | } 62 | 63 | closeEditDialog(updateWith: Keybind): void { 64 | root.unmount(); 65 | dialog.remove(); 66 | if (updateWith != null) 67 | this.setState({keybind: updateWith}); 68 | } 69 | 70 | unbind(): void { 71 | this.setState({keybind: null}); 72 | Config.config[this.props.option] = null; 73 | } 74 | } 75 | 76 | export default KeybindComponent; -------------------------------------------------------------------------------- /src/components/options/ToggleOptionComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Config from "../../config"; 4 | 5 | export interface ToggleOptionProps { 6 | configKey: string; 7 | label: string; 8 | disabled?: boolean; 9 | style?: React.CSSProperties; 10 | } 11 | 12 | export interface ToggleOptionState { 13 | enabled: boolean; 14 | } 15 | 16 | class ToggleOptionComponent extends React.Component { 17 | 18 | constructor(props: ToggleOptionProps) { 19 | super(props); 20 | 21 | // Setup state 22 | this.state = { 23 | enabled: Config.config[props.configKey] 24 | } 25 | } 26 | 27 | render(): React.ReactElement { 28 | return ( 29 |
30 |
31 | 39 | 42 |
43 |
44 | ); 45 | } 46 | 47 | clicked(event: React.ChangeEvent): void { 48 | Config.config[this.props.configKey] = event.target.checked; 49 | 50 | this.setState({ 51 | enabled: event.target.checked 52 | }); 53 | } 54 | 55 | } 56 | 57 | export default ToggleOptionComponent; -------------------------------------------------------------------------------- /src/components/options/UnsubmittedVideoListComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Config from "../../config"; 4 | import UnsubmittedVideoListItem from "./UnsubmittedVideoListItem"; 5 | 6 | export interface UnsubmittedVideoListProps { 7 | 8 | } 9 | 10 | export interface UnsubmittedVideoListState { 11 | 12 | } 13 | 14 | class UnsubmittedVideoListComponent extends React.Component { 15 | 16 | constructor(props: UnsubmittedVideoListProps) { 17 | super(props); 18 | 19 | // Setup state 20 | this.state = { 21 | 22 | }; 23 | } 24 | 25 | render(): React.ReactElement { 26 | // Render nothing if there are no unsubmitted segments 27 | if (Object.keys(Config.local.unsubmittedSegments).length == 0) 28 | return <>; 29 | 30 | return ( 31 | 34 | 35 | {/* Headers */} 36 | 38 | 41 | 42 | 45 | 46 | 49 | 50 | 51 | 52 | {this.getUnsubmittedVideos()} 53 | 54 |
39 | {chrome.i18n.getMessage("videoID")} 40 | 43 | {chrome.i18n.getMessage("segmentCount")} 44 | 47 | {chrome.i18n.getMessage("actions")} 48 |
55 | ); 56 | } 57 | 58 | getUnsubmittedVideos(): JSX.Element[] { 59 | const elements: JSX.Element[] = []; 60 | 61 | for (const videoID of Object.keys(Config.local.unsubmittedSegments)) { 62 | elements.push( 63 | 64 | 65 | ); 66 | } 67 | 68 | return elements; 69 | } 70 | } 71 | 72 | export default UnsubmittedVideoListComponent; 73 | -------------------------------------------------------------------------------- /src/components/options/UnsubmittedVideoListItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Config from "../../config"; 4 | import { exportTimes, exportTimesAsHashParam } from "../../utils/exporter"; 5 | 6 | export interface UnsubmittedVideosListItemProps { 7 | videoID: string; 8 | children?: React.ReactNode; 9 | } 10 | 11 | export interface UnsubmittedVideosListItemState { 12 | } 13 | 14 | class UnsubmittedVideoListItem extends React.Component { 15 | 16 | constructor(props: UnsubmittedVideosListItemProps) { 17 | super(props); 18 | 19 | // Setup state 20 | this.state = { 21 | 22 | }; 23 | } 24 | 25 | render(): React.ReactElement { 26 | const segmentCount = Config.local.unsubmittedSegments[this.props.videoID]?.length ?? 0; 27 | 28 | return ( 29 | <> 30 | 32 | 34 | 36 | {this.props.videoID} 37 | 38 | 39 | 40 | 41 | {segmentCount} 42 | 43 | 44 | 45 |
48 | {chrome.i18n.getMessage("exportSegments")} 49 |
50 | {" "} 51 |
54 | {chrome.i18n.getMessage("exportSegmentsAsURL")} 55 |
56 | {" "} 57 |
60 | {chrome.i18n.getMessage("clearTimes")} 61 |
62 | 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | 70 | clearSegments(): void { 71 | if (confirm(chrome.i18n.getMessage("clearThis"))) { 72 | delete Config.local.unsubmittedSegments[this.props.videoID]; 73 | Config.forceLocalUpdate("unsubmittedSegments"); 74 | } 75 | } 76 | 77 | exportSegments(): void { 78 | this.copyToClipboard(exportTimes(Config.local.unsubmittedSegments[this.props.videoID])); 79 | } 80 | 81 | exportSegmentsAsURL(): void { 82 | this.copyToClipboard(`https://youtube.com/watch?v=${this.props.videoID}${exportTimesAsHashParam(Config.local.unsubmittedSegments[this.props.videoID])}`) 83 | } 84 | 85 | copyToClipboard(text: string): void { 86 | navigator.clipboard.writeText(text) 87 | .then(() => { 88 | alert(chrome.i18n.getMessage("CopiedExclamation")); 89 | }) 90 | .catch(() => { 91 | alert(chrome.i18n.getMessage("copyDebugInformationFailed")); 92 | }); 93 | } 94 | } 95 | 96 | export default UnsubmittedVideoListItem; 97 | -------------------------------------------------------------------------------- /src/components/options/UnsubmittedVideosComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Config from "../../config"; 3 | import UnsubmittedVideoListComponent from "./UnsubmittedVideoListComponent"; 4 | 5 | export interface UnsubmittedVideosProps { 6 | 7 | } 8 | 9 | export interface UnsubmittedVideosState { 10 | tableVisible: boolean; 11 | } 12 | 13 | class UnsubmittedVideosComponent extends React.Component { 14 | 15 | constructor(props: UnsubmittedVideosProps) { 16 | super(props); 17 | 18 | this.state = { 19 | tableVisible: false, 20 | }; 21 | } 22 | 23 | render(): React.ReactElement { 24 | const videoCount = Object.keys(Config.local.unsubmittedSegments).length; 25 | const segmentCount = Object.values(Config.local.unsubmittedSegments).reduce((acc: number, vid: Array) => acc + vid.length, 0); 26 | 27 | return <> 28 |
29 | {segmentCount == 0 ? 30 | chrome.i18n.getMessage("unsubmittedSegmentCountsZero") : 31 | chrome.i18n.getMessage("unsubmittedSegmentCounts") 32 | .replace("{0}", `${segmentCount} ${chrome.i18n.getMessage("unsubmittedSegments" + (segmentCount == 1 ? "Singular" : "Plural"))}`) 33 | .replace("{1}", `${videoCount} ${chrome.i18n.getMessage("videos" + (videoCount == 1 ? "Singular" : "Plural"))}`) 34 | } 35 |
36 | 37 | {videoCount > 0 &&
this.setState({tableVisible: !this.state.tableVisible})}> 38 | {chrome.i18n.getMessage(this.state.tableVisible ? "hideUnsubmittedSegments" : "showUnsubmittedSegments")} 39 |
} 40 | {" "} 41 |
42 | {chrome.i18n.getMessage("clearUnsubmittedSegments")} 43 |
44 | 45 | {this.state.tableVisible && } 46 | ; 47 | } 48 | 49 | clearAllSegments(): void { 50 | if (confirm(chrome.i18n.getMessage("clearUnsubmittedSegmentsConfirm"))) 51 | Config.local.unsubmittedSegments = {}; 52 | } 53 | } 54 | 55 | export default UnsubmittedVideosComponent; 56 | -------------------------------------------------------------------------------- /src/dearrowPromotion.ts: -------------------------------------------------------------------------------- 1 | import { waitFor } from "../maze-utils/src"; 2 | import { getYouTubeTitleNode } from "../maze-utils/src/elements"; 3 | import { getHash } from "../maze-utils/src/hash"; 4 | import { getVideoID, isOnInvidious, isOnMobileYouTube } from "../maze-utils/src/video"; 5 | import Config from "./config"; 6 | import { Tooltip } from "./render/Tooltip"; 7 | import { isDeArrowInstalled } from "./utils/crossExtension"; 8 | import { isVisible } from "./utils/pageUtils"; 9 | import { asyncRequestToServer } from "./utils/requests"; 10 | 11 | let tooltip: Tooltip = null; 12 | const showDeArrowPromotion = false; 13 | export async function tryShowingDeArrowPromotion() { 14 | if (showDeArrowPromotion 15 | && Config.config.showDeArrowPromotion 16 | && !isOnMobileYouTube() 17 | && !isOnInvidious() 18 | && document.URL.includes("watch") 19 | && Config.config.showUpsells 20 | && Config.config.showNewFeaturePopups 21 | && (Config.config.skipCount > 30 || !Config.config.trackViewCount)) { 22 | 23 | if (!await isDeArrowInstalled()) { 24 | try { 25 | const element = await waitFor(() => getYouTubeTitleNode(), 5000, 500, (e) => isVisible(e)) as HTMLElement; 26 | if (element && element.innerText && badTitle(element.innerText)) { 27 | const hashPrefix = (await getHash(getVideoID(), 1)).slice(0, 4); 28 | const deArrowData = await asyncRequestToServer("GET", "/api/branding/" + hashPrefix); 29 | if (!deArrowData.ok) return; 30 | 31 | const deArrowDataJson = JSON.parse(deArrowData.responseText); 32 | const title = deArrowDataJson?.[getVideoID()]?.titles?.[0]; 33 | if (title && title.title && (title.locked || title.votes > 0)) { 34 | Config.config.showDeArrowPromotion = false; 35 | 36 | tooltip = new Tooltip({ 37 | text: chrome.i18n.getMessage("DeArrowTitleReplacementSuggestion") + "\n\n" + title.title, 38 | linkOnClick: () => { 39 | window.open("https://dearrow.ajay.app"); 40 | Config.config.shownDeArrowPromotion = true; 41 | }, 42 | secondButtonText: chrome.i18n.getMessage("hideNewFeatureUpdates"), 43 | referenceNode: element, 44 | prependElement: element.firstElementChild as HTMLElement, 45 | timeout: 15000, 46 | positionRealtive: false, 47 | containerAbsolute: true, 48 | bottomOffset: "inherit", 49 | topOffset: "55px", 50 | leftOffset: "0", 51 | rightOffset: "0", 52 | topTriangle: true, 53 | center: true, 54 | opacity: 1 55 | }); 56 | } 57 | } 58 | } catch { } // eslint-disable-line no-empty 59 | } else { 60 | Config.config.showDeArrowPromotion = false; 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Two upper case words (at least 2 letters long) 67 | */ 68 | function badTitle(title: string): boolean { 69 | return !!title.match(/\p{Lu}{2,} \p{Lu}{2,}[.!? ]/u); 70 | } 71 | 72 | export function hideDeArrowPromotion(): void { 73 | if (tooltip) tooltip.close(); 74 | } -------------------------------------------------------------------------------- /src/document.ts: -------------------------------------------------------------------------------- 1 | import { init } from "../maze-utils/src/injected/document"; 2 | 3 | init(); -------------------------------------------------------------------------------- /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | import { SBObject } from "./config"; 2 | declare global { 3 | interface Window { SB: SBObject } 4 | } 5 | -------------------------------------------------------------------------------- /src/help.ts: -------------------------------------------------------------------------------- 1 | import { localizeHtmlPage } from "../maze-utils/src/setup"; 2 | import Config from "./config"; 3 | import { showDonationLink } from "./utils/configUtils"; 4 | 5 | import { waitFor } from "../maze-utils/src"; 6 | import { isDeArrowInstalled } from "./utils/crossExtension"; 7 | 8 | if (document.readyState === "complete") { 9 | init(); 10 | } else { 11 | document.addEventListener("DOMContentLoaded", init); 12 | } 13 | 14 | // DeArrow promotion 15 | waitFor(() => Config.isReady()).then(() => { 16 | if (Config.config.showNewFeaturePopups && Config.config.showUpsells) { 17 | isDeArrowInstalled().then((installed) => { 18 | if (!installed) { 19 | const deArrowPromotion = document.getElementById("dearrow-link"); 20 | deArrowPromotion.classList.remove("hidden"); 21 | 22 | deArrowPromotion.addEventListener("click", () => Config.config.showDeArrowPromotion = false); 23 | 24 | const text = deArrowPromotion.querySelector("#dearrow-link-text"); 25 | text.textContent = `${chrome.i18n.getMessage("DeArrowPromotionMessage2").split("?")[0]}? ${chrome.i18n.getMessage("DeArrowPromotionMessage3")}`; 26 | 27 | const closeButton = deArrowPromotion.querySelector(".close-button"); 28 | closeButton.addEventListener("click", (e) => { 29 | e.preventDefault(); 30 | 31 | deArrowPromotion.classList.add("hidden"); 32 | Config.config.showDeArrowPromotion = false; 33 | Config.config.showDeArrowInSettings = false; 34 | }); 35 | } 36 | }); 37 | } 38 | }); 39 | 40 | async function init() { 41 | localizeHtmlPage(); 42 | 43 | await waitFor(() => Config.config !== null); 44 | 45 | if (!Config.config.darkMode) { 46 | document.documentElement.setAttribute("data-theme", "light"); 47 | } 48 | 49 | if (!showDonationLink()) { 50 | document.getElementById("sbDonate").style.display = "none"; 51 | } 52 | } -------------------------------------------------------------------------------- /src/messageTypes.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Message and Response Types 3 | // 4 | 5 | import { SegmentUUID, SponsorHideType, SponsorTime, VideoID } from "./types"; 6 | 7 | interface BaseMessage { 8 | from?: string; 9 | } 10 | 11 | interface DefaultMessage { 12 | message: 13 | "update" 14 | | "sponsorStart" 15 | | "getVideoID" 16 | | "getChannelID" 17 | | "isChannelWhitelisted" 18 | | "submitTimes" 19 | | "refreshSegments" 20 | | "closePopup" 21 | | "getLogs" 22 | | "getLoopedChapter"; 23 | } 24 | 25 | interface BoolValueMessage { 26 | message: "whitelistChange"; 27 | value: boolean; 28 | } 29 | 30 | interface IsInfoFoundMessage { 31 | message: "isInfoFound"; 32 | updating: boolean; 33 | } 34 | 35 | interface SkipMessage { 36 | message: "unskip" | "reskip" | "selectSegment"; 37 | UUID: SegmentUUID; 38 | } 39 | 40 | interface SubmitVoteMessage { 41 | message: "submitVote"; 42 | type: number; 43 | UUID: SegmentUUID; 44 | } 45 | 46 | interface HideSegmentMessage { 47 | message: "hideSegment"; 48 | type: SponsorHideType; 49 | UUID: SegmentUUID; 50 | } 51 | 52 | interface CopyToClipboardMessage { 53 | message: "copyToClipboard"; 54 | text: string; 55 | } 56 | 57 | interface ImportSegmentsMessage { 58 | message: "importSegments"; 59 | data: string; 60 | } 61 | 62 | interface LoopChapterMessage { 63 | message: "loopChapter"; 64 | UUID: SegmentUUID; 65 | } 66 | 67 | interface KeyDownMessage { 68 | message: "keydown"; 69 | key: string; 70 | keyCode: number; 71 | code: string; 72 | which: number; 73 | shiftKey: boolean; 74 | ctrlKey: boolean; 75 | altKey: boolean; 76 | metaKey: boolean; 77 | } 78 | 79 | export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoFoundMessage | SkipMessage | SubmitVoteMessage | HideSegmentMessage | CopyToClipboardMessage | ImportSegmentsMessage | KeyDownMessage | LoopChapterMessage); 80 | 81 | export interface IsInfoFoundMessageResponse { 82 | found: boolean; 83 | status: number; 84 | sponsorTimes: SponsorTime[]; 85 | time: number; 86 | onMobileYouTube: boolean; 87 | videoID: VideoID; 88 | loopedChapter: SegmentUUID | null; 89 | channelWhitelisted: boolean; 90 | } 91 | 92 | interface GetVideoIdResponse { 93 | videoID: string; 94 | } 95 | 96 | export interface GetChannelIDResponse { 97 | channelID: string; 98 | isYTTV: boolean; 99 | } 100 | 101 | export interface SponsorStartResponse { 102 | creatingSegment: boolean; 103 | } 104 | 105 | export interface IsChannelWhitelistedResponse { 106 | value: boolean; 107 | } 108 | 109 | export interface LoopedChapterResponse { 110 | UUID: SegmentUUID; 111 | } 112 | 113 | export type MessageResponse = 114 | IsInfoFoundMessageResponse 115 | | GetVideoIdResponse 116 | | GetChannelIDResponse 117 | | SponsorStartResponse 118 | | IsChannelWhitelistedResponse 119 | | Record // empty object response {} 120 | | VoteResponse 121 | | ImportSegmentsResponse 122 | | RefreshSegmentsResponse 123 | | LogResponse 124 | | LoopedChapterResponse; 125 | 126 | export interface VoteResponse { 127 | successType: number; 128 | statusCode: number; 129 | responseText: string; 130 | } 131 | 132 | interface ImportSegmentsResponse { 133 | importedSegments: SponsorTime[]; 134 | } 135 | 136 | export interface RefreshSegmentsResponse { 137 | hasVideo: boolean; 138 | } 139 | 140 | export interface LogResponse { 141 | debug: string[]; 142 | warn: string[]; 143 | } 144 | 145 | export interface TimeUpdateMessage { 146 | message: "time"; 147 | time: number; 148 | } 149 | 150 | export type InfoUpdatedMessage = IsInfoFoundMessageResponse & { 151 | message: "infoUpdated"; 152 | } 153 | 154 | export interface VideoChangedPopupMessage { 155 | message: "videoChanged"; 156 | videoID: string; 157 | whitelisted: boolean; 158 | } 159 | 160 | export type PopupMessage = TimeUpdateMessage | InfoUpdatedMessage | VideoChangedPopupMessage; 161 | -------------------------------------------------------------------------------- /src/permissions.ts: -------------------------------------------------------------------------------- 1 | import Config from "./config"; 2 | import Utils from "./utils"; 3 | import { localizeHtmlPage } from "../maze-utils/src/setup"; 4 | const utils = new Utils(); 5 | 6 | // This is needed, if Config is not imported before Utils, things break. 7 | // Probably due to cyclic dependencies 8 | Config.config; 9 | 10 | if (document.readyState === "complete") { 11 | init(); 12 | } else { 13 | document.addEventListener("DOMContentLoaded", init); 14 | } 15 | 16 | async function init() { 17 | localizeHtmlPage(); 18 | 19 | const acceptButton = document.getElementById("acceptPermissionButton"); 20 | acceptButton.addEventListener("click", () => { 21 | utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => { 22 | Config.config.supportInvidious = enabled; 23 | 24 | if (enabled) { 25 | alert(chrome.i18n.getMessage("permissionRequestSuccess")); 26 | window.close(); 27 | } else { 28 | alert(chrome.i18n.getMessage("permissionRequestFailed")); 29 | } 30 | }) 31 | }); 32 | } -------------------------------------------------------------------------------- /src/popup/SegmentSubmissionComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { VideoID } from "../types"; 3 | import Config from "../config"; 4 | import { Message, MessageResponse } from "../messageTypes"; 5 | import { LoadingStatus } from "./PopupComponent"; 6 | 7 | interface SegmentSubmissionComponentProps { 8 | videoID: VideoID; 9 | status: LoadingStatus; 10 | 11 | sendMessage: (request: Message) => Promise; 12 | } 13 | 14 | export const SegmentSubmissionComponent = (props: SegmentSubmissionComponentProps) => { 15 | const segments = Config.local.unsubmittedSegments[props.videoID]; 16 | 17 | const [showSubmitButton, setShowSubmitButton] = React.useState(segments && segments.length > 0); 18 | const [showStartSegment, setShowStartSegment] = React.useState(!segments || segments[segments.length - 1].segment.length === 2); 19 | 20 | return ( 21 |
22 |

23 | {chrome.i18n.getMessage("recordTimesDescription")} 24 |

25 | 26 | {chrome.i18n.getMessage("popupHint")} 27 | 28 |
29 | 51 | 60 |
61 | 62 | {chrome.i18n.getMessage("submissionEditHint")} 63 | 64 |
65 | ); 66 | }; -------------------------------------------------------------------------------- /src/popup/popup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import { PopupComponent } from "./PopupComponent"; 4 | import { waitFor } from "../../maze-utils/src"; 5 | import Config from "../config"; 6 | 7 | 8 | document.addEventListener("DOMContentLoaded", async () => { 9 | await waitFor(() => Config.isReady()); 10 | 11 | const root = createRoot(document.body); 12 | root.render(); 13 | }) -------------------------------------------------------------------------------- /src/popup/popupUtils.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageResponse } from "../messageTypes"; 2 | 3 | export function copyToClipboardPopup(text: string, sendMessage: (request: Message) => Promise): void { 4 | if (window === window.top) { 5 | window.navigator.clipboard.writeText(text); 6 | } else { 7 | sendMessage({ 8 | message: "copyToClipboard", 9 | text 10 | }); 11 | } 12 | } -------------------------------------------------------------------------------- /src/render/CategoryChooser.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import CategoryChooserComponent from "../components/options/CategoryChooserComponent"; 5 | 6 | class CategoryChooser { 7 | 8 | ref: React.RefObject; 9 | 10 | constructor(element: Element) { 11 | this.ref = React.createRef(); 12 | 13 | const root = createRoot(element); 14 | root.render( 15 | 16 | ); 17 | } 18 | 19 | update(): void { 20 | this.ref.current?.forceUpdate(); 21 | } 22 | } 23 | 24 | export default CategoryChooser; -------------------------------------------------------------------------------- /src/render/ChapterVote.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, Root } from 'react-dom/client'; 3 | import ChapterVoteComponent, { ChapterVoteState } from "../components/ChapterVoteComponent"; 4 | import { VoteResponse } from "../messageTypes"; 5 | import { Category, SegmentUUID, SponsorTime } from "../types"; 6 | 7 | export class ChapterVote { 8 | container: HTMLElement; 9 | ref: React.RefObject; 10 | root: Root; 11 | 12 | unsavedState: ChapterVoteState; 13 | 14 | mutationObserver?: MutationObserver; 15 | 16 | constructor(vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise) { 17 | this.ref = React.createRef(); 18 | 19 | this.container = document.createElement('span'); 20 | this.container.id = "chapterVote"; 21 | this.container.style.height = "100%"; 22 | 23 | if (document.location.host === "tv.youtube.com") { 24 | this.container.style.lineHeight = "initial"; 25 | } 26 | 27 | this.root = createRoot(this.container); 28 | this.root.render(); 29 | } 30 | 31 | getContainer(): HTMLElement { 32 | return this.container; 33 | } 34 | 35 | close(): void { 36 | this.root.unmount(); 37 | this.container.remove(); 38 | } 39 | 40 | setVisibility(show: boolean): void { 41 | const newState = { 42 | show, 43 | ...(!show ? { segment: null } : {}) 44 | }; 45 | 46 | if (this.ref.current) { 47 | this.ref.current?.setState(newState); 48 | } else { 49 | this.unsavedState = newState; 50 | } 51 | } 52 | 53 | async setSegment(segment: SponsorTime): Promise { 54 | if (this.ref.current?.state?.segment !== segment) { 55 | const newState = { 56 | segment, 57 | show: true 58 | }; 59 | 60 | if (this.ref.current) { 61 | this.ref.current?.setState(newState); 62 | } else { 63 | this.unsavedState = newState; 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/render/RectangleTooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, Root } from 'react-dom/client'; 3 | 4 | export interface RectangleTooltipProps { 5 | text: string; 6 | link?: string; 7 | referenceNode: HTMLElement; 8 | prependElement?: HTMLElement; // Element to append before 9 | bottomOffset?: string; 10 | leftOffset?: string; 11 | timeout?: number; 12 | htmlId?: string; 13 | maxHeight?: string; 14 | maxWidth?: string; 15 | backgroundColor?: string; 16 | fontSize?: string; 17 | buttonFunction?: () => void; 18 | } 19 | 20 | export class RectangleTooltip { 21 | text: string; 22 | container: HTMLDivElement; 23 | root: Root; 24 | timer: NodeJS.Timeout; 25 | 26 | constructor(props: RectangleTooltipProps) { 27 | props.bottomOffset ??= "0px"; 28 | props.leftOffset ??= "0px"; 29 | props.maxHeight ??= "100px"; 30 | props.maxWidth ??= "300px"; 31 | props.backgroundColor ??= "rgba(28, 28, 28, 0.7)"; 32 | this.text = props.text; 33 | props.fontSize ??= "10px"; 34 | 35 | this.container = document.createElement('div'); 36 | props.htmlId ??= "sponsorRectangleTooltip" + props.text; 37 | this.container.id = props.htmlId; 38 | this.container.style.display = "relative"; 39 | 40 | if (props.prependElement) { 41 | props.referenceNode.insertBefore(this.container, props.prependElement); 42 | } else { 43 | props.referenceNode.appendChild(this.container); 44 | } 45 | 46 | if (props.timeout) { 47 | this.timer = setTimeout(() => this.close(), props.timeout * 1000); 48 | } 49 | 50 | this.root = createRoot(this.container); 51 | this.root.render( 52 |
60 |
61 | 63 | 64 | 65 | {this.text + (props.link ? ". " : "")} 66 | {props.link ? 67 | 71 | {chrome.i18n.getMessage("LearnMore")} 72 | 73 | : null} 74 | 75 |
76 | 85 |
86 | ) 87 | } 88 | 89 | close(): void { 90 | this.root.unmount(); 91 | this.container.remove(); 92 | 93 | if (this.timer) clearTimeout(this.timer); 94 | } 95 | } -------------------------------------------------------------------------------- /src/render/SkipNotice.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, Root } from 'react-dom/client'; 3 | 4 | import Utils from "../utils"; 5 | const utils = new Utils(); 6 | 7 | import SkipNoticeComponent from "../components/SkipNoticeComponent"; 8 | import { SponsorTime, ContentContainer, NoticeVisibilityMode } from "../types"; 9 | import Config from "../config"; 10 | import { SkipNoticeAction } from "../utils/noticeUtils"; 11 | 12 | class SkipNotice { 13 | segments: SponsorTime[]; 14 | autoSkip: boolean; 15 | // Contains functions and variables from the content script needed by the skip notice 16 | contentContainer: ContentContainer; 17 | 18 | noticeElement: HTMLDivElement; 19 | 20 | skipNoticeRef: React.MutableRefObject; 21 | root: Root; 22 | 23 | constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer, componentDidMount: () => void, unskipTime: number = null, startReskip = false, upcomingNoticeShown: boolean, voteNotice = false) { 24 | this.skipNoticeRef = React.createRef(); 25 | 26 | this.segments = segments; 27 | this.autoSkip = autoSkip; 28 | this.contentContainer = contentContainer; 29 | 30 | const referenceNode = utils.findReferenceNode(); 31 | 32 | const amountOfPreviousNotices = document.getElementsByClassName("sponsorSkipNotice").length; 33 | //this is the suffix added at the end of every id 34 | let idSuffix = ""; 35 | for (const segment of this.segments) { 36 | idSuffix += segment.UUID; 37 | } 38 | idSuffix += amountOfPreviousNotices; 39 | 40 | this.noticeElement = document.createElement("div"); 41 | this.noticeElement.className = "sponsorSkipNoticeContainer"; 42 | this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix; 43 | 44 | referenceNode.prepend(this.noticeElement); 45 | this.root = createRoot(this.noticeElement); 46 | this.root.render( 47 | this.close()} 54 | smaller={!voteNotice && (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.MiniForAll 55 | || (Config.config.noticeVisibilityMode >= NoticeVisibilityMode.MiniForAutoSkip && autoSkip))} 56 | fadeIn={!upcomingNoticeShown && !voteNotice} 57 | unskipTime={unskipTime} 58 | componentDidMount={componentDidMount} /> 59 | ); 60 | } 61 | 62 | setShowKeybindHint(value: boolean): void { 63 | this.skipNoticeRef?.current?.setState({ 64 | showKeybindHint: value 65 | }); 66 | } 67 | 68 | close(): void { 69 | this.root.unmount(); 70 | 71 | this.noticeElement.remove(); 72 | 73 | const skipNotices = this.contentContainer().skipNotices; 74 | skipNotices.splice(skipNotices.indexOf(this), 1); 75 | } 76 | 77 | toggleSkip(): void { 78 | this.skipNoticeRef?.current?.prepAction(SkipNoticeAction.Unskip0); 79 | } 80 | 81 | unmutedListener(time: number): void { 82 | this.skipNoticeRef?.current?.unmutedListener(time); 83 | } 84 | 85 | async waitForSkipNoticeRef(): Promise { 86 | const waitForRef = () => new Promise((resolve) => { 87 | const observer = new MutationObserver(() => { 88 | if (this.skipNoticeRef.current) { 89 | observer.disconnect(); 90 | resolve(this.skipNoticeRef.current); 91 | } 92 | }); 93 | 94 | observer.observe(document.getElementsByClassName("sponsorSkipNoticeContainer")[0], { childList: true, subtree: true}); 95 | 96 | if (this.skipNoticeRef.current) { 97 | observer.disconnect(); 98 | resolve(this.skipNoticeRef.current); 99 | } 100 | }); 101 | 102 | return this.skipNoticeRef?.current || await waitForRef(); 103 | } 104 | } 105 | 106 | export default SkipNotice; -------------------------------------------------------------------------------- /src/render/SubmissionNotice.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, Root } from 'react-dom/client'; 3 | 4 | import Utils from "../utils"; 5 | const utils = new Utils(); 6 | 7 | import SubmissionNoticeComponent from "../components/SubmissionNoticeComponent"; 8 | import { ContentContainer } from "../types"; 9 | 10 | class SubmissionNotice { 11 | // Contains functions and variables from the content script needed by the skip notice 12 | contentContainer: () => unknown; 13 | 14 | callback: () => Promise; 15 | 16 | noticeRef: React.MutableRefObject; 17 | 18 | noticeElement: HTMLDivElement; 19 | 20 | root: Root; 21 | 22 | constructor(contentContainer: ContentContainer, callback: () => Promise) { 23 | this.noticeRef = React.createRef(); 24 | 25 | this.contentContainer = contentContainer; 26 | this.callback = callback; 27 | 28 | const referenceNode = utils.findReferenceNode(); 29 | 30 | this.noticeElement = document.createElement("div"); 31 | this.noticeElement.id = "submissionNoticeContainer"; 32 | 33 | referenceNode.prepend(this.noticeElement); 34 | 35 | this.root = createRoot(this.noticeElement); 36 | this.root.render( 37 | this.close(false)} /> 42 | ); 43 | } 44 | 45 | update(): void { 46 | this.noticeRef.current.forceUpdate(); 47 | } 48 | 49 | close(callRef = true): void { 50 | if (callRef) this.noticeRef.current.cancel(); 51 | this.root.unmount(); 52 | 53 | this.noticeElement.remove(); 54 | } 55 | 56 | submit(): void { 57 | this.noticeRef.current?.submit?.(); 58 | } 59 | 60 | scrollToBottom(): void { 61 | this.noticeRef.current?.scrollToBottom?.(); 62 | } 63 | } 64 | 65 | export default SubmissionNotice; -------------------------------------------------------------------------------- /src/render/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { GenericTooltip, TooltipProps } from "../../maze-utils/src/components/Tooltip"; 2 | 3 | export class Tooltip extends GenericTooltip { 4 | constructor(props: TooltipProps) { 5 | super(props, "icons/IconSponsorBlocker256px.png") 6 | } 7 | } -------------------------------------------------------------------------------- /src/render/UnsubmittedVideos.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot } from 'react-dom/client'; 3 | import UnsubmittedVideosComponent from "../components/options/UnsubmittedVideosComponent"; 4 | 5 | class UnsubmittedVideos { 6 | 7 | ref: React.RefObject; 8 | 9 | constructor(element: Element) { 10 | this.ref = React.createRef(); 11 | 12 | const root = createRoot(element); 13 | root.render( 14 | 15 | ); 16 | } 17 | 18 | update(): void { 19 | this.ref.current?.forceUpdate(); 20 | } 21 | 22 | } 23 | 24 | export default UnsubmittedVideos; 25 | -------------------------------------------------------------------------------- /src/render/UpcomingNotice.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot, Root } from "react-dom/client"; 3 | import { ContentContainer, SponsorTime } from "../types"; 4 | 5 | import Utils from "../utils"; 6 | import SkipNoticeComponent from "../components/SkipNoticeComponent"; 7 | const utils = new Utils(); 8 | 9 | class UpcomingNotice { 10 | segments: SponsorTime[]; 11 | // Contains functions and variables from the content script needed by the skip notice 12 | contentContainer: ContentContainer; 13 | 14 | noticeElement: HTMLDivElement; 15 | 16 | upcomingNoticeRef: React.MutableRefObject; 17 | root: Root; 18 | 19 | closed = false; 20 | 21 | constructor(segments: SponsorTime[], contentContainer: ContentContainer, timeLeft: number, autoSkip: boolean) { 22 | this.upcomingNoticeRef = React.createRef(); 23 | 24 | this.segments = segments; 25 | this.contentContainer = contentContainer; 26 | 27 | const referenceNode = utils.findReferenceNode(); 28 | 29 | this.noticeElement = document.createElement("div"); 30 | this.noticeElement.className = "sponsorSkipNoticeContainer"; 31 | 32 | referenceNode.prepend(this.noticeElement); 33 | 34 | this.root = createRoot(this.noticeElement); 35 | this.root.render( 36 | this.close()} 42 | smaller={true} 43 | fadeIn={true} 44 | maxCountdownTime={timeLeft} /> 45 | ); 46 | } 47 | 48 | close(): void { 49 | this.root.unmount(); 50 | this.noticeElement.remove(); 51 | 52 | this.closed = true; 53 | } 54 | 55 | sameNotice(segments: SponsorTime[]): boolean { 56 | if (segments.length !== this.segments.length) return false; 57 | 58 | for (let i = 0; i < segments.length; i++) { 59 | if (segments[i].UUID !== this.segments[i].UUID) return false; 60 | } 61 | 62 | return true; 63 | } 64 | } 65 | 66 | export default UpcomingNotice; -------------------------------------------------------------------------------- /src/svg-icons/checkIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface CheckIconProps { 4 | id?: string; 5 | style?: React.CSSProperties; 6 | className?: string; 7 | onClick?: () => void; 8 | } 9 | 10 | const CheckIcon = ({ 11 | id = "", 12 | className = "", 13 | style = {}, 14 | onClick 15 | }: CheckIconProps): JSX.Element => ( 16 | 23 | 24 | 25 | ); 26 | 27 | export default CheckIcon; -------------------------------------------------------------------------------- /src/svg-icons/clipboardIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface ClipboardIconProps { 4 | id?: string; 5 | style?: React.CSSProperties; 6 | className?: string; 7 | onClick?: () => void; 8 | } 9 | 10 | const ClipboardIcon = ({ 11 | id = "", 12 | className = "", 13 | style = {}, 14 | onClick 15 | }: ClipboardIconProps): JSX.Element => ( 16 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default ClipboardIcon; -------------------------------------------------------------------------------- /src/svg-icons/lock_svg.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const lockSvg = ({ 4 | fill = "#fcba03", 5 | className = "", 6 | width = "20", 7 | height = "20", 8 | onClick 9 | }): JSX.Element => ( 10 | 17 | 19 | 20 | ); 21 | 22 | export default lockSvg; 23 | -------------------------------------------------------------------------------- /src/svg-icons/pencilIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface PencilIconProps { 4 | id?: string; 5 | style?: React.CSSProperties; 6 | className?: string; 7 | onClick?: () => void; 8 | } 9 | 10 | const PencilIcon = ({ 11 | id = "", 12 | className = "", 13 | style = {}, 14 | onClick 15 | }: PencilIconProps): JSX.Element => ( 16 | 23 | 24 | 25 | ); 26 | 27 | export default PencilIcon; -------------------------------------------------------------------------------- /src/svg-icons/pencil_svg.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const pencilSvg = ({ 4 | fill = "#ffffff" 5 | }): JSX.Element => ( 6 | 13 | 15 | 16 | ); 17 | 18 | export default pencilSvg; 19 | -------------------------------------------------------------------------------- /src/svg-icons/sb_svg.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface SbIconProps { 4 | id?: string; 5 | fill?: string; 6 | className?: string; 7 | width?: string; 8 | height?: string; 9 | onClick?: () => void; 10 | } 11 | 12 | export default function SbSvg({ 13 | id = "", 14 | fill = "#ff0000", 15 | className = "", 16 | onClick 17 | }: SbIconProps): JSX.Element { 18 | return ( 19 | onClick?.() } > 25 | 28 | 34 | 40 | 46 | 47 | 48 | 53 | 54 | ); 55 | } -------------------------------------------------------------------------------- /src/svg-icons/thumbs_down_svg.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const thumbsDownSvg = ({ 4 | fill = "#ffffff", 5 | className = "", 6 | width = "18", 7 | height = "18" 8 | }): JSX.Element => ( 9 | 17 | 20 | 21 | 24 | 25 | ); 26 | 27 | export default thumbsDownSvg; 28 | -------------------------------------------------------------------------------- /src/svg-icons/thumbs_up_svg.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const thumbsUpSvg = ({ 4 | fill = "#ffffff", 5 | className = "", 6 | width = "18", 7 | height = "18" 8 | }): JSX.Element => ( 9 | 17 | 20 | 23 | 24 | ); 25 | 26 | export default thumbsUpSvg; 27 | -------------------------------------------------------------------------------- /src/utils/arrayUtils.ts: -------------------------------------------------------------------------------- 1 | export function partition(array: T[], filter: (element: T) => boolean): [T[], T[]] { 2 | const pass = [], fail = []; 3 | array.forEach((element) => (filter(element) ? pass : fail).push(element)); 4 | 5 | return [pass, fail]; 6 | } -------------------------------------------------------------------------------- /src/utils/categoryUtils.ts: -------------------------------------------------------------------------------- 1 | import { ActionType, Category, SponsorTime } from "../types"; 2 | 3 | export function getSkippingText(segments: SponsorTime[], autoSkip: boolean): string { 4 | const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments" 5 | : "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category); 6 | if (autoSkip) { 7 | let messageId = ""; 8 | switch (segments[0].actionType) { 9 | case ActionType.Skip: 10 | messageId = "skipped"; 11 | break; 12 | case ActionType.Mute: 13 | messageId = "muted"; 14 | break; 15 | case ActionType.Poi: 16 | messageId = "skipped_to_category"; 17 | break; 18 | } 19 | 20 | return chrome.i18n.getMessage(messageId).replace("{0}", categoryName); 21 | } else { 22 | let messageId = ""; 23 | switch (segments[0].actionType) { 24 | case ActionType.Skip: 25 | messageId = "skip_category"; 26 | break; 27 | case ActionType.Mute: 28 | messageId = "mute_category"; 29 | break; 30 | case ActionType.Poi: 31 | messageId = "skip_to_category"; 32 | break; 33 | } 34 | 35 | return chrome.i18n.getMessage(messageId).replace("{0}", categoryName); 36 | } 37 | } 38 | 39 | export function getUpcomingText(segments: SponsorTime[]): string { 40 | const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments" 41 | : "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category); 42 | 43 | const messageId = "upcoming"; 44 | return chrome.i18n.getMessage(messageId).replace("{0}", categoryName); 45 | } 46 | 47 | export function getVoteText(segments: SponsorTime[]): string { 48 | const categoryName = chrome.i18n.getMessage(segments.length > 1 ? "multipleSegments" 49 | : "category_" + segments[0].category + "_short") || chrome.i18n.getMessage("category_" + segments[0].category); 50 | 51 | const messageId = "voted_on"; 52 | return chrome.i18n.getMessage(messageId).replace("{0}", categoryName); 53 | } 54 | 55 | 56 | export function getCategorySuffix(category: Category): string { 57 | if (category.startsWith("poi_")) { 58 | return "_POI"; 59 | } else if (category === "exclusive_access") { 60 | return "_full"; 61 | } else if (category === "chapter") { 62 | return "_chapter"; 63 | } else { 64 | return ""; 65 | } 66 | } 67 | 68 | export function shortCategoryName(categoryName: string): string { 69 | return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName); 70 | } 71 | export const DEFAULT_CATEGORY = "chooseACategory"; -------------------------------------------------------------------------------- /src/utils/compatibility.ts: -------------------------------------------------------------------------------- 1 | import Config from "../config"; 2 | 3 | export function runCompatibilityChecks() { 4 | if (Config.config.showZoomToFillError2 && document.URL.includes("watch?v=")) { 5 | setTimeout(() => { 6 | const zoomToFill = document.querySelector(".zoomtofillBtn"); 7 | 8 | if (zoomToFill) { 9 | alert(chrome.i18n.getMessage("zoomToFillUnsupported")); 10 | } 11 | 12 | Config.config.showZoomToFillError2 = false; 13 | }, 10000); 14 | } 15 | } 16 | 17 | export function isVorapisInstalled() { 18 | return document.querySelector(`.v3`); 19 | } -------------------------------------------------------------------------------- /src/utils/configUtils.ts: -------------------------------------------------------------------------------- 1 | import Config from "../config"; 2 | 3 | export function showDonationLink(): boolean { 4 | return navigator.vendor !== "Apple Computer, Inc." && Config.config.showDonationLink; 5 | } -------------------------------------------------------------------------------- /src/utils/crossExtension.ts: -------------------------------------------------------------------------------- 1 | import * as CompileConfig from "../../config.json"; 2 | 3 | import Config from "../config"; 4 | import { isSafari } from "../../maze-utils/src/config"; 5 | import { isFirefoxOrSafari } from "../../maze-utils/src"; 6 | 7 | export function isDeArrowInstalled(): Promise { 8 | if (Config.config.deArrowInstalled) { 9 | return Promise.resolve(true); 10 | } else { 11 | return new Promise((resolve) => { 12 | const extensionIds = getExtensionIdsToImportFrom(); 13 | 14 | let count = 0; 15 | for (const id of extensionIds) { 16 | chrome.runtime.sendMessage(id, { message: "isInstalled" }, (response) => { 17 | if (chrome.runtime.lastError) { 18 | count++; 19 | 20 | if (count === extensionIds.length) { 21 | resolve(false); 22 | } 23 | return; 24 | } 25 | 26 | resolve(response); 27 | if (response) { 28 | Config.config.deArrowInstalled = true; 29 | } 30 | }); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | export function getExtensionIdsToImportFrom(): string[] { 37 | if (isSafari()) { 38 | return CompileConfig.extensionImportList.safari; 39 | } else if (isFirefoxOrSafari()) { 40 | return CompileConfig.extensionImportList.firefox; 41 | } else { 42 | return CompileConfig.extensionImportList.chromium; 43 | } 44 | } -------------------------------------------------------------------------------- /src/utils/genericUtils.ts: -------------------------------------------------------------------------------- 1 | /* Gets percieved luminance of a color */ 2 | function getLuminance(color: string): number { 3 | const {r, g, b} = hexToRgb(color); 4 | return Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); 5 | } 6 | 7 | /* Converts hex color to rgb color */ 8 | const hexChars = "0123456789abcdef"; 9 | function hexToRgb(hex: string): { r: number; g: number; b: number } | null { 10 | if (hex.length == 4) 11 | hex = "#" + hex[1] + hex[1] + hex[2] + hex[2] + hex[3] + hex[3]; 12 | return /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) 13 | ? { 14 | r: hexChars.indexOf(hex[1]) * 16 + hexChars.indexOf(hex[2]), 15 | g: hexChars.indexOf(hex[3]) * 16 + hexChars.indexOf(hex[4]), 16 | b: hexChars.indexOf(hex[5]) * 16 + hexChars.indexOf(hex[6]), 17 | }: null; 18 | } 19 | 20 | /** 21 | * List of all indexes that have the specified value 22 | * https://stackoverflow.com/a/54954694/1985387 23 | */ 24 | function indexesOf(array: T[], value: T): number[] { 25 | return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1); 26 | } 27 | 28 | export const GenericUtils = { 29 | getLuminance, 30 | indexesOf 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | if (typeof (window) !== "undefined") { 2 | window["SBLogs"] = { 3 | debug: [], 4 | warn: [] 5 | }; 6 | } 7 | 8 | export function logDebug(message: string) { 9 | if (typeof (window) !== "undefined") { 10 | window["SBLogs"].debug.push(`[${new Date().toISOString()}] ${message}`); 11 | } else { 12 | console.log(`[${new Date().toISOString()}] ${message}`) 13 | } 14 | } 15 | 16 | export function logWarn(message: string) { 17 | if (typeof (window) !== "undefined") { 18 | window["SBLogs"].warn.push(`[${new Date().toISOString()}] ${message}`); 19 | } else { 20 | console.warn(`[${new Date().toISOString()}] ${message}`) 21 | } 22 | } -------------------------------------------------------------------------------- /src/utils/mobileUtils.ts: -------------------------------------------------------------------------------- 1 | export function isMobileControlsOpen(): boolean { 2 | const overlay = document.getElementById("player-control-overlay"); 3 | 4 | if (overlay) { 5 | return !!overlay?.classList?.contains("fadein"); 6 | } 7 | 8 | return false; 9 | } -------------------------------------------------------------------------------- /src/utils/noticeUtils.ts: -------------------------------------------------------------------------------- 1 | import Config from "../config"; 2 | import { SponsorTime } from "../types"; 3 | 4 | export enum SkipNoticeAction { 5 | None, 6 | Upvote, 7 | Downvote, 8 | CategoryVote, 9 | CopyDownvote, 10 | Unskip0, 11 | Unskip1 12 | } 13 | 14 | export function downvoteButtonColor(segments: SponsorTime[], actionState: SkipNoticeAction, downvoteType: SkipNoticeAction): string { 15 | // Also used for "Copy and Downvote" 16 | if (segments?.length > 1) { 17 | return (actionState === downvoteType) ? Config.config.colorPalette.red : Config.config.colorPalette.white; 18 | } else { 19 | // You dont have segment selectors so the lockbutton needs to be colored and cannot be selected. 20 | return Config.config.isVip && segments?.[0].locked === 1 ? Config.config.colorPalette.locked : Config.config.colorPalette.white; 21 | } 22 | } -------------------------------------------------------------------------------- /src/utils/pageCleaner.ts: -------------------------------------------------------------------------------- 1 | export function cleanPage() { 2 | // For live-updates 3 | if (document.readyState === "complete") { 4 | for (const element of document.querySelectorAll("#categoryPillParent, .playerButton, .sponsorThumbnailLabel, #submissionNoticeContainer, .sponsorSkipNoticeContainer, #sponsorBlockPopupContainer, .skipButtonControlBarContainer, #previewbar, .sponsorBlockChapterBar")) { 5 | element.remove(); 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/utils/pageUtils.ts: -------------------------------------------------------------------------------- 1 | import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types"; 2 | import { getFormattedTimeToSeconds } from "../../maze-utils/src/formating"; 3 | import Config from "../config"; 4 | 5 | export function getControls(): HTMLElement { 6 | const controlsSelectors = [ 7 | // New YouTube (2025 April) 8 | ".ytp-right-controls-right", 9 | // YouTube 10 | ".ytp-right-controls", 11 | // Mobile YouTube 12 | ".player-controls-top", 13 | // Invidious/videojs video element's controls element 14 | ".vjs-control-bar", 15 | // Piped shaka player 16 | ".shaka-bottom-controls", 17 | // Vorapis v3 18 | ".html5-player-chrome", 19 | // tv.youtube.com 20 | ".ypcs-control-buttons-right" 21 | ]; 22 | 23 | for (const controlsSelector of controlsSelectors) { 24 | const controls = Array.from(document.querySelectorAll(controlsSelector)).filter(el => !isInPreviewPlayer(el)); 25 | 26 | if (controls.length > 0) { 27 | return controls[controls.length - 1]; 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | 34 | export function isInPreviewPlayer(element: Element): boolean { 35 | return !!element.closest("#inline-preview-player"); 36 | } 37 | 38 | export function isVisible(element: HTMLElement): boolean { 39 | return element && element.offsetWidth > 0 && element.offsetHeight > 0; 40 | } 41 | 42 | export function getHashParams(): Record { 43 | const windowHash = window.location.hash.slice(1); 44 | if (windowHash) { 45 | const params: Record = windowHash.split('&').reduce((acc, param) => { 46 | const [key, value] = param.split('='); 47 | const decoded = decodeURIComponent(value); 48 | try { 49 | acc[key] = decoded?.match(/{|\[/) ? JSON.parse(decoded) : value; 50 | } catch (e) { 51 | console.error(`Failed to parse hash parameter ${key}: ${value}`); 52 | } 53 | 54 | return acc; 55 | }, {}); 56 | 57 | return params; 58 | } 59 | 60 | return {}; 61 | } 62 | 63 | export function hasAutogeneratedChapters(): boolean { 64 | return !!document.querySelector("ytd-engagement-panel-section-list-renderer ytd-macro-markers-list-renderer #menu"); 65 | } 66 | 67 | export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] { 68 | const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer"); 69 | const title = chaptersBox?.closest("ytd-engagement-panel-section-list-renderer")?.querySelector("#title-text.ytd-engagement-panel-title-header-renderer"); 70 | if (title?.textContent?.includes("Key moment")) return []; 71 | if (!Config.config.showAutogeneratedChapters && hasAutogeneratedChapters()) return []; 72 | 73 | const chapters: SponsorTime[] = []; 74 | // .ytp-timed-markers-container indicates that key-moments are present, which should not be divided 75 | if (chaptersBox) { 76 | let lastSegment: SponsorTime = null; 77 | const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a"); 78 | for (const link of links) { 79 | const timeElement = link.querySelector("#time") as HTMLElement; 80 | const description = link.querySelector("#details h4") as HTMLElement; 81 | if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) { 82 | const time = getFormattedTimeToSeconds(timeElement.innerText.replace(/\./g, ":")); 83 | if (time === null) return []; 84 | 85 | if (lastSegment) { 86 | lastSegment.segment[1] = time; 87 | chapters.push(lastSegment); 88 | } 89 | 90 | lastSegment = { 91 | segment: [time, null], 92 | category: "chapter" as Category, 93 | actionType: ActionType.Chapter, 94 | description: description.innerText, 95 | source: SponsorSourceType.YouTube, 96 | UUID: null 97 | }; 98 | } 99 | } 100 | 101 | if (lastSegment) { 102 | lastSegment.segment[1] = duration; 103 | chapters.push(lastSegment); 104 | } 105 | } 106 | 107 | return chapters; 108 | } 109 | 110 | export function isPlayingPlaylist() { 111 | return !!document.URL.includes("&list="); 112 | } -------------------------------------------------------------------------------- /src/utils/requests.ts: -------------------------------------------------------------------------------- 1 | import Config from "../config"; 2 | import * as CompileConfig from "../../config.json"; 3 | import { FetchResponse, sendRequestToCustomServer } from "../../maze-utils/src/background-request-proxy"; 4 | 5 | /** 6 | * Sends a request to a custom server 7 | * 8 | * @param type The request type. "GET", "POST", etc. 9 | * @param address The address to add to the SponsorBlock server address 10 | * @param callback 11 | */ 12 | export function asyncRequestToCustomServer(type: string, url: string, data = {}, headers = {}): Promise { 13 | return sendRequestToCustomServer(type, url, data, headers); 14 | } 15 | 16 | /** 17 | * Sends a request to the SponsorBlock server with address added as a query 18 | * 19 | * @param type The request type. "GET", "POST", etc. 20 | * @param address The address to add to the SponsorBlock server address 21 | * @param callback 22 | */ 23 | export async function asyncRequestToServer(type: string, address: string, data = {}, headers = {}): Promise { 24 | const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; 25 | 26 | return await (asyncRequestToCustomServer(type, serverAddress + address, data, headers)); 27 | } 28 | 29 | /** 30 | * Sends a request to the SponsorBlock server with address added as a query 31 | * 32 | * @param type The request type. "GET", "POST", etc. 33 | * @param address The address to add to the SponsorBlock server address 34 | * @param callback 35 | */ 36 | export function sendRequestToServer(type: string, address: string, callback?: (response: FetchResponse) => void): void { 37 | const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress; 38 | 39 | // Ask the background script to do the work 40 | chrome.runtime.sendMessage({ 41 | message: "sendRequest", 42 | type, 43 | url: serverAddress + address 44 | }, (response) => { 45 | callback(response); 46 | }); 47 | } -------------------------------------------------------------------------------- /src/utils/segmentData.ts: -------------------------------------------------------------------------------- 1 | import { DataCache } from "../../maze-utils/src/cache"; 2 | import { getHash, HashedValue } from "../../maze-utils/src/hash"; 3 | import Config from "../config"; 4 | import * as CompileConfig from "../../config.json"; 5 | import { ActionType, ActionTypes, SponsorSourceType, SponsorTime, VideoID } from "../types"; 6 | import { getHashParams } from "./pageUtils"; 7 | import { asyncRequestToServer } from "./requests"; 8 | import { extensionUserAgent } from "../../maze-utils/src"; 9 | 10 | const segmentDataCache = new DataCache(() => { 11 | return { 12 | segments: null, 13 | status: 200 14 | }; 15 | }, 5); 16 | 17 | const pendingList: Record> = {}; 18 | 19 | export interface SegmentResponse { 20 | segments: SponsorTime[] | null; 21 | status: number; 22 | } 23 | 24 | export async function getSegmentsForVideo(videoID: VideoID, ignoreCache: boolean): Promise { 25 | if (!ignoreCache) { 26 | const cachedData = segmentDataCache.getFromCache(videoID); 27 | if (cachedData) { 28 | segmentDataCache.cacheUsed(videoID); 29 | return cachedData; 30 | } 31 | } 32 | 33 | if (pendingList[videoID]) { 34 | return await pendingList[videoID]; 35 | } 36 | 37 | const pendingData = fetchSegmentsForVideo(videoID); 38 | pendingList[videoID] = pendingData; 39 | 40 | const result = await pendingData; 41 | delete pendingList[videoID]; 42 | 43 | return result; 44 | } 45 | 46 | async function fetchSegmentsForVideo(videoID: VideoID): Promise { 47 | const categories: string[] = Config.config.categorySelections.map((category) => category.name); 48 | 49 | const extraRequestData: Record = {}; 50 | const hashParams = getHashParams(); 51 | if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment; 52 | 53 | const hashPrefix = (await getHash(videoID, 1)).slice(0, 5) as VideoID & HashedValue; 54 | const hasDownvotedSegments = !!Config.local.downvotedSegments[hashPrefix.slice(0, 4)]; 55 | const response = await asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { 56 | categories: CompileConfig.categoryList, 57 | actionTypes: ActionTypes, 58 | trimUUIDs: hasDownvotedSegments ? null : 5, 59 | ...extraRequestData 60 | }, { 61 | "X-CLIENT-NAME": extensionUserAgent(), 62 | }); 63 | 64 | if (response.ok) { 65 | const enabledActionTypes = getEnabledActionTypes(); 66 | 67 | const receivedSegments: SponsorTime[] = JSON.parse(response.responseText) 68 | ?.filter((video) => video.videoID === videoID) 69 | ?.map((video) => video.segments)?.[0] 70 | ?.filter((segment) => enabledActionTypes.includes(segment.actionType) && categories.includes(segment.category)) 71 | ?.map((segment) => ({ 72 | ...segment, 73 | source: SponsorSourceType.Server 74 | })) 75 | ?.sort((a, b) => a.segment[0] - b.segment[0]); 76 | 77 | if (receivedSegments && receivedSegments.length) { 78 | const result = { 79 | segments: receivedSegments, 80 | status: response.status 81 | }; 82 | 83 | segmentDataCache.setupCache(videoID).segments = result.segments; 84 | return result; 85 | } else { 86 | // Setup with null data 87 | segmentDataCache.setupCache(videoID); 88 | } 89 | } 90 | 91 | return { 92 | segments: null, 93 | status: response.status 94 | }; 95 | } 96 | 97 | function getEnabledActionTypes(forceFullVideo = false): ActionType[] { 98 | const actionTypes = [ActionType.Skip, ActionType.Poi, ActionType.Chapter]; 99 | if (Config.config.muteSegments) { 100 | actionTypes.push(ActionType.Mute); 101 | } 102 | if (Config.config.fullVideoSegments || forceFullVideo) { 103 | actionTypes.push(ActionType.Full); 104 | } 105 | 106 | return actionTypes; 107 | } 108 | -------------------------------------------------------------------------------- /src/utils/urlParser.ts: -------------------------------------------------------------------------------- 1 | 2 | export function getStartTimeFromUrl(url: string): number { 3 | const urlParams = new URLSearchParams(url); 4 | const time = urlParams?.get('t') || urlParams?.get('time_continue'); 5 | 6 | return urlTimeToSeconds(time); 7 | } 8 | 9 | export function urlTimeToSeconds(time: string): number { 10 | if (!time) { 11 | return 0; 12 | } 13 | 14 | const re = /(?:(\d{1,3})h)?(?:(\d{1,2})m)?(\d+)s?/; 15 | const match = re.exec(time); 16 | 17 | if (match) { 18 | const hours = parseInt(match[1] ?? '0', 10); 19 | const minutes = parseInt(match[2] ?? '0', 10); 20 | const seconds = parseInt(match[3] ?? '0', 10); 21 | 22 | return hours * 3600 + minutes * 60 + seconds; 23 | } else if (/\d+/.test(time)) { 24 | return parseInt(time, 10); 25 | } else { 26 | return 0; 27 | } 28 | } -------------------------------------------------------------------------------- /src/utils/videoLabels.ts: -------------------------------------------------------------------------------- 1 | import { Category, CategorySkipOption, VideoID } from "../types"; 2 | import { getHash } from "../../maze-utils/src/hash"; 3 | import Utils from "../utils"; 4 | import { logWarn } from "./logger"; 5 | import { asyncRequestToServer } from "./requests"; 6 | 7 | const utils = new Utils(); 8 | 9 | interface VideoLabelsCacheData { 10 | category: Category; 11 | hasStartSegment: boolean; 12 | } 13 | 14 | export interface LabelCacheEntry { 15 | timestamp: number; 16 | videos: Record; 17 | } 18 | 19 | const labelCache: Record = {}; 20 | const cacheLimit = 1000; 21 | 22 | async function getLabelHashBlock(hashPrefix: string): Promise { 23 | // Check cache 24 | const cachedEntry = labelCache[hashPrefix]; 25 | if (cachedEntry) { 26 | return cachedEntry; 27 | } 28 | 29 | const response = await asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}?hasStartSegment=true`); 30 | if (response.status !== 200) { 31 | // No video labels or server down 32 | labelCache[hashPrefix] = { 33 | timestamp: Date.now(), 34 | videos: {}, 35 | }; 36 | return null; 37 | } 38 | 39 | try { 40 | const data = JSON.parse(response.responseText); 41 | 42 | const newEntry: LabelCacheEntry = { 43 | timestamp: Date.now(), 44 | videos: Object.fromEntries(data.map(video => [video.videoID, { 45 | category: video.segments[0]?.category, 46 | hasStartSegment: video.hasStartSegment 47 | }])), 48 | }; 49 | labelCache[hashPrefix] = newEntry; 50 | 51 | if (Object.keys(labelCache).length > cacheLimit) { 52 | // Remove oldest entry 53 | const oldestEntry = Object.entries(labelCache).reduce((a, b) => a[1].timestamp < b[1].timestamp ? a : b); 54 | delete labelCache[oldestEntry[0]]; 55 | } 56 | 57 | return newEntry; 58 | } catch (e) { 59 | logWarn(`Error parsing video labels: ${e}`); 60 | 61 | return null; 62 | } 63 | } 64 | 65 | export async function getVideoLabel(videoID: VideoID): Promise { 66 | const prefix = (await getHash(videoID, 1)).slice(0, 4); 67 | const result = await getLabelHashBlock(prefix); 68 | 69 | if (result) { 70 | const category = result.videos[videoID]?.category; 71 | if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) { 72 | return category; 73 | } else { 74 | return null; 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | 81 | export async function getHasStartSegment(videoID: VideoID): Promise { 82 | const prefix = (await getHash(videoID, 1)).slice(0, 4); 83 | const result = await getLabelHashBlock(prefix); 84 | 85 | if (result) { 86 | return result?.videos[videoID]?.hasStartSegment ?? false; 87 | } 88 | 89 | return null; 90 | } -------------------------------------------------------------------------------- /src/utils/warnings.ts: -------------------------------------------------------------------------------- 1 | import { objectToURI } from "../../maze-utils/src"; 2 | import { getHash } from "../../maze-utils/src/hash"; 3 | import Config from "../config"; 4 | import GenericNotice, { NoticeOptions } from "../render/GenericNotice"; 5 | import { ContentContainer } from "../types"; 6 | import { asyncRequestToServer } from "./requests"; 7 | 8 | export interface ChatConfig { 9 | displayName: string; 10 | composerInitialValue?: string; 11 | customDescription?: string; 12 | } 13 | 14 | export async function openWarningDialog(contentContainer: ContentContainer): Promise { 15 | const userInfo = await asyncRequestToServer("GET", "/api/userInfo", { 16 | publicUserID: await getHash(Config.config.userID), 17 | values: ["warningReason"] 18 | }); 19 | 20 | if (userInfo.ok) { 21 | const warningReason = JSON.parse(userInfo.responseText)?.warningReason; 22 | const userNameData = await asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID); 23 | const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : ""; 24 | const publicUserID = await getHash(Config.config.userID); 25 | 26 | let notice: GenericNotice = null; 27 | const options: NoticeOptions = { 28 | title: chrome.i18n.getMessage("deArrowMessageRecieved"), 29 | textBoxes: [{ 30 | text: chrome.i18n.getMessage("warningChatInfo"), 31 | icon: null 32 | }, ...warningReason.split("\n").map((reason) => ({ 33 | text: reason, 34 | icon: null 35 | }))], 36 | buttons: [{ 37 | name: chrome.i18n.getMessage("questionButton"), 38 | listener: () => openChat({ 39 | displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}` 40 | }) 41 | }, 42 | { 43 | name: chrome.i18n.getMessage("warningConfirmButton"), 44 | listener: async () => { 45 | const result = await asyncRequestToServer("POST", "/api/warnUser", { 46 | userID: Config.config.userID, 47 | enabled: false 48 | }); 49 | 50 | if (result.ok) { 51 | notice?.close(); 52 | } else { 53 | alert(`${chrome.i18n.getMessage("warningError")} ${result.status}`); 54 | } 55 | } 56 | }], 57 | timed: false 58 | }; 59 | 60 | notice = new GenericNotice(contentContainer, "warningNotice", options); 61 | } 62 | } 63 | 64 | export function openChat(config: ChatConfig): void { 65 | window.open("https://chat.sponsor.ajay.app/#" + objectToURI("", config, false)); 66 | } 67 | -------------------------------------------------------------------------------- /test/urlParser.test.ts: -------------------------------------------------------------------------------- 1 | import { getStartTimeFromUrl } from '../src/utils/urlParser'; 2 | 3 | describe("getStartTimeFromUrl", () => { 4 | it("parses with a number", () => { 5 | expect(getStartTimeFromUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=123")).toBe(123); 6 | }); 7 | 8 | it("parses with seconds", () => { 9 | expect(getStartTimeFromUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=123s")).toBe(123); 10 | }); 11 | 12 | it("parses with minutes", () => { 13 | expect(getStartTimeFromUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=23m3s")).toBe(23 * 60 + 3); 14 | }); 15 | 16 | it("parses with hours", () => { 17 | expect(getStartTimeFromUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=1h2m3s")).toBe(1 * 60 * 60 + 2 * 60 + 3); 18 | }); 19 | 20 | it("works with time_continue", () => { 21 | expect(getStartTimeFromUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ&time_continue=123")).toBe(123); 22 | }); 23 | 24 | it("works with no time", () => { 25 | expect(getStartTimeFromUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ")).toBe(0); 26 | }); 27 | }); -------------------------------------------------------------------------------- /tsconfig-production.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "noImplicitReturns": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "sourceMap": false, 9 | "outDir": "dist/js", 10 | "noEmitOnError": false, 11 | "typeRoots": [ "node_modules/@types" ], 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "lib": [ 15 | "es2019", 16 | "dom", 17 | "dom.iterable" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "noImplicitAny": false, 6 | "noImplicitReturns": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "sourceMap": true, 9 | "outDir": "dist/js", 10 | "noEmitOnError": false, 11 | "typeRoots": [ "node_modules/@types" ], 12 | "resolveJsonModule": true, 13 | "jsx": "react", 14 | "lib": [ 15 | "es2019", 16 | "dom", 17 | "dom.iterable" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*" 22 | ] 23 | } -------------------------------------------------------------------------------- /webpack/configDiffPlugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { readFile } = require("fs/promises") 3 | let logger; 4 | 5 | const readFileContents = (name) => readFile(name) 6 | .then(data => JSON.parse(data)) 7 | 8 | // partialDeepEquals from ajayyy/SponsorBlockServer 9 | function partialDeepEquals (actual, expected, logger) { 10 | // loop over key, value of expected 11 | let failed = false; 12 | for (const [ key, value ] of Object.entries(expected)) { 13 | if (key === "serverAddress" || key === "testingServerAddress" || key === "serverAddressComment" || key === "freeChapterAccess") continue 14 | // if value is object, recurse 15 | const actualValue = actual?.[key] 16 | if (typeof value !== "string" && Array.isArray(value)) { 17 | if (!arrayPartialDeepEquals(actualValue, value)) { 18 | printActualExpected(key, actualValue, value, logger) 19 | failed = true 20 | } 21 | } else if (typeof value === "object") { 22 | if (partialDeepEquals(actualValue, value, logger)) { 23 | console.log("obj failed") 24 | printActualExpected(key, actualValue, value, logger) 25 | failed = true 26 | } 27 | } else if (actualValue !== value) { 28 | printActualExpected(key, actualValue, value, logger) 29 | failed = true 30 | } 31 | } 32 | return failed 33 | } 34 | 35 | const arrayPartialDeepEquals = (actual, expected) => 36 | expected.every(a => actual?.includes(a)) 37 | 38 | function printActualExpected(key, actual, expected, logger) { 39 | logger.error(`Differing value for: ${key}`) 40 | logger.error(`Actual: ${JSON.stringify(actual)}`) 41 | logger.error(`Expected: ${JSON.stringify(expected)}`) 42 | } 43 | 44 | class configDiffPlugin { 45 | apply(compiler) { 46 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 47 | compiler.hooks.done.tapAsync("configDiffPlugin", async (stats, callback) => { 48 | logger = compiler.getInfrastructureLogger('configDiffPlugin') 49 | logger.log('Checking for config.json diff...') 50 | 51 | // check example 52 | const exampleConfig = await readFileContents("./config.json.example") 53 | const currentConfig = await readFileContents("./config.json") 54 | 55 | const difference = partialDeepEquals(currentConfig, exampleConfig, logger) 56 | if (difference) { 57 | logger.warn("config.json is missing values from config.json.example") 58 | } else { 59 | logger.info("config.json is not missing any values from config.json.example") 60 | } 61 | callback() 62 | }) 63 | } 64 | } 65 | 66 | module.exports = configDiffPlugin; 67 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | 5 | module.exports = env => merge(common(env), { 6 | devtool: 'inline-source-map', 7 | mode: 'development' 8 | }); -------------------------------------------------------------------------------- /webpack/webpack.manifest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | const webpack = require("webpack"); 4 | const path = require('path'); 5 | const { validate } = require('schema-utils'); 6 | const fs = require('fs'); 7 | 8 | const manifest = require("../manifest/manifest.json"); 9 | const firefoxManifestExtra = require("../manifest/firefox-manifest-extra.json"); 10 | const chromeManifestExtra = require("../manifest/chrome-manifest-extra.json"); 11 | const safariManifestExtra = require("../manifest/safari-manifest-extra.json"); 12 | const betaManifestExtra = require("../manifest/beta-manifest-extra.json"); 13 | const firefoxBetaManifestExtra = require("../manifest/firefox-beta-manifest-extra.json"); 14 | const manifestV2ManifestExtra = require("../manifest/manifest-v2-extra.json"); 15 | 16 | // schema for options object 17 | const schema = { 18 | type: 'object', 19 | properties: { 20 | browser: { 21 | type: 'string' 22 | }, 23 | pretty: { 24 | type: 'boolean' 25 | }, 26 | steam: { 27 | type: 'string' 28 | } 29 | } 30 | }; 31 | 32 | class BuildManifest { 33 | constructor (options = {}) { 34 | validate(schema, options, "Build Manifest Plugin"); 35 | 36 | this.options = options; 37 | } 38 | 39 | apply() { 40 | const distFolder = path.resolve(__dirname, "../dist/"); 41 | const distManifestFile = path.resolve(distFolder, "manifest.json"); 42 | 43 | // Add missing manifest elements 44 | if (this.options.browser.toLowerCase() === "firefox") { 45 | mergeObjects(manifest, manifestV2ManifestExtra); 46 | mergeObjects(manifest, firefoxManifestExtra); 47 | } else if (this.options.browser.toLowerCase() === "chrome" 48 | || this.options.browser.toLowerCase() === "chromium" 49 | || this.options.browser.toLowerCase() === "edge") { 50 | mergeObjects(manifest, chromeManifestExtra); 51 | } else if (this.options.browser.toLowerCase() === "safari") { 52 | mergeObjects(manifest, manifestV2ManifestExtra); 53 | mergeObjects(manifest, safariManifestExtra); 54 | manifest.optional_permissions = manifest.optional_permissions.filter((a) => a !== "*://*/*"); 55 | } 56 | 57 | if (this.options.stream === "beta") { 58 | mergeObjects(manifest, betaManifestExtra); 59 | 60 | if (this.options.browser.toLowerCase() === "firefox") { 61 | mergeObjects(manifest, firefoxBetaManifestExtra); 62 | } 63 | } 64 | 65 | let result = JSON.stringify(manifest); 66 | if (this.options.pretty) result = JSON.stringify(manifest, null, 2); 67 | 68 | fs.mkdirSync(distFolder, {recursive: true}); 69 | fs.writeFileSync(distManifestFile, result); 70 | } 71 | } 72 | 73 | function mergeObjects(object1, object2) { 74 | for (const key in object2) { 75 | if (key in object1) { 76 | if (Array.isArray(object1[key])) { 77 | object1[key] = object1[key].concat(object2[key]); 78 | } else if (typeof object1[key] == 'object') { 79 | mergeObjects(object1[key], object2[key]); 80 | } else { 81 | object1[key] = object2[key]; 82 | } 83 | } else { 84 | object1[key] = object2[key]; 85 | } 86 | } 87 | } 88 | 89 | module.exports = BuildManifest; -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | 5 | module.exports = env => { 6 | let mode = "production"; 7 | env.mode = mode; 8 | 9 | return merge(common(env), { 10 | mode 11 | }); 12 | }; --------------------------------------------------------------------------------