├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── SECURITY.md ├── pull_request_template.md └── workflows │ ├── tests.yml │ ├── submit.yml │ └── deploying.yml ├── assets ├── active.png ├── icon.png ├── inactive.png └── inter │ ├── bold.ttf │ ├── regular.ttf │ └── OFL.txt ├── src ├── types │ ├── request.ts │ └── response.ts ├── utils │ ├── domains.ts │ ├── extension.ts │ ├── fetcher.ts │ ├── tabs.ts │ ├── storage.ts │ └── declarativeNetRequest.ts ├── components │ ├── Frame.tsx │ ├── Frame.css │ ├── DisabledScreen.css │ ├── DisabledScreen.tsx │ ├── BottomLabel.css │ ├── BottomLabel.tsx │ ├── Button.tsx │ ├── SetupScreen.css │ ├── Button.css │ ├── SetupScreen.tsx │ ├── ToggleButton.tsx │ ├── ToggleButton.css │ └── Icon.tsx ├── hooks │ ├── useVersion.ts │ ├── useDomain.ts │ ├── usePermission.ts │ └── useDomainWhitelist.ts ├── contents │ └── movie-web.ts ├── background.ts ├── Popup.css ├── font.css ├── background │ └── messages │ │ ├── hello.ts │ │ ├── openPage.ts │ │ ├── prepareStream.ts │ │ └── makeRequest.ts ├── popup.tsx └── tabs │ ├── PermissionGrant.css │ ├── PermissionGrant.tsx │ ├── PermissionRequest.css │ └── PermissionRequest.tsx ├── .editorconfig ├── .vscode ├── extensions.json └── settings.json ├── pnpm-workspace.yaml ├── .prettierrc.mjs ├── tsconfig.json ├── manifest.json ├── .gitignore ├── LICENSE ├── .eslintrc.js ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @movie-web/core 2 | -------------------------------------------------------------------------------- /assets/active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-stream/extension/HEAD/assets/active.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-stream/extension/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-stream/extension/HEAD/assets/inactive.png -------------------------------------------------------------------------------- /assets/inter/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-stream/extension/HEAD/assets/inter/bold.ttf -------------------------------------------------------------------------------- /assets/inter/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p-stream/extension/HEAD/assets/inter/regular.ttf -------------------------------------------------------------------------------- /src/types/request.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 2 | export interface BaseRequest {} 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "editorconfig.editorconfig" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please visit the [main document at primary repository](https://github.com/sussy-code/smov/blob/dev/.github/CONTRIBUTING.md). 2 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Please visit the [main document at primary repository](https://github.com/sussy-code/smov/blob/dev/.github/CODE_OF_CONDUCT.md). 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@parcel/watcher' 3 | - '@swc/core' 4 | - esbuild 5 | - lmdb 6 | - msgpackr-extract 7 | - sharp 8 | - unrs-resolver 9 | -------------------------------------------------------------------------------- /src/types/response.ts: -------------------------------------------------------------------------------- 1 | export type BaseResponse = 2 | | ({ 3 | success: true; 4 | } & T) 5 | | { 6 | success: false; 7 | error: string; 8 | }; 9 | -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | export default { 5 | printWidth: 120, 6 | trailingComma: 'all', 7 | singleQuote: true, 8 | endOfLine: 'auto', 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/domains.ts: -------------------------------------------------------------------------------- 1 | export function makeUrlIntoDomain(url: string): string | null { 2 | try { 3 | const u = new URL(url); 4 | if (!['http:', 'https:'].includes(u.protocol)) return null; 5 | return u.host.toLowerCase(); 6 | } catch { 7 | return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Frame.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | import './Frame.css'; 4 | 5 | export interface FrameProps { 6 | children?: ReactNode; 7 | } 8 | 9 | export function Frame(props: FrameProps) { 10 | return
{props.children}
; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Frame.css: -------------------------------------------------------------------------------- 1 | .frame { 2 | height: 100%; 3 | width: 100%; 4 | background-color: #0a0a0a; 5 | /* background-image: radial-gradient(271.48% 132.05% at 136.13% 65.62%, #271945b3 0%, #1c1c2c00 100%), radial-gradient(671.15% 123.02% at 76.68% -34.38%, #272753 0%, #17172000 100%); */ 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/extension.ts: -------------------------------------------------------------------------------- 1 | export const isChrome = () => { 2 | return chrome.runtime.getURL('').startsWith('chrome-extension://'); 3 | }; 4 | 5 | export const isFirefox = () => { 6 | try { 7 | return browser.runtime.getURL('').startsWith('moz-extension://'); 8 | } catch { 9 | return false; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "eslint.format.enable": true, 5 | "[json]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 10 | } 11 | } -------------------------------------------------------------------------------- /src/hooks/useVersion.ts: -------------------------------------------------------------------------------- 1 | export function getVersion(ops?: { prefixed?: boolean }) { 2 | const prefix = ops?.prefixed ? 'v' : ''; 3 | const manifest = (chrome || browser).runtime.getManifest(); 4 | return `${prefix}${manifest.version}`; 5 | } 6 | 7 | export function useVersion(ops?: { prefixed?: boolean }) { 8 | return getVersion(ops); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plasmo/templates/tsconfig.base", 3 | "exclude": ["node_modules"], 4 | "include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"], 5 | 6 | "compilerOptions": { 7 | "jsx": "react-jsx", 8 | "strict": true, 9 | "paths": { 10 | "~*": ["./src/*"] 11 | }, 12 | "baseUrl": "." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The latest version of P-Stream is the only version that is supported, as it is the only version that is being actively developed. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | You can contact the P-Stream maintainers to report a vulnerability: 10 | - Report the vulnerability in the [Discord server](https://discord.gg/g742e7Mu2W) 11 | -------------------------------------------------------------------------------- /src/components/DisabledScreen.css: -------------------------------------------------------------------------------- 1 | .disabled { 2 | color: white; 3 | text-align: center; 4 | max-width: 230px; 5 | font-size: 16px; 6 | } 7 | 8 | .disabled .icon { 9 | font-size: 1.5rem; 10 | margin-bottom: 1rem; 11 | text-align: center; 12 | display: inline-block; 13 | color: #B44868; 14 | } 15 | 16 | .disabled p { 17 | color: #4A4864; 18 | } 19 | 20 | .disabled strong { 21 | color: white; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/DisabledScreen.tsx: -------------------------------------------------------------------------------- 1 | import './DisabledScreen.css'; 2 | import { Icon } from './Icon'; 3 | 4 | export function DisabledScreen() { 5 | return ( 6 |
7 |
8 | 9 |
10 |

11 | The P-Stream extension can not be used on this page 12 |

13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/contents/movie-web.ts: -------------------------------------------------------------------------------- 1 | import { relayMessage } from '@plasmohq/messaging'; 2 | import type { PlasmoCSConfig } from 'plasmo'; 3 | 4 | export const config: PlasmoCSConfig = { 5 | matches: [''], 6 | }; 7 | 8 | relayMessage({ 9 | name: 'hello', 10 | }); 11 | 12 | relayMessage({ 13 | name: 'makeRequest', 14 | }); 15 | 16 | relayMessage({ 17 | name: 'prepareStream', 18 | }); 19 | 20 | relayMessage({ 21 | name: 'openPage', 22 | }); 23 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import { isChrome } from '~utils/extension'; 2 | 3 | // Both brave and firefox for some reason need this extension reload, 4 | // If this isn't done, they will never load properly and will fail updateDynamicRules() 5 | if (isChrome()) { 6 | chrome.runtime.onStartup.addListener(() => { 7 | chrome.runtime.reload(); 8 | }); 9 | } else { 10 | browser.runtime.onStartup.addListener(() => { 11 | browser.runtime.reload(); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "P-Stream extension", 4 | "optional_host_permissions": ["\u003Call_urls>"], 5 | "permissions": ["storage", "declarativeNetRequest", "activeTab", "cookies"], 6 | "update_url": "https://clients2.google.com/service/update2/crx", 7 | "version": "${version}", 8 | "web_accessible_resources": [ 9 | { 10 | "matches": ["\u003Call_urls>"], 11 | "resources": ["assets/active.png", "assets/inactive.png"] 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/Popup.css: -------------------------------------------------------------------------------- 1 | @import url("./font.css"); 2 | 3 | html { 4 | height: 300px; 5 | width: 300px; 6 | } 7 | 8 | body { 9 | min-height: 300px; 10 | min-width: 300px; 11 | margin: 0; 12 | height: 100%; 13 | max-height: 100%; 14 | } 15 | 16 | .popup { 17 | height: 100%; 18 | width: 100%; 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | padding: 20px; 24 | box-sizing: border-box; 25 | } 26 | 27 | #__plasmo { 28 | height: 100%; 29 | background-color: #0a0a0a; 30 | } 31 | -------------------------------------------------------------------------------- /src/font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Inter"; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(data-base64:../assets/inter/regular.ttf); 6 | } 7 | 8 | @font-face { 9 | font-family: "Inter"; 10 | font-style: bold; 11 | font-weight: 700; 12 | src: url(data-base64:../assets/inter/bold.ttf); 13 | } 14 | 15 | body, html { 16 | font-family: "Inter", Arial, Helvetica, sans-serif; 17 | font-size: 13px; 18 | } 19 | 20 | * { 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | button { 26 | font-family: inherit; 27 | font-size: inherit; 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | #cache 13 | .turbo 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | 25 | # local env files 26 | .env* 27 | 28 | out/ 29 | build/ 30 | dist/ 31 | 32 | # plasmo - https://www.plasmo.com 33 | .plasmo 34 | 35 | # bpp - http://bpp.browser.market/ 36 | keys.json 37 | 38 | # typescript 39 | .tsbuildinfo 40 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | This pull request resolves #XXX 2 | 3 | - [ ] I have read and agreed to the [code of conduct](https://github.com/sussy-code/smov/blob/dev/.github/CODE_OF_CONDUCT.md). 4 | - [ ] I have read and complied with the [contributing guidelines](https://github.com/sussy-code/smov/blob/dev/.github/CONTRIBUTING.md). 5 | - [ ] What I'm implementing was assigned to me and is an [approved issue](https://github.com/sussy-code/smov/issues?q=is%3Aopen+is%3Aissue+label%3Aapproved). For reference, please take a look at our [GitHub projects](https://github.com/sussy-code/smov/projects). 6 | - [ ] I have tested all of my changes. 7 | -------------------------------------------------------------------------------- /src/hooks/useDomain.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | import { makeUrlIntoDomain } from '~utils/domains'; 4 | import { listenToTabChanges, queryCurrentDomain, stopListenToTabChanges } from '~utils/tabs'; 5 | 6 | export function useDomain(): null | string { 7 | const [domain, setDomain] = useState(null); 8 | 9 | useEffect(() => { 10 | const listen = () => queryCurrentDomain(setDomain); 11 | listen(); 12 | listenToTabChanges(listen); 13 | return () => { 14 | stopListenToTabChanges(listen); 15 | }; 16 | }, []); 17 | 18 | return domain ? makeUrlIntoDomain(domain) : null; 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | 10 | jobs: 11 | testing: 12 | name: Testing 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - uses: pnpm/action-setup@v2 20 | with: 21 | version: 8 22 | 23 | - name: Install Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: "pnpm" 28 | 29 | - name: Install packages 30 | run: pnpm install 31 | 32 | - name: Run linting 33 | run: pnpm run lint 34 | -------------------------------------------------------------------------------- /src/components/BottomLabel.css: -------------------------------------------------------------------------------- 1 | .bottom-label { 2 | position: absolute; 3 | bottom: 1rem; 4 | display: flex; 5 | align-items: center; 6 | gap: .5rem; 7 | font-weight: normal; 8 | color: #4A4863; 9 | font-size: 14px; 10 | } 11 | 12 | .top-right-label { 13 | position: absolute; 14 | right: 1rem; 15 | top: 1rem; 16 | font-weight: normal; 17 | color: #4A4863; 18 | font-size: 14px; 19 | } 20 | 21 | .github-link { 22 | color: #4A4863; 23 | text-decoration: none; 24 | transition: opacity 0.2s ease; 25 | } 26 | 27 | .github-link:hover { 28 | text-decoration: underline; 29 | } 30 | 31 | .dot { 32 | width: 3px; 33 | height: 3px; 34 | background: currentColor; 35 | border-radius: 100px; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/BottomLabel.tsx: -------------------------------------------------------------------------------- 1 | import { useVersion } from '~hooks/useVersion'; 2 | import './BottomLabel.css'; 3 | 4 | export function BottomLabel() { 5 | const version = useVersion({ prefixed: true }); 6 | 7 | return ( 8 |

9 | {version} 10 |
11 | P-Stream 12 |

17 | ); 18 | } 19 | 20 | export function TopRightLabel() { 21 | const version = useVersion({ prefixed: true }); 22 | 23 | return

{version}

; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | import './Button.css'; 4 | 5 | export interface ButtonProps { 6 | type?: 'primary' | 'secondary'; 7 | href?: string; 8 | children?: ReactNode; 9 | onClick?: () => void; 10 | full?: boolean; 11 | className?: string; 12 | } 13 | 14 | export function Button(props: ButtonProps) { 15 | const classes = `button button-${props.type ?? 'primary'} ${props.className ?? ''} ${props.full ? 'full' : ''}`; 16 | if (props.href) 17 | return ( 18 | 19 | {props.children} 20 | 21 | ); 22 | return ( 23 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/SetupScreen.css: -------------------------------------------------------------------------------- 1 | .setup-screen .title { 2 | font-size: 1.5rem; 3 | color: white; 4 | } 5 | 6 | .setup-screen .paragraph { 7 | font-size: 1rem; 8 | color: #666485; 9 | max-width: 220px; 10 | } 11 | 12 | .setup-screen .top { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | flex: 1; 18 | } 19 | 20 | .setup-screen { 21 | display: flex; 22 | flex-direction: column; 23 | height: 100%; 24 | width: 100%; 25 | box-sizing: border-box; 26 | padding: 2rem; 27 | text-align: center; 28 | } 29 | 30 | .setup-screen .icon { 31 | /* background-color: #0b0b1b77; */ 32 | color: #8288FE; 33 | height: 40px; 34 | font-size: 40px; 35 | /* width: 40px; */ 36 | /* border-radius: 9999px; */ 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | margin-bottom: 20px; 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/usePermission.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | 3 | import { useDomainWhitelist } from './useDomainWhitelist'; 4 | 5 | export async function hasPermission() { 6 | return chrome.permissions.contains({ 7 | origins: [''], 8 | }); 9 | } 10 | 11 | export function usePermission() { 12 | const { addDomain } = useDomainWhitelist(); 13 | const [permission, setPermission] = useState(false); 14 | 15 | const grantPermission = useCallback(async (domain?: string) => { 16 | const granted = await chrome.permissions.request({ 17 | origins: [''], 18 | }); 19 | setPermission(granted); 20 | if (granted && domain) addDomain(domain); 21 | return granted; 22 | }, []); 23 | 24 | useEffect(() => { 25 | hasPermission().then((has) => setPermission(has)); 26 | }, []); 27 | 28 | return { 29 | hasPermission: permission, 30 | grantPermission, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { type Request as MakeRequest } from '~background/messages/makeRequest'; 2 | 3 | export function makeFullUrl(url: string, ops?: MakeRequest): string { 4 | // glue baseUrl and rest of url together 5 | let leftSide = ops?.baseUrl ?? ''; 6 | let rightSide = url; 7 | 8 | // left side should always end with slash, if its set 9 | if (leftSide.length > 0 && !leftSide.endsWith('/')) leftSide += '/'; 10 | 11 | // right side should never start with slash 12 | if (rightSide.startsWith('/')) rightSide = rightSide.slice(1); 13 | 14 | const fullUrl = leftSide + rightSide; 15 | if (!fullUrl.startsWith('http://') && !fullUrl.startsWith('https://')) 16 | throw new Error(`Invald URL -- URL doesn't start with a http scheme: '${fullUrl}'`); 17 | 18 | const parsedUrl = new URL(fullUrl); 19 | Object.entries(ops?.query ?? {}).forEach(([k, v]) => { 20 | parsedUrl.searchParams.set(k, v); 21 | }); 22 | 23 | return parsedUrl.toString(); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | padding: 1rem; 3 | border-radius: 12px; 4 | border: 0; 5 | font-size: 1rem; 6 | text-align: center; 7 | cursor: pointer; 8 | transition: background-color 100ms ease-in-out, color 100ms ease-in-out, transform 100ms ease-in-out; 9 | } 10 | 11 | .button.full { 12 | width: 100%; 13 | } 14 | 15 | .button, .button:focus, .button:active, .button:visited { 16 | text-decoration: none; 17 | } 18 | 19 | .button:active { 20 | transform: scale(1.05); 21 | } 22 | 23 | .button.button-secondary { 24 | background-color: hsl(240 3.7% 15.9%); 25 | color: hsl(0 0% 98%); 26 | } 27 | 28 | .button.button-secondary:hover { 29 | background-color: hsl(240 3.7% 15.9%/.8); 30 | color: hsl(0 0% 98%); 31 | } 32 | 33 | .button.button-primary { 34 | background-color: hsl(0 0% 98%); 35 | color: hsl(240 5.9% 10%); 36 | } 37 | 38 | .button.button-primary:hover { 39 | background-color: hsl(0 0% 98%/.9); 40 | color: hsl(240 5.9% 10%); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/SetupScreen.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { Button } from '~components/Button'; 4 | import { Icon } from '~components/Icon'; 5 | 6 | import './SetupScreen.css'; 7 | 8 | export function SetupScreen() { 9 | const open = useCallback(() => { 10 | const url = (chrome || browser).runtime.getURL(`/tabs/PermissionRequest.html`); 11 | (chrome || browser).tabs.create({ url }); 12 | window.close(); 13 | }, []); 14 | 15 | return ( 16 |
17 |
18 |
19 | 20 |
21 |

Let's get things configured.

22 |

23 | To get started, we need to setup some things first. Click the button below to continue. 24 |

25 |
26 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: latest 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build the extension 27 | run: pnpm build 28 | - name: Package the extension into a zip artifact 29 | run: pnpm package 30 | - name: Browser Platform Publish 31 | uses: PlasmoHQ/bpp@v3 32 | with: 33 | keys: ${{ secrets.SUBMIT_KEYS }} 34 | artifact: build/chrome-mv3-prod.zip 35 | -------------------------------------------------------------------------------- /src/background/messages/hello.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoMessaging } from '@plasmohq/messaging'; 2 | 3 | import { hasPermission } from '~hooks/usePermission'; 4 | import { getVersion } from '~hooks/useVersion'; 5 | import type { BaseRequest } from '~types/request'; 6 | import type { BaseResponse } from '~types/response'; 7 | import { isDomainWhitelisted } from '~utils/storage'; 8 | 9 | type Response = BaseResponse<{ 10 | version: string; 11 | allowed: boolean; 12 | hasPermission: boolean; 13 | }>; 14 | 15 | const handler: PlasmoMessaging.MessageHandler = async (req, res) => { 16 | try { 17 | if (!req.sender?.tab?.url) throw new Error('No tab URL found in the request.'); 18 | 19 | const version = getVersion(); 20 | res.send({ 21 | success: true, 22 | version, 23 | allowed: await isDomainWhitelisted(req.sender.tab.url), 24 | hasPermission: await hasPermission(), 25 | }); 26 | } catch (err) { 27 | res.send({ 28 | success: false, 29 | error: err instanceof Error ? err.message : String(err), 30 | }); 31 | } 32 | }; 33 | 34 | export default handler; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 movie-web 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/tabs.ts: -------------------------------------------------------------------------------- 1 | import { isChrome } from './extension'; 2 | 3 | export function queryCurrentDomain(cb: (domain: string | null) => void) { 4 | const handle = (tabUrl: string | undefined) => { 5 | if (!tabUrl) cb(null); 6 | else cb(tabUrl); 7 | }; 8 | const ops = { active: true, currentWindow: true } as const; 9 | 10 | if (isChrome()) chrome.tabs.query(ops).then((tabs) => handle(tabs[0]?.url)); 11 | else browser.tabs.query(ops).then((tabs) => handle(tabs[0]?.url)); 12 | } 13 | 14 | export function listenToTabChanges(cb: () => void) { 15 | if (isChrome()) { 16 | chrome.tabs.onActivated.addListener(cb); 17 | chrome.tabs.onUpdated.addListener(cb); 18 | } else if (browser) { 19 | browser.tabs.onActivated.addListener(cb); 20 | browser.tabs.onUpdated.addListener(cb); 21 | } 22 | } 23 | 24 | export function stopListenToTabChanges(cb: () => void) { 25 | if (isChrome()) { 26 | chrome.tabs.onActivated.removeListener(cb); 27 | chrome.tabs.onUpdated.removeListener(cb); 28 | } else if (browser) { 29 | browser.tabs.onActivated.removeListener(cb); 30 | browser.tabs.onUpdated.removeListener(cb); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/background/messages/openPage.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoMessaging } from '@plasmohq/messaging'; 2 | 3 | import type { BaseRequest } from '~types/request'; 4 | import type { BaseResponse } from '~types/response'; 5 | import { isChrome } from '~utils/extension'; 6 | 7 | type Request = BaseRequest & { 8 | page: string; 9 | redirectUrl: string; 10 | }; 11 | 12 | const handler: PlasmoMessaging.MessageHandler = async (req, res) => { 13 | try { 14 | if (!req.sender?.tab?.id) throw new Error('No tab ID found in the request.'); 15 | if (!req.body) throw new Error('No body found in the request.'); 16 | 17 | const searchParams = new URLSearchParams(); 18 | searchParams.set('redirectUrl', req.body.redirectUrl); 19 | const url = (chrome || browser).runtime.getURL(`/tabs/${req.body.page}.html?${searchParams.toString()}`); 20 | 21 | if (isChrome()) { 22 | await chrome.tabs.update(req.sender.tab.id, { 23 | url, 24 | }); 25 | } else { 26 | await browser.tabs.update(req.sender.tab.id, { url }); 27 | } 28 | 29 | res.send({ 30 | success: true, 31 | }); 32 | } catch (err) { 33 | res.send({ 34 | success: false, 35 | error: err instanceof Error ? err.message : String(err), 36 | }); 37 | } 38 | }; 39 | 40 | export default handler; 41 | -------------------------------------------------------------------------------- /src/popup.tsx: -------------------------------------------------------------------------------- 1 | import { BottomLabel, TopRightLabel } from '~components/BottomLabel'; 2 | import { DisabledScreen } from '~components/DisabledScreen'; 3 | import { Frame } from '~components/Frame'; 4 | import { SetupScreen } from '~components/SetupScreen'; 5 | import { ToggleButton } from '~components/ToggleButton'; 6 | import { useDomain } from '~hooks/useDomain'; 7 | import { useToggleWhitelistDomain } from '~hooks/useDomainWhitelist'; 8 | import { usePermission } from '~hooks/usePermission'; 9 | 10 | import './Popup.css'; 11 | 12 | function IndexPopup() { 13 | const domain = useDomain(); 14 | const { isWhitelisted, toggle } = useToggleWhitelistDomain(domain); 15 | const { hasPermission } = usePermission(); 16 | 17 | let page = 'toggle'; 18 | if (!hasPermission) page = 'perm'; 19 | else if (!domain) page = 'disabled'; 20 | 21 | return page === 'perm' ? ( 22 | 23 |
24 | 25 | 26 |
27 | 28 | ) : ( 29 | 30 |
31 | {page === 'toggle' && domain ? : null} 32 | {page === 'disabled' ? : null} 33 | 34 |
35 | 36 | ); 37 | } 38 | 39 | export default IndexPopup; 40 | -------------------------------------------------------------------------------- /src/components/ToggleButton.tsx: -------------------------------------------------------------------------------- 1 | import { Icon } from './Icon'; 2 | import './ToggleButton.css'; 3 | 4 | export interface ToggleButtonProps { 5 | onClick?: () => void; 6 | active?: boolean; 7 | domain: string; 8 | } 9 | 10 | export function ToggleButton(props: ToggleButtonProps) { 11 | const opacityStyle = { 12 | opacity: props.active ? 1 : 0, 13 | }; 14 | 15 | return ( 16 |
17 |
18 | 37 |
38 |

39 | Extension {props.active ? 'enabled' : 'disabled'}
on {props.domain} 40 |

41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/tabs/PermissionGrant.css: -------------------------------------------------------------------------------- 1 | @import url("../font.css"); 2 | 3 | html { 4 | height: 100%; 5 | font-size: 16px; 6 | } 7 | 8 | body { 9 | height: 100%; 10 | max-height: 100%; 11 | } 12 | 13 | #__plasmo { 14 | display: flex; 15 | flex-direction: column; 16 | height: 100%; 17 | background-color: #0a0a0a; 18 | } 19 | 20 | .container.permission-grant { 21 | height: 100%; 22 | width: 100%; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .permission-grant .inner-container { 29 | width: 400px; 30 | display: flex; 31 | flex-direction: column; 32 | gap: 1rem; 33 | } 34 | 35 | .permission-grant .permission-card { 36 | display: flex; 37 | align-items: center; 38 | flex-direction: column; 39 | justify-content: center; 40 | border-radius: 20px; 41 | padding: 40px; 42 | padding-top: 50px; 43 | font-size: 14px; 44 | border: 1px solid #272A37; 45 | } 46 | 47 | .permission-grant h1 { 48 | font-size: 1.25rem; 49 | margin-bottom: 0.5rem; 50 | } 51 | 52 | .permission-grant .permission-card > p { 53 | font-size: 1rem; 54 | margin-top: .5rem; 55 | margin-bottom: 1rem; 56 | } 57 | 58 | .permission-grant .color-white { 59 | color: #ffffff; 60 | } 61 | 62 | .permission-grant .text-color { 63 | color: #73739d; 64 | } 65 | 66 | .permission-grant .buttons { 67 | width: 100%; 68 | margin-top: 2rem; 69 | } 70 | 71 | .permission-grant .buttons>*+* { 72 | margin-top: 1rem; 73 | } 74 | -------------------------------------------------------------------------------- /src/hooks/useDomainWhitelist.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { usePermission } from '~hooks/usePermission'; 4 | import { useDomainStorage } from '~utils/storage'; 5 | 6 | export function useDomainWhitelist() { 7 | const [domainWhitelist, setDomainWhitelist] = useDomainStorage(); 8 | 9 | const removeDomain = useCallback((domain: string | null) => { 10 | if (!domain) return; 11 | setDomainWhitelist((s) => [...(s ?? []).filter((v) => v !== domain)]); 12 | }, []); 13 | 14 | const addDomain = useCallback((domain: string | null) => { 15 | if (!domain) return; 16 | setDomainWhitelist((s) => [...(s ?? []).filter((v) => v !== domain), domain]); 17 | }, []); 18 | 19 | return { 20 | removeDomain, 21 | addDomain, 22 | domainWhitelist, 23 | }; 24 | } 25 | 26 | export function useToggleWhitelistDomain(domain: string | null) { 27 | const { domainWhitelist, addDomain, removeDomain } = useDomainWhitelist(); 28 | const isWhitelisted = domainWhitelist.includes(domain ?? ''); 29 | const { grantPermission } = usePermission(); 30 | const iconPath = (chrome || browser).runtime.getURL(isWhitelisted ? 'assets/active.png' : 'assets/inactive.png'); 31 | 32 | (chrome || browser).action.setIcon({ 33 | path: iconPath, 34 | }); 35 | 36 | const toggle = useCallback(() => { 37 | if (!isWhitelisted) { 38 | addDomain(domain); 39 | return; 40 | } 41 | 42 | removeDomain(domain); 43 | }, [isWhitelisted, domain, addDomain, removeDomain, grantPermission]); 44 | 45 | return { 46 | toggle, 47 | isWhitelisted, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/background/messages/prepareStream.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoMessaging } from '@plasmohq/messaging'; 2 | 3 | import type { BaseRequest } from '~types/request'; 4 | import type { BaseResponse } from '~types/response'; 5 | import { setDynamicRules } from '~utils/declarativeNetRequest'; 6 | import { assertDomainWhitelist, modifiableResponseHeaders } from '~utils/storage'; 7 | 8 | interface Request extends BaseRequest { 9 | ruleId: number; 10 | targetDomains?: [string, ...string[]]; 11 | targetRegex?: string; 12 | requestHeaders?: Record; 13 | responseHeaders?: Record; 14 | } 15 | 16 | const handler: PlasmoMessaging.MessageHandler = async (req, res) => { 17 | try { 18 | if (!req.sender?.tab?.url) throw new Error('No tab URL found in the request.'); 19 | if (!req.body) throw new Error('No request body found in the request.'); 20 | 21 | // restrict what response headers can be modified 22 | req.body.responseHeaders = Object.keys(req.body.responseHeaders ?? {}) 23 | .filter((key) => modifiableResponseHeaders.includes(key.toLowerCase())) 24 | .reduce( 25 | (obj, key) => { 26 | obj[key] = (req.body?.responseHeaders ?? {})[key]; 27 | return obj; 28 | }, 29 | {} as Record, 30 | ); 31 | 32 | await assertDomainWhitelist(req.sender.tab.url); 33 | await setDynamicRules(req.body); 34 | res.send({ 35 | success: true, 36 | }); 37 | } catch (err) { 38 | res.send({ 39 | success: false, 40 | error: err instanceof Error ? err.message : String(err), 41 | }); 42 | } 43 | }; 44 | 45 | export default handler; 46 | -------------------------------------------------------------------------------- /src/tabs/PermissionGrant.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '~components/Button'; 2 | import { usePermission } from '~hooks/usePermission'; 3 | import { makeUrlIntoDomain } from '~utils/domains'; 4 | 5 | import './PermissionGrant.css'; 6 | 7 | export default function PermissionGrant() { 8 | const { grantPermission } = usePermission(); 9 | 10 | const queryParams = new URLSearchParams(window.location.search); 11 | const redirectUrl = queryParams.get('redirectUrl') ?? undefined; 12 | const domain = redirectUrl ? makeUrlIntoDomain(redirectUrl) : undefined; 13 | 14 | if (!domain) { 15 | return ( 16 |
17 |
18 |
19 |

Permission

20 |

21 | No domain found to grant permission to. 22 |

23 |
24 |
25 |
26 | ); 27 | } 28 | 29 | const redirectBack = () => { 30 | chrome.tabs.getCurrent((tab) => { 31 | if (!tab?.id) return; 32 | chrome.tabs.update(tab.id, { url: redirectUrl }); 33 | }); 34 | }; 35 | 36 | const handleGrantPermission = () => { 37 | grantPermission(domain).then(() => { 38 | redirectBack(); 39 | }); 40 | }; 41 | 42 | return ( 43 |
44 |
45 |
46 |

Permission

47 |

48 | The website {domain} wants to
use the extension on their page. 49 | Do you trust them? 50 |

51 |
52 | 55 | 58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from '@plasmohq/storage'; 2 | import { useStorage } from '@plasmohq/storage/hook'; 3 | 4 | import { makeUrlIntoDomain } from '~utils/domains'; 5 | 6 | export const DEFAULT_DOMAIN_WHITELIST = []; 7 | 8 | export const modifiableResponseHeaders = [ 9 | 'access-control-allow-origin', 10 | 'access-control-allow-methods', 11 | 'access-control-allow-headers', 12 | 'content-security-policy', 13 | 'content-security-policy-report-only', 14 | 'content-disposition', 15 | ]; 16 | 17 | const hostsWithCookiesAccess: RegExp[] = [ 18 | /^(?:.*\.)?ee3\.me$/, 19 | /^(?:.*\.)?rips\.cc$/, 20 | /^(?:.*\.)?m4ufree\.(?:tv|to|pw)$/, 21 | /^(?:.*\.)?goojara\.to$/, 22 | /^(?:.*\.)?levidia\.ch$/, 23 | /^(?:.*\.)?wootly\.ch$/, 24 | /^(?:.*\.)?multimovies\.(?:sbs|online|cloud)$/, 25 | ]; 26 | 27 | export function canAccessCookies(host: string): boolean { 28 | if (hostsWithCookiesAccess.some((regex) => regex.test(host))) return true; 29 | return false; 30 | } 31 | 32 | export const storage = new Storage(); 33 | 34 | const getDomainWhiteList = async () => { 35 | const whitelist = await storage.get('domainWhitelist'); 36 | if (!whitelist) await storage.set('domainWhitelist', DEFAULT_DOMAIN_WHITELIST); 37 | return whitelist ?? DEFAULT_DOMAIN_WHITELIST; 38 | }; 39 | 40 | const domainIsInWhitelist = async (domain: string) => { 41 | const whitelist = await getDomainWhiteList(); 42 | return whitelist?.some((d) => d.includes(domain)) ?? false; 43 | }; 44 | 45 | export function useDomainStorage() { 46 | return useStorage('domainWhitelist', (v) => v ?? DEFAULT_DOMAIN_WHITELIST); 47 | } 48 | 49 | export const isDomainWhitelisted = async (url: string | undefined) => { 50 | if (!url) return false; 51 | const domain = makeUrlIntoDomain(url); 52 | if (!domain) return false; 53 | return domainIsInWhitelist(domain); 54 | }; 55 | 56 | export const assertDomainWhitelist = async (url: string) => { 57 | const isWhiteListed = await isDomainWhitelisted(url); 58 | const currentDomain = makeUrlIntoDomain(url); 59 | if (!isWhiteListed) 60 | throw new Error( 61 | `${currentDomain} is not whitelisted. Open the extension and click on the power button to whitelist the site.`, 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | extends: ['airbnb', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'], 6 | ignorePatterns: ['dist/*', 'plugins/*', 'tests/*', '/*.cjs', '/*.js', '/*.ts', '/**/*.test.ts', 'test/*'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | project: './tsconfig.json', 10 | tsconfigRootDir: __dirname, 11 | }, 12 | settings: { 13 | 'import/resolver': { 14 | typescript: { 15 | project: './tsconfig.json', 16 | }, 17 | }, 18 | }, 19 | plugins: ['@typescript-eslint', 'import', 'prettier'], 20 | rules: { 21 | 'react/jsx-uses-react': 'off', 22 | 'react/react-in-jsx-scope': 'off', 23 | 'react/require-default-props': 'off', 24 | 'react/destructuring-assignment': 'off', 25 | 'no-underscore-dangle': 'off', 26 | '@typescript-eslint/no-explicit-any': 'off', 27 | 'no-console': 'off', 28 | '@typescript-eslint/no-this-alias': 'off', 29 | 'import/prefer-default-export': 'off', 30 | '@typescript-eslint/no-empty-function': 'off', 31 | 'no-shadow': 'off', 32 | '@typescript-eslint/no-shadow': ['error'], 33 | 'no-restricted-syntax': 'off', 34 | 'import/no-unresolved': ['error', { ignore: ['^virtual:'] }], 35 | 'consistent-return': 'off', 36 | 'no-continue': 'off', 37 | 'no-eval': 'off', 38 | 'no-await-in-loop': 'off', 39 | 'no-nested-ternary': 'off', 40 | 'no-param-reassign': ['error', { props: false }], 41 | 'prefer-destructuring': 'off', 42 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 43 | 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.tsx', '.jsx'] }], 44 | 'import/extensions': [ 45 | 'error', 46 | 'ignorePackages', 47 | { 48 | ts: 'never', 49 | tsx: 'never', 50 | }, 51 | ], 52 | 'import/order': [ 53 | 'error', 54 | { 55 | groups: ['builtin', 'external', 'internal', ['sibling', 'parent'], 'index', 'unknown'], 56 | 'newlines-between': 'always', 57 | alphabetize: { 58 | order: 'asc', 59 | caseInsensitive: true, 60 | }, 61 | }, 62 | ], 63 | 'sort-imports': [ 64 | 'error', 65 | { 66 | ignoreCase: false, 67 | ignoreDeclarationSort: true, 68 | ignoreMemberSort: false, 69 | memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], 70 | allowSeparatedGroups: true, 71 | }, 72 | ], 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/components/ToggleButton.css: -------------------------------------------------------------------------------- 1 | .button-container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | text-align: center; 7 | } 8 | 9 | 10 | .button-wrapper { 11 | width: 70px; 12 | height: 70px; 13 | position: relative; 14 | } 15 | 16 | .button-wrapper::after { 17 | position: absolute; 18 | z-index: 0; 19 | content: ''; 20 | width: 120%; 21 | height: 120%; 22 | top: 50%; 23 | left: 50%; 24 | transform: translate(-50%, -50%); 25 | background-color: rgba(119, 66, 233, 0.25); 26 | filter: blur(35px); 27 | opacity: 0; 28 | transition: opacity 300ms; 29 | } 30 | 31 | .button-wrapper.active::after { 32 | opacity: 1; 33 | } 34 | 35 | .toggle-button { 36 | cursor: pointer; 37 | position: absolute; 38 | z-index: 1; 39 | top: 0; 40 | left: 0; 41 | width: 100%; 42 | height: 100%; 43 | border-radius: 1000px; 44 | overflow: hidden; 45 | border: 0; 46 | background-color: transparent; 47 | transition: transform 50ms; 48 | } 49 | 50 | .inside-glow { 51 | transition: transform 300ms; 52 | transform: rotate(0deg); 53 | } 54 | 55 | .toggle-button:hover .inside-glow { 56 | transform: rotate(45deg); 57 | } 58 | 59 | .toggle-button:active { 60 | transform: scale(0.95); 61 | } 62 | 63 | .actual-button-style { 64 | position: absolute; 65 | top: 0; 66 | left: 0; 67 | width: 100%; 68 | height: 100%; 69 | border-radius: 1000px; 70 | box-sizing: border-box; 71 | border-color: rgba(96, 66, 166, 0.50); 72 | transition: opacity 300ms ease; 73 | border: 1px solid #322E48; 74 | background-image: linear-gradient(180deg, #232236 0%, #0E0D15 100%); 75 | } 76 | 77 | .actual-button-style.active { 78 | border-color: #48307F; 79 | background-image: linear-gradient(180deg, #463177 0%, #2D1C54 100%); 80 | } 81 | 82 | .toggle-button .inside-glow, .toggle-button svg { 83 | position: absolute; 84 | left: 50%; 85 | top: 50%; 86 | transform: translate(-50%, -50%); 87 | transition: color 300ms, transform 300ms; 88 | } 89 | 90 | .toggle-button .inside-glow { 91 | position: absolute; 92 | width: 90%; 93 | height: 90%; 94 | border-radius: 1000px; 95 | filter: blur(10px); 96 | z-index: 1; 97 | } 98 | 99 | .toggle-button svg { 100 | width: 35px; 101 | height: 35px; 102 | display: block; 103 | z-index: 10; 104 | } 105 | 106 | .toggle-button svg * { 107 | box-shadow: 0px 0px 0 4px rgba(0, 0, 0, 1) inset; 108 | } 109 | 110 | .button-container p { 111 | color: #4A4863; 112 | text-align: center; 113 | font-size: 16px; 114 | margin-top: 1rem; 115 | } 116 | 117 | .button-container strong { 118 | color: white; 119 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@p-stream/extension", 3 | "displayName": "P-Stream extension", 4 | "version": "1.3.7", 5 | "description": "Enhance your streaming experience with just one click", 6 | "author": "P-Stream", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build", 10 | "build:firefox": "plasmo build --target=firefox-mv3", 11 | "package": "plasmo package", 12 | "package:firefox": "npm run build:firefox && plasmo package --target=firefox-mv3 && mv build/firefox-mv3-prod.zip build/firefox-mv3-prod.xpi", 13 | "lint": "eslint --ext .tsx,.ts src", 14 | "lint:fix": "eslint --fix --ext .tsx,.ts src", 15 | "lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src", 16 | "preinstall": "npx -y only-allow pnpm" 17 | }, 18 | "dependencies": { 19 | "@plasmohq/messaging": "^0.7.2", 20 | "@plasmohq/storage": "^1.15.0", 21 | "react": "19.2.0", 22 | "react-dom": "19.2.0", 23 | "sharp": "^0.34.5" 24 | }, 25 | "devDependencies": { 26 | "@types/chrome": "0.1.27", 27 | "@types/firefox-webext-browser": "^120.0.5", 28 | "@types/node": "24.10.0", 29 | "@types/react": "19.2.2", 30 | "@types/react-dom": "19.2.2", 31 | "@typescript-eslint/eslint-plugin": "^8.46.3", 32 | "@typescript-eslint/parser": "^8.46.3", 33 | "eslint": "^8.57.1", 34 | "eslint-config-airbnb": "^19.0.4", 35 | "eslint-config-prettier": "^10.1.8", 36 | "eslint-import-resolver-typescript": "^3.10.1", 37 | "eslint-plugin-import": "^2.32.0", 38 | "eslint-plugin-prettier": "^5.5.4", 39 | "eslint-plugin-react": "^7.37.5", 40 | "eslint-plugin-react-hooks": "^7.0.1", 41 | "plasmo": "0.90.5", 42 | "prettier": "3.6.2", 43 | "typescript": "5.9.3" 44 | }, 45 | "manifest": { 46 | "permissions": [ 47 | "declarativeNetRequest", 48 | "activeTab", 49 | "cookies" 50 | ], 51 | "optional_host_permissions": [ 52 | "" 53 | ], 54 | "browser_specific_settings": { 55 | "gecko": { 56 | "id": "{e613be14-63c3-4bd9-8a4a-502c12bcf201}", 57 | "data_collection_permissions": { 58 | "required": [ 59 | "browsingActivity", 60 | "authenticationInfo", 61 | "websiteContent" 62 | ] 63 | } 64 | }, 65 | "gecko_android": { 66 | "id": "{e613be14-63c3-4bd9-8a4a-502c12bcf201}", 67 | "data_collection_permissions": { 68 | "required": [ 69 | "browsingActivity", 70 | "authenticationInfo", 71 | "websiteContent" 72 | ] 73 | } 74 | } 75 | }, 76 | "web_accessible_resources": [ 77 | { 78 | "resources": [ 79 | "assets/active.png", 80 | "assets/inactive.png" 81 | ], 82 | "matches": [ 83 | "" 84 | ] 85 | } 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/tabs/PermissionRequest.css: -------------------------------------------------------------------------------- 1 | @import url("../font.css"); 2 | 3 | html { 4 | font-size: 16px; 5 | } 6 | 7 | body { 8 | background-color: #0a0a0a; 9 | color: white; 10 | padding-bottom: 50px; 11 | } 12 | 13 | #__plasmo { 14 | height: unset; 15 | } 16 | 17 | .permission-request.container { 18 | width: 90%; 19 | margin: 100px auto; 20 | max-width: 628px; 21 | } 22 | 23 | .permission-request .inner-container { 24 | display: flex; 25 | flex-direction: column; 26 | } 27 | 28 | .permission-request h1 { 29 | font-size: 2rem; 30 | font-weight: bold; 31 | } 32 | 33 | .permission-request h2 { 34 | font-size: 1.5rem; 35 | font-weight: bold; 36 | margin-top: 4rem; 37 | margin-bottom: 1rem; 38 | } 39 | 40 | .permission-request .text-color { 41 | color: #7C7C97; 42 | } 43 | 44 | .permission-request .paragraph { 45 | font-size: 1rem; 46 | margin-top: 20px; 47 | max-width: 500px; 48 | line-height: 1.3; 49 | } 50 | 51 | .permission-request .card-list { 52 | margin: 1rem 0; 53 | } 54 | 55 | .permission-request .card-list>*+* { 56 | margin-top: 1rem; 57 | } 58 | 59 | .permission-request .card { 60 | padding: 25px 20px; 61 | border: 1px solid #272A37; 62 | display: grid; 63 | grid-template-columns: auto 1fr auto; 64 | gap: 1rem; 65 | border-radius: 11px; 66 | } 67 | 68 | @media screen and (max-width: 550px) { 69 | .permission-request .card { 70 | grid-template-columns: auto; 71 | grid-template-rows: auto auto auto; 72 | } 73 | } 74 | 75 | .permission-request .card .icon-circle { 76 | width: 2rem; 77 | height: 2rem; 78 | font-size: 1rem; 79 | background-color: rgba(39, 42, 55, 0.35); 80 | border: 1px solid #272A37; 81 | border-radius: 50%; 82 | display: flex; 83 | justify-content: center; 84 | align-items: center; 85 | } 86 | 87 | .permission-request .card.purple, .permission-request .card.purple .icon-circle { 88 | border-color: hsl(0 0% 98%); 89 | } 90 | 91 | .permission-request .card.purple .icon-circle { 92 | background-color: rgba(51, 27, 87, .4); 93 | } 94 | 95 | .permission-request .card .icon-circle > div { 96 | display: block; 97 | width: 1rem; 98 | height: 1rem; 99 | } 100 | 101 | .permission-request .card h3 { 102 | font-size: 1rem; 103 | font-weight: bold; 104 | color: white; 105 | margin-top: 5px; 106 | } 107 | 108 | .permission-request .card .paragraph { 109 | margin-top: 0.5rem; 110 | } 111 | 112 | .permission-request .card .center-y { 113 | display: flex; 114 | align-items: center; 115 | } 116 | 117 | .permission-request .card button { 118 | padding: 1rem; 119 | border-radius: 10px; 120 | border: 0; 121 | font-size: 1rem; 122 | font-weight: bold; 123 | background-color: #222033; 124 | color: white; 125 | } 126 | 127 | .permission-request .card:not(.purple) .icon-circle { 128 | color: #7C7C97; 129 | } 130 | 131 | .permission-request .footer { 132 | position: fixed; 133 | bottom: 0; 134 | left: 0; 135 | width: 100%; 136 | height: 100px; 137 | background: linear-gradient(to top, #0A0A10 30%, transparent); 138 | display: flex; 139 | justify-content: center; 140 | align-items: center; 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/deploying.yml: -------------------------------------------------------------------------------- 1 | name: Deploying 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - uses: pnpm/action-setup@v2 18 | with: 19 | version: 8 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20 25 | cache: 'pnpm' 26 | 27 | - name: Install pnpm packages 28 | run: pnpm install 29 | 30 | - name: Build chrome project 31 | run: pnpm run build 32 | 33 | - name: Package for chrome 34 | run: pnpm run package 35 | 36 | - name: Build firefox project 37 | run: pnpm run build:firefox 38 | 39 | - name: Package for firefox 40 | run: pnpm run package:firefox 41 | 42 | - name: Upload chrome package 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: chrome 46 | path: ./build/chrome-mv3-prod.zip 47 | 48 | - name: Upload firefox package 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: firefox 52 | path: ./build/firefox-mv3-prod.zip 53 | 54 | release: 55 | name: Release 56 | needs: build 57 | runs-on: ubuntu-latest 58 | 59 | steps: 60 | - name: Checkout code 61 | uses: actions/checkout@v4 62 | 63 | - name: Download Chrome artifact 64 | uses: actions/download-artifact@v4 65 | with: 66 | name: chrome 67 | path: ./chrome 68 | 69 | - name: Download Firefox artifact 70 | uses: actions/download-artifact@v4 71 | with: 72 | name: firefox 73 | path: ./firefox 74 | 75 | - name: Get version 76 | id: package-version 77 | uses: martinbeentjes/npm-get-version-action@main 78 | 79 | - name: Create Release 80 | id: create_release 81 | uses: actions/create-release@v1 82 | env: 83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 84 | with: 85 | tag_name: ${{ steps.package-version.outputs.current-version }} 86 | release_name: Extension v${{ steps.package-version.outputs.current-version }} 87 | draft: false 88 | prerelease: false 89 | 90 | - name: Upload Chrome release 91 | uses: actions/upload-release-asset@v1 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | with: 95 | upload_url: ${{ steps.create_release.outputs.upload_url }} 96 | asset_path: ./chrome/chrome-mv3-prod.zip 97 | asset_name: extension-mw.chrome.zip 98 | asset_content_type: application/zip 99 | 100 | - name: Upload Firefox release 101 | uses: actions/upload-release-asset@v1 102 | env: 103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | with: 105 | upload_url: ${{ steps.create_release.outputs.upload_url }} 106 | asset_path: ./firefox/firefox-mv3-prod.zip 107 | asset_name: extension-mw.firefox.xpi 108 | asset_content_type: application/x-xpinstall.xpi 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # P-Stream Extension 2 | 3 | Enhance your experience with just one click! 4 | 5 | ## About 6 | 7 | The extension is an optional "plugin" for P-Stream (and all movie-web forks) that adds a more sources that usually yield a better-quality stream! 8 | 9 | In simple terms: it acts as a local proxy. Imagine it opens an invisible tab to extract (scrape) the stream from the desired website. The only difference is that the extension can send specific headers, cookies, and is locally based. Some sources have restrictions that block scrapers; the extension helps us bypass that, such as IP restrictions, where the stream needs to be loaded on the same IP that it was originally scraped from. 10 | 11 | In more complex terms: It uses the declarativeNetRequest API to connect to third party sites and fetch their html content. Because browsers have Cross Origin Resource Security (CORS) that protects sites from fetching content from other sites, a proxy is needed. This extension acts as a local proxy to bypass CORS and fetch the content. 12 | 13 | During the onboarding process (/onboarding) the user can select between 3 options: 14 | - Extension - This gives the user the most sources —sometimes with the best quality. (This extension) 15 | - Custom Proxy - The user can host their own remote proxy on a service like Netlify to bypass restrictions, however, it can't do everything the extension can. 16 | - Default Setup - This is the default and requires no user action, just pick it and watch. The site comes with default proxies already configured. The downsides are you are sharing bandwidth with other users and there are fewer sources. 17 | 18 | ### Why does the extension ask to "Access Data on All Sites"? 19 | - This project (movie-web) is designed to be 100% self-hostable and is completely open-source. Hard coding a set list of sites that it asks for permission would prevent the extension from working on self-hosted sites. The user would need to edit the code to manually add their site to the permission list. 20 | - Because we scrape from many sites, it would be tedious to allow every site. 21 | 22 | ## Safety 23 | - It's **not required** to use P-Stream, it's entirely optional. 24 | - It doesn't track the user in any way or send data to any site besides the site you intend to use it with. 25 | - **All of the code is open-source and anyone can inspect it.** 26 | - The extension **must** be enabled on a per site basis, otherwise the connecting site cannot talk to the extension (using runtime.sendMessage). 27 | > Previously offical sites were hardcoded to automatically be enabled on, but due to concerns about how this could be abused, that has been removed. **We do not believe this was ever exploited.** 28 | 29 | ### Will this ever work with Safari? 30 | TL:DR: No, Apple's restrictions make it impossible currently. 31 | 32 | 1. Problem's with WebKit’s declarativeNetRequest API: 33 | The extension is a local CORS proxy used to change request headers so we can scrape content. However, WebKit’s implementation is “incomplete”… Half of the headers that we need to change, simply don’t work because Apple has blacklisted many, and some are simply half-baked. https://bugs.webkit.org/show_bug.cgi?id=290922 34 | 35 | 2. Orion (A Webkit browser with extension support) had 2 issues: 36 | Firstly, Orion is using WebKit’s DNR API, which is the main problem. Technically it’s possible for them to use Firefox’s for example, but it hasn’t been done yet. Secondly, the runtime.sendMessage API is currently broken in Orion. So the extension literally cannot talk to the website and vice verca. 37 | https://orionfeedback.org/d/8053-extensions-support-for-declarativenetrequest-redirects/11 38 | 39 | These can both be fixed, and I’m surprised WebKit’s APIs are so under-baked but it is what it is. 40 | 41 | ## Running for development 42 | 43 | We use pnpm with the latest version of NodeJS. 44 | 45 | ```sh 46 | pnpm i 47 | pnpm dev 48 | ``` 49 | 50 | ## Building for production 51 | 52 | ```sh 53 | pnpm i 54 | pnpm build 55 | or 56 | pnpm build:firefox 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /src/background/messages/makeRequest.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoMessaging } from '@plasmohq/messaging'; 2 | 3 | import type { BaseRequest } from '~types/request'; 4 | import type { BaseResponse } from '~types/response'; 5 | import { removeDynamicRules, setDynamicRules } from '~utils/declarativeNetRequest'; 6 | import { isFirefox } from '~utils/extension'; 7 | import { makeFullUrl } from '~utils/fetcher'; 8 | import { assertDomainWhitelist, canAccessCookies } from '~utils/storage'; 9 | 10 | const MAKE_REQUEST_DYNAMIC_RULE = 23498; 11 | 12 | export interface Request extends BaseRequest { 13 | baseUrl?: string; 14 | headers?: Record; 15 | method?: string; 16 | query?: Record; 17 | readHeaders?: Record; 18 | url: string; 19 | body?: any; 20 | bodyType?: 'string' | 'FormData' | 'URLSearchParams' | 'object'; 21 | } 22 | 23 | type Response = BaseResponse<{ 24 | response: { 25 | statusCode: number; 26 | headers: Record; 27 | finalUrl: string; 28 | body: T; 29 | }; 30 | }>; 31 | 32 | const mapBodyToFetchBody = (body: Request['body'], bodyType: Request['bodyType']): BodyInit => { 33 | if (bodyType === 'FormData') { 34 | const formData = new FormData(); 35 | body.forEach(([key, value]: [any, any]) => { 36 | formData.append(key, value.toString()); 37 | }); 38 | } 39 | if (bodyType === 'URLSearchParams') { 40 | return new URLSearchParams(body); 41 | } 42 | if (bodyType === 'object') { 43 | return JSON.stringify(body); 44 | } 45 | if (bodyType === 'string') { 46 | return body; 47 | } 48 | return body; 49 | }; 50 | 51 | const handler: PlasmoMessaging.MessageHandler> = async (req, res) => { 52 | try { 53 | if (!req.sender?.tab?.url) throw new Error('No tab URL found in the request.'); 54 | if (!req.body) throw new Error('No request body found in the request.'); 55 | 56 | const url = makeFullUrl(req.body.url, req.body); 57 | await assertDomainWhitelist(req.sender.tab.url); 58 | 59 | await setDynamicRules({ 60 | ruleId: MAKE_REQUEST_DYNAMIC_RULE, 61 | targetDomains: [new URL(url).hostname], 62 | requestHeaders: req.body.headers, 63 | // set Access-Control-Allow-Credentials if the reqested host has access to cookies 64 | responseHeaders: { 65 | ...(canAccessCookies(new URL(url).hostname) && { 66 | 'Access-Control-Allow-Credentials': 'true', 67 | }), 68 | }, 69 | }); 70 | 71 | const response = await fetch(url, { 72 | method: req.body.method, 73 | headers: req.body.headers, 74 | body: mapBodyToFetchBody(req.body.body, req.body.bodyType), 75 | }); 76 | await removeDynamicRules([MAKE_REQUEST_DYNAMIC_RULE]); 77 | const contentType = response.headers.get('content-type'); 78 | const body = contentType?.includes('application/json') ? await response.json() : await response.text(); 79 | 80 | const cookies = await (chrome || browser).cookies.getAll({ 81 | url: response.url, 82 | ...(isFirefox() && { 83 | firstPartyDomain: new URL(response.url).hostname, 84 | }), 85 | }); 86 | 87 | res.send({ 88 | success: true, 89 | response: { 90 | statusCode: response.status, 91 | headers: { 92 | ...Object.fromEntries(response.headers.entries()), 93 | // include cookies if allowed for the reqested host 94 | ...(canAccessCookies(new URL(url).hostname) && { 95 | 'Set-Cookie': cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join(', '), 96 | }), 97 | }, 98 | body, 99 | finalUrl: response.url, 100 | }, 101 | }); 102 | } catch (err) { 103 | console.error('failed request', err); 104 | res.send({ 105 | success: false, 106 | error: err instanceof Error ? err.message : String(err), 107 | }); 108 | } 109 | }; 110 | 111 | export default handler; 112 | -------------------------------------------------------------------------------- /src/tabs/PermissionRequest.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { Button } from '~components/Button'; 4 | import { Icon } from '~components/Icon'; 5 | import { usePermission } from '~hooks/usePermission'; 6 | 7 | import './PermissionRequest.css'; 8 | 9 | function Card(props: { purple?: boolean; children: React.ReactNode; icon?: React.ReactNode; right?: React.ReactNode }) { 10 | return ( 11 |
12 |
13 |
{props.icon}
14 |
15 |
{props.children}
16 | {props.right ?
{props.right}
: null} 17 |
18 | ); 19 | } 20 | 21 | export default function PermissionRequest() { 22 | const { grantPermission } = usePermission(); 23 | 24 | const grant = useCallback(() => { 25 | grantPermission().then(() => window.close()); 26 | }, [grantPermission]); 27 | 28 | return ( 29 |
30 |
31 |

32 | We need some
browser permissions 33 |

34 |

35 | We don't like it either, but the P-Stream extension needs quite a few permissions to function. Listed 36 | below is an explanation for all permissions we need. 37 |

38 | 39 |
40 | } 43 | right={ 44 | 47 | } 48 | > 49 |

Read the source code on GitHub

50 |

51 | Don't trust us? Read the code and decide for yourself if it's safe! 52 |

53 |
54 |
55 | 56 |

Permission list

57 |
58 | }> 59 |

Read & change data from all sites

60 |

61 | This is so the extension can gather content from the sources. We need to be able to reach those sources. 62 | Unfortunately that requires us to request the permissions from all sites. 63 |

64 |
65 | }> 66 |

Network Requests

67 |

68 | This permission allows the extension to instruct the browser how to request data from sites. In more 69 | technical terms, this allows P-Stream, movie-web, sudo-flix, watch.lonelil.ru, etc to modify HTTP headers 70 | that it wouldn't normally be allowed to. 71 |

72 |

73 | You won't be prompted for this permission, it's included in “Read & change data from all sites”. 74 |

75 |
76 | }> 77 |

Read and write cookies

78 |

79 | Some sources use cookies for authentication. We need to be able to read and set those cookies. 80 |

81 |

82 | You won't be prompted for this permission, it's included in “Read & change data from all sites”. 83 |

84 |
85 | }> 86 |

Active tab

87 |

88 | To determine which site has access to the extension or not, we need to know what tab you're currently 89 | using. 90 |

91 |

92 | This permission is given to all extensions by default, so your browser won't prompt you for it. 93 |

94 |
95 |
96 | 97 |
98 |
99 | 102 |
103 |
104 |
105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/utils/declarativeNetRequest.ts: -------------------------------------------------------------------------------- 1 | import { isChrome } from './extension'; 2 | 3 | interface DynamicRule { 4 | ruleId: number; 5 | targetDomains?: [string, ...string[]]; 6 | targetRegex?: string; 7 | requestHeaders?: Record; 8 | responseHeaders?: Record; 9 | } 10 | 11 | const mapHeadersToDeclarativeNetRequestHeaders = ( 12 | headers: Record, 13 | op: string, 14 | ): { header: string; operation: any; value: string }[] => { 15 | return Object.entries(headers).map(([name, value]) => ({ 16 | header: name, 17 | operation: op, 18 | value, 19 | })); 20 | }; 21 | 22 | export const setDynamicRules = async (body: DynamicRule) => { 23 | if (isChrome()) { 24 | await chrome.declarativeNetRequest.updateDynamicRules({ 25 | removeRuleIds: [body.ruleId], 26 | addRules: [ 27 | { 28 | id: body.ruleId, 29 | condition: { 30 | ...(body.targetDomains && { requestDomains: body.targetDomains }), 31 | ...(body.targetRegex && { regexFilter: body.targetRegex }), 32 | }, 33 | action: { 34 | type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS, 35 | ...(body.requestHeaders && Object.keys(body.requestHeaders).length > 0 36 | ? { 37 | requestHeaders: mapHeadersToDeclarativeNetRequestHeaders( 38 | body.requestHeaders, 39 | chrome.declarativeNetRequest.HeaderOperation.SET, 40 | ), 41 | } 42 | : {}), 43 | responseHeaders: [ 44 | { 45 | header: 'Access-Control-Allow-Origin', 46 | operation: chrome.declarativeNetRequest.HeaderOperation.SET, 47 | value: '*', 48 | }, 49 | { 50 | header: 'Access-Control-Allow-Methods', 51 | operation: chrome.declarativeNetRequest.HeaderOperation.SET, 52 | value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 53 | }, 54 | { 55 | header: 'Access-Control-Allow-Headers', 56 | operation: chrome.declarativeNetRequest.HeaderOperation.SET, 57 | value: '*', 58 | }, 59 | ...mapHeadersToDeclarativeNetRequestHeaders( 60 | body.responseHeaders ?? {}, 61 | chrome.declarativeNetRequest.HeaderOperation.SET, 62 | ), 63 | ], 64 | }, 65 | }, 66 | ], 67 | }); 68 | if (chrome.runtime.lastError?.message) throw new Error(chrome.runtime.lastError.message); 69 | } else { 70 | await browser.declarativeNetRequest.updateDynamicRules({ 71 | removeRuleIds: [body.ruleId], 72 | addRules: [ 73 | { 74 | id: body.ruleId, 75 | condition: { 76 | ...(body.targetDomains && { requestDomains: body.targetDomains }), 77 | ...(body.targetRegex && { regexFilter: body.targetRegex }), 78 | }, 79 | action: { 80 | type: 'modifyHeaders', 81 | ...(body.requestHeaders && Object.keys(body.requestHeaders).length > 0 82 | ? { 83 | requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(body.requestHeaders, 'set'), 84 | } 85 | : {}), 86 | responseHeaders: [ 87 | { 88 | header: 'Access-Control-Allow-Origin', 89 | operation: 'set', 90 | value: '*', 91 | }, 92 | { 93 | header: 'Access-Control-Allow-Methods', 94 | operation: 'set', 95 | value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 96 | }, 97 | { 98 | header: 'Access-Control-Allow-Headers', 99 | operation: 'set', 100 | value: '*', 101 | }, 102 | ...mapHeadersToDeclarativeNetRequestHeaders(body.responseHeaders ?? {}, 'set'), 103 | ], 104 | }, 105 | }, 106 | ], 107 | }); 108 | if (browser.runtime.lastError?.message) throw new Error(browser.runtime.lastError.message); 109 | } 110 | }; 111 | 112 | export const removeDynamicRules = async (ruleIds: number[]) => { 113 | await (chrome || browser).declarativeNetRequest.updateDynamicRules({ 114 | removeRuleIds: ruleIds, 115 | }); 116 | if ((chrome || browser).runtime.lastError?.message) 117 | throw new Error((chrome || browser).runtime.lastError?.message ?? 'Unknown error'); 118 | }; 119 | -------------------------------------------------------------------------------- /assets/inter/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | https://openfontlicense.org 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/components/Icon.tsx: -------------------------------------------------------------------------------- 1 | const icons = { 2 | power: ``, 3 | warningCircle: ``, 4 | github: ``, 5 | cookie: ``, 6 | windows: ``, 7 | shield: ``, 8 | logo: ``, 9 | network: ``, 10 | }; 11 | 12 | export type Icons = keyof typeof icons; 13 | 14 | export function Icon(props: { name: Icons }) { 15 | return ( 16 |
24 | ); 25 | } 26 | --------------------------------------------------------------------------------