mirror of
https://github.com/movie-web/extension.git
synced 2025-09-13 13:33:25 +00:00
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
74
.eslintrc.js
Normal file
74
.eslintrc.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
extends: ['airbnb', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||||
|
ignorePatterns: ['dist/*', 'plugins/*', 'tests/*', '/*.cjs', '/*.js', '/*.ts', '/**/*.test.ts', 'test/*'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
tsconfigRootDir: './',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint', 'import', 'prettier'],
|
||||||
|
rules: {
|
||||||
|
'react/jsx-uses-react': 'off',
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'react/destructuring-assignment': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'@typescript-eslint/no-this-alias': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': ['error'],
|
||||||
|
'no-restricted-syntax': 'off',
|
||||||
|
'import/no-unresolved': ['error', { ignore: ['^virtual:'] }],
|
||||||
|
'consistent-return': 'off',
|
||||||
|
'no-continue': 'off',
|
||||||
|
'no-eval': 'off',
|
||||||
|
'no-await-in-loop': 'off',
|
||||||
|
'no-nested-ternary': 'off',
|
||||||
|
'no-param-reassign': ['error', { props: false }],
|
||||||
|
'prefer-destructuring': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
'react/jsx-filename-extension': ['error', { extensions: ['.js', '.tsx', '.jsx'] }],
|
||||||
|
'import/extensions': [
|
||||||
|
'error',
|
||||||
|
'ignorePackages',
|
||||||
|
{
|
||||||
|
ts: 'never',
|
||||||
|
tsx: 'never',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: ['builtin', 'external', 'internal', ['sibling', 'parent'], 'index', 'unknown'],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'sort-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
ignoreCase: false,
|
||||||
|
ignoreDeclarationSort: true,
|
||||||
|
ignoreMemberSort: false,
|
||||||
|
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
|
||||||
|
allowSeparatedGroups: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @movie-web/core
|
1
.github/CODE_OF_CONDUCT.md
vendored
Normal file
1
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Please visit the [main document at primary repository](https://github.com/movie-web/movie-web/blob/dev/.github/CODE_OF_CONDUCT.md).
|
1
.github/CONTRIBUTING.md
vendored
Normal file
1
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Please visit the [main document at primary repository](https://github.com/movie-web/movie-web/blob/dev/.github/CONTRIBUTING.md).
|
14
.github/SECURITY.md
vendored
Normal file
14
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
The movie-web maintainers only support the latest version of movie-web published at https://movie-web.app.
|
||||||
|
This published version is equivalent to the master branch.
|
||||||
|
|
||||||
|
Support is not provided for any forks or mirrors of movie-web.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
There are two ways you can contact the movie-web maintainers to report a vulnerability:
|
||||||
|
- Email [security@movie-web.app](mailto:security@movie-web.app)
|
||||||
|
- Report the vulnerability in the [movie-web Discord server](https://discord.movie-web.app)
|
6
.github/pull_request_template.md
vendored
Normal file
6
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
This pull request resolves #XXX
|
||||||
|
|
||||||
|
- [ ] I have read and agreed to the [code of conduct](https://github.com/movie-web/movie-web/blob/dev/.github/CODE_OF_CONDUCT.md).
|
||||||
|
- [ ] I have read and complied with the [contributing guidelines](https://github.com/movie-web/movie-web/blob/dev/.github/CONTRIBUTING.md).
|
||||||
|
- [ ] What I'm implementing was assigned to me and is an [approved issue](https://github.com/movie-web/movie-web/issues?q=is%3Aopen+is%3Aissue+label%3Aapproved). For reference, please take a look at our [GitHub projects](https://github.com/movie-web/movie-web/projects).
|
||||||
|
- [ ] I have tested all of my changes.
|
34
.github/workflows/submit.yml
vendored
Normal file
34
.github/workflows/submit.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: "Submit to Web Store"
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Cache pnpm modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.pnpm-store
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-
|
||||||
|
- uses: pnpm/action-setup@v2.2.4
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
run_install: true
|
||||||
|
- name: Use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3.4.1
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
cache: "pnpm"
|
||||||
|
- name: Build the extension
|
||||||
|
run: pnpm build
|
||||||
|
- name: Package the extension into a zip artifact
|
||||||
|
run: pnpm package
|
||||||
|
- name: Browser Platform Publish
|
||||||
|
uses: PlasmoHQ/bpp@v3
|
||||||
|
with:
|
||||||
|
keys: ${{ secrets.SUBMIT_KEYS }}
|
||||||
|
artifact: build/chrome-mv3-prod.zip
|
36
.github/workflows/tests.yml
vendored
Normal file
36
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
testing:
|
||||||
|
name: Testing
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
# - name: Run tests
|
||||||
|
# run: pnpm run test
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
run: pnpm run lint
|
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
#cache
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*
|
||||||
|
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# plasmo - https://www.plasmo.com
|
||||||
|
.plasmo
|
||||||
|
|
||||||
|
# bpp - http://bpp.browser.market/
|
||||||
|
keys.json
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
.tsbuildinfo
|
9
.prettierrc.mjs
Normal file
9
.prettierrc.mjs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* @type {import('prettier').Options}
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
printWidth: 120,
|
||||||
|
trailingComma: 'all',
|
||||||
|
singleQuote: true,
|
||||||
|
endOfLine: 'auto',
|
||||||
|
};
|
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"editorconfig.editorconfig"
|
||||||
|
]
|
||||||
|
}
|
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
||||||
|
"eslint.format.enable": true,
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||||
|
}
|
||||||
|
}
|
55
README.md
55
README.md
@@ -1,2 +1,57 @@
|
|||||||
# extension
|
# extension
|
||||||
|
|
||||||
movie-web extension, allows providers to work without proxy
|
movie-web extension, allows providers to work without proxy
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
1. Messaging
|
||||||
|
- `makeRequest` message. Make requests that ignore CORS and can set forbidden headers. Replies with request results.
|
||||||
|
- `prepareStream` message. For a list of domains, set required or preferred headers.
|
||||||
|
- `hello` message. Gives details on the extension, like the version. If not allowed, simply respond with an error describing it.
|
||||||
|
|
||||||
|
2. Popout
|
||||||
|
- The popout should have a simple interface for trusting or untrusting the current site. Only trusted sites should be able to communicate with the extension.
|
||||||
|
|
||||||
|
3. Storage
|
||||||
|
- The extension will need to store active rules for setting headers. And which sites (origins) are trusted.
|
||||||
|
|
||||||
|
## How will the client work
|
||||||
|
1. When creating providers, first send a hello message to identify if there is an extension installed at all and if its correct version.
|
||||||
|
2. If no extension (or not suitable) fallback on standard providers.
|
||||||
|
3. Else, make new provider controls, target set to BROWSER_EXTENSION, with custom fetcher that uses the extension to send requests instead.
|
||||||
|
4. If any message to the extension fail. Fallback to standard providers again.
|
||||||
|
5. When a stream will be played, communicate to extension through a `prepareStream`
|
||||||
|
|
||||||
|
# Plasmo
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`.
|
||||||
|
|
||||||
|
You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser.
|
||||||
|
|
||||||
|
For further guidance, [visit our Documentation](https://docs.plasmo.com/)
|
||||||
|
|
||||||
|
## Making production build
|
||||||
|
|
||||||
|
Run the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
# or
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This should create a production bundle for your extension, ready to be zipped and published to the stores.
|
||||||
|
|
||||||
|
## Submit to the webstores
|
||||||
|
|
||||||
|
The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission!
|
||||||
|
BIN
assets/icon.png
Normal file
BIN
assets/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
60
package.json
Normal file
60
package.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "@movie-web/extension",
|
||||||
|
"displayName": "movie-web extension",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "movie-web extension, allows providers to work without proxy",
|
||||||
|
"author": "movie-web",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "plasmo dev",
|
||||||
|
"build": "plasmo build",
|
||||||
|
"build:firefox": "plasmo build --target=firefox-mv3",
|
||||||
|
"package": "plasmo package",
|
||||||
|
"package:firefox": "plasmo package --target=firefox-mv3",
|
||||||
|
"lint": "eslint --ext .tsx,.ts src",
|
||||||
|
"lint:fix": "eslint --fix --ext .tsx,.ts src",
|
||||||
|
"lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src",
|
||||||
|
"preinstall": "npx -y only-allow pnpm"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@plasmohq/messaging": "^0.6.1",
|
||||||
|
"@plasmohq/storage": "^1.9.0",
|
||||||
|
"plasmo": "0.84.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chrome": "0.0.251",
|
||||||
|
"@types/firefox-webext-browser": "^120.0.0",
|
||||||
|
"@types/node": "20.9.0",
|
||||||
|
"@types/react": "18.2.37",
|
||||||
|
"@types/react-dom": "18.2.15",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
|
"@typescript-eslint/parser": "^6.15.0",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
|
"eslint-plugin-import": "^2.29.1",
|
||||||
|
"eslint-plugin-prettier": "^5.1.1",
|
||||||
|
"eslint-plugin-react": "^7.33.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"prettier": "3.0.3",
|
||||||
|
"typescript": "5.2.2"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"permissions": [
|
||||||
|
"declarativeNetRequest",
|
||||||
|
"tabs"
|
||||||
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>",
|
||||||
|
"https://dev.movie-web.app/*",
|
||||||
|
"https://movie-web.app/*"
|
||||||
|
],
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "{3fd86354-c73f-4395-9e26-2c5c984579bf}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7210
pnpm-lock.yaml
generated
Normal file
7210
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
src/Popup.css
Normal file
14
src/Popup.css
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;800&display=swap');
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
32
src/background/messages/hello.ts
Normal file
32
src/background/messages/hello.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import { hasPermission } from '~hooks/usePermission';
|
||||||
|
import { getVersion } from '~hooks/useVersion';
|
||||||
|
import type { BaseRequest } from '~types/request';
|
||||||
|
import type { BaseResponse } from '~types/response';
|
||||||
|
import { isDomainWhitelisted } from '~utils/storage';
|
||||||
|
|
||||||
|
type Response = BaseResponse<{
|
||||||
|
version: string;
|
||||||
|
allowed: boolean;
|
||||||
|
hasPermission: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<BaseRequest, Response> = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const version = getVersion();
|
||||||
|
res.send({
|
||||||
|
success: true,
|
||||||
|
version,
|
||||||
|
allowed: await isDomainWhitelisted(req.sender.tab.url),
|
||||||
|
hasPermission: await hasPermission(),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
56
src/background/messages/makeRequest.ts
Normal file
56
src/background/messages/makeRequest.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import type { BaseRequest } from '~types/request';
|
||||||
|
import type { BaseResponse } from '~types/response';
|
||||||
|
import { makeFullUrl } from '~utils/fetcher';
|
||||||
|
import { assertDomainWhitelist } from '~utils/storage';
|
||||||
|
|
||||||
|
export interface Request extends BaseRequest {
|
||||||
|
baseUrl?: string;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
method?: string;
|
||||||
|
query?: Record<string, string>;
|
||||||
|
readHeaders?: Record<string, string>;
|
||||||
|
url: string;
|
||||||
|
body?: string | FormData | URLSearchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response<T> = BaseResponse<{
|
||||||
|
response: {
|
||||||
|
statusCode: number;
|
||||||
|
headers: Record<string, string>;
|
||||||
|
finalUrl: string;
|
||||||
|
body: T;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (req, res) => {
|
||||||
|
try {
|
||||||
|
await assertDomainWhitelist(req.sender.tab.url);
|
||||||
|
|
||||||
|
const response = await fetch(makeFullUrl(req.body.url, req.body), {
|
||||||
|
method: req.body.method,
|
||||||
|
headers: req.body.headers,
|
||||||
|
body: req.body.body,
|
||||||
|
});
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
const body = contentType?.includes('application/json') ? await response.json() : await response.text();
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
success: true,
|
||||||
|
response: {
|
||||||
|
statusCode: response.status,
|
||||||
|
headers: Object.fromEntries(response.headers.entries()), // Headers object isn't serializable
|
||||||
|
body,
|
||||||
|
finalUrl: response.url,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
37
src/background/messages/openPage.ts
Normal file
37
src/background/messages/openPage.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import type { BaseRequest } from '~types/request';
|
||||||
|
import type { BaseResponse } from '~types/response';
|
||||||
|
import { isChrome } from '~utils/extension';
|
||||||
|
|
||||||
|
type Request = BaseRequest & {
|
||||||
|
page: string;
|
||||||
|
redirectUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<Request, BaseResponse> = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
searchParams.set('redirectUrl', req.body.redirectUrl);
|
||||||
|
const url = (chrome || browser).runtime.getURL(`/tabs/${req.body.page}.html?${searchParams.toString()}`);
|
||||||
|
|
||||||
|
if (isChrome()) {
|
||||||
|
await chrome.tabs.update(req.sender.tab.id, {
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await browser.tabs.update(req.sender.tab.id, { url });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
126
src/background/messages/prepareStream.ts
Normal file
126
src/background/messages/prepareStream.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import type { BaseRequest } from '~types/request';
|
||||||
|
import type { BaseResponse } from '~types/response';
|
||||||
|
import { isChrome } from '~utils/extension';
|
||||||
|
import { assertDomainWhitelist } from '~utils/storage';
|
||||||
|
|
||||||
|
interface Request extends BaseRequest {
|
||||||
|
ruleId: number;
|
||||||
|
targetDomains: [string, ...string[]];
|
||||||
|
requestHeaders?: Record<string, string>;
|
||||||
|
responseHeaders?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapHeadersToDeclarativeNetRequestHeaders = (
|
||||||
|
headers: Record<string, string>,
|
||||||
|
op: string,
|
||||||
|
): { header: string; operation: any; value: string }[] => {
|
||||||
|
return Object.entries(headers).map(([name, value]) => ({
|
||||||
|
header: name,
|
||||||
|
operation: op,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<Request, BaseResponse> = async (req, res) => {
|
||||||
|
try {
|
||||||
|
await assertDomainWhitelist(req.sender.tab.url);
|
||||||
|
if (isChrome()) {
|
||||||
|
await chrome.declarativeNetRequest.updateDynamicRules({
|
||||||
|
removeRuleIds: [req.body.ruleId],
|
||||||
|
addRules: [
|
||||||
|
{
|
||||||
|
id: req.body.ruleId,
|
||||||
|
condition: {
|
||||||
|
requestDomains: req.body.targetDomains,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
|
||||||
|
...(req.body.requestHeaders && Object.keys(req.body.requestHeaders).length > 0
|
||||||
|
? {
|
||||||
|
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(
|
||||||
|
req.body.requestHeaders,
|
||||||
|
chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
responseHeaders: [
|
||||||
|
{
|
||||||
|
header: 'Access-Control-Allow-Origin',
|
||||||
|
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||||
|
value: '*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Access-Control-Allow-Methods',
|
||||||
|
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||||
|
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Access-Control-Allow-Headers',
|
||||||
|
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||||
|
value: '*',
|
||||||
|
},
|
||||||
|
...mapHeadersToDeclarativeNetRequestHeaders(
|
||||||
|
req.body.responseHeaders ?? {},
|
||||||
|
chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (chrome.runtime.lastError?.message) throw new Error(chrome.runtime.lastError.message);
|
||||||
|
} else {
|
||||||
|
await browser.declarativeNetRequest.updateDynamicRules({
|
||||||
|
removeRuleIds: [req.body.ruleId],
|
||||||
|
addRules: [
|
||||||
|
{
|
||||||
|
id: req.body.ruleId,
|
||||||
|
condition: {
|
||||||
|
requestDomains: req.body.targetDomains,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: 'modifyHeaders',
|
||||||
|
...(req.body.requestHeaders && Object.keys(req.body.requestHeaders).length > 0
|
||||||
|
? {
|
||||||
|
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(req.body.requestHeaders, 'set'),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
responseHeaders: [
|
||||||
|
{
|
||||||
|
header: 'Access-Control-Allow-Origin',
|
||||||
|
operation: 'set',
|
||||||
|
value: '*',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Access-Control-Allow-Methods',
|
||||||
|
operation: 'set',
|
||||||
|
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Access-Control-Allow-Headers',
|
||||||
|
operation: 'set',
|
||||||
|
value: '*',
|
||||||
|
},
|
||||||
|
...mapHeadersToDeclarativeNetRequestHeaders(req.body.responseHeaders ?? {}, 'set'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (browser.runtime.lastError?.message) throw new Error(browser.runtime.lastError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.send({
|
||||||
|
success: false,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
17
src/components/BottomLabel.css
Normal file
17
src/components/BottomLabel.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.bottom-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: .25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #4A4863;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 2px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
14
src/components/BottomLabel.tsx
Normal file
14
src/components/BottomLabel.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useVersion } from '~hooks/useVersion';
|
||||||
|
import './BottomLabel.css';
|
||||||
|
|
||||||
|
export function BottomLabel() {
|
||||||
|
const version = useVersion({ prefixed: true });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h3 className="bottom-label">
|
||||||
|
{version}
|
||||||
|
<div className="dot" />
|
||||||
|
movie-web
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
22
src/components/DisabledScreen.css
Normal file
22
src/components/DisabledScreen.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.disabled {
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 230px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled svg {
|
||||||
|
display: block;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #B44868;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled p {
|
||||||
|
color: #4A4864;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled strong {
|
||||||
|
color: white;
|
||||||
|
}
|
13
src/components/DisabledScreen.tsx
Normal file
13
src/components/DisabledScreen.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import './DisabledScreen.css';
|
||||||
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
|
export function DisabledScreen() {
|
||||||
|
return (
|
||||||
|
<div className="disabled">
|
||||||
|
<Icon name="warningCircle" />
|
||||||
|
<p>
|
||||||
|
The <strong>movie-web extension</strong> can not be used on this page
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
4
src/components/Frame.css
Normal file
4
src/components/Frame.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.frame {
|
||||||
|
background-color: #0a080e;
|
||||||
|
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%);
|
||||||
|
}
|
15
src/components/Frame.tsx
Normal file
15
src/components/Frame.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import './Frame.css';
|
||||||
|
|
||||||
|
export interface FrameProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Frame(props: FrameProps) {
|
||||||
|
return (
|
||||||
|
<div className="frame" style={{ width: 300, height: 300 }}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
10
src/components/Icon.tsx
Normal file
10
src/components/Icon.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const icons = {
|
||||||
|
power: `<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg"><g filter="url(#filter0_i_1153_291)"><path fill-rule="evenodd" clip-rule="evenodd" d="M16.4375 2.875C16.4375 2.36114 16.2334 1.86833 15.87 1.50498C15.5067 1.14163 15.0139 0.9375 14.5 0.9375C13.9861 0.9375 13.4933 1.14163 13.13 1.50498C12.7666 1.86833 12.5625 2.36114 12.5625 2.875V15.7917C12.5625 16.3055 12.7666 16.7983 13.13 17.1617C13.4933 17.525 13.9861 17.7292 14.5 17.7292C15.0139 17.7292 15.5067 17.525 15.87 17.1617C16.2334 16.7983 16.4375 16.3055 16.4375 15.7917V2.875ZM9.14475 6.42708C9.35678 6.28621 9.53899 6.10495 9.68097 5.89366C9.82295 5.68237 9.92192 5.44519 9.97224 5.19565C10.0226 4.94611 10.0232 4.6891 9.97422 4.4393C9.92521 4.1895 9.82748 3.9518 9.68661 3.73977C9.54574 3.52774 9.36448 3.34553 9.15319 3.20355C8.9419 3.06157 8.70471 2.9626 8.45517 2.91228C8.20563 2.86197 7.94863 2.86129 7.69883 2.9103C7.44903 2.95931 7.21132 3.05704 6.99929 3.19792C5.13427 4.43483 3.60458 6.11435 2.54683 8.08651C1.48907 10.0587 0.936176 12.2621 0.937502 14.5C0.937502 21.9904 7.00963 28.0625 14.5 28.0625C21.9904 28.0625 28.0625 21.9904 28.0625 14.5C28.0625 9.78025 25.651 5.62625 22.0007 3.19792C21.5725 2.91358 21.0489 2.811 20.545 2.91274C20.0412 3.01448 19.5984 3.3122 19.314 3.74042C19.0297 4.16863 18.9271 4.69226 19.0289 5.19611C19.1306 5.69995 19.4283 6.14275 19.8565 6.42708C21.5907 7.5775 22.9083 9.25578 23.6143 11.2134C24.3203 13.1711 24.3771 15.3041 23.7763 17.2965C23.1755 19.289 21.9491 21.035 20.2786 22.2761C18.6081 23.5172 16.5824 24.1873 14.5013 24.1873C12.4202 24.1873 10.3945 23.5172 8.72399 22.2761C7.05349 21.035 5.82705 19.289 5.22627 17.2965C4.62548 15.3041 4.68228 13.1711 5.38826 11.2134C6.09424 9.25578 7.41057 7.5775 9.14475 6.42708Z" fill="currentColor" /></g><defs><filter id="filter0_i_1153_291" x="0.9375" y="0.9375" width="27.125" height="27.125"filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix" /><feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" /><feOffset /><feGaussianBlur stdDeviation="2" /><feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1" /><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.3 0" /><feBlend mode="normal" in2="shape" result="effect1_innerShadow_1153_291" /></filter></defs></svg>`,
|
||||||
|
warningCircle: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>`,
|
||||||
|
};
|
||||||
|
export type Icons = keyof typeof icons;
|
||||||
|
|
||||||
|
export function Icon(props: { name: Icons }) {
|
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: icons[props.name] }} />;
|
||||||
|
}
|
19
src/components/PermissionMissingScreen.tsx
Normal file
19
src/components/PermissionMissingScreen.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Icon } from '~components/Icon';
|
||||||
|
|
||||||
|
import '~tabs/PermissionGrant.css';
|
||||||
|
|
||||||
|
export interface PermissionMissingProps {
|
||||||
|
onGrant?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermissionMissingScreen(props: PermissionMissingProps) {
|
||||||
|
return (
|
||||||
|
<div className="disabled">
|
||||||
|
<Icon name="warningCircle" />
|
||||||
|
<p style={{ paddingBottom: 25, paddingTop: 10 }}>The extension is missing permissions it needs to function</p>
|
||||||
|
<button type="button" className="grant-permission-btn" onClick={props.onGrant}>
|
||||||
|
Grant Permission
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
119
src/components/ToggleButton.css
Normal file
119
src/components/ToggleButton.css
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper::after {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 0;
|
||||||
|
content: '';
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: rgba(119, 66, 233, 0.25);
|
||||||
|
filter: blur(35px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper.active::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1000px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: transform 50ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inside-glow {
|
||||||
|
transition: transform 300ms;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:hover .inside-glow {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actual-button-style {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 1000px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-color: rgba(96, 66, 166, 0.50);
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
border: 1px solid #322E48;
|
||||||
|
background-image: linear-gradient(180deg, #232236 0%, #0E0D15 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actual-button-style.active {
|
||||||
|
border-color: #48307F;
|
||||||
|
background-image: linear-gradient(180deg, #463177 0%, #2D1C54 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button .inside-glow, .toggle-button svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
transition: color 300ms, transform 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button .inside-glow {
|
||||||
|
position: absolute;
|
||||||
|
width: 90%;
|
||||||
|
height: 90%;
|
||||||
|
border-radius: 1000px;
|
||||||
|
filter: blur(10px);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button svg {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
display: block;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button svg * {
|
||||||
|
box-shadow: 0px 0px 0 4px rgba(0, 0, 0, 1) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container p {
|
||||||
|
color: #4A4863;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container strong {
|
||||||
|
color: white;
|
||||||
|
}
|
43
src/components/ToggleButton.tsx
Normal file
43
src/components/ToggleButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Icon } from './Icon';
|
||||||
|
import './ToggleButton.css';
|
||||||
|
|
||||||
|
export interface ToggleButtonProps {
|
||||||
|
onClick?: () => void;
|
||||||
|
active?: boolean;
|
||||||
|
domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToggleButton(props: ToggleButtonProps) {
|
||||||
|
const opacityStyle = {
|
||||||
|
opacity: props.active ? 1 : 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="button-container">
|
||||||
|
<div className={['button-wrapper', props.active ? 'active' : ''].join(' ')}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={props.onClick}
|
||||||
|
aria-label="Toggle extension on/off"
|
||||||
|
className="toggle-button"
|
||||||
|
style={{
|
||||||
|
color: props.active ? '#9B93CC' : '#4B4765',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="actual-button-style" />
|
||||||
|
<div className="actual-button-style active" style={opacityStyle} />
|
||||||
|
<Icon name="power" />
|
||||||
|
<div
|
||||||
|
className="inside-glow"
|
||||||
|
style={{
|
||||||
|
backgroundColor: props.active ? '#452D7C' : '#181724',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Extension {props.active ? 'enabled' : 'disabled'} <br /> on <strong>{props.domain}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
23
src/contents/movie-web.ts
Normal file
23
src/contents/movie-web.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { relayMessage } from '@plasmohq/messaging';
|
||||||
|
import type { PlasmoCSConfig } from 'plasmo';
|
||||||
|
|
||||||
|
export const config: PlasmoCSConfig = {
|
||||||
|
// <all_urls> works for chrome, but not for firefox, so we add explicit domains for firefox
|
||||||
|
matches: ['<all_urls>', 'https://dev.movie-web.app/*', 'https://movie-web.app/*'],
|
||||||
|
};
|
||||||
|
|
||||||
|
relayMessage({
|
||||||
|
name: 'hello',
|
||||||
|
});
|
||||||
|
|
||||||
|
relayMessage({
|
||||||
|
name: 'makeRequest',
|
||||||
|
});
|
||||||
|
|
||||||
|
relayMessage({
|
||||||
|
name: 'prepareStream',
|
||||||
|
});
|
||||||
|
|
||||||
|
relayMessage({
|
||||||
|
name: 'openPage',
|
||||||
|
});
|
19
src/hooks/useDomain.ts
Normal file
19
src/hooks/useDomain.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { makeUrlIntoDomain } from '~utils/domains';
|
||||||
|
import { listenToTabChanges, queryCurrentDomain, stopListenToTabChanges } from '~utils/tabs';
|
||||||
|
|
||||||
|
export function useDomain(): null | string {
|
||||||
|
const [domain, setDomain] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const listen = () => queryCurrentDomain(setDomain);
|
||||||
|
listen();
|
||||||
|
listenToTabChanges(listen);
|
||||||
|
return () => {
|
||||||
|
stopListenToTabChanges(listen);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return makeUrlIntoDomain(domain);
|
||||||
|
}
|
44
src/hooks/useDomainWhitelist.ts
Normal file
44
src/hooks/useDomainWhitelist.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { usePermission } from '~hooks/usePermission';
|
||||||
|
import { useDomainStorage } from '~utils/storage';
|
||||||
|
|
||||||
|
export function useDomainWhitelist() {
|
||||||
|
const [domainWhitelist, setDomainWhitelist] = useDomainStorage();
|
||||||
|
|
||||||
|
const removeDomain = useCallback((domain: string | null) => {
|
||||||
|
if (!domain) return;
|
||||||
|
setDomainWhitelist((s) => [...s.filter((v) => v !== domain)]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addDomain = useCallback((domain: string | null) => {
|
||||||
|
if (!domain) return;
|
||||||
|
setDomainWhitelist((s) => [...s.filter((v) => v !== domain), domain]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
removeDomain,
|
||||||
|
addDomain,
|
||||||
|
domainWhitelist,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToggleWhitelistDomain(domain: string) {
|
||||||
|
const { domainWhitelist, addDomain, removeDomain } = useDomainWhitelist();
|
||||||
|
const isWhitelisted = domainWhitelist.includes(domain);
|
||||||
|
const { grantPermission } = usePermission();
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
if (!isWhitelisted) {
|
||||||
|
addDomain(domain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDomain(domain);
|
||||||
|
}, [isWhitelisted, domain, addDomain, removeDomain, grantPermission]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
toggle,
|
||||||
|
isWhitelisted,
|
||||||
|
};
|
||||||
|
}
|
32
src/hooks/usePermission.ts
Normal file
32
src/hooks/usePermission.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useDomainWhitelist } from './useDomainWhitelist';
|
||||||
|
|
||||||
|
export async function hasPermission() {
|
||||||
|
return chrome.permissions.contains({
|
||||||
|
origins: ['<all_urls>'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePermission() {
|
||||||
|
const { addDomain } = useDomainWhitelist();
|
||||||
|
const [permission, setPermission] = useState(false);
|
||||||
|
|
||||||
|
const grantPermission = useCallback(async (domain?: string) => {
|
||||||
|
const granted = await chrome.permissions.request({
|
||||||
|
origins: ['<all_urls>'],
|
||||||
|
});
|
||||||
|
setPermission(granted);
|
||||||
|
if (granted && domain) addDomain(domain);
|
||||||
|
return granted;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
hasPermission().then((has) => setPermission(has));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasPermission: permission,
|
||||||
|
grantPermission,
|
||||||
|
};
|
||||||
|
}
|
9
src/hooks/useVersion.ts
Normal file
9
src/hooks/useVersion.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function getVersion(ops?: { prefixed?: boolean }) {
|
||||||
|
const prefix = ops?.prefixed ? 'v' : '';
|
||||||
|
const manifest = (chrome || browser).runtime.getManifest();
|
||||||
|
return `${prefix}${manifest.version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVersion(ops?: { prefixed?: boolean }) {
|
||||||
|
return getVersion(ops);
|
||||||
|
}
|
32
src/popup.tsx
Normal file
32
src/popup.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { BottomLabel } from '~components/BottomLabel';
|
||||||
|
import { DisabledScreen } from '~components/DisabledScreen';
|
||||||
|
import { Frame } from '~components/Frame';
|
||||||
|
import { PermissionMissingScreen } from '~components/PermissionMissingScreen';
|
||||||
|
import { ToggleButton } from '~components/ToggleButton';
|
||||||
|
import { useDomain } from '~hooks/useDomain';
|
||||||
|
import { useToggleWhitelistDomain } from '~hooks/useDomainWhitelist';
|
||||||
|
import './Popup.css';
|
||||||
|
import { usePermission } from '~hooks/usePermission';
|
||||||
|
|
||||||
|
function IndexPopup() {
|
||||||
|
const domain = useDomain();
|
||||||
|
const { isWhitelisted, toggle } = useToggleWhitelistDomain(domain);
|
||||||
|
const { grantPermission, hasPermission } = usePermission();
|
||||||
|
|
||||||
|
let page = 'toggle';
|
||||||
|
if (!hasPermission) page = 'perm';
|
||||||
|
else if (!domain) page = 'disabled';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Frame>
|
||||||
|
<div className="popup">
|
||||||
|
{page === 'toggle' ? <ToggleButton active={isWhitelisted} onClick={toggle} domain={domain} /> : null}
|
||||||
|
{page === 'disabled' ? <DisabledScreen /> : null}
|
||||||
|
{page === 'perm' ? <PermissionMissingScreen onGrant={grantPermission} /> : null}
|
||||||
|
<BottomLabel />
|
||||||
|
</div>
|
||||||
|
</Frame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexPopup;
|
103
src/tabs/PermissionGrant.css
Normal file
103
src/tabs/PermissionGrant.css
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@500;800&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#__plasmo {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #0a0a10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-container {
|
||||||
|
width: 400px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permission-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #0f0f1b;
|
||||||
|
border-radius: 10px;
|
||||||
|
height: 125px;
|
||||||
|
padding-right: 40px;
|
||||||
|
padding-left: 40px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #20202d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-white {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-color {
|
||||||
|
color: #73739d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-btn,
|
||||||
|
.grant-permission-btn {
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #0000;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-btn {
|
||||||
|
background:
|
||||||
|
linear-gradient(to right, #151522, #181b2a) padding-box,
|
||||||
|
linear-gradient(50deg, #151522, #181b2a, #456b95) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-permission-btn {
|
||||||
|
background:
|
||||||
|
linear-gradient(to right, #482179, #8a39e6) padding-box,
|
||||||
|
linear-gradient(50deg, #482179, #4f3585, #b79ae0) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-back-btn:hover {
|
||||||
|
background:
|
||||||
|
linear-gradient(to right, #2a334e, #2f3552) padding-box,
|
||||||
|
linear-gradient(50deg, #2a334e, #2f3552, #6086b7) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-permission-btn:hover {
|
||||||
|
background:
|
||||||
|
linear-gradient(to right, #603a9a, #a25ff5) padding-box,
|
||||||
|
linear-gradient(50deg, #603a9a, #653c9f, #d9aef1) border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grant-permission-btn:disabled {
|
||||||
|
background:
|
||||||
|
linear-gradient(to right, #311e4b, #6b4b99) padding-box,
|
||||||
|
linear-gradient(50deg, #311e4b, #3b265b, #704fa5) border-box;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
58
src/tabs/PermissionGrant.tsx
Normal file
58
src/tabs/PermissionGrant.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useDomainWhitelist } from '~hooks/useDomainWhitelist';
|
||||||
|
import { usePermission } from '~hooks/usePermission';
|
||||||
|
import { makeUrlIntoDomain } from '~utils/domains';
|
||||||
|
|
||||||
|
import './PermissionGrant.css';
|
||||||
|
|
||||||
|
export default function PermissionGrant() {
|
||||||
|
const { domainWhitelist } = useDomainWhitelist();
|
||||||
|
const { hasPermission, grantPermission } = usePermission();
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
const redirectUrl = queryParams.get('redirectUrl') ?? 'https://movie-web.app';
|
||||||
|
const domain = makeUrlIntoDomain(redirectUrl);
|
||||||
|
|
||||||
|
const permissionsGranted = domainWhitelist.includes(domain) && hasPermission;
|
||||||
|
|
||||||
|
const redirectBack = () => {
|
||||||
|
chrome.tabs.getCurrent((tab) => {
|
||||||
|
chrome.tabs.update(tab.id, { url: redirectUrl });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGrantPermission = () => {
|
||||||
|
grantPermission(domain).then(() => {
|
||||||
|
redirectBack();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="inner-container">
|
||||||
|
<h1 className="color-white">Permission</h1>
|
||||||
|
<p className="text-color" style={{ fontSize: 13 }}>
|
||||||
|
Websites need to ask for permission <br /> before they can use this extension
|
||||||
|
</p>
|
||||||
|
<div className="permission-card">
|
||||||
|
<p className="text-color" style={{ textAlign: 'center' }}>
|
||||||
|
The website <span className="color-white">{domain}</span> wants to <br /> use the extension on their page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="footer">
|
||||||
|
<button type="button" className="go-back-btn" onClick={redirectBack}>
|
||||||
|
Go back
|
||||||
|
</button>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grant-permission-btn"
|
||||||
|
onClick={handleGrantPermission}
|
||||||
|
disabled={permissionsGranted}
|
||||||
|
>
|
||||||
|
Grant Permission
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
1
src/types/request.ts
Normal file
1
src/types/request.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export interface BaseRequest {}
|
8
src/types/response.ts
Normal file
8
src/types/response.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type BaseResponse<T = object> =
|
||||||
|
| ({
|
||||||
|
success: true;
|
||||||
|
} & T)
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
};
|
9
src/utils/domains.ts
Normal file
9
src/utils/domains.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export function makeUrlIntoDomain(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
if (!['http:', 'https:'].includes(u.protocol)) return null;
|
||||||
|
return u.host.toLowerCase();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
3
src/utils/extension.ts
Normal file
3
src/utils/extension.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const isChrome = () => {
|
||||||
|
return chrome.runtime.getURL('').startsWith('chrome-extension://');
|
||||||
|
};
|
24
src/utils/fetcher.ts
Normal file
24
src/utils/fetcher.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { type Request as MakeRequest } from '~background/messages/makeRequest';
|
||||||
|
|
||||||
|
export function makeFullUrl(url: string, ops?: MakeRequest): string {
|
||||||
|
// glue baseUrl and rest of url together
|
||||||
|
let leftSide = ops?.baseUrl ?? '';
|
||||||
|
let rightSide = url;
|
||||||
|
|
||||||
|
// left side should always end with slash, if its set
|
||||||
|
if (leftSide.length > 0 && !leftSide.endsWith('/')) leftSide += '/';
|
||||||
|
|
||||||
|
// right side should never start with slash
|
||||||
|
if (rightSide.startsWith('/')) rightSide = rightSide.slice(1);
|
||||||
|
|
||||||
|
const fullUrl = leftSide + rightSide;
|
||||||
|
if (!fullUrl.startsWith('http://') && !fullUrl.startsWith('https://'))
|
||||||
|
throw new Error(`Invald URL -- URL doesn't start with a http scheme: '${fullUrl}'`);
|
||||||
|
|
||||||
|
const parsedUrl = new URL(fullUrl);
|
||||||
|
Object.entries(ops?.query ?? {}).forEach(([k, v]) => {
|
||||||
|
parsedUrl.searchParams.set(k, v);
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedUrl.toString();
|
||||||
|
}
|
35
src/utils/storage.ts
Normal file
35
src/utils/storage.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Storage } from '@plasmohq/storage';
|
||||||
|
import { useStorage } from '@plasmohq/storage/hook';
|
||||||
|
|
||||||
|
import { makeUrlIntoDomain } from '~utils/domains';
|
||||||
|
|
||||||
|
export const DEFAULT_DOMAIN_WHITELIST = ['movie-web.app', 'dev.movie-web.app'];
|
||||||
|
|
||||||
|
export const storage = new Storage();
|
||||||
|
|
||||||
|
const getDomainWhiteList = async () => {
|
||||||
|
const whitelist = await storage.get<string[]>('domainWhitelist');
|
||||||
|
if (!whitelist) await storage.set('domainWhitelist', DEFAULT_DOMAIN_WHITELIST);
|
||||||
|
return whitelist ?? DEFAULT_DOMAIN_WHITELIST;
|
||||||
|
};
|
||||||
|
|
||||||
|
const domainIsInWhitelist = async (domain: string) => {
|
||||||
|
const whitelist = await getDomainWhiteList();
|
||||||
|
return whitelist?.some((d) => d.includes(domain)) ?? false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDomainStorage() {
|
||||||
|
return useStorage<string[]>('domainWhitelist', (v) => v ?? DEFAULT_DOMAIN_WHITELIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isDomainWhitelisted = async (url: string | undefined) => {
|
||||||
|
if (!url) return false;
|
||||||
|
const domain = makeUrlIntoDomain(url);
|
||||||
|
if (!domain) return false;
|
||||||
|
return domainIsInWhitelist(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assertDomainWhitelist = async (url: string) => {
|
||||||
|
const isWhiteListed = await isDomainWhitelisted(url);
|
||||||
|
if (!isWhiteListed) throw new Error('Domain is not whitelisted');
|
||||||
|
};
|
32
src/utils/tabs.ts
Normal file
32
src/utils/tabs.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { isChrome } from './extension';
|
||||||
|
|
||||||
|
export function queryCurrentDomain(cb: (domain: string | null) => void) {
|
||||||
|
const handle = (tabUrl: string | null) => {
|
||||||
|
if (!tabUrl) cb(null);
|
||||||
|
else cb(tabUrl);
|
||||||
|
};
|
||||||
|
const ops = { active: true, currentWindow: true } as const;
|
||||||
|
|
||||||
|
if (isChrome()) chrome.tabs.query(ops).then((tabs) => handle(tabs[0]?.url));
|
||||||
|
else browser.tabs.query(ops).then((tabs) => handle(tabs[0]?.url));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listenToTabChanges(cb: () => void) {
|
||||||
|
if (isChrome()) {
|
||||||
|
chrome.tabs.onActivated.addListener(cb);
|
||||||
|
chrome.tabs.onUpdated.addListener(cb);
|
||||||
|
} else if (browser) {
|
||||||
|
browser.tabs.onActivated.addListener(cb);
|
||||||
|
browser.tabs.onUpdated.addListener(cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopListenToTabChanges(cb: () => void) {
|
||||||
|
if (isChrome()) {
|
||||||
|
chrome.tabs.onActivated.removeListener(cb);
|
||||||
|
chrome.tabs.onUpdated.removeListener(cb);
|
||||||
|
} else if (browser) {
|
||||||
|
browser.tabs.onActivated.removeListener(cb);
|
||||||
|
browser.tabs.onUpdated.removeListener(cb);
|
||||||
|
}
|
||||||
|
}
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "plasmo/templates/tsconfig.base",
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
".plasmo/index.d.ts",
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"~*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"baseUrl": "."
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user