├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── ask-questions.md │ ├── bug--------.md │ └── bug_report.md └── workflows │ ├── dev.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── LICENSE ├── README.md ├── build.config.js ├── docs ├── .dumi │ └── theme │ │ └── locales │ │ └── zh-TW.json ├── .dumirc.ts ├── .gitignore ├── README.md ├── docs │ ├── guide │ │ ├── FAQ.en-US.md │ │ ├── FAQ.md │ │ ├── FAQ.zh-TW.md │ │ ├── cloud-backup.en-US.md │ │ ├── cloud-backup.md │ │ ├── cloud-backup.zh-TW.md │ │ ├── custom-function.en-US.md │ │ ├── custom-function.md │ │ ├── custom-function.zh-TW.md │ │ ├── index.en-US.md │ │ ├── index.md │ │ ├── index.zh-TW.md │ │ ├── modify-body.en-US.md │ │ ├── modify-body.md │ │ ├── modify-body.zh-TW.md │ │ ├── rule.en-US.md │ │ ├── rule.md │ │ ├── rule.zh-TW.md │ │ ├── third-party-rules.en-US.md │ │ ├── third-party-rules.md │ │ └── third-party-rules.zh-TW.md │ ├── index.en-US.md │ ├── index.md │ └── index.zh-TW.md └── package.json ├── extension.json ├── locale ├── create-pr.js ├── locales.js ├── original │ └── messages.json ├── output │ └── .gitkeep ├── sort-origin.js └── transifex.yml ├── package.json ├── pnpm-lock.yaml ├── public ├── _locales │ ├── .gitkeep │ ├── en │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── pl │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── zh_CN │ │ └── messages.json │ └── zh_TW │ │ └── messages.json ├── assets │ └── images │ │ ├── 128.png │ │ └── 128w.png └── index.html ├── scripts ├── config.mjs ├── get-snapshot-version.mjs ├── pack-utils │ ├── amo.mjs │ ├── crx.mjs │ ├── cws.mjs │ ├── edge.mjs │ ├── index.mjs │ └── xpi.mjs ├── pack.mjs ├── release.mjs ├── webpack │ ├── dev.js │ ├── externals.js │ ├── remove-html.js │ └── webpack.plugin.js └── www │ └── CNAME ├── src ├── global.d.ts ├── manifest.json ├── pages │ ├── background │ │ ├── api-handler.ts │ │ ├── core │ │ │ ├── db.ts │ │ │ └── rules.ts │ │ ├── index.ts │ │ ├── request-handler.ts │ │ ├── upgrade.ts │ │ └── utils.ts │ ├── options │ │ ├── components │ │ │ └── bool-radio.tsx │ │ ├── index.tsx │ │ ├── sections │ │ │ ├── group-select │ │ │ │ └── index.tsx │ │ │ ├── import-and-export │ │ │ │ ├── cloud │ │ │ │ │ └── index.tsx │ │ │ │ ├── import-drawer │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ │ ├── options │ │ │ │ └── index.tsx │ │ │ └── rules │ │ │ │ ├── edit │ │ │ │ ├── code-editor.tsx │ │ │ │ ├── encoding.ts │ │ │ │ ├── headers.ts │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ │ ├── float.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── rule-group-card.tsx │ │ │ │ └── utils.ts │ │ └── utils.ts │ └── popup │ │ ├── index.tsx │ │ └── rule │ │ ├── group.tsx │ │ └── rules.tsx └── share │ ├── components │ ├── rule-detail.tsx │ └── semi-locale.tsx │ ├── core │ ├── constant.ts │ ├── emitter.ts │ ├── logger.ts │ ├── notify.ts │ ├── prefs.ts │ ├── rule-utils.ts │ ├── storage.ts │ ├── types.ts │ └── utils.ts │ ├── hooks │ └── use-mark-common.ts │ └── pages │ ├── api.ts │ ├── browser-sync.ts │ ├── file.ts │ └── is-dark-mode.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # 忽略目录 2 | build/ 3 | tests/ 4 | demo/ 5 | .ice/ 6 | scripts/ 7 | locale/ 8 | 9 | # node 覆盖率文件 10 | coverage/ 11 | 12 | # 忽略文件 13 | **/*-min.js 14 | **/*.min.js 15 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { getESLintConfig } = require('@iceworks/spec'); 2 | 3 | // https://www.npmjs.com/package/@iceworks/spec 4 | module.exports = getESLintConfig('react-ts', { 5 | plugins: [ 6 | 'eslint-plugin-unused-imports', 7 | 'eslint-plugin-import', 8 | ], 9 | rules: { 10 | 'react/jsx-filename-extension': 0, 11 | 'react/no-access-state-in-setstate': 0, 12 | 'react-hooks/exhaustive-deps': 0, 13 | '@typescript-eslint/member-ordering': 0, 14 | '@typescript-eslint/no-require-imports': 0, 15 | '@typescript-eslint/explicit-function-return-type': 0, 16 | '@typescript-eslint/ban-ts-comment': 0, 17 | '@iceworks/best-practices/recommend-polyfill': 0, 18 | '@iceworks/best-practices/no-js-in-ts-project': 0, 19 | '@iceworks/best-practices/recommend-functional-component': 0, 20 | 'no-await-in-loop': 0, 21 | 'no-console': 0, 22 | 'no-prototype-builtins': 0, 23 | 'no-return-assign': 0, 24 | 'no-param-reassign': 0, 25 | 'unused-imports/no-unused-imports': 'warn', 26 | 'import/order': [ 27 | 'warn', 28 | { 29 | 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type', 'unknown'], 30 | 'pathGroups': [ 31 | { 32 | 'pattern': '@/**', 33 | 'group': 'parent', 34 | 'position': 'before' 35 | } 36 | ], 37 | 'pathGroupsExcludedImportTypes': ['builtin'], 38 | 'newlines-between': 'never' 39 | } 40 | ] 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ask-questions.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Ask Questions 3 | about: Ask any questions 4 | title: "[Question]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug--------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug报告(中文用户) 3 | about: Bug报告 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **BUG** 11 | 简单描述此BUG,包括你做了什么,你期望发生什么以及实际上发生了什么。 12 | 13 | **规则和测试地址** 14 | 请提供规则内容和测试地址。 15 | 16 | **附加信息** 17 | 其他可能有帮助的信息,如你所做的尝试、相关资料等 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is, and describe what you expected to happen. 12 | 13 | **Rule and Test address** 14 | Please provide rule contents and a test address. 15 | 16 | **Extra informations** 17 | Other information that may be helpful, such as your attempts, related materials, etc. 18 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: dev 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: pnpm/action-setup@v2 21 | with: 22 | version: 7 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'pnpm' 28 | - name: Install dependencies 29 | run: pnpm i --frozen-lockfile 30 | - name: Get snapshot version 31 | env: 32 | TOKEN: ${{ secrets.SNAPSHOT_TOKEN }} 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: node ./scripts/get-snapshot-version.mjs 35 | - name: Build 36 | run: npm run build 37 | - name: Upload bundle 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: dist 41 | path: dist 42 | - name: Upload bundle analyze 43 | uses: actions/upload-artifact@v3 44 | with: 45 | name: bundle-analyze 46 | path: temp/bundle-analyze.html 47 | - name: Publish snapshot 48 | env: 49 | AMO_KEY: ${{ secrets.AMO_KEY }} 50 | AMO_SECRET: ${{ secrets.AMO_SECRET }} 51 | CRX_PRIV_KEY: ${{ secrets.CRX_PRIV_KEY }} 52 | run: npm run pack -- --platform=xpi,crx 53 | - name: Upload snapshot release 54 | uses: actions/upload-artifact@v3 55 | with: 56 | name: release 57 | path: | 58 | temp/release 59 | !temp/release/*-id.txt 60 | 61 | sync-locale: 62 | runs-on: ubuntu-latest 63 | strategy: 64 | matrix: 65 | node-version: [16.x] 66 | steps: 67 | - uses: actions/checkout@v3 68 | - name: Use Node.js ${{ matrix.node-version }} 69 | uses: actions/setup-node@v3 70 | with: 71 | node-version: ${{ matrix.node-version }} 72 | - name: Sort 73 | run: node ./locale/sort-origin.js 74 | - name: Deploy 75 | uses: JamesIves/github-pages-deploy-action@v4 76 | with: 77 | branch: sync-locale 78 | folder: locale 79 | clean: false 80 | commit-message: '[skip ci] sync locale' 81 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: pnpm/action-setup@v2 21 | with: 22 | version: 7 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'pnpm' 28 | - name: Install dependencies 29 | run: pnpm i --frozen-lockfile 30 | - name: Build 31 | run: npm run build 32 | - name: Upload bundles 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: dist 36 | path: dist 37 | build-docs: 38 | runs-on: ubuntu-latest 39 | strategy: 40 | matrix: 41 | node-version: [16.x] 42 | steps: 43 | - uses: actions/checkout@v3 44 | - uses: pnpm/action-setup@v2 45 | with: 46 | version: 7 47 | - name: Use Node.js ${{ matrix.node-version }} 48 | uses: actions/setup-node@v3 49 | with: 50 | node-version: ${{ matrix.node-version }} 51 | - name: Install dependencies 52 | run: | 53 | cd $GITHUB_WORKSPACE/docs 54 | pnpm i 55 | - name: Build 56 | run: | 57 | cd $GITHUB_WORKSPACE/docs 58 | npm run build 59 | cp $GITHUB_WORKSPACE/scripts/www/* $GITHUB_WORKSPACE/docs/dist/ 60 | - name: Deploy 61 | uses: JamesIves/github-pages-deploy-action@v4 62 | with: 63 | branch: gh-pages 64 | folder: docs/dist 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '[0-9].[0-9]+.[0-9]+' 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [16.x] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: pnpm/action-setup@v2 21 | with: 22 | version: 7 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'pnpm' 28 | - name: Install dependencies 29 | run: pnpm i --frozen-lockfile 30 | - name: Build 31 | run: npm run build 32 | - name: Upload bundles 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: dist 36 | path: dist 37 | - name: Pack 38 | env: 39 | AMO_KEY: ${{ secrets.AMO_KEY }} 40 | AMO_SECRET: ${{ secrets.AMO_SECRET }} 41 | CRX_PRIV_KEY: ${{ secrets.CRX_PRIV_KEY }} 42 | CWS_CLIENT_ID: ${{ secrets.CWS_CLIENT_ID }} 43 | CWS_CLIENT_SECRET: ${{ secrets.CWS_CLIENT_SECRET }} 44 | CWS_TOKEN: ${{ secrets.CWS_TOKEN }} 45 | MS_ACCESS_TOKEN_URL: ${{ secrets.MS_ACCESS_TOKEN_URL }} 46 | MS_CLIENT_ID: ${{ secrets.MS_CLIENT_ID }} 47 | MS_CLIENT_SECRET: ${{ secrets.MS_CLIENT_SECRET }} 48 | MS_PRODUCT_ID: ${{ secrets.MS_PRODUCT_ID }} 49 | run: npm run pack 50 | - name: Release 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | SERVER_TOKEN: ${{ secrets.SNAPSHOT_TOKEN }} 54 | run: npm run release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | .idea/ 5 | dist-merge/ 6 | .theia/ 7 | .recore/ 8 | ~* 9 | package-lock.json 10 | 11 | # Packages # 12 | ############ 13 | # it's better to unpack these files and commit the raw source 14 | # git has its own built in compression methods 15 | *.7z 16 | *.dmg 17 | *.gz 18 | *.iso 19 | *.jar 20 | *.rar 21 | *.tar 22 | *.zip 23 | 24 | # Logs and databases # 25 | ###################### 26 | *.log 27 | *.sql 28 | *.sqlite 29 | 30 | # OS generated files # 31 | ###################### 32 | .DS_Store 33 | .Trash* 34 | *.swp 35 | ._* 36 | .Spotlight-V100 37 | .Trashes 38 | ehthumbs.db 39 | Thumbs.db 40 | 41 | encrypt 42 | 43 | .ice 44 | .eslintcache 45 | .vscode 46 | temp 47 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | tests/ 3 | demo/ 4 | .ice/ 5 | coverage/ 6 | **/*-min.js 7 | **/*.min.js 8 | package-lock.json 9 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const { getPrettierConfig } = require('@iceworks/spec'); 2 | 3 | module.exports = getPrettierConfig('react'); 4 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | # 忽略目录 2 | build/ 3 | tests/ 4 | demo/ 5 | 6 | # node 覆盖率文件 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const { getStylelintConfig } = require('@iceworks/spec');; 2 | 3 | module.exports = getStylelintConfig('react'); 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Header Editor 3 |

4 | 5 | [![Release](https://img.shields.io/github/release/FirefoxBar/HeaderEditor.svg?label=Release)](https://github.com/FirefoxBar/HeaderEditor/releases) 6 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/users/eningockdidmgiojffjmkdblpjocbhgh?label=Chrome)](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) 7 | [![Mozilla Addons](https://img.shields.io/amo/users/header-editor?label=Firefox)](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) 8 | [![license](https://img.shields.io/github/license/FirefoxBar/HeaderEditor.svg?label=License)](https://github.com/FirefoxBar/HeaderEditor/blob/master/LICENSE) 9 | [![Discussions](https://img.shields.io/github/discussions/FirefoxBar/HeaderEditor?label=Discussions)](https://github.com/FirefoxBar/HeaderEditor/discussions) 10 | [![Build Status](https://github.com/FirefoxBar/HeaderEditor/actions/workflows/dev.yml/badge.svg)](https://github.com/FirefoxBar/HeaderEditor/actions/workflows/dev.yml) 11 | 12 | An extension which can modify the request, include request headers, response headers, redirect requests, and cancel requests. 13 | 14 | It's 100% FREE, no ADs, no data collection. 15 | 16 | For more documentations, Please visit [he.firefoxcn.net](https://he.firefoxcn.net) 17 | 18 | ## Get this extension 19 | 20 | ![Firefox Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/firefox/firefox_16x16.png) [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/). 21 | 22 | ![Chrome Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/chrome/chrome_16x16.png) [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh). 23 | 24 | ![Edge Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/edge/edge_16x16.png) [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko). 25 | 26 | ![Firefox Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/firefox/firefox_16x16.png) Install our [self-distributed version](https://github.com/FirefoxBar/HeaderEditor/releases). 27 | 28 | ## About Permissions 29 | 30 | Header Editor require those permissions: 31 | 32 | * `tabs`: Open links (such as the options page) or switch to a tab 33 | 34 | * `webRequest`, `webRequestBlocking`, `_all_urls_`: Modify requests 35 | 36 | * `storage`, `unlimitedStorage`: Storage rules and settings 37 | 38 | * `unsafe-eval`: Execute custom function, code at [src/share/core/rule-utils.ts#L8](https://github.com/FirefoxBar/HeaderEditor/blob/dev/src/share/core/rule-utils.ts#L8) (may change in the future, you can search for the newest location by `new Function`) 39 | 40 | ## Contribution 41 | 42 | Contribute codes: [Submitting a pull request](https://github.com/FirefoxBar/HeaderEditor/compare) 43 | 44 | Thanks to them for their contribution: [YFdyh000](https://github.com/yfdyh000) [iNaru](https://github.com/Inaru) 45 | 46 | ### Translation 47 | 48 | English: Please submit a issue or pull request for file `locale/original/messages.json` 49 | 50 | Other language: Please translate them on [Transifex](https://www.transifex.com/sytec/header-editor/) 51 | 52 | Please note that some languages (such as zh-Hans) will not be translated on transifex because the browser does not support them, click [here](https://developer.chrome.com/docs/webstore/i18n/#choosing-locales-to-support) to view full list 53 | 54 | ## How to build 55 | 56 | ### Build 57 | 58 | * Install node (14+) and pnpm. 59 | 60 | * Clone this project, or download the source code and extract it. 61 | 62 | * Run `pnpm i`. 63 | 64 | * Run `npm run build` 65 | 66 | * Find build result at `/dist` 67 | 68 | #### Development 69 | 70 | * Run `npm run start` 71 | 72 | * Open browser, load extension from `/dist` directory or `/dist/manifest.json` 73 | 74 | ## Licenses 75 | 76 | Copyright © 2017-2023 [FirefoxBar Team](https://team.firefoxcn.net) 77 | 78 | Open source licensed under [GPLv2](LICENSE). 79 | -------------------------------------------------------------------------------- /build.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | externals: { 3 | react: 'React', 4 | 'react-dom': 'ReactDOM', 5 | }, 6 | outputDir: 'dist', 7 | outputAssetsPath: { 8 | js: 'assets/js', 9 | css: 'assets/css', 10 | }, 11 | mpa: true, 12 | vendor: false, 13 | browserslist: { 14 | chrome: 85, 15 | firefox: 77, 16 | edge: 85, 17 | }, 18 | plugins: [ 19 | [ 20 | 'build-plugin-css-assets-local', 21 | { 22 | outputPath: 'assets/css-assets', 23 | relativeCssPath: '/', 24 | }, 25 | ], 26 | './scripts/webpack/webpack.plugin.js', 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /docs/.dumi/theme/locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "header.search.placeholder": "输入关键字搜索...", 3 | "header.color.mode.light": "亮色模式", 4 | "header.color.mode.dark": "暗色模式", 5 | "header.color.mode.auto": "跟随系统", 6 | "header.social.github": "GitHub", 7 | "header.social.weibo": "微博", 8 | "header.social.twitter": "Twitter", 9 | "header.social.gitlab": "GitLab", 10 | "header.social.facebook": "Facebook", 11 | "header.social.zhihu": "知乎", 12 | "header.social.yuque": "语雀", 13 | "header.social.linkedin": "Linkedin", 14 | "previewer.actions.code.expand": "展开代码", 15 | "previewer.actions.code.shrink": "收起代码", 16 | "previewer.actions.codesandbox": "在 CodeSandbox 中打开", 17 | "previewer.actions.codepen": "在 CodePen 中打开(未实现)", 18 | "previewer.actions.stackblitz": "在 StackBlitz 中打开", 19 | "previewer.actions.separate": "在独立页面中打开", 20 | "404.title": "页面未找到", 21 | "404.back": "返回首页", 22 | "api.component.name": "属性名", 23 | "api.component.description": "描述", 24 | "api.component.type": "类型", 25 | "api.component.default": "默认值", 26 | "api.component.required": "(必选)", 27 | "api.component.unavailable": "必须启用 apiParser 才能使用自动 API 特性", 28 | "api.component.loading": "属性定义正在解析中,稍等片刻...", 29 | "api.component.not.found": "未找到 {id} 组件的属性定义", 30 | "content.tabs.default": "文档", 31 | "search.not.found": "未找到相关内容", 32 | "layout.sidebar.btn": "侧边菜单" 33 | } -------------------------------------------------------------------------------- /docs/.dumirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'dumi'; 2 | 3 | export default defineConfig({ 4 | themeConfig: { 5 | name: 'Header Editor', 6 | logo: false, 7 | footer: false, 8 | socialLinks: { 9 | github: 'https://github.com/FirefoxBar/HeaderEditor', 10 | }, 11 | }, 12 | locales: [ 13 | { id: 'zh-CN', name: '简体中文' }, 14 | { id: 'en-US', name: 'English' }, 15 | { id: 'zh-TW', name: '繁體中文' }, 16 | ], 17 | analytics: { 18 | baidu: 'eddab75c23e1853a476011bb95a585c9', 19 | } 20 | }); -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .dumi/tmp 3 | .dumi/tmp-production 4 | dist -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Header Editor Docs 2 | 3 | A static site base on [dumi](https://d.umijs.org). 4 | 5 | ## Development 6 | 7 | ```bash 8 | # install dependencies 9 | $ pnpm install 10 | 11 | # start dev server 12 | $ pnpm start 13 | 14 | # build docs 15 | $ pnpm run build 16 | ``` 17 | 18 | ## LICENSE 19 | 20 | MIT 21 | -------------------------------------------------------------------------------- /docs/docs/guide/FAQ.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: Introduction 4 | title: FAQ 5 | order: 2 6 | --- 7 | 8 | ## Why is "header name" reduced to lowercase? 9 | 10 | [RFC 2616](https://tools.ietf.org/html/rfc2616.html#section-4.2) says: 11 | 12 | > Each header field consists of a name followed by a colon `(":")` and the field value. Field names are case-insensitive. 13 | 14 | So, since 4.0.0, Header Editor will reduce "header name" to lowercase. Except for custom functions: the custom function will still get the original header (except that it has been modified by other rules) 15 | 16 | ## Can I delete a header in a simple way? 17 | 18 | Yes, just modify it to `_header_editor_remove_` 19 | 20 | ## Rules disappear 21 | 22 | As we know, the rules will disappear or not work 23 | 24 | **Note: Before doing all of the following, please back up your Chrome/Firefox profile folder!** 25 | 26 | ### Not work in Private Mode 27 | 28 | Popup panel and management page is not work in Private Mode of Firefox. But the main features are available 29 | 30 | #### Chrome 31 | 32 | * Open `chrome://extensions/?id=eningockdidmgiojffjmkdblpjocbhgh`, enable "Enabled in incognito mode" 33 | 34 | #### Firefox 35 | 36 | * Open about:debugging, find the Internal UUID of Header Editor (e.g. d52e1cf2-22e5-448d-a882-e68f3211fa76). 37 | * Open Firefox options. 38 | * Go to Privacy & Security. 39 | * Set History mode to "Use custom settings". 40 | * Click "Exceptions". 41 | * Paste our URL: `moz-extension://{Internal UUID}/` (the `{Internal UUID}` is the Internal UUID of Header Editor you found in step one), for example, `moz-extension://d52e1cf2-22e5-448d-a882-e68f3211fa76/`, Then click 'Allow'. 42 | * Click "Save Changes". 43 | 44 | ### Rules automatically deleted in Firefox 45 | 46 | Thanks to [Thorin-Oakenpants](https://github.com/Thorin-Oakenpants) and [henshin](https://github.com/henshin) 47 | 48 | * Open `about:config`, make sure that `dom.indexedDB.enabled` is `true` 49 | * Try to change `extensions.webextensions.keepUuidOnUninstall` into true, is your problem solved? 50 | * Open your Firefox profile folder, if there are many files (about a thousand or more) named prefs-xxxx.js files with 0 bytes, closed firefox and deleted them. 51 | 52 | ## Other questions? 53 | 54 | Just [submit a issue](https://github.com/FirefoxBar/HeaderEditor/issues/new/choose) 55 | -------------------------------------------------------------------------------- /docs/docs/guide/FAQ.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 指南 3 | group: 介绍 4 | title: FAQ 5 | order: 2 6 | --- 7 | 8 | ## 为什么“头名称”变成小写了? 9 | 10 | [RFC 2616](https://tools.ietf.org/html/rfc2616.html#section-4.2)中写到: 11 | 12 | > Each header field consists of a name followed by a colon `(":")` and the field value. Field names are case-insensitive. 13 | 14 | 因此,从4.0.0开始,Header Editor会将“头名称”变为小写。但自定义函数除外:除了已被其他规则修改的头外,自定义函数获取到的仍然是原始头 15 | 16 | ## 我能以简单的方式删除头吗? 17 | 18 | 可以,只需将其修改为`_header_editor_remove_` 19 | 20 | ## 规则消失 21 | 22 | 我们已知,在某些情况下,规则会消失或不起作用 23 | 24 | **注意:在执行以下所有操作之前,请备份您的Chrome/Firefox配置文件文件夹!** 25 | 26 | ### 在隐私模式下无作用 27 | 28 | 小面板和管理页面在Firefox的隐私模式下不能使用。但是主要功能可用。 29 | 30 | #### Chrome 31 | 32 | * 打开`chrome://extensions/?id=eningockdidmgiojffjmkdblpjocbhgh`,启用“以隐身模式启用” 33 | 34 | #### Firefox 35 | 36 | * 打开about:debugging,找到Header Editor的内部UUID(例如d52e1cf2-22e5-448d-a882-e68f3211fa76)。 37 | * 打开Firefox选项。 38 | * 转到隐私和安全。 39 | * 将历史记录模式设置为“使用自定义设置”。 40 | * 单击“例外”。 41 | * 粘贴我们的URL:`moz-extension://{Internal UUID}/`(`{Internal UUID}`是您在第一步中找到的Header Editor的内部UUID),例如,`moz-extension://d52e1cf2-22e5-448d-a882-e68f3211fa76/`,然后点击“允许”。 42 | * 单击“保存更改”。 43 | 44 | ### 规则在Firefox中自动删除 45 | 46 | 感谢[Thorin-Oakenpants](https://github.com/Thorin-Oakenpants)和[henshin](https://github.com/henshin) 47 | 48 | * 打开`about:config`,确保`dom.indexedDB.enabled`为`true` 49 | * 尝试将`extensions.webextensions.keepUuidOnUninstall`更改为true,您的问题是否解决? 50 | * 打开Firefox配置文件文件夹,如果存在许多(一千+或更多)名为prefs-xxxx.js且文件大小为0的文件,请关闭Firefox并将它们删除。 51 | 52 | ## 还有问题? 53 | 54 | 请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new/choose) 55 | -------------------------------------------------------------------------------- /docs/docs/guide/FAQ.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 介紹 4 | title: FAQ 5 | order: 2 6 | --- 7 | 8 | ## 为什么“头名称”变成小写了? 9 | 10 | [RFC 2616](https://tools.ietf.org/html/rfc2616.html#section-4.2)中写到: 11 | 12 | > Each header field consists of a name followed by a colon `(":")` and the field value. Field names are case-insensitive. 13 | 14 | 因此,从4.0.0开始,Header Editor会将“头名称”变为小写。但自定义函数除外:除了已被其他规则修改的头外,自定义函数获取到的仍然是原始头 15 | 16 | ## 我能以简单的方式删除头吗? 17 | 18 | 可以,只需将其修改为`_header_editor_remove_` 19 | 20 | ## 规则消失 21 | 22 | 我们已知,在某些情况下,规则会消失或不起作用 23 | 24 | **注意:在执行以下所有操作之前,请备份您的Chrome/Firefox配置文件文件夹!** 25 | 26 | ### 在隐私模式下无作用 27 | 28 | 小面板和管理页面在Firefox的隐私模式下不能使用。但是主要功能可用。 29 | 30 | #### Chrome 31 | 32 | * 打开`chrome://extensions/?id=eningockdidmgiojffjmkdblpjocbhgh`,启用“以隐身模式启用” 33 | 34 | #### Firefox 35 | 36 | * 打开about:debugging,找到Header Editor的内部UUID(例如d52e1cf2-22e5-448d-a882-e68f3211fa76)。 37 | * 打开Firefox选项。 38 | * 转到隐私和安全。 39 | * 将历史记录模式设置为“使用自定义设置”。 40 | * 单击“例外”。 41 | * 粘贴我们的URL:`moz-extension://{Internal UUID}/`(`{Internal UUID}`是您在第一步中找到的Header Editor的内部UUID),例如,`moz-extension://d52e1cf2-22e5-448d-a882-e68f3211fa76/`,然后点击“允许”。 42 | * 单击“保存更改”。 43 | 44 | ### 规则在Firefox中自动删除 45 | 46 | 感谢[Thorin-Oakenpants](https://github.com/Thorin-Oakenpants)和[henshin](https://github.com/henshin) 47 | 48 | * 打开`about:config`,确保`dom.indexedDB.enabled`为`true` 49 | * 尝试将`extensions.webextensions.keepUuidOnUninstall`更改为true,您的问题是否解决? 50 | * 打开Firefox配置文件文件夹,如果存在许多(一千+或更多)名为prefs-xxxx.js且文件大小为0的文件,请关闭Firefox并将它们删除。 51 | 52 | ## 还有问题? 53 | 54 | 请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new/choose) 55 | -------------------------------------------------------------------------------- /docs/docs/guide/cloud-backup.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: Basic features 4 | title: Cloud backup 5 | order: 2 6 | --- 7 | 8 | ## Summary 9 | 10 | Support for cloud backup started with Header Editor 4.0.5. 11 | 12 | **Important: To use cloud backup, you should login your browser's account (like Firefox account, Google account, etc), and enable synchronize in browser's setting.** 13 | 14 | Cloud backup is supported through your browser's sync feature, as in Firefox Sync, or Chrome Sync, i.e. It means that HE has no server to storage your backup, your backup is storage at your browser's provider's server (like Mozilla, Google, etc). If your browser does not support sync, this feature will take no effect. 15 | 16 | ## What contents can be backup? 17 | 18 | Your setting will be synchronize automatically, the backup feature only backup your rules, include groups. 19 | 20 | ## Limit 21 | 22 | Both Chrome and Firefox have its space limit, about 100KB. If you have too many rules, upload will be failed, but you can use the export normally. 23 | 24 | As I know, Chrome has limits on the number of operations per unit of time. It means that you **can not** upload frequently. 25 | 26 | ## Other caveats 27 | 28 | ### Chrome/Chromium 29 | 30 | * See [chrome.storage API](https://developer.chrome.com/extensions/storage#property-sync) for more technical details. 31 | 32 | ### Firefox 33 | 34 | * It seems that Firefox Sync is executed regularly, however if you want to force the cloud export you've to launch Firefox Sync manually. 35 | ![](https://user-images.githubusercontent.com/886325/41821498-e081fe7e-77e1-11e8-81de-03a09d826cb9.png) 36 | * A new installation may cause cloud storage data to be blanked. 37 | * See [browser.storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage) for more technical details. 38 | -------------------------------------------------------------------------------- /docs/docs/guide/cloud-backup.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 指南 3 | group: 常用功能 4 | title: 云同步 5 | order: 2 6 | --- 7 | 8 | ## 综述 9 | 10 | 自Header Editor 4.0.5起,支持云同步。 11 | 12 | **注意:你需要登录你的浏览器账号(如Firefox账号、Google账号等),并启用浏览器的同步功能** 13 | 14 | 云同步基于浏览器的同步功能,如Firefox Sync、Chrome Sync等。这意味着,HE并不会在自己的服务器上存储您的备份。您的备份存储在您的浏览器提供商的服务器上(如Mozilla、Google的服务器上)。如果您的浏览器不支持云同步,此功能不会有任何效果。 15 | 16 | ## 哪些内容会被备份? 17 | 18 | 您的设置会被自动备份。备份功能仅会备份您的规则,包括分组信息。 19 | 20 | ## 限制 21 | 22 | Firefox和Chrome都有各自的空间限制,大约100KB。如果您的规则过多,上传过程会失败,但您依然可以通过传统方式导入和导出。 23 | 24 | 就我所知,Chrome还会限制上传频率,也就是说,您**不能**过快的进行上传。 25 | 26 | ## 其他技术细节 27 | 28 | ### Chrome/Chromium 29 | 30 | * 请查看[chrome.storage API](https://developer.chrome.com/extensions/storage#property-sync)获取更多技术细节。 31 | 32 | ### Firefox 33 | 34 | * 据推测,Firefox会定期进行同步。但您可以通过手动运行,强行进行一次同步。 35 | ![](https://user-images.githubusercontent.com/886325/41821498-e081fe7e-77e1-11e8-81de-03a09d826cb9.png) 36 | * 重新安装扩展可能导致同步内容丢失。 37 | * 请查看[browser.storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)获取更多技术细节。 38 | -------------------------------------------------------------------------------- /docs/docs/guide/cloud-backup.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 常用功能 4 | title: 雲同步 5 | order: 2 6 | --- 7 | 8 | ## 综述 9 | 10 | 自Header Editor 4.0.5起,支持云同步。 11 | 12 | **注意:你需要登录你的浏览器账号(如Firefox账号、Google账号等),并启用浏览器的同步功能** 13 | 14 | 云同步基于浏览器的同步功能,如Firefox Sync、Chrome Sync等。这意味着,HE并不会在自己的服务器上存储您的备份。您的备份存储在您的浏览器提供商的服务器上(如Mozilla、Google的服务器上)。如果您的浏览器不支持云同步,此功能不会有任何效果。 15 | 16 | ## 哪些内容会被备份? 17 | 18 | 您的设置会被自动备份。备份功能仅会备份您的规则,包括分组信息。 19 | 20 | ## 限制 21 | 22 | Firefox和Chrome都有各自的空间限制,大约100KB。如果您的规则过多,上传过程会失败,但您依然可以通过传统方式导入和导出。 23 | 24 | 就我所知,Chrome还会限制上传频率,也就是说,您**不能**过快的进行上传。 25 | 26 | ## 其他技术细节 27 | 28 | ### Chrome/Chromium 29 | 30 | * 请查看[chrome.storage API](https://developer.chrome.com/extensions/storage#property-sync)获取更多技术细节。 31 | 32 | ### Firefox 33 | 34 | * 据推测,Firefox会定期进行同步。但您可以通过手动运行,强行进行一次同步。 35 | ![](https://user-images.githubusercontent.com/886325/41821498-e081fe7e-77e1-11e8-81de-03a09d826cb9.png) 36 | * 重新安装扩展可能导致同步内容丢失。 37 | * 请查看[browser.storage API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)获取更多技术细节。 38 | -------------------------------------------------------------------------------- /docs/docs/guide/custom-function.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: Advanced 4 | order: 3 5 | title: Custom function 6 | order: 1 7 | --- 8 | 9 | ## Summary 10 | 11 | Use custom functions to achieve more flexible functionality. So far, custom functions can be used in the following events: redirect request, modify the request headers, modify the response headers. 12 | 13 | Custom functions are also limited by matching rules and exclusion rules. Only requests that meet the matching rules and do not satisfy the exclusion rule are processed by the custom function. 14 | 15 | The priority of the custom function is not determined. It may be possible to customize the function earlier than the normal rule to the request, or it may be later. The order of execution of multiple custom functions is also variable. 16 | 17 | When you can use normal rules to complete the case, please try to use the general rules, rather than custom function 18 | 19 | Custom function writing does **NOT** include the function head and tail, including only the function body. which is: 20 | 21 | ```javascript 22 | function(val, detail) { // does not include this line 23 | // The codes you need to write 24 | } // does not include this line 25 | ``` 26 | 27 | For example: 28 | 29 | ![image](https://user-images.githubusercontent.com/5326684/54876966-6bd6c480-4e53-11e9-8e9d-6c950f8b5cd2.png) 30 | 31 | The custom function passes the arguments `val` and `detail`, where `detail` is the new parameter in version 2.3.0, see the description below. The return type varies depending on the rule type. 32 | 33 | ## Redirect request 34 | 35 | Pass the string with the full URL, if the function is not processed to return NULL or the original argument. For example, the following code will add a `_test` to every request: 36 | 37 | ```javascript 38 | if (val.includes('_test.')) { 39 | return val; 40 | } 41 | const a = val.lastIndexOf('.'); 42 | if (a < 0) { 43 | return val; 44 | } else { 45 | return val.substr(0, a) + '_test' + val.substr(a); 46 | } 47 | ``` 48 | 49 | Since 4.0.3, return `_header_editor_cancel_` will cancel this request, for example: 50 | 51 | ```javascript 52 | if (val.includes('utm_source')) { 53 | return '_header_editor_cancel_'; 54 | } 55 | ``` 56 | 57 | ## Modify the request headers and response headers 58 | 59 | The incoming parameter is an array containing all header information in the following format: `[{"name: "header name", "value": "header content"} ... ]`. 60 | 61 | Because JS pass the Object by reference, the custom function does not need any return value, only need to modify the incoming parameters to take effect. For example, this code will add ` HE/2.0.0` to the end of `User-Agent`: 62 | 63 | ```javascript 64 | for (const a in val) { 65 | if (val[a].name.toLowerCase() === 'user-agent') { 66 | val[a].value += ' HE/2.0.0'; 67 | break; 68 | } 69 | } 70 | ``` 71 | 72 | Note: the browser requires that value must be String, i.e. 73 | 74 | ```javascript 75 | let value = 123; 76 | val.push({"name": "test", "value": value}); //Invalid, because value is number 77 | val.push({"name": "test", "value": value.toString()}); //Valid 78 | ``` 79 | 80 | ## detail object 81 | 82 | Since 2.3.0, the custom function adds the parameter `detail` for the more precise control 83 | 84 | This parameter is Object and is a read-only parameter. The structure is as follows: 85 | 86 | ```javascript 87 | { 88 | // Request id. Since 4.0.3 89 | id: 1234, 90 | // Request url. If this request has been redirected, this url is redirected url 91 | url: "http://example.com/example_redirected.png", 92 | // Tab ID. Note that this ID may be duplicated if user open multiple browser windows. Since 4.1.0 93 | tab: 2, 94 | // Request method, such as "GET", "POST", etc. 95 | method: "GET", 96 | // Request frame ID. Since 4.1.0 97 | frame: 123, 98 | // Request's parent frame ID. Since 4.1.0 99 | parentFrame: -1, 100 | // Request's proxy info. Since 4.1.0 101 | proxy: { 102 | host: "localhost", 103 | port: 8080 104 | }, 105 | // Resource type 106 | type: "image", 107 | // Request time 108 | time: 1505613357577.7522, 109 | // URL of the resource which triggered the request. For example, if "https://example.com" contains a link, and the user clicks the link, then the originUrl for the resulting request is "https://example.com". 110 | // Since 4.1.0 111 | originUrl: '', 112 | // URL of the document in which the resource will be loaded. Only avaliable in Firefox. Since 4.1.0 113 | documentUrl: '', 114 | // Contains request header if enable "Include request headers in custom function" and this time is response 115 | // May be null. Since 4.1.0 116 | requestHeaders: null 117 | } 118 | ``` 119 | 120 | Available resource type see [here](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType) 121 | 122 | You can use this to implement some advanced features, for example, the following code will only redirect images and videos from example.com to example.org: 123 | 124 | ```javascript 125 | if (detail.type === "media") { 126 | return val.replace("example.com", "example.org"); 127 | } 128 | ``` 129 | 130 | ## How to debug a custom function 131 | 132 | All custom functions are run in the background page, so to debug custom functions, open the console of the background page 133 | 134 | Chrome: Enable developer mode in `chrome://extensions/`, then click the "Inspect views" - "background page" at the bottom of Header Editor 135 | 136 | Firefox: Open `about:debugging`, enable add-on debugging, click the "Debug" at the bottom of Header Editor 137 | -------------------------------------------------------------------------------- /docs/docs/guide/custom-function.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 指南 3 | group: 4 | title: 高级 5 | order: 3 6 | title: 自定义函数 7 | order: 1 8 | --- 9 | 10 | ## 综述 11 | 12 | 使用自定义函数可以实现更灵活的功能。目前为止,自定义函数可以在以下事件中使用:重定向请求、修改请求头、修改响应头。 13 | 14 | 自定义函数也受匹配规则和排除规则的限制。只有满足匹配规则且不满足排除规则的请求会被自定义函数处理。 15 | 16 | 自定义函数的优先级不是确定的。可能自定义函数比普通规则更早的作用到请求上,也可能更迟。多个自定义函数的执行顺序也不定。 17 | 18 | 在可以使用普通规则完成的情况下,请尽量使用普通规则,而不是自定义函数 19 | 20 | 自定义函数编写**不包括**函数头尾,只包括函数主体。即: 21 | 22 | ```javascript 23 | function(val, detail) { //不包括这一行 24 | // 你需要编写的部分 25 | } //不包括这一行 26 | ``` 27 | 28 | 例如: 29 | 30 | ![image](https://user-images.githubusercontent.com/5326684/54876966-6bd6c480-4e53-11e9-8e9d-6c950f8b5cd2.png) 31 | 32 | 自定义函数会传入参数`val`和`detail`,其中`detail`是2.3.0版本新增的参数,请参见页面下方说明。返回类型根据规则类型不同而不同。 33 | 34 | ## 重定向请求 35 | 36 | 传入参数为完整URL的字符串,若函数不处理可返回NULL或原参数。例如,下面代码会将请求都加上一个`_test`: 37 | 38 | ```javascript 39 | if (val.includes('_test.')) { 40 | return val; 41 | } 42 | let a = val.lastIndexOf('.'); 43 | if (a < 0) { 44 | return val; 45 | } else { 46 | return val.substr(0, a) + '_test' + val.substr(a); 47 | } 48 | ``` 49 | 50 | 自4.0.3起,返回`_header_editor_cancel_`可取消此请求,如: 51 | 52 | ```javascript 53 | if (val.includes('utm_source')) { 54 | return '_header_editor_cancel_'; 55 | } 56 | ``` 57 | 58 | ## 修改请求头和响应头 59 | 60 | 传入参数为一个数组,包含所有头信息,格式为:`[{"name: "头名称", "value": "头内容"} …… ]`。 61 | 62 | 因JS传递Object时是引用传递,因此自定义函数不需要任何返回值,只需要修改传入的参数即可生效。例如,此代码会将`User-Agent`加上` HE/2.0.0`: 63 | 64 | ```javascript 65 | for (const a in val) { 66 | if (val[a].name.toLowerCase() === 'user-agent') { 67 | val[a].value += ' HE/2.0.0'; 68 | break; 69 | } 70 | } 71 | ``` 72 | 73 | 注意:浏览器要求value必须是String,即: 74 | 75 | ```javascript 76 | let value = 123; 77 | val.push({"name": "test", "value": value}); //不合法,因为value是number 78 | val.push({"name": "test", "value": value.toString()}); //合法 79 | ``` 80 | 81 | ## detail对象 82 | 83 | 自2.3.0开始,自定义函数增加参数`detail`,用于实现更精确的控制 84 | 85 | 此参数为Object,且为只读参数。结构如下: 86 | 87 | ```javascript 88 | { 89 | // 请求ID,自4.0.3可用 90 | id: 1234, 91 | // 请求地址,如果有跳转,此地址是跳转后的地址 92 | url: "http://example.com/example_redirected.png", 93 | // 标签页ID,注意如果用户打开了多个浏览器窗口,这个ID可能会重复,自4.1.0可用 94 | tab: 2, 95 | // 请求方式,如GET、POST 96 | method: "GET", 97 | // 请求所属的frame ID,自4.1.0可用 98 | frame: 123, 99 | // 请求所属的frame的父级ID,自4.1.0可用 100 | parentFrame: -1, 101 | // 请求当前的代理信息,可能为null,自4.1.0可用 102 | proxy: { 103 | host: "localhost", 104 | port: 8080 105 | }, 106 | // 资源类型 107 | type: "image", 108 | // 请求发起的时间戳 109 | time: 1505613357577.7522, 110 | // 触发此请求的URL,例如在页面A上点击了链接B,则B中可以通过此参数获取到A的地址。可能为空 111 | originUrl: '', 112 | // 资源将会被加载到的地址,仅Firefox可用,可能为空 113 | documentUrl: '', 114 | // 如果开启了“在自定义函数中包含请求头”且此次触发是在响应时,则此处是请求时的头信息,可能为null,自4.1.0可用 115 | requestHeaders: null 116 | } 117 | ``` 118 | 119 | 可用资源类型见[此处](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType) 120 | 121 | 您可以借此实现一些高级功能,例如,下面的代码只会将example.com域名下的图片和视频重定向到example.org: 122 | 123 | ```javascript 124 | if (detail.type === "media") { 125 | return val.replace("example.com", "example.org"); 126 | } 127 | ``` 128 | 129 | ## 如何调试自定义函数 130 | 131 | 所有自定义函数的运行均位于后台页面,因此,要调试自定义函数,请打开后台页面的控制台 132 | 133 | Chrome:在`chrome://extensions/`中,启用“开发者模式”,点击Header Editor下方的“检查视图”-“背景页” 134 | 135 | Firefox:打开`about:debugging`,启用附加组件调试,点击Header Editor下方的“调试” 136 | -------------------------------------------------------------------------------- /docs/docs/guide/custom-function.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 高級 4 | order: 3 5 | title: 自訂函數 6 | order: 1 7 | --- 8 | 9 | ## 綜述 10 | 11 | 使用自訂函數可以實現更靈活的功能。目前為止,自訂函數可以在以下事件中使用:重新導向要求、變更要求標頭、變更回應標頭。 12 | 13 | 自訂函數也受比較規則和排除規則的限制。只有滿足比較規則且不滿足排除規則的要求會被自訂函數處理。 14 | 15 | 自訂函數的優先等級不是確定的。可能自訂函數比普通規則更早的作用到要求上,也可能更遲。多個自訂函數的執行順序也不定。 16 | 17 | 在可以使用普通規則完成的情況下,請盡量使用普通規則,而不是自訂函數 18 | 19 | 自訂函數編寫**不包括**函數頭尾,只包括函數主體。即: 20 | 21 | ```javascript 22 | function(val, detail) { //不包括這一行 23 | // 你需要編寫的部分 24 | } //不包括這一行 25 | ``` 26 | 27 | 例如: 28 | 29 | ![image](https://user-images.githubusercontent.com/5326684/54876966-6bd6c480-4e53-11e9-8e9d-6c950f8b5cd2.png) 30 | 31 | 自訂函數會傳入參數`val`和`detail`,其中`detail`是2.3.0版本新增的參數,請參見頁面下方說明。返回類型根據規則類型不同而不同。 32 | 33 | ## 重新導向要求 34 | 35 | 傳入參數為完整URL的字串,若函數不處理可返回NULL或原參數。例如,下面語法會將要求都加上一個`_test`: 36 | 37 | ```javascript 38 | if (val.includes('_test.')) { 39 | return val; 40 | } 41 | let a = val.lastIndexOf('.'); 42 | if (a < 0) { 43 | return val; 44 | } else { 45 | return val.substr(0, a) + '_test' + val.substr(a); 46 | } 47 | ``` 48 | 49 | 自4.0.3起,返回`_header_editor_cancel_`可取消此请求,如: 50 | 51 | ```javascript 52 | if (val.includes('utm_source')) { 53 | return '_header_editor_cancel_'; 54 | } 55 | ``` 56 | 57 | ## 變更要求標頭和回應標頭 58 | 59 | 傳入參數為一個陣列,包含所有標頭資訊,格式為:`[{"name: "標頭名稱", "value": "標內容"} …… ]`。 60 | 61 | 因JS傳遞Object時是參照傳遞,因此自訂函數不需要任何傳回值,只需要變更傳入的參數即可生效。例如,此語法會將`User-Agent`加上` HE/2.0.0`: 62 | 63 | ```javascript 64 | for (let a in val) { 65 | if (val[a].name.toLowerCase() === 'user-agent') { 66 | val[a].value += ' HE/2.0.0'; 67 | break; 68 | } 69 | } 70 | ``` 71 | 72 | 注意:浏览器要求value必须是String,即: 73 | 74 | ```javascript 75 | let value = 123; 76 | val.push({"name": "test", "value": value}); //不合法,因为value是number 77 | val.push({"name": "test", "value": value.toString()}); //合法 78 | ``` 79 | 80 | ## detail对象 81 | 82 | 自2.3.0開始,自訂函數增加參數`detail`,用於實現更精確的控制 83 | 84 | 此參數為Object,且為唯讀參數。結構下列: 85 | 86 | ```javascript 87 | { 88 | // 要求ID,自4.0.3可用 89 | "id": 123456, 90 | // 要求位址,如果有跳轉,此位址是跳轉後的位址 91 | "url": "http://example.com/example_redirected.js", 92 | // 要求方式,如GET、POST 93 | "method": "GET", 94 | // 是否為iframe的要求 95 | "isFrame": 0, 96 | // 資源類型 97 | "type": "script", 98 | // 要求發起的時間戳 99 | "time": 1505613357577.7522, 100 | // 要求發起時的URL,可能為空 101 | "originUrl": "http://example.com/" 102 | } 103 | ``` 104 | 105 | 可用類型參見[此处](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/ResourceType) 106 | 107 | 您可以借此实现一些高级功能,例如,下面的代码只会将example.com域名下的图片和视频重定向到example.org: 108 | 109 | ```javascript 110 | if (detail.type === "media") { 111 | return val.replace("example.com", "example.org"); 112 | } 113 | ``` 114 | 115 | ## 如何调试自定义函数 116 | 117 | 所有自定义函数的运行均位于后台页面,因此,要调试自定义函数,请打开后台页面的控制台 118 | 119 | Chrome:在`chrome://extensions/`中,启用“开发者模式”,点击Header Editor下方的“检查视图”-“背景页” 120 | 121 | Firefox:打开`about:debugging`,启用附加组件调试,点击Header Editor下方的“调试” 122 | -------------------------------------------------------------------------------- /docs/docs/guide/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | title: Guide 4 | order: 1 5 | group: 6 | title: Introduction 7 | order: 1 8 | title: Setup 9 | order: 1 10 | --- 11 | 12 | ## Install 13 | 14 | Please choose a different installation method depending on your browser: 15 | 16 | | Browser | Installation | 17 | | --- | --- | 18 | | ![Firefox Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/firefox/firefox_16x16.png) Firefox | [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) or our [self-distributed version](https://github.com/FirefoxBar/HeaderEditor/releases) | 19 | | ![Chrome Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/chrome/chrome_16x16.png) Chrome | [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) | 20 | | ![Edge Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/edge/edge_16x16.png) Edge(Chromium) | [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko) | 21 | 22 | ## Basic usage 23 | 24 | * Click the HE icon in the upper right corner of your browser to open the HE Management Panel 25 | * Create a new rule: Click the Add button in the lower right corner, fill in the rules, and save. 26 | * Alternatively, you can download the rules of others in "Import and Export". 27 | 28 | ## Migrate from other similar extensions 29 | 30 | We provide a small tool that can help you migrate from other similar extensions to Header Editor: [migrate-to-he.firefoxcn.net](https://migrate-to-he.firefoxcn.net/index_en.html) 31 | -------------------------------------------------------------------------------- /docs/docs/guide/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | title: 指南 4 | order: 1 5 | group: 6 | title: 介绍 7 | order: 1 8 | title: 安装 9 | order: 1 10 | --- 11 | 12 | ## 安装 13 | 14 | 请根据您的平台不同,选择不同的安装方式: 15 | 16 | | 浏览器 | 安装 | 17 | | --- | --- | 18 | | ![Firefox Logo](https://cdn.staticfile.org/browser-logos/73.0.0/firefox/firefox_16x16.png) Firefox | [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) 或 我们的[自分发版本](https://github.com/FirefoxBar/HeaderEditor/releases) | 19 | | ![Chrome Logo](https://cdn.staticfile.org/browser-logos/73.0.0/chrome/chrome_16x16.png) Chrome | [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) | 20 | | ![Edge Logo](https://cdn.staticfile.org/browser-logos/73.0.0/edge/edge_16x16.png) Edge(Chromium) | [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko) | 21 | 22 | ## 基本使用 23 | 24 | * 点击右上角的HE图标,打开HE管理面板 25 | * 新建规则:点击右下角的添加按钮,填写规则内容后,保存即可。 26 | * 或者,您可以在“导入和导出”中下载他人的规则。 27 | 28 | ## 从其他类似扩展迁移 29 | 30 | 我们提供了一个小工具,可以协助你从一些类似的扩展,快速迁移到 Header Editor: [migrate-to-he.firefoxcn.net](https://migrate-to-he.firefoxcn.net/) 31 | -------------------------------------------------------------------------------- /docs/docs/guide/index.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 3 | title: 指南 4 | order: 1 5 | group: 6 | title: 介紹 7 | order: 1 8 | title: 安裝 9 | order: 1 10 | --- 11 | 12 | ## 安裝 13 | 14 | 请根据您的平台不同,选择不同的安装方式: 15 | 16 | | 浏览器 | 安装 | 17 | | --- | --- | 18 | | ![Firefox Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/firefox/firefox_16x16.png) Firefox | [Mozilla Add-on](https://addons.mozilla.org/en-US/firefox/addon/header-editor/) 或 我们的[自分发版本](https://github.com/FirefoxBar/HeaderEditor/releases) | 19 | | ![Chrome Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/chrome/chrome_16x16.png) Chrome | [Chrome Web Store](https://chrome.google.com/webstore/detail/header-editor/eningockdidmgiojffjmkdblpjocbhgh) | 20 | | ![Edge Logo](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/73.0.0/edge/edge_16x16.png) Edge(Chromium) | [Edge Addons](https://microsoftedge.microsoft.com/addons/detail/header-editor/afopnekiinpekooejpchnkgfffaeceko) | 21 | 22 | ## 基本使用 23 | 24 | * 点击右上角的HE图标,打开HE管理面板 25 | * 在规则界面新建规则:点击右下角的添加按钮,填写规则内容后,保存即可。 26 | * 或者,您可以在“导入和导出”中下载他人的规则。 27 | 28 | ## 從其他類似擴展遷移 29 | 30 | 我們提供了一個小工具,可以協助你從一些類似的擴展,快速遷移到 Header Editor: [migrate-to-he.firefoxcn.net](https://migrate-to-he.firefoxcn.net/index_zh_tw.html) 31 | -------------------------------------------------------------------------------- /docs/docs/guide/modify-body.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: Advanced 4 | title: Modify response body 5 | order: 2 6 | --- 7 | 8 | ## Before use 9 | 10 | This feature can modify the response body, but it has the following requirements: 11 | * Use Firefox 57+ 12 | * Check "Modify the response body (only supports Firefox)" in the options of HE 13 | 14 | If you enable this feature, you may have the following problems: 15 | * To some extent affect access speed and resource occupation 16 |   * Regardless of whether you have written relevant rules, HE will intercept the request data. 17 | * Affects some content downloads 18 | 19 | ## How to use 20 | 21 | > As of now, this feature only supports custom functions. 22 | 23 | ### Encoding 24 | You need to specify the webpage encoding in order for HE to decode the data. 25 | 26 | If you don't know what encoding the webpage uses, please open the console (press F12), switch to the Network tab, refresh the current page, and observe the Content-Type in the Response Headers. 27 | 28 | ### Function writing 29 | The function has two parameters: the first parameter is the decoded text, and the second parameter is the detail object of the custom function. Returns the modified text. 30 | 31 | Detail object please see [custom function](custom-function.md) document 32 | 33 | For example, the following function will replace all "baidu" in the page with "Google" 34 | ```js 35 | return val.replace(/baidu/g, 'Google'); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/docs/guide/modify-body.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 指南 3 | group: 高级 4 | title: 修改响应体 5 | order: 2 6 | --- 7 | 8 | ## 使用前必读 9 | 10 | 该功能可以修改请求的响应体,但有以下要求: 11 | * 使用Firefox 57+ 12 | * 在HE的选项中勾选“修改响应体(仅支持Firefox)” 13 | 14 | 如果使用了此功能,可能会有以下问题: 15 | * 一定程度上影响访问速度和资源占用 16 | * 不论您是否有编写相关规则,HE均会拦截请求数据。 17 | * 影响部分内容下载 18 | 19 | ## 如何使用 20 | 21 | > 截止目前,此功能只支持自定义函数。 22 | 23 | ### 编码 24 | 您需要指定网页相关编码,才能让HE成功解码数据。 25 | 26 | 如果您不知道网页使用何种编码,请打开控制台(按F12),切换到 Network/网络 标签,刷新当前页面,观察 Response Headers/响应头 中的 Content-Type。 27 | 28 | ### 函数编写 29 | 函数共有两个参数:首个参数为解码后的文本,第二个参数为自定义函数的detail对象。返回修改后的文本。 30 | 31 | detail对象请查看[自定义函数](custom-function.md)文档 32 | 33 | 例如,下面函数,会将网页中的所有“baidu”替换为“Google” 34 | ```js 35 | return val.replace(/baidu/g, 'Google'); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/docs/guide/modify-body.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 高級 4 | title: 修改響應體 5 | order: 2 6 | --- 7 | 8 | ## 使用前必读 9 | 10 | 该功能可以修改请求的响应体,但有以下要求: 11 | * 使用Firefox 57+ 12 | * 在HE的选项中勾选“修改响应体(仅支持Firefox)” 13 | 14 | 如果使用了此功能,可能会有以下问题: 15 | * 一定程度上影响访问速度和资源占用 16 | * 不论您是否有编写相关规则,HE均会拦截请求数据。 17 | * 影响部分内容下载 18 | 19 | ## 如何使用 20 | 21 | > 截止目前,此功能只支持自定义函数。 22 | 23 | ### 编码 24 | 您需要指定网页相关编码,才能让HE成功解码数据。 25 | 26 | 如果您不知道网页使用何种编码,请打开控制台(按F12),切换到 Network/网络 标签,刷新当前页面,观察 Response Headers/响应头 中的 Content-Type。 27 | 28 | ### 函数编写 29 | 函数共有两个参数:首个参数为解码后的文本,第二个参数为自定义函数的detail对象。返回修改后的文本。 30 | 31 | detail对象请查看[自定义函数](custom-function.md)文档 32 | 33 | 例如,下面函数,会将网页中的所有“baidu”替换为“Google” 34 | ```js 35 | return val.replace(/baidu/g, 'Google'); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/docs/guide/rule.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: Basic features 4 | order: 2 5 | title: Rule 6 | order: 1 7 | --- 8 | 9 | ## Rule 10 | 11 | ### Match type 12 | 13 | Rules will apply to the URL that meets the matching criteria 14 | 15 | * All: Correspond to all urls, including the Header Editor itself 16 | * Regular expression 17 | * Supports standard JS regular expressions. For example, the regular expression you entered is `str`, then, in fact, the program will use the internal `new RegExp(str)` to initialize the regular expression. 18 | * If the match rule is a regular expression, the modification result (currently including redirect to) supports the use placeholder like `$1` 19 | * Learn more about regular expression on [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp) 20 | * URL prefix: Including `http://` URL prefix 21 | * Domain name: The full domain name that contains the subdomain 22 | * URL: Including "?" And the full address of all subsequent content 23 | 24 | ### Exclude 25 | 26 | The rule will not take effect on the URL which is match the "exclude". 27 | 28 | ### Custom function 29 | 30 | Through a custom function to realize a more flexible function, the specific use please see [here](./custom-function.md) 31 | 32 | ## Other special features 33 | 34 | * When using "Modify request header" or "Modify response header", set the header content to `_header_editor_remove_` will remove this header (valid since 3.0.5) 35 | 36 | * When using "Redirect request" with custom function, return `_header_editor_cancel_` will cancel this request (valid since 4.0.3) 37 | 38 | ## Other considerations 39 | 40 | * If you want to set a header content to empty, different browsers have different behaviors. Chrome will keep this header but its content will be empty. Firefox will remove this header 41 | 42 | ## A common feature example 43 | 44 | The following example is not guaranteed to be valid, only as an example to help users familiarize themselves with the rules of the Header Editor 45 | 46 | ### Redirect requests 47 | 48 | For example, the Google public library is redirected to the mirror image of University of Science and Technology of China: 49 | 50 | Regular expressions is `^http(s?)://(ajax|fonts)\.googleapis\.com/(.*)`, redirect to `https://$2.proxy.ustclug.org/$3` 51 | 52 | Redirect all HTTP requests of `sale.jd.com`, `item.jd.com` and `www.jd.com` to the HTTPS: 53 | 54 | Regular expressions is `http://(sale|item|www).jd.com`, redirect to `https://$1.jd.com` 55 | 56 | Redirect all wikipedia's HTTP requests to HTTPS: 57 | 58 | Regular expressions is `^http://([^\/]+\.wikipedia\.org/.+)`, redirect to `https://$1` 59 | 60 | ### Camouflage UA 61 | 62 | Just modify the request header named User-Agent, but the function can only affect the ability of the server to determine UA, which can not be pseudo in local through JS 63 | -------------------------------------------------------------------------------- /docs/docs/guide/rule.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 指南 3 | group: 4 | title: 常用功能 5 | order: 2 6 | title: 规则 7 | order: 1 8 | --- 9 | 10 | ## 规则 11 | 12 | HE本身并不具备任何功能,它只是提供了管理和编写规则的能力。您需要通过编写规则,来实现相应的功能。 13 | 14 | ### 匹配类型 15 | 16 | 规则会应用到满足相应匹配条件的URL上。 17 | 18 | * 全部:对应所有URL,包括Header Editor自身。 19 | * 正则表达式: 20 | * 支持标准的JS正则表达式。例如你输入的正则表达式是`str`,那么,实际上,程序内部就会使用`new RegExp(str)`初始化正则表达式。 21 | * 如果匹配规则是正则表达式,则修改结果(目前包括重定向至)支持使用形似`$1`的占位符 22 | * 在[Mozilla Developer Network](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp)上了解更多关于正则表达式的内容 23 | * 网址前缀:包括`http://`在内的网址前缀 24 | * 域名:包含子域名在内的完整的域名 25 | * 网址:包括“?”及之后的所有内容的完整地址 26 | 27 | ### 排除规则 28 | 29 | 不论是否满足匹配规则,只要满足了排除规则,那么此条均不会对当前URL生效 30 | 31 | ### 自定义函数 32 | 33 | 通过自定义函数实现更灵活的功能,具体使用请参见[此处](./custom-function.md) 34 | 35 | ## 其他特殊功能 36 | 37 | * 使用功能“修改请求头”或“修改响应头”时,将头内容设置为`_header_editor_remove_`将会移除此头(自3.0.5起有效) 38 | 39 | * 使用功能“重定向请求”且使用自定义函数时,返回`_header_editor_cancel_`将阻止此请求(自4.0.3开始有效) 40 | 41 | ## 其他注意事项 42 | 43 | * 将头内容设置为空,不同浏览器对此处理方式不同。Chrome将会保留此头信息,但其内容为空。Firefox则会移除此头信息 44 | 45 | ## 常见功能示例 46 | 47 | 下面的例子不保证均有效,只作为示例,用于帮助用户熟悉Header Editor的规则编写 48 | 49 | #### 反-防盗链 50 | 51 | 使用说明:将URL匹配至图片域名,功能为“修改请求头”,将头内容Referer修改为任意可显示图片的网址。下列有一些常用的规则: 52 | 53 | 前缀为`http://imgsrc.baidu.com/`,修改Referer为`http://tieba.baidu.com` 54 | 55 | 正则表达式为`http://(\w?\.?)hiphotos\.baidu\.com/`,修改Referer为`http://tieba.baidu.com` 56 | 57 | #### 重定向请求 58 | 59 | 例如,将Google公共库重定向至中科大的镜像上: 60 | 61 | 正则表达式为`^http(s?)://(ajax|fonts)\.googleapis\.com/(.*)`,重定向至`https://$2.proxy.ustclug.org/$3` 62 | 63 | 将所有对`sale.jd.com`、`item.jd.com`、`www.jd.com`的HTTP请求重定向到HTTPS: 64 | 65 | 正则表达式为`http://(sale|item|www).jd.com`,重定向至`https://$1.jd.com` 66 | 67 | 将所有维基百科的HTTP请求重定向至HTTPS: 68 | 69 | 正则表达式为`^http://([^\/]+\.wikipedia\.org/.+)`,重定向至`https://$1` 70 | 71 | #### 伪装UA 72 | 73 | 修改请求头的User-Agent即可,但功能只能影响服务器判断UA的能力,对于在本地通过JS判断的,无法伪装 74 | -------------------------------------------------------------------------------- /docs/docs/guide/rule.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 常用功能 4 | order: 2 5 | title: 规则 6 | order: 1 7 | --- 8 | 9 | ## 规则 10 | 11 | HE本身并不具备任何功能,它只是提供了管理和编写规则的能力。您需要通过编写规则,来实现相应的功能。 12 | 13 | ### 匹配类型 14 | 15 | 规则会应用到满足相应匹配条件的URL上。 16 | 17 | * 全部:对应所有URL,包括Header Editor自身。 18 | * 正規表示式: 19 | * 支援標準的JS正規表示式。例如你輸出的正規表示式是`str`,那麼,實際上,程式內部就會使用`new RegExp(str)`初始化正規表示式。 20 | * 如果對應規則是正規表示式,則變更結果(目前包括重新導向至)支援使用形似`$1`的預留位置。 21 | * 在[Mozilla Developer Network](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/RegExp)上了解更多关于正则表达式的内容 22 | * 網址首碼:包括`http://`在內的網址首碼。 23 | * 域名:包含子域名在內的完整的域名。 24 | * 網址:包括“?”及之後的所有內容的完整位址。 25 | 26 | ### 排除规则 27 | 28 | 不论是否满足匹配规则,只要满足了排除规则,那么此条均不会对当前URL生效 29 | 30 | ### 自定义函数 31 | 32 | 通过自定义函数实现更灵活的功能,具体使用请参见[此处](./custom-function.md) 33 | 34 | ## 其他特殊功能 35 | 36 | * 使用功能“修改请求头”或“修改响应头”时,将头内容设置为`_header_editor_remove_`将会移除此头(自3.0.5起有效) 37 | 38 | * 使用功能“重定向请求”且使用自定义函数时,返回`_header_editor_cancel_`将阻止此请求(自4.0.3开始有效) 39 | 40 | ## 其他注意事项 41 | 42 | * 将头内容设置为空,不同浏览器对此处理方式不同。Chrome将会保留此头信息,但其内容为空。Firefox则会移除此头信息 43 | 44 | ## 常见功能示例 45 | 46 | 下面的例子不保证均有效,只作为示例,用于帮助用户熟悉Header Editor的规则编写 47 | 48 | #### 反-防盗链 49 | 50 | 使用说明:将URL匹配至图片域名,功能为“修改请求头”,将头内容Referer修改为任意可显示图片的网址。下列有一些常用的规则: 51 | 52 | 前缀为`http://imgsrc.baidu.com/`,修改Referer为`http://tieba.baidu.com` 53 | 54 | 正则表达式为`http://(\w?\.?)hiphotos\.baidu\.com/`,修改Referer为`http://tieba.baidu.com` 55 | 56 | #### 重定向请求 57 | 58 | 例如,将Google公共库重定向至中科大的镜像上: 59 | 60 | 正则表达式为`^http(s?)://(ajax|fonts)\.googleapis\.com/(.*)`,重定向至`https://$2.proxy.ustclug.org/$3` 61 | 62 | 将所有对`sale.jd.com`、`item.jd.com`、`www.jd.com`的HTTP请求重定向到HTTPS: 63 | 64 | 正则表达式为`http://(sale|item|www).jd.com`,重定向至`https://$1.jd.com` 65 | 66 | 将所有维基百科的HTTP请求重定向至HTTPS: 67 | 68 | 正则表达式为`^http://([^\/]+\.wikipedia\.org/.+)`,重定向至`https://$1` 69 | 70 | #### 伪装UA 71 | 72 | 修改请求头的User-Agent即可,但功能只能影响服务器判断UA的能力,对于在本地通过JS判断的,无法伪装 73 | -------------------------------------------------------------------------------- /docs/docs/guide/third-party-rules.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: More 4 | order: 4 5 | title: Third-party rules 6 | order: 1 7 | --- 8 | 9 | ## Notice 10 | 11 | The following rules are maintained by a third party. Header Editor does not guarantee the timeliness and security of the rules. If there is any problem, please contact the rule maintainer. 12 | 13 | ## Lists 14 | 15 | * [Amazon > Amazon Smile](https://github.com/FirefoxBar/HeaderEditor/files/2384019/Header.Editor.-.Amazon.Smile.zip) By [vertigo220](https://github.com/vertigo220) 16 | 17 | ## Submitting rules here 18 | 19 | If you want your maintenance rules to appear here, please [submit an issue](https://github.com/FirefoxBar/HeaderEditor/issues/new) 20 | -------------------------------------------------------------------------------- /docs/docs/guide/third-party-rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | nav: 指南 3 | group: 4 | title: 更多 5 | order: 4 6 | title: 第三方规则 7 | order: 1 8 | --- 9 | 10 | 11 | ## 注意 12 | 13 | 下面的规则由第三方维护,Header Editor不保证规则的时效性、安全性,若出现问题请联系规则维护者 14 | 15 | ## 列表 16 | 17 | * [dupontjoy](https://github.com/dupontjoy/customization/tree/master/Rules/HeaderEditor) 主要为中文站点 18 | 19 | ## 提交规则 20 | 21 | 如果您希望您维护的规则出现在此处,请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new) 22 | -------------------------------------------------------------------------------- /docs/docs/guide/third-party-rules.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | group: 3 | title: 更多 4 | order: 4 5 | title: 第三方規則 6 | order: 1 7 | --- 8 | 9 | 10 | ## 注意 11 | 12 | 下面的规则由第三方维护,Header Editor不保证规则的时效性、安全性,若出现问题请联系规则维护者 13 | 14 | ## 列表 15 | 16 | * [dupontjoy](https://github.com/dupontjoy/customization/tree/master/Rules/HeaderEditor) 主要为中文站点 17 | 18 | ## 提交规则 19 | 20 | 如果您希望您维护的规则出现在此处,请[提交issue](https://github.com/FirefoxBar/HeaderEditor/issues/new) 21 | -------------------------------------------------------------------------------- /docs/docs/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Header Editor 3 | hero: 4 | title: Header Editor 5 | description: Manage browser's requests, include modify the request headers and response headers, redirect requests, cancel requests 6 | actions: 7 | - text: Setup 8 | link: /guide 9 | - text: GitHub 10 | link: https://github.com/FirefoxBar/HeaderEditor 11 | features: 12 | - title: Modify requests 13 | emoji: 🚥 14 | description: Based on rules, modify request headers, response headers, and redirect 15 | - title: Custom function 16 | emoji: ⚙️ 17 | description: Use custom functions to achieve more flexible functionality 18 | - title: Export and Sync 19 | emoji: ☁️ 20 | description: Rules can import and export, and you can use cloud sync 21 | --- 22 | -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Header Editor 3 | hero: 4 | title: Header Editor 5 | description: 管理浏览器请求,包括修改请求头和响应头、重定向请求、取消请求 6 | actions: 7 | - text: 开始使用 8 | link: /guide 9 | - text: GitHub 10 | link: https://github.com/FirefoxBar/HeaderEditor 11 | features: 12 | - title: 修改请求 13 | emoji: 🚥 14 | description: 基于规则,修改请求头、响应头,进行重定向 15 | - title: 自定义函数 16 | emoji: ⚙️ 17 | description: 通过自定义函数,更精确的控制请求 18 | - title: 导出和同步 19 | emoji: ☁️ 20 | description: 规则可以自由导入和导出,并可使用云同步 21 | --- 22 | -------------------------------------------------------------------------------- /docs/docs/index.zh-TW.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Header Editor 3 | hero: 4 | title: Header Editor 5 | description: 透過此附加元件,您可以變更要求標頭和回應標頭,取消要求、重新導向的要求 6 | actions: 7 | - text: 開始使用 8 | link: /guide 9 | - text: GitHub 10 | link: https://github.com/FirefoxBar/HeaderEditor 11 | features: 12 | - title: 修改请求 13 | emoji: 🚥 14 | description: 基于规则,修改请求头、响应头,进行重定向 15 | - title: 自定义函数 16 | emoji: ⚙️ 17 | description: 通过自定义函数,更精确的控制请求 18 | - title: 导出和同步 19 | emoji: ☁️ 20 | description: 规则可以自由导入和导出,并可使用云同步 21 | --- 22 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "header-editor-docs", 3 | "version": "0.0.1", 4 | "description": "Header Editor official manual", 5 | "scripts": { 6 | "start": "npm run dev", 7 | "dev": "dumi dev", 8 | "build": "dumi build", 9 | "prepare": "dumi setup" 10 | }, 11 | "authors": [ 12 | "ShuangYa" 13 | ], 14 | "devDependencies": { 15 | "dumi": "^2.0.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist": "HeaderEditor-{VER}", 3 | "autobuild": { 4 | "xpi": true, 5 | "amo": true, 6 | "cws": true, 7 | "crx": true 8 | }, 9 | "firefox": { 10 | "xpi": "headereditor@addon.firefoxcn.net", 11 | "amo": "headereditor-amo@addon.firefoxcn.net" 12 | }, 13 | "chrome": { 14 | "id": "eningockdidmgiojffjmkdblpjocbhgh", 15 | "crx": "jhigoaelcgmfbidkocglkcnhmfacajle" 16 | }, 17 | "github": { 18 | "enable": true 19 | } 20 | } -------------------------------------------------------------------------------- /locale/create-pr.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const token = process.env.GITHUB_TOKEN; 4 | 5 | const baseURL = process.env.GITHUB_API_URL + '/repos/' + process.env.GITHUB_REPOSITORY; 6 | const request = axios.create({ 7 | baseURL: baseURL, 8 | validateStatus: () => true, 9 | }); 10 | 11 | request.defaults.headers.common['Accept'] = 'application/vnd.github+json'; 12 | request.defaults.headers.common['Authorization'] = 'Bearer ' + token; 13 | request.defaults.headers.common['X-GitHub-Api-Version'] = '2022-11-28'; 14 | 15 | async function main() { 16 | if (!token) { 17 | console.log('No token'); 18 | return; 19 | } 20 | 21 | console.log('baseURL: ' + baseURL); 22 | 23 | const pulls = await request.get('/pulls', { 24 | params: { 25 | state: 'open', 26 | head: 'dev-locale', 27 | base: 'dev', 28 | } 29 | }); 30 | 31 | if (pulls.data.length > 0) { 32 | // already has PR 33 | const item = pulls.data[0]; 34 | console.log("PR already exists: " + item.html_url); 35 | return; 36 | } 37 | 38 | // Create new PR 39 | const create = await request.post('/pulls', JSON.stringify({ 40 | title: '[locale] update locales', 41 | body: '', 42 | head: 'dev-locale', 43 | base: 'dev', 44 | })); 45 | 46 | if (create.status === 201) { 47 | console.log("PR created: " + create.data.html_url); 48 | } else { 49 | console.log("PR created failed: " + create.status); 50 | } 51 | } 52 | 53 | main(); -------------------------------------------------------------------------------- /locale/locales.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const ORIGINAL_NAME = 'en'; 5 | const originalDir = path.join(__dirname, 'original'); 6 | const outputDir = path.join(__dirname, 'output'); 7 | 8 | function ksort(obj) { 9 | let objKeys = Object.keys(obj); 10 | objKeys.sort((k1, k2) => { 11 | let i = 0; 12 | while (i < (k1.length - 1) && i < (k2.length - 1) && k1[i] === k2[i]) { 13 | i++; 14 | } 15 | if (k1[i] === k2[i]) { 16 | return i < (k1.length - 1) ? 1 : -1; 17 | } else { 18 | return k1[i].charCodeAt() > k2[i].charCodeAt() ? 1 : -1; 19 | } 20 | }); 21 | let result = {}; 22 | objKeys.forEach(k => result[k] = obj[k]); 23 | return result; 24 | } 25 | 26 | function readJSON(filePath) { 27 | return JSON.parse(fs.readFileSync(filePath, { 28 | encoding: "utf8" 29 | })); 30 | } 31 | 32 | let _basicLanguage = {}; 33 | function getBasicLanguage(fileName) { 34 | if (typeof _basicLanguage[fileName] === 'undefined') { 35 | _basicLanguage[fileName] = readJSON(path.join(originalDir, fileName)); 36 | } 37 | return _basicLanguage[fileName]; 38 | } 39 | 40 | // Get default language 41 | function main() { 42 | const dir = fs.readdirSync(outputDir); 43 | 44 | for (const lang of dir) { 45 | const langDir = path.join(outputDir, lang); 46 | // skip not a dir 47 | const stat = fs.statSync(langDir); 48 | if (!stat.isDirectory()) { 49 | console.log("[" + lang + "] skip"); 50 | continue; 51 | } 52 | 53 | // get detail messages 54 | const files = fs.readdirSync(langDir); 55 | for (const file of files) { 56 | if (!file.endsWith('.json')) { 57 | console.log("[" + lang + "/" + file + "] skip file"); 58 | continue; 59 | } 60 | 61 | console.log("[" + lang + "/" + file + "] read file"); 62 | const basicLanguage = getBasicLanguage(file); 63 | const orignalCurrentLanguage = readJSON(path.join(langDir, file)); 64 | // sort 65 | const currentLanguage = ksort(orignalCurrentLanguage); 66 | 67 | Object.keys(basicLanguage).forEach(k => { 68 | // add not exists 69 | if (typeof currentLanguage[k] === 'undefined') { 70 | console.log("[" + lang + "/" + file + "] add default locale: " + k); 71 | currentLanguage[k] = basicLanguage[k]; 72 | } 73 | // add placeholder 74 | if (basicLanguage[k].placeholders) { 75 | console.log("[" + lang + "/" + file + "] add placeholder: " + k); 76 | currentLanguage[k].placeholders = basicLanguage[k].placeholders; 77 | } 78 | }); 79 | 80 | Object.keys(currentLanguage).forEach(k => { 81 | // remove description 82 | delete currentLanguage[k].description; 83 | }); 84 | 85 | fs.writeFileSync(path.join(langDir, file), JSON.stringify(currentLanguage), { 86 | encoding: "utf8" 87 | }); 88 | console.log("[" + lang + "/" + file + "] write ok"); 89 | } 90 | } 91 | 92 | // Copy original language 93 | const files = fs.readdirSync(originalDir); 94 | const originalOutput = path.join(outputDir, ORIGINAL_NAME); 95 | if (!fs.existsSync(originalOutput)) { 96 | fs.mkdirSync(originalOutput, { 97 | recursive: true, 98 | }); 99 | } 100 | for (const file of files) { 101 | const basicLanguage = getBasicLanguage(file); 102 | // sort 103 | const currentLanguage = ksort(basicLanguage); 104 | Object.keys(currentLanguage).forEach(k => { 105 | // remove description 106 | delete currentLanguage[k].description; 107 | }); 108 | fs.writeFileSync(path.join(originalOutput, file), JSON.stringify(currentLanguage), { 109 | encoding: "utf8" 110 | }); 111 | console.log("[" + ORIGINAL_NAME + "/" + file + "] write ok"); 112 | } 113 | } 114 | 115 | main(); -------------------------------------------------------------------------------- /locale/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/locale/output/.gitkeep -------------------------------------------------------------------------------- /locale/sort-origin.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function ksort(obj) { 5 | let objKeys = Object.keys(obj); 6 | objKeys.sort((k1, k2) => { 7 | let i = 0; 8 | while (i < (k1.length - 1) && i < (k2.length - 1) && k1[i] === k2[i]) { 9 | i++; 10 | } 11 | if (k1[i] === k2[i]) { 12 | return i < (k1.length - 1) ? 1 : -1; 13 | } else { 14 | return k1[i].charCodeAt() > k2[i].charCodeAt() ? 1 : -1; 15 | } 16 | }); 17 | let result = {}; 18 | objKeys.forEach(k => result[k] = obj[k]); 19 | return result; 20 | } 21 | 22 | let lang = require(path.join(__dirname, 'original/messages.json')); 23 | lang = ksort(lang); 24 | fs.writeFileSync(path.join(__dirname, 'original/messages.json'), JSON.stringify(lang, null, "\t"), { 25 | encoding: "utf8" 26 | }); 27 | console.log("Sort success"); -------------------------------------------------------------------------------- /locale/transifex.yml: -------------------------------------------------------------------------------- 1 | git: 2 | filters: 3 | - filter_type: file 4 | file_format: CHROME 5 | source_language: en 6 | source_file: 'original/messages.json' 7 | translation_files_expression: 'output//messages.json' 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "header-editor", 3 | "version": "5.0.0", 4 | "description": "Header Editor", 5 | "author": "ShuangYa", 6 | "license": "GPL-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/FirefoxBar/HeaderEditor.git" 10 | }, 11 | "scripts": { 12 | "start": "icejs start --disable-open --config build.config.js", 13 | "build": "icejs build --config build.config.js", 14 | "lint": "npm run eslint && npm run stylelint", 15 | "eslint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./", 16 | "eslint:fix": "npm run eslint -- --fix", 17 | "stylelint": "stylelint \"**/*.{css,scss,less}\"", 18 | "release": "node ./scripts/release.mjs", 19 | "pack": "node ./scripts/pack.mjs", 20 | "precommit": "lint-staged" 21 | }, 22 | "dependencies": { 23 | "@codemirror/lang-javascript": "^6.1.4", 24 | "@douyinfe/semi-icons": "^2.30.1", 25 | "@douyinfe/semi-ui": "^2.30.1", 26 | "@emotion/css": "^11.10.6", 27 | "@ice/runtime": "^0.1.2", 28 | "@uiw/codemirror-theme-github": "^4.19.9", 29 | "@uiw/react-codemirror": "^4.19.9", 30 | "ahooks": "^3.7.5", 31 | "create-app-shared": "^1.2.6", 32 | "dayjs": "^1.9.5", 33 | "eventemitter3": "^4.0.0", 34 | "fast-deep-equal": "^2.0.1", 35 | "lodash-es": "^4.17.21", 36 | "query-string": "^8.1.0", 37 | "react": "^17.0.0", 38 | "react-app-renderer": "^3.1.0", 39 | "react-dom": "^17.0.0", 40 | "regenerator-runtime": "^0.13.11", 41 | "text-encoding": "^0.7.0", 42 | "tslib": "^2.5.0", 43 | "webextension-polyfill": "^0.10.0" 44 | }, 45 | "devDependencies": { 46 | "@iceworks/spec": "^1.0.0", 47 | "@plasmo-corp/ewu": "^0.6.0", 48 | "@types/chrome": "^0.0.72", 49 | "@types/lodash-es": "^4.17.6", 50 | "@types/node": "^18.15.5", 51 | "@types/react": "^16.9.16", 52 | "@types/react-dom": "^16.9.4", 53 | "@types/text-encoding": "^0.0.35", 54 | "@types/webextension-polyfill": "^0.10.0", 55 | "build-plugin-css-assets-local": "^0.1.0", 56 | "copy-webpack-plugin": "^11.0.0", 57 | "crx": "^5.0.1", 58 | "eslint": "^7.30.0", 59 | "eslint-plugin-import": "^2.27.5", 60 | "eslint-plugin-unused-imports": "^2.0.0", 61 | "fs-extra": "^11.1.1", 62 | "husky": "^3.1.0", 63 | "ice.js": "^2.0.0", 64 | "lint-staged": "^9.5.0", 65 | "node-fetch": "^3.3.1", 66 | "prettier": "^1.19.1", 67 | "publish-release": "^1.6.0", 68 | "sign-addon": "^6.0.0", 69 | "stylelint": "^13.7.2", 70 | "typescript": "^3.7.3", 71 | "webpack-bundle-analyzer": "^4.8.0" 72 | }, 73 | "husky": { 74 | "hooks": { 75 | "pre-commit": "npm run precommit" 76 | } 77 | }, 78 | "lint-staged": { 79 | "./src/**/*.{ts,tsx}": [ 80 | "eslint --cache --fix", 81 | "git add" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /public/_locales/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/public/_locales/.gitkeep -------------------------------------------------------------------------------- /public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | {"action":{"message":"Action"},"add":{"message":"Add"},"auto":{"message":"Auto"},"batch_delete":{"message":"batch deletion"},"batch_mode":{"message":"Batch operation"},"cancel":{"message":"Cancel"},"choose":{"message":"Choose"},"clone":{"message":"Clone"},"cloud_backup":{"message":"Cloud backup"},"cloud_backup_at":{"message":"Last backup at $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"No backup"},"cloud_over_limit":{"message":"Exceeded backup size limit"},"code":{"message":"Code"},"code_empty":{"message":"Code can not be empty"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"},"delete":{"message":"Delete"},"delete_confirm":{"message":"Do you want to delete these rules?"},"description":{"message":"Manage browser's requests, include modify the request headers and response headers, redirect requests, cancel requests"},"disable":{"message":"Disable"},"display_common_header":{"message":"Display common header"},"download":{"message":"Download"},"download_rule":{"message":"Download rule"},"edit":{"message":"Edit"},"enable":{"message":"Enable"},"enable_he":{"message":"Enable Header Editor"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Please enter a group name"},"excludeRule":{"message":"Exclude rule"},"exec_function":{"message":"Custom function"},"exec_normal":{"message":"normal"},"exec_type":{"message":"Execute type"},"export":{"message":"Export"},"export_and_import":{"message":"Export and Import"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Group"},"headerName":{"message":"Header name"},"headerValue":{"message":"Header value"},"header_empty":{"message":"Header name can not be empty"},"help":{"message":"Help"},"import":{"message":"Import"},"import_drop":{"message":"Do not import"},"import_new":{"message":"Add"},"import_override":{"message":"Overrides existing"},"import_success":{"message":"Successfully imported"},"include_header_in_custom_function":{"message":"Include request headers in custom function"},"manage":{"message":"Manage"},"manage_collapse_group":{"message":"Collapse groups by default"},"matchRule":{"message":"Match rules"},"matchType":{"message":"Match type"},"match_all":{"message":"All"},"match_domain":{"message":"Domain"},"match_prefix":{"message":"URL prefix"},"match_regexp":{"message":"Regular expression"},"match_rule_empty":{"message":"Match rule can not be empty"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Name"},"name_empty":{"message":"Name can not be empty"},"ok":{"message":"OK"},"options":{"message":"Options"},"pack_up_or_unfurl":{"message":"Pack up or unfurl group"},"redirectTo":{"message":"Redirect to"},"redirect_empty":{"message":"Redirect target can not be empty"},"rename":{"message":"Rename"},"ruleType":{"message":"Rule type"},"rule_cancel":{"message":"Cancel request"},"rule_list":{"message":"Rules list"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modify response header"},"rule_modifySendHeader":{"message":"Modify request header"},"rule_redirect":{"message":"Redirect request"},"rules_no_effect_for_he":{"message":"Rules take no effect on Header Editor"},"save":{"message":"Save"},"save_to":{"message":"Save to"},"saved":{"message":"Saved"},"select_all":{"message":"Select/Unselect all"},"select_or_download":{"message":"Please import from a local file or download rules"},"share":{"message":"Share"},"suggested_group":{"message":"Suggested group"},"test_custom_code":{"message":"Does not support testing custom code now"},"test_exclude":{"message":"Matched but excluded"},"test_invalid_regexp":{"message":"Regular expression is invalid"},"test_mismatch":{"message":"Mismatch"},"test_url":{"message":"Test (Won't be saved)"},"third_party_rules":{"message":"Third party rules"},"ungrouped":{"message":"Ungrouped"},"upload":{"message":"Upload"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en-US/guide/cloud-backup/"},"url_help":{"message":"https://he.firefoxcn.net/en-US/"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en-US/guide/third-party-rules/"},"view":{"message":"View"}} -------------------------------------------------------------------------------- /public/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | {"action":{"message":"Acción"},"add":{"message":"Añadir"},"auto":{"message":"Auto"},"batch_delete":{"message":"batch deletion"},"batch_mode":{"message":"Batch operation"},"cancel":{"message":"Cancelar"},"choose":{"message":"Escoger"},"clone":{"message":"Duplicar"},"cloud_backup":{"message":"Copia de seguridad en línea"},"cloud_backup_at":{"message":"Última copia de seguridad del $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"No existe ninguna copia de seguridad"},"cloud_over_limit":{"message":"Número de copias de seguridad excedido"},"code":{"message":"Código"},"code_empty":{"message":"El código no puede estar vacío"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"delete":{"message":"Eliminar"},"delete_confirm":{"message":"¿Quiere eliminar estas reglas?"},"description":{"message":"Manage browser's requests, include modify the request headers and response headers, redirect requests, cancel requests"},"disable":{"message":"Disable"},"display_common_header":{"message":"Display common header"},"download":{"message":"Descarga"},"download_rule":{"message":"Regla de descarga"},"edit":{"message":"Editar"},"enable":{"message":"Habilitar"},"enable_he":{"message":"Enable Header Editor"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Favor de ingresar un nombre de grupo"},"excludeRule":{"message":"Regla de exclusión"},"exec_function":{"message":"Función personalizado"},"exec_normal":{"message":"normal"},"exec_type":{"message":"Execute type"},"export":{"message":"Exportar"},"export_and_import":{"message":"Exportar e importar"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Grupo"},"headerName":{"message":"Header name"},"headerValue":{"message":"Header value"},"header_empty":{"message":"Header name can not be empty"},"help":{"message":"Ayuda"},"import":{"message":"Importar"},"import_drop":{"message":"No importar"},"import_new":{"message":"Añadir"},"import_override":{"message":"Overrides existing"},"import_success":{"message":"Importación exitosa"},"include_header_in_custom_function":{"message":"Include request headers in custom function"},"manage":{"message":"Manerar"},"manage_collapse_group":{"message":"Colapsar grupos por predeterminado"},"matchRule":{"message":"Match rules"},"matchType":{"message":"Match type"},"match_all":{"message":"Todo"},"match_domain":{"message":"Dominio"},"match_prefix":{"message":"URL de prefijo"},"match_regexp":{"message":"Expresión regular"},"match_rule_empty":{"message":"Match rule can not be empty"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Nombre"},"name_empty":{"message":"No puede haber nombres vacíos"},"ok":{"message":"OK"},"options":{"message":"Opciones"},"pack_up_or_unfurl":{"message":"Pack up or unfurl group"},"redirectTo":{"message":"Redireccionar a"},"redirect_empty":{"message":"Redirect target can not be empty"},"rename":{"message":"Renombrar"},"ruleType":{"message":"Tipo de regla"},"rule_cancel":{"message":"Cancelar solicitud"},"rule_list":{"message":"Rules list"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modify response header"},"rule_modifySendHeader":{"message":"Modify request header"},"rule_redirect":{"message":"Redirect request"},"rules_no_effect_for_he":{"message":"Rules take no effect on Header Editor"},"save":{"message":"Guardar"},"save_to":{"message":"Guardar en..."},"saved":{"message":"Guardado"},"select_all":{"message":"Seleccionar/Deseleccionar todo"},"select_or_download":{"message":"De favor, importe desde un archivo local o por reglas de descarga"},"share":{"message":"Compartir"},"suggested_group":{"message":"Grupo sugerido"},"test_custom_code":{"message":"Does not support testing custom code now"},"test_exclude":{"message":"Matched but excluded"},"test_invalid_regexp":{"message":"La expresión regular es inválida"},"test_mismatch":{"message":"Mismatch"},"test_url":{"message":"Prueba (no se guardará)"},"third_party_rules":{"message":"Third party rules"},"ungrouped":{"message":"Desagrupado "},"upload":{"message":"Subir"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/en/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en/third-party-rules.html"},"view":{"message":"Ver"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"}} -------------------------------------------------------------------------------- /public/_locales/pl/messages.json: -------------------------------------------------------------------------------- 1 | {"action":{"message":"Akcja"},"add":{"message":"Dodaj"},"add_anti_hot_link":{"message":"Dodaj regułę anti-anti-hotlinking"},"add_anti_hot_link_to_menu":{"message":"Dodaj \"Add anti-anti-hotlinking\" do menu kontekstowego"},"auto":{"message":"Auto"},"batch_delete":{"message":"Usuń kilka"},"batch_mode":{"message":"Wybierz"},"cancel":{"message":"Anuluj"},"choose":{"message":"Wybierz"},"clone":{"message":"Sklonuj"},"cloud_backup":{"message":"Kopia zapasowa w chmurze"},"cloud_backup_at":{"message":"Ostatnia kopia w $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"Brak kopii zapasowej"},"cloud_over_limit":{"message":"Przekroczono limit rozmiaru kopii zapasowej"},"code":{"message":"Kod"},"code_empty":{"message":"Kod nie może być pusty"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"delete":{"message":"Usuń"},"delete_confirm":{"message":"Czy chcesz usunąć te reguły?"},"description":{"message":"Zarządzaj żądaniami przeglądarki, modyfikuj nagłówki żądania i odpowiedzi, przekierowuj lub anuluj żądania"},"disable":{"message":"Disable"},"display_common_header":{"message":"Display common header"},"download":{"message":"Pobierz"},"download_rule":{"message":"Pobierz regułę"},"edit":{"message":"Edytuj"},"enable":{"message":"Włącz"},"enable_he":{"message":"Włącz Header Editor"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Wprowadź nazwę grupy"},"excludeRule":{"message":"Wyklucz regułę"},"exec_function":{"message":"Funkcja niestandardowa"},"exec_normal":{"message":"Normalne"},"exec_type":{"message":"Typ wykonania"},"export":{"message":"Eksportuj"},"export_and_import":{"message":"Eksport i Import"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Grupa"},"headerName":{"message":"Nazwa nagłówka"},"headerValue":{"message":"Wartość nagłówka"},"header_empty":{"message":"Nazwa nagłówka nie może być pusta"},"help":{"message":"Pomoc"},"import":{"message":"Importuj"},"import_drop":{"message":"Nie importuj"},"import_new":{"message":"Dodaj"},"import_override":{"message":"Zastąp istniejącą"},"import_success":{"message":"Importowano pomyślnie"},"include_header_in_custom_function":{"message":"Include request headers in custom function"},"manage":{"message":"Zarządzaj"},"manage_collapse_group":{"message":"Domyślnie zwiń grupy"},"matchRule":{"message":"Dopasuj reguły"},"matchType":{"message":"Typ dopasowania"},"match_all":{"message":"Wszystkie"},"match_domain":{"message":"Domena"},"match_prefix":{"message":"Prefiks URL"},"match_regexp":{"message":"Wyrażenie regularne"},"match_rule_empty":{"message":"Reguła dopasowania nie może być pusta"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Nazwa"},"name_empty":{"message":"Nazwa nie może być pusta"},"ok":{"message":"OK"},"options":{"message":"Opcje"},"pack_up_or_unfurl":{"message":"Zwiń lub rozwiń grupę"},"redirectTo":{"message":"Przekieruj do"},"redirect_empty":{"message":"Cel przekierowania nie może być pusty"},"rename":{"message":"Zmień nazwę"},"ruleType":{"message":"Typ reguły"},"rule_cancel":{"message":"Anuluj żądanie"},"rule_list":{"message":"Lista reguł"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modyfikuj nagłówek odpowiedzi"},"rule_modifySendHeader":{"message":"Modyfikuj nagłówek żądania"},"rule_redirect":{"message":"Przekieruj żądanie"},"rules_no_effect_for_he":{"message":"Reguły nie mają wpływu na Header Editor"},"save":{"message":"Zapisz"},"save_to":{"message":"Zapisz do"},"saved":{"message":"Zapisano"},"select_all":{"message":"Zaznacz/Odznacz wszystko"},"select_or_download":{"message":"Importuj z lokalnego pliku lub pobierz reguły"},"share":{"message":"Udostępnij"},"suggested_group":{"message":"Sugerowana grupa"},"test_custom_code":{"message":"Nie obsługuje jeszcze testowania niestandardowego kodu"},"test_exclude":{"message":"Dopasowane, ale wykluczone"},"test_invalid_regexp":{"message":"Wyrażenie regularne jest nieprawidłowe"},"test_mismatch":{"message":"Brak dopasowania"},"test_url":{"message":"Test (Nie będzie zapisany)"},"third_party_rules":{"message":"Reguły zewnętrzne"},"ungrouped":{"message":"Bez grupy"},"upload":{"message":"Prześlij"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/en/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en/third-party-rules.html"},"view":{"message":"Zobacz"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"}} -------------------------------------------------------------------------------- /public/_locales/pt_BR/messages.json: -------------------------------------------------------------------------------- 1 | {"action":{"message":"Ação"},"add":{"message":"Adicionar"},"auto":{"message":"Auto"},"batch_delete":{"message":"eliminação do lote"},"batch_mode":{"message":"Operação por lotes"},"cancel":{"message":"Cancelar"},"choose":{"message":"Escolher"},"clone":{"message":"Clonar"},"cloud_backup":{"message":"Backup em nuvem"},"cloud_backup_at":{"message":"Último backup em $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"Sem backup "},"cloud_over_limit":{"message":"Limite do tamanho de backup excedido"},"code":{"message":"Código"},"code_empty":{"message":"O código não pode estar vazio"},"dark_mode":{"message":"Dark mode"},"dark_mode_help":{"message":"Dark mode is experimental, refresh this page to see the effect"},"delete":{"message":"Apagar"},"delete_confirm":{"message":"Deseja excluir estas regras?"},"description":{"message":"Gerencie pedidos do navegador, incluindo modificar cabeçalhos das solicitações e resposta, solicitações de redirecionamento, cancelar solicitações"},"disable":{"message":"Disable"},"display_common_header":{"message":"Exibir cabeçalho comum"},"download":{"message":"Download"},"download_rule":{"message":"Regra de download"},"edit":{"message":"Editar"},"enable":{"message":"Habilitar"},"enable_he":{"message":"Ativar editor de cabeçalho"},"encoding":{"message":"Encoding"},"enter_group_name":{"message":"Digite um nome de grupo"},"excludeRule":{"message":"Excluir regra"},"exec_function":{"message":"Função personalizada"},"exec_normal":{"message":"normal"},"exec_type":{"message":"Execute o tipo"},"export":{"message":"Exportar"},"export_and_import":{"message":"Exportar e Importar"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"Grupo"},"headerName":{"message":"Nome do cabeçalho"},"headerValue":{"message":"Valor do cabeçalho"},"header_empty":{"message":"Nome do cabeçalho não pode estar vazio"},"help":{"message":"Ajuda"},"import":{"message":"Importar"},"import_drop":{"message":"Não importar"},"import_new":{"message":"Adicionar"},"import_override":{"message":"Substituições existentes"},"import_success":{"message":"Importado com sucesso"},"include_header_in_custom_function":{"message":"Incluir cabeçalhos na função personalizada"},"manage":{"message":"Gerir"},"manage_collapse_group":{"message":"Reduzir grupos por padrão"},"matchRule":{"message":"Regras correspondentes"},"matchType":{"message":"Tipo de combinação"},"match_all":{"message":"Todos"},"match_domain":{"message":"Domínio"},"match_prefix":{"message":"Prefixo da URL"},"match_regexp":{"message":"Expressão regular"},"match_rule_empty":{"message":"A regra não pode estar vazia"},"match_url":{"message":"URL"},"modify_body":{"message":"Modify response body (only supports Firefox)"},"name":{"message":"Nome"},"name_empty":{"message":"Nome não pode estar vazio"},"ok":{"message":"OK"},"options":{"message":"Opções"},"pack_up_or_unfurl":{"message":"Pacote acima ou desenrolar grupo"},"redirectTo":{"message":"Redirecionar para"},"redirect_empty":{"message":"O alvo de redirecionamento não pode estar vazio"},"rename":{"message":"Renomear"},"ruleType":{"message":"Tipo de regra"},"rule_cancel":{"message":"Cancelar pedido"},"rule_list":{"message":"Lista de regras"},"rule_modifyReceiveBody":{"message":"Modify response body"},"rule_modifyReceiveHeader":{"message":"Modificar o cabeçalho da resposta"},"rule_modifySendHeader":{"message":"Modificar o cabeçalho solicitado"},"rule_redirect":{"message":"Pedido de redirecionamento"},"rules_no_effect_for_he":{"message":"Regras não têm efeito no Header Editor"},"save":{"message":"Salvar"},"save_to":{"message":"Salvar para"},"saved":{"message":"Salvo"},"select_all":{"message":"Selecionar/Desmarcar tudo"},"select_or_download":{"message":"Importe de um arquivo local ou regras de download"},"share":{"message":"Compartilhar"},"suggested_group":{"message":"Grupo sugerido"},"test_custom_code":{"message":"Não suporta testar código personalizado agora"},"test_exclude":{"message":"Combinado, mas excluído"},"test_invalid_regexp":{"message":"A expressão regular é inválida"},"test_mismatch":{"message":"Incompatibilidade"},"test_url":{"message":"Teste (Não será salvo)"},"third_party_rules":{"message":"Regras de terceiros"},"ungrouped":{"message":"Desagrupado"},"upload":{"message":"Upload"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/en/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/en/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/en/third-party-rules.html"},"view":{"message":"Visualizar"},"common_mark":{"message":"Mark as common"},"common_mark_tip":{"message":"When you mark any rule or group as common, you'll see it here"},"common_unmark":{"message":"Unmark"},"debug_mode_enable":{"message":"Enable debug mode"},"debug_mode_help":{"message":"When debug mode is turned on, some logs will be printed in the background page"}} -------------------------------------------------------------------------------- /public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | {"action":{"message":"操作"},"add":{"message":"添加"},"auto":{"message":"自动"},"batch_delete":{"message":"批量删除"},"batch_mode":{"message":"批量操作"},"cancel":{"message":"取消"},"choose":{"message":"选择"},"clone":{"message":"克隆"},"cloud_backup":{"message":"云备份"},"cloud_backup_at":{"message":"最后备份于 $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"无备份"},"cloud_over_limit":{"message":"超出备份大小限制"},"code":{"message":"代码"},"code_empty":{"message":"代码不能为空"},"common_mark":{"message":"设为常用"},"common_mark_tip":{"message":"将任意规则或分组设为常用后,会在此处显示"},"common_unmark":{"message":"取消常用"},"dark_mode":{"message":"暗黑模式"},"dark_mode_help":{"message":"暗黑模式为试验性特性,刷新页面查看效果"},"debug_mode_enable":{"message":"开启调试模式"},"debug_mode_help":{"message":"当调试模式开启时,一些日志将会打印在背景页"},"delete":{"message":"删除"},"delete_confirm":{"message":"您确认要删除这些规则吗?"},"description":{"message":"管理浏览器请求,包括修改请求头和响应头、重定向请求、取消请求"},"disable":{"message":"禁用"},"display_common_header":{"message":"编辑时显示常用头"},"download":{"message":"下载"},"download_rule":{"message":"下载规则"},"edit":{"message":"编辑"},"enable":{"message":"启用"},"enable_he":{"message":"启用Header Editor"},"encoding":{"message":"编码"},"enter_group_name":{"message":"请输入组名称"},"excludeRule":{"message":"排除规则"},"exec_function":{"message":"自定义函数"},"exec_normal":{"message":"常规"},"exec_type":{"message":"执行类型"},"export":{"message":"导出"},"export_and_import":{"message":"导出和导入"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"分组"},"headerName":{"message":"头名称"},"headerValue":{"message":"头内容"},"header_empty":{"message":"头名称不能为空"},"help":{"message":"帮助"},"import":{"message":"导入"},"import_drop":{"message":"不导入"},"import_new":{"message":"添加"},"import_override":{"message":"覆盖已有"},"import_success":{"message":"导入成功"},"include_header_in_custom_function":{"message":"在自定义函数中包含请求头"},"manage":{"message":"管理"},"manage_collapse_group":{"message":"默认折叠分组"},"matchRule":{"message":"匹配规则"},"matchType":{"message":"匹配类型"},"match_all":{"message":"全部"},"match_domain":{"message":"域名"},"match_prefix":{"message":"网址前缀"},"match_regexp":{"message":"正则表达式"},"match_rule_empty":{"message":"匹配规则不能为空"},"match_url":{"message":"网址"},"modify_body":{"message":"修改响应体(仅支持Firefox)"},"name":{"message":"名称"},"name_empty":{"message":"名称不能为空"},"ok":{"message":"确定"},"options":{"message":"选项"},"pack_up_or_unfurl":{"message":"收起/展开分组"},"redirectTo":{"message":"重定向至"},"redirect_empty":{"message":"重定向目标不能为空"},"rename":{"message":"重命名"},"ruleType":{"message":"规则类型"},"rule_cancel":{"message":"阻止请求"},"rule_list":{"message":"规则列表"},"rule_modifyReceiveBody":{"message":"修改响应体"},"rule_modifyReceiveHeader":{"message":"修改响应头"},"rule_modifySendHeader":{"message":"修改请求头"},"rule_redirect":{"message":"重定向请求"},"rules_no_effect_for_he":{"message":"规则对Header Editor无效"},"save":{"message":"保存"},"save_to":{"message":"保存至"},"saved":{"message":"已保存"},"select_all":{"message":"全选/全不选"},"select_or_download":{"message":"请从文件导入或下载规则"},"share":{"message":"分享"},"suggested_group":{"message":"建议分组"},"test_custom_code":{"message":"暂不支持自定义函数"},"test_exclude":{"message":"已匹配但被排除"},"test_invalid_regexp":{"message":"正则表达式无效"},"test_mismatch":{"message":"不匹配"},"test_url":{"message":"测试(不会保存)"},"third_party_rules":{"message":"第三方规则"},"ungrouped":{"message":"未分组"},"upload":{"message":"上传"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/guide/cloud-backup/"},"url_help":{"message":"https://he.firefoxcn.net/"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/guide/third-party-rules/"},"view":{"message":"查看"}} -------------------------------------------------------------------------------- /public/_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | {"action":{"message":"動作"},"add":{"message":"新增"},"auto":{"message":"自動"},"batch_delete":{"message":"批次刪除"},"batch_mode":{"message":"批次作業"},"cancel":{"message":"取消"},"choose":{"message":"選擇"},"clone":{"message":"複製"},"cloud_backup":{"message":"雲端備份"},"cloud_backup_at":{"message":"最後備份於 $date$","placeholders":{"date":{"content":"$1"}}},"cloud_no_backup":{"message":"無備份"},"cloud_over_limit":{"message":"超出備份大小限制"},"code":{"message":"代碼"},"code_empty":{"message":"代碼不能為空"},"common_mark":{"message":"設為常用"},"common_mark_tip":{"message":"將任意規則或分組設為常用後,會在此處顯示"},"common_unmark":{"message":"取消常用"},"dark_mode":{"message":"深色模式"},"dark_mode_help":{"message":"深色模式為實驗性功能,重新整理頁面以確認效果"},"debug_mode_enable":{"message":"啟用偵錯模式"},"debug_mode_help":{"message":"當偵錯模式啟用時,將於背景頁面輸出執行記錄"},"delete":{"message":"刪除"},"delete_confirm":{"message":"您確定要刪除這些規則嗎?"},"description":{"message":"管理瀏覽器要求,包括修改要求標頭和回應標頭、重新導向要求、取消要求"},"disable":{"message":"停用"},"display_common_header":{"message":"編輯時顯示常用標頭"},"download":{"message":"下載"},"download_rule":{"message":"下載規則"},"edit":{"message":"編輯"},"enable":{"message":"啟用"},"enable_he":{"message":"啟用 Header Editor"},"encoding":{"message":"編碼"},"enter_group_name":{"message":"請輸入分組名稱"},"excludeRule":{"message":"排除規則"},"exec_function":{"message":"自訂函數"},"exec_normal":{"message":"一般"},"exec_type":{"message":"執行類型"},"export":{"message":"匯出"},"export_and_import":{"message":"匯出與匯入"},"extButtonTitle":{"message":"Header Editor"},"extName":{"message":"Header Editor"},"group":{"message":"分組"},"headerName":{"message":"標頭名稱"},"headerValue":{"message":"標頭內容"},"header_empty":{"message":"標頭名稱不能為空"},"help":{"message":"說明"},"import":{"message":"匯入"},"import_drop":{"message":"不要匯入"},"import_new":{"message":"新增"},"import_override":{"message":"覆寫現有"},"import_success":{"message":"匯入成功"},"include_header_in_custom_function":{"message":"在自訂函數中包含要求標頭"},"manage":{"message":"管理"},"manage_collapse_group":{"message":"預設摺疊分組"},"matchRule":{"message":"比對規則"},"matchType":{"message":"比對類型"},"match_all":{"message":"全部"},"match_domain":{"message":"域名"},"match_prefix":{"message":"網址首碼"},"match_regexp":{"message":"規則運算式"},"match_rule_empty":{"message":"比對規則不能為空"},"match_url":{"message":"網址"},"modify_body":{"message":"修改回應本文(僅支援 Firefox)"},"name":{"message":"名稱"},"name_empty":{"message":"名稱不能為空"},"ok":{"message":"確定"},"options":{"message":"選項"},"pack_up_or_unfurl":{"message":"收起/展開分組"},"redirectTo":{"message":"重新導向至"},"redirect_empty":{"message":"重新導向目標不能為空"},"rename":{"message":"重新命名"},"ruleType":{"message":"規則類型"},"rule_cancel":{"message":"取消要求"},"rule_list":{"message":"規則清單"},"rule_modifyReceiveBody":{"message":"修改回應本文"},"rule_modifyReceiveHeader":{"message":"修改回應標頭"},"rule_modifySendHeader":{"message":"修改要求標頭"},"rule_redirect":{"message":"重新導向要求"},"rules_no_effect_for_he":{"message":"規則對 Header Editor 無效"},"save":{"message":"儲存"},"save_to":{"message":"儲存至"},"saved":{"message":"已儲存"},"select_all":{"message":"全選/取消全選"},"select_or_download":{"message":"請從本機檔案匯入或下載規則"},"share":{"message":"分享"},"suggested_group":{"message":"建議分組"},"test_custom_code":{"message":"暫不支援自訂函數"},"test_exclude":{"message":"已符合但被排除"},"test_invalid_regexp":{"message":"正規表示式無效"},"test_mismatch":{"message":"不相符"},"test_url":{"message":"測試(不會被儲存)"},"third_party_rules":{"message":"第三方規則"},"ungrouped":{"message":"未分組"},"upload":{"message":"上傳"},"url_cloud_backup":{"message":"https://he.firefoxcn.net/zh-TW/cloud-backup.html"},"url_help":{"message":"https://he.firefoxcn.net/zh-TW/guide.html"},"url_third_party_rules":{"message":"https://he.firefoxcn.net/zh-TW/third-party-rules.html"},"view":{"message":"檢視"}} -------------------------------------------------------------------------------- /public/assets/images/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/public/assets/images/128.png -------------------------------------------------------------------------------- /public/assets/images/128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FirefoxBar/HeaderEditor/ec1cf31f6711d829d43a86e43a7e95aefe7bb278/public/assets/images/128w.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Header Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/config.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join, dirname } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | function readJSONSync(fullPath) { 9 | return JSON.parse(readFileSync(fullPath, { 10 | encoding: 'utf8' 11 | })); 12 | } 13 | 14 | const root = join(__dirname, '..'); 15 | const dist = join(root, 'dist'); 16 | 17 | const extension = readJSONSync(join(root, 'extension.json')); 18 | const manifest = readJSONSync(join(dist, 'manifest.json')); 19 | 20 | const pack = join(root, 'temp/dist-pack'); 21 | const release = join(root, 'temp/release'); 22 | 23 | export const version = manifest.version; 24 | export const resolve = join; 25 | export const path = { root, dist, pack, release }; 26 | export { extension }; 27 | -------------------------------------------------------------------------------- /scripts/get-snapshot-version.mjs: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { readFile, mkdir, writeFile } from 'fs/promises'; 3 | import { join, dirname } from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | async function main() { 10 | const token = process.env.TOKEN; 11 | 12 | if (!token) { 13 | return; 14 | } 15 | 16 | // Get latest release version 17 | const gitHubToken = process.env.GITHUB_TOKEN; 18 | const gitHubBaseURL = process.env.GITHUB_API_URL + '/repos/' + process.env.GITHUB_REPOSITORY; 19 | const latestReleaseResp = await fetch(gitHubBaseURL + '/releases/latest', { 20 | headers: { 21 | 'Accept': 'application/vnd.github+json', 22 | 'Authorization': 'Bearer ' + gitHubToken, 23 | 'X-GitHub-Api-Version': '2022-11-28', 24 | } 25 | }); 26 | const latestRelease = await latestReleaseResp.json(); 27 | const versionPrefix = latestRelease.tag_name.replace(/^v/, ''); 28 | 29 | // Get remote version 30 | const params = new URLSearchParams(); 31 | params.append('name', 'header-editor'); 32 | params.append('ver', versionPrefix); 33 | params.append('token', token); 34 | 35 | const resp = await fetch('https://ext.firefoxcn.net/api/snapshot.php?' + params.toString()); 36 | const text = await resp.text(); 37 | 38 | const filePath = join(__dirname, '../temp/version.txt'); 39 | if (/^(\d+)$/.test(text)) { 40 | await mkdir(join(__dirname, '../temp/'), { 41 | recursive: true, 42 | }); 43 | await writeFile(filePath, versionPrefix + '.' + text, { 44 | encoding: 'utf8', 45 | }); 46 | } 47 | 48 | console.log('Got version: ' + text + ', wrote to: ' + filePath); 49 | } 50 | 51 | main(); -------------------------------------------------------------------------------- /scripts/pack-utils/amo.mjs: -------------------------------------------------------------------------------- 1 | import { version as _version, extension } from '../config.mjs'; 2 | import { signAddon } from 'sign-addon'; 3 | 4 | export default function (zipPath) { 5 | if (!process.env.AMO_KEY) { 6 | return Promise.reject(new Error('AMO_KEY not found')); 7 | } 8 | if (!process.env.AMO_SECRET) { 9 | return Promise.reject(new Error('AMO_SECRET not found')); 10 | } 11 | 12 | return signAddon({ 13 | xpiPath: zipPath, 14 | version: _version, 15 | apiKey: process.env.AMO_KEY, 16 | apiSecret: process.env.AMO_SECRET, 17 | id: extension.firefox.amo, 18 | disableProgressBar: true, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /scripts/pack-utils/crx.mjs: -------------------------------------------------------------------------------- 1 | import ChromeExtension from 'crx'; 2 | import { readFile, writeFile } from 'fs/promises'; 3 | import { resolve, extension, version } from '../config.mjs'; 4 | 5 | async function createCrx(fileContent) { 6 | const keyContent = process.env.CRX_PRIV_KEY; 7 | if (!keyContent) { 8 | throw new Error('CRX_PRIV_KEY not found'); 9 | } 10 | const crx = new ChromeExtension({ 11 | codebase: 'http://localhost:8000/myExtension.crx', 12 | privateKey: keyContent, 13 | }); 14 | 15 | crx.loaded = true; 16 | 17 | const crxBuffer = await crx.pack(fileContent); 18 | 19 | return crxBuffer; 20 | } 21 | 22 | async function packCrx(zipPath, outputDir) { 23 | const fileContent = await readFile(zipPath); 24 | const content = await createCrx(fileContent); 25 | const out = resolve(outputDir, `${extension.dist.replace('{VER}', version)}.crx`); 26 | await writeFile(out, content); 27 | const idFile = resolve(outputDir, `${extension.dist.replace('{VER}', version)}.crx-id.txt`); 28 | await writeFile(idFile, extension.chrome.crx); 29 | return out; 30 | } 31 | 32 | export default packCrx; 33 | -------------------------------------------------------------------------------- /scripts/pack-utils/cws.mjs: -------------------------------------------------------------------------------- 1 | import { createReadStream } from 'fs'; 2 | import fetch from 'node-fetch'; 3 | import { extension } from '../config.mjs'; 4 | 5 | const webStoreId = process.env.CWS_CLIENT_ID; 6 | const webStoreToken = process.env.CWS_TOKEN; 7 | const webStoreSecret = process.env.CWS_CLIENT_SECRET; 8 | 9 | let _webStoreToken = null; 10 | async function getToken() { 11 | if (_webStoreToken) { 12 | return _webStoreToken; 13 | } 14 | const resp = await fetch('https://www.googleapis.com/oauth2/v4/token', { 15 | method: 'POST', 16 | headers: { 17 | 'Content-Type': 'application/x-www-form-urlencoded', 18 | }, 19 | body: new URLSearchParams({ 20 | client_id: webStoreId, 21 | client_secret: webStoreSecret, 22 | refresh_token: webStoreToken, 23 | grant_type: 'refresh_token', 24 | }).toString(), 25 | }); 26 | const res = await resp.json(); 27 | if (res.access_token) { 28 | _webStoreToken = res.access_token; 29 | return _webStoreToken; 30 | } else { 31 | throw new Error(res.error); 32 | } 33 | } 34 | 35 | async function upload(readStream, token) { 36 | const res = await fetch(`https://www.googleapis.com/upload/chromewebstore/v1.1/items/${extension.chrome.id}`, { 37 | method: 'PUT', 38 | headers: { 39 | Authorization: `Bearer ${token}`, 40 | 'x-goog-api-version': '2', 41 | }, 42 | body: readStream, 43 | }); 44 | 45 | return res.json(); 46 | } 47 | 48 | async function publish(target = 'default', token) { 49 | const url = `https://www.googleapis.com/chromewebstore/v1.1/items/${extension.chrome.id}/publish?publishTarget=${target}`; 50 | const res = await fetch(url, { 51 | method: 'POST', 52 | headers: { 53 | Authorization: `Bearer ${token}`, 54 | 'x-goog-api-version': '2', 55 | }, 56 | }); 57 | 58 | return res.json(); 59 | } 60 | 61 | async function packCws(zipPath) { 62 | if (!process.env.CWS_CLIENT_ID) { 63 | return Promise.reject(new Error('CWS_CLIENT_ID not found')); 64 | } 65 | if (!process.env.CWS_CLIENT_SECRET) { 66 | return Promise.reject(new Error('CWS_CLIENT_SECRET not found')); 67 | } 68 | if (!process.env.CWS_TOKEN) { 69 | return Promise.reject(new Error('CWS_TOKEN not found')); 70 | } 71 | 72 | const distStream = createReadStream(zipPath); 73 | const token = await getToken(); 74 | await upload(distStream, token); 75 | return publish('default', token); 76 | } 77 | 78 | export default packCws; 79 | -------------------------------------------------------------------------------- /scripts/pack-utils/edge.mjs: -------------------------------------------------------------------------------- 1 | import { EdgeWebstoreClient } from '@plasmo-corp/ewu'; 2 | 3 | export default function (zipPath) { 4 | if (!process.env.MS_PRODUCT_ID) { 5 | return Promise.reject(new Error('MS_PRODUCT_ID not found')); 6 | } 7 | if (!process.env.MS_CLIENT_ID) { 8 | return Promise.reject(new Error('MS_CLIENT_ID not found')); 9 | } 10 | if (!process.env.MS_CLIENT_SECRET) { 11 | return Promise.reject(new Error('MS_CLIENT_SECRET not found')); 12 | } 13 | if (!process.env.MS_ACCESS_TOKEN_URL) { 14 | return Promise.reject(new Error('MS_ACCESS_TOKEN_URL not found')); 15 | } 16 | 17 | const client = new EdgeWebstoreClient({ 18 | productId: process.env.MS_PRODUCT_ID, 19 | clientId: process.env.MS_CLIENT_ID, 20 | clientSecret: process.env.MS_CLIENT_SECRET, 21 | accessTokenUrl: process.env.MS_ACCESS_TOKEN_URL, 22 | }); 23 | 24 | return client.submit({ 25 | filePath: zipPath, 26 | notes: "release" 27 | }); 28 | }; 29 | -------------------------------------------------------------------------------- /scripts/pack-utils/index.mjs: -------------------------------------------------------------------------------- 1 | import amo from './amo.mjs'; 2 | import cws from './cws.mjs'; 3 | import xpi from './xpi.mjs'; 4 | import edge from './edge.mjs'; 5 | import crx from './crx.mjs'; 6 | 7 | // const packUtils = { amo, cws, xpi, edge, crx }; 8 | const packUtils = { xpi, crx }; 9 | 10 | export default packUtils; 11 | -------------------------------------------------------------------------------- /scripts/pack-utils/xpi.mjs: -------------------------------------------------------------------------------- 1 | import { rename, writeFile } from 'fs/promises'; 2 | import { version as _version, extension, resolve } from '../config.mjs'; 3 | import { signAddon } from 'sign-addon'; 4 | 5 | async function packXpi(zipPath, outputDir) { 6 | if (!process.env.AMO_KEY) { 7 | return Promise.reject(new Error('AMO_KEY not found')); 8 | } 9 | if (!process.env.AMO_SECRET) { 10 | return Promise.reject(new Error('AMO_SECRET not found')); 11 | } 12 | 13 | const result = await signAddon({ 14 | xpiPath: zipPath, 15 | version: _version, 16 | apiKey: process.env.AMO_KEY, 17 | apiSecret: process.env.AMO_SECRET, 18 | id: extension.firefox.xpi, 19 | downloadDir: outputDir, 20 | disableProgressBar: true, 21 | }); 22 | if (!result.success) { 23 | throw new Error('Sign failed'); 24 | } 25 | const res = result.downloadedFiles; 26 | if (res.length === 0) { 27 | throw new Error('No signed addon found'); 28 | } 29 | console.log(`Downloaded signed addon: ${res.join(', ')}`); 30 | const out = resolve(outputDir, `${extension.dist.replace('{VER}', _version)}.xpi`); 31 | const idFile = resolve(outputDir, `${extension.dist.replace('{VER}', _version)}.xpi-id.txt`); 32 | // Move download file to output dir 33 | await rename(res[0], out); 34 | await writeFile(idFile, extension.firefox.xpi); 35 | return out; 36 | } 37 | 38 | export default packXpi; 39 | -------------------------------------------------------------------------------- /scripts/pack.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * 进行多渠道打包 3 | * 4 | * dist:原本的输出文件夹 5 | * dist-pack:用于打包的文件夹 6 | * dist-pack/{platform}:各个平台的文件夹 7 | * dist-pack/{platform}.zip:各个平台的打包文件 8 | * dist-pack/release:其他平台打包输出结果 9 | * 在这里,打包文件夹统一命名为pack 10 | */ 11 | import { unlink, mkdir } from 'fs/promises'; 12 | import { readJSON, outputJSON } from 'fs-extra/esm'; 13 | import { join } from 'path'; 14 | import { extension, resolve as _resolve, path as _path } from './config.mjs'; 15 | import { exec as processExec } from 'child_process'; 16 | import packUtils from './pack-utils/index.mjs'; 17 | 18 | let platform = null; 19 | for (const it of process.argv) { 20 | if (it.startsWith('--platform=')) { 21 | platform = it.substr(11); 22 | if (platform.indexOf(',') > 0) { 23 | platform = platform.trim().split(','); 24 | } 25 | break; 26 | } 27 | } 28 | 29 | function exec(commands) { 30 | return new Promise((resolve, reject) => { 31 | processExec(commands, (error, stdout, stderr) => { 32 | if (error) { 33 | reject(error); 34 | } else { 35 | resolve(stdout); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | async function removeManifestKeys(manifest, name) { 42 | console.log('start convert manifest(' + manifest + ') for ' + name); 43 | const wantKey = `__${name}__`; 44 | 45 | const removeObjKeys = obj => { 46 | Object.keys(obj).forEach((it) => { 47 | if (it.startsWith('__')) { 48 | if (it.startsWith(wantKey)) { 49 | const finalKey = it.substr(wantKey.length); 50 | console.log('copy key ' + finalKey + ' from ' + it); 51 | obj[finalKey] = obj[it]; 52 | } 53 | console.log('remove key ' + it); 54 | delete obj[it]; 55 | } else if (typeof obj[it] === 'object' && !Array.isArray(obj[it])) { 56 | removeObjKeys(obj[it]); 57 | } 58 | }); 59 | } 60 | 61 | try { 62 | const content = await readJSON(manifest); 63 | removeObjKeys(content); 64 | await outputJSON(manifest, content); 65 | } catch (e) { 66 | console.log(e); 67 | } 68 | } 69 | 70 | async function packOnePlatform(name) { 71 | if (typeof packUtils[name] === 'undefined') { 72 | console.error(`pack-utils for ${name} not found`); 73 | return; 74 | } 75 | const thisPack = _resolve(_path.pack, name); 76 | const zipPath = _resolve(_path.pack, `${name}.zip`); 77 | try { 78 | // 复制一份到dist下面 79 | await exec(`cp -r ${_path.dist} ${thisPack}`); 80 | // 移除掉manifest中的非本平台key 81 | await removeManifestKeys(join(thisPack, 'manifest.json'), name); 82 | // 打包成zip 83 | await exec(`cd ${thisPack} && zip -r ${zipPath} ./*`); 84 | // 执行上传等操作 85 | const res = await packUtils[name](zipPath, _path.release); 86 | console.log(`${name}: ${res}`); 87 | await unlink(zipPath); 88 | } catch (e) { 89 | console.error(e); 90 | } 91 | } 92 | 93 | async function main() { 94 | // 检查打包目录是否存在 95 | await exec(`cd ${_path.root} && rm -rf ./temp/dist-pack`); 96 | await exec(`cd ${_path.root} && rm -rf ./temp/release`); 97 | await mkdir(_path.pack, { 98 | recursive: true, 99 | }); 100 | await mkdir(_path.release, { 101 | recursive: true, 102 | }); 103 | 104 | if (platform) { 105 | if (Array.isArray(platform)) { 106 | platform.forEach((it) => { 107 | if (typeof packUtils[it] !== 'undefined') { 108 | packOnePlatform(it); 109 | } else { 110 | console.log(`${it} not found`); 111 | } 112 | }); 113 | return; 114 | } 115 | 116 | if (typeof packUtils[platform] !== 'undefined') { 117 | packOnePlatform(platform); 118 | return; 119 | } 120 | console.log(`${platform} not found`); 121 | 122 | return; 123 | } 124 | 125 | const queue = []; 126 | Object.keys(extension.autobuild).forEach((it) => { 127 | if (extension.autobuild[it]) { 128 | queue.push(packOnePlatform(it)); 129 | } else { 130 | console.log(`Skip ${it.toUpperCase()}`); 131 | } 132 | }); 133 | } 134 | 135 | main(); 136 | -------------------------------------------------------------------------------- /scripts/release.mjs: -------------------------------------------------------------------------------- 1 | import { readFile, readdir, stat } from 'fs/promises'; 2 | import { join } from 'path'; 3 | import { createHash } from 'crypto'; 4 | import publishRelease from 'publish-release'; 5 | import { extension, path as _path, version } from './config.mjs'; 6 | 7 | async function hashFile(filePath) { 8 | const buffer = await readFile(filePath); 9 | const fsHash = createHash('sha256'); 10 | fsHash.update(buffer); 11 | return fsHash.digest('hex'); 12 | } 13 | 14 | function publishReleasePromise(options) { 15 | return new Promise((resolve, reject) => { 16 | publishRelease(options, (err, release) => { 17 | if (err) { 18 | reject(err); 19 | } else { 20 | resolve(release.html_url); 21 | } 22 | }); 23 | }); 24 | } 25 | 26 | async function publishUpdate(params) { 27 | const token = process.env.SERVER_TOKEN; 28 | if (!token) { 29 | return; 30 | } 31 | const query = new URLSearchParams(params); 32 | query.append('name', 'header-editor'); 33 | query.append('token', token); 34 | 35 | const resp = await fetch('https://ext.firefoxcn.net/api/update.php?' + query.toString()); 36 | return await resp.text(); 37 | } 38 | 39 | async function main() { 40 | if (!extension.github.enable) { 41 | return; 42 | } 43 | const repo = process.env.GITHUB_REPOSITORY; 44 | if (!repo) { 45 | console.log('GITHUB_REPOSITORY not found'); 46 | return; 47 | } 48 | if (!process.env.GITHUB_TOKEN) { 49 | console.log('GITHUB_TOKEN not found'); 50 | return; 51 | } 52 | 53 | const assets = []; 54 | 55 | const dirContent = await readdir(_path.release); 56 | for (const file of dirContent) { 57 | if (!file.endsWith('.xpi') && !file.endsWith('.crx')) { 58 | continue; 59 | } 60 | const fullPath = join(_path.release, file); 61 | const idFilePath = join(_path.release, file + '-id.txt'); 62 | const statResult = await stat(fullPath); 63 | if (statResult.isFile()) { 64 | const fileHash = await hashFile(fullPath); 65 | const id = await readFile(idFilePath, { 66 | encoding: 'utf8', 67 | }); 68 | assets.push({ 69 | id, 70 | name: file, 71 | path: fullPath, 72 | hash: fileHash, 73 | }); 74 | } 75 | } 76 | 77 | // Get git names 78 | const gitName = repo.split('/'); 79 | const tagName = process.env.GITHUB_REF_NAME; 80 | await publishReleasePromise({ 81 | token: process.env.GITHUB_TOKEN, 82 | owner: gitName[0], 83 | repo: gitName[1], 84 | tag: tagName, 85 | name: version, 86 | notes: assets.map(item => `> ${item.name} SHA256: ${item.hash} \n`).join('\n'), 87 | draft: false, 88 | prerelease: false, 89 | reuseRelease: false, 90 | reuseDraftOnly: false, 91 | skipAssetsCheck: false, 92 | skipDuplicatedAssets: false, 93 | skipIfPublished: true, 94 | editRelease: false, 95 | deleteEmptyTag: false, 96 | assets: assets.map(item => item.path), 97 | }); 98 | 99 | // update "update info" file 100 | for (const it of assets) { 101 | const url = `https://github.com/${repo}/releases/download/${tagName}/${it.name}`; 102 | const browser = it.name.endsWith('.xpi') ? 'gecko' : 'chrome'; 103 | const result = await publishUpdate({ 104 | id: it.id, 105 | ver: version, 106 | url, 107 | browser, 108 | hash: it.hash, 109 | }); 110 | console.log('Publish update info ', it.name, result); 111 | } 112 | } 113 | 114 | main(); -------------------------------------------------------------------------------- /scripts/webpack/dev.js: -------------------------------------------------------------------------------- 1 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 2 | 3 | module.exports = function (config, context) { 4 | // 调试模式下,开启自动重载和自动编译 5 | if (config.get('mode') === 'development') { 6 | // config.plugin('reload').use(ChromeExtensionReloader); 7 | config.devServer.hot(false); 8 | config.devServer.open(false); 9 | const devMiddleware = config.devServer.store.get('devMiddleware'); 10 | config.devServer.store.set('devMiddleware', { 11 | ...devMiddleware, 12 | writeToDisk: true, 13 | }); 14 | } 15 | 16 | config.plugin('bundle-analyzer').use(new BundleAnalyzerPlugin({ 17 | analyzerMode: 'static', 18 | reportFilename: '../temp/bundle-analyze.html', 19 | })) 20 | 21 | return config; 22 | }; 23 | -------------------------------------------------------------------------------- /scripts/webpack/externals.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 4 | 5 | const copy = [ 6 | { 7 | from: './node_modules/react/umd/react.production.min.js', 8 | to: 'external/react.min.js', 9 | }, 10 | { 11 | from: './node_modules/react-dom/umd/react-dom.production.min.js', 12 | to: 'external/react-dom.min.js', 13 | }, 14 | ]; 15 | 16 | const root = path.join(__dirname, '../..'); 17 | 18 | module.exports = function (config) { 19 | const { version } = require(path.join(root, 'package.json')); 20 | 21 | // 添加 snapshot 版本号 22 | let versionText = version; 23 | const forceVersionFile = path.join(__dirname, '../../temp/version.txt'); 24 | if (fs.existsSync(forceVersionFile)) { 25 | versionText = fs.readFileSync(forceVersionFile, { encoding: 'utf8' }).trim(); 26 | console.log('Got force version: ' + versionText); 27 | } else { 28 | console.log('No force version ' + forceVersionFile); 29 | } 30 | // 如果是tag触发的CI,强制用tag的版本号 31 | if (process.env.GITHUB_REF_TYPE && process.env.GITHUB_REF_TYPE === 'tag') { 32 | const tagName = process.env.GITHUB_REF_NAME; 33 | if (/^[0-9]\.[0-9]+\.[0-9]+$/.test(tagName)) { 34 | versionText = tagName; 35 | } 36 | } 37 | 38 | // dev 环境复制 development 的 react 资源 39 | if (config.get('mode') === 'development') { 40 | copy.forEach((x) => { 41 | if (x.from.includes('.production.min.js')) { 42 | x.from = x.from.replace('.production.min.js', '.development.js'); 43 | } 44 | }); 45 | } 46 | 47 | // 复制 manifest 48 | copy.push({ 49 | from: './src/manifest.json', 50 | to: 'manifest.json', 51 | transform: (content) => { 52 | const jsonContent = JSON.parse(content); 53 | jsonContent.version = versionText; 54 | 55 | if (config.mode === 'development') { 56 | jsonContent['content_security_policy'] = "script-src 'self' 'unsafe-eval'; object-src 'self'"; 57 | } 58 | 59 | return JSON.stringify(jsonContent); 60 | }, 61 | }); 62 | 63 | // 复制其他静态文件 64 | config.plugin('copy').use(new CopyWebpackPlugin({ 65 | patterns: copy, 66 | })); 67 | 68 | // Add manaco into a standalone chunk 69 | config.optimization.splitChunks({ 70 | chunks: 'all', 71 | minChunks: 100, 72 | cacheGroups: { 73 | default: false, 74 | codemirror: { 75 | name: 'codemirror', 76 | test: /codemirror/, 77 | enforce: true, 78 | }, 79 | semi: { 80 | name: 'semi', 81 | test: /@douyinfe[/+]semi-/, 82 | enforce: true, 83 | }, 84 | }, 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /scripts/webpack/remove-html.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, context) { 2 | const plugins = config.plugins.values(); 3 | 4 | for (const item of plugins) { 5 | if (item.name.indexOf('HtmlWebpackPlugin_') !== 0) { 6 | continue; 7 | } 8 | const pageName = item.name.substr(18); 9 | if (pageName === 'background' || pageName.indexOf('inject-') === 0 || pageName.indexOf('worker-') === 0) { 10 | config.plugins.delete(item.name); 11 | console.log('Remove html entry: ' + item.name); 12 | } 13 | } 14 | 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /scripts/webpack/webpack.plugin.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ context, registerCliOption, onGetWebpackConfig }) => { 2 | onGetWebpackConfig(config => { 3 | require('./externals')(config, context); 4 | require('./dev')(config, context); 5 | require('./remove-html')(config, context); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/www/CNAME: -------------------------------------------------------------------------------- 1 | he.firefoxcn.net -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | IS_BACKGROUND?: boolean; 3 | } 4 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extName__", 3 | "short_name": "__MSG_extName__", 4 | "version": null, 5 | "description": "__MSG_description__", 6 | "homepage_url": "https://he.firefoxcn.net", 7 | "manifest_version": 2, 8 | "icons": { 9 | "128": "assets/images/128.png" 10 | }, 11 | "permissions": [ 12 | "tabs", 13 | "webRequest", 14 | "webRequestBlocking", 15 | "storage", 16 | "*://*/*", 17 | "unlimitedStorage" 18 | ], 19 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self';", 20 | "background": { 21 | "scripts": [ 22 | "assets/js/background.js" 23 | ] 24 | }, 25 | "browser_action": { 26 | "default_icon": { 27 | "128": "assets/images/128.png" 28 | }, 29 | "default_title": "__MSG_extButtonTitle__", 30 | "default_popup": "popup.html" 31 | }, 32 | "default_locale": "en", 33 | "options_ui": { 34 | "page": "options.html", 35 | "open_in_tab": true 36 | }, 37 | "__amo__browser_specific_settings": { 38 | "gecko": { 39 | "id": "headereditor-amo@addon.firefoxcn.net", 40 | "strict_min_version": "77.0" 41 | } 42 | }, 43 | "__xpi__browser_specific_settings": { 44 | "gecko": { 45 | "id": "headereditor@addon.firefoxcn.net", 46 | "strict_min_version": "77.0", 47 | "update_url": "https://ext.firefoxcn.net/header-editor/install/update.json" 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/pages/background/api-handler.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import logger from '@/share/core/logger'; 3 | import { APIs, TABLE_NAMES_ARR } from '@/share/core/constant'; 4 | import { prefs } from '@/share/core/prefs'; 5 | import rules from './core/rules'; 6 | import { openURL } from './utils'; 7 | import { getDatabase } from './core/db'; 8 | 9 | function execute(request: any) { 10 | if (request.method === 'notifyBackground') { 11 | request.method = request.reason; 12 | delete request.reason; 13 | } 14 | switch (request.method) { 15 | case APIs.HEALTH_CHECK: 16 | return new Promise((resolve) => { 17 | getDatabase() 18 | .then(() => resolve(true)) 19 | .catch(() => resolve(false)); 20 | }); 21 | case APIs.OPEN_URL: 22 | return openURL(request); 23 | case APIs.GET_RULES: 24 | return Promise.resolve(rules.get(request.type, request.options)); 25 | case APIs.SAVE_RULE: 26 | return rules.save(request.rule); 27 | case APIs.DELETE_RULE: 28 | return rules.remove(request.type, request.id); 29 | case APIs.SET_PREFS: 30 | return prefs.set(request.key, request.value); 31 | case APIs.UPDATE_CACHE: 32 | if (request.type === 'all') { 33 | return Promise.all(TABLE_NAMES_ARR.map((tableName) => rules.updateCache(tableName))); 34 | } else { 35 | return rules.updateCache(request.type); 36 | } 37 | default: 38 | break; 39 | } 40 | // return false; 41 | } 42 | 43 | export default function createApiHandler() { 44 | browser.runtime.onMessage.addListener((request) => { 45 | logger.debug('Background Receive Message', request); 46 | if (request.method === 'batchExecute') { 47 | const queue = request.batch.map((item) => { 48 | const res = execute(item); 49 | if (res) { 50 | return res; 51 | } 52 | return Promise.resolve(); 53 | }); 54 | return Promise.allSettled(queue); 55 | } 56 | return execute(request); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/pages/background/core/db.ts: -------------------------------------------------------------------------------- 1 | import { TABLE_NAMES_ARR } from '@/share/core/constant'; 2 | import { upgradeRuleFormat } from '@/share/core/rule-utils'; 3 | import { getGlobal } from '@/share/core/utils'; 4 | 5 | export function getDatabase(): Promise { 6 | return new Promise((resolve, reject) => { 7 | const dbOpenRequest = getGlobal().indexedDB.open('headereditor', 4); 8 | dbOpenRequest.onsuccess = (e) => { 9 | // @ts-ignore 10 | resolve(e.target.result); 11 | }; 12 | dbOpenRequest.onerror = (e) => { 13 | console.error(e); 14 | reject(e); 15 | }; 16 | dbOpenRequest.onupgradeneeded = (event) => { 17 | if (event.oldVersion === 0) { 18 | // Installed 19 | TABLE_NAMES_ARR.forEach((t) => { 20 | // @ts-ignore 21 | event.target.result.createObjectStore(t, { keyPath: 'id', autoIncrement: true }); 22 | }); 23 | } else { 24 | TABLE_NAMES_ARR.forEach((k) => { 25 | // @ts-ignore 26 | const tx = event.target.transaction; 27 | if (!tx.objectStoreNames.contains(k)) { 28 | // @ts-ignore 29 | event.target.result.createObjectStore(k, { keyPath: 'id', autoIncrement: true }); 30 | return; 31 | } 32 | const os = tx.objectStore(k); 33 | os.openCursor().onsuccess = (e: any) => { 34 | const cursor = e.target.result; 35 | if (cursor) { 36 | const s = cursor.value; 37 | s.id = cursor.key; 38 | // upgrade rule format 39 | os.put(upgradeRuleFormat(s)); 40 | cursor.continue(); 41 | } 42 | }; 43 | }); 44 | } 45 | }; 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/background/core/rules.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash-es'; 2 | import { convertToRule, convertToBasicRule, isMatchUrl, upgradeRuleFormat, initRule } from '@/share/core/rule-utils'; 3 | import { getLocal } from '@/share/core/storage'; 4 | import { getTableName } from '@/share/core/utils'; 5 | import { APIs, EVENTs, IS_MATCH, TABLE_NAMES, TABLE_NAMES_ARR } from '@/share/core/constant'; 6 | import type { InitdRule, Rule, RuleFilterOptions } from '@/share/core/types'; 7 | import notify from '@/share/core/notify'; 8 | import { getDatabase } from './db'; 9 | 10 | const cache: { [key: string]: null | InitdRule[] } = {}; 11 | TABLE_NAMES_ARR.forEach((t) => { 12 | cache[t] = null; 13 | }); 14 | 15 | const updateCacheQueue: { [x: string]: Array<{ resolve: () => void; reject: (error: any) => void }> } = {}; 16 | 17 | async function updateCache(type: TABLE_NAMES): Promise { 18 | return new Promise((resolve, reject) => { 19 | // 如果正在Update,则放到回调组里面 20 | if (typeof updateCacheQueue[type] !== 'undefined') { 21 | updateCacheQueue[type].push({ resolve, reject }); 22 | return; 23 | } else { 24 | updateCacheQueue[type] = [{ resolve, reject }]; 25 | } 26 | getDatabase() 27 | .then((db) => { 28 | const tx = db.transaction([type], 'readonly'); 29 | const os = tx.objectStore(type); 30 | const all: InitdRule[] = []; 31 | os.openCursor().onsuccess = (event) => { 32 | // @ts-ignore 33 | const cursor = event.target.result; 34 | if (cursor) { 35 | const s: InitdRule = cursor.value; 36 | s.id = cursor.key; 37 | // Init function here 38 | try { 39 | all.push(initRule(s)); 40 | } catch (e) { 41 | console.error('Cannot init rule', s, e); 42 | } 43 | cursor.continue(); 44 | } else { 45 | cache[type] = all; 46 | updateCacheQueue[type].forEach((it) => { 47 | it.resolve(); 48 | }); 49 | delete updateCacheQueue[type]; 50 | } 51 | }; 52 | }) 53 | .catch((e) => { 54 | updateCacheQueue[type].forEach((it) => { 55 | it.reject(e); 56 | }); 57 | delete updateCacheQueue[type]; 58 | }); 59 | }); 60 | } 61 | 62 | function filter(fromRules: InitdRule[], options: RuleFilterOptions) { 63 | let rules = Array.from(fromRules); 64 | if (options === null || typeof options !== 'object') { 65 | return rules; 66 | } 67 | const url = typeof options.url !== 'undefined' ? options.url : null; 68 | 69 | if (typeof options.id !== 'undefined') { 70 | rules = rules.filter((rule) => { 71 | if (Array.isArray(options.id)) { 72 | return options.id.includes(rule.id); 73 | } 74 | return rule.id === Number(options.id); 75 | }); 76 | } 77 | 78 | if (options.name) { 79 | rules = rules.filter((rule) => { 80 | return rule.name === options.name; 81 | }); 82 | } 83 | 84 | if (typeof options.enable !== 'undefined') { 85 | rules = rules.filter((rule) => { 86 | return rule.enable === options.enable; 87 | }); 88 | } 89 | 90 | if (url != null) { 91 | rules = rules.filter((rule) => isMatchUrl(rule, url) === IS_MATCH.MATCH); 92 | } 93 | return rules; 94 | } 95 | 96 | async function save(o: Rule) { 97 | const tableName = getTableName(o.ruleType); 98 | if (!tableName) { 99 | throw new Error(`Unknown type ${o.ruleType}`); 100 | } 101 | const rule = convertToRule(o); 102 | return new Promise((resolve) => { 103 | getDatabase().then((db) => { 104 | const tx = db.transaction([tableName], 'readwrite'); 105 | const os = tx.objectStore(tableName); 106 | // Check base informations 107 | upgradeRuleFormat(rule); 108 | // Update 109 | if (rule.id && rule.id !== -1) { 110 | const request = os.get(Number(rule.id)); 111 | request.onsuccess = () => { 112 | const existsRule = request.result || {}; 113 | const originalRule = cloneDeep(existsRule); 114 | for (const prop in rule) { 115 | if (prop === 'id') { 116 | continue; 117 | } 118 | existsRule[prop] = rule[prop]; 119 | } 120 | const req = os.put(existsRule); 121 | req.onsuccess = () => { 122 | updateCache(tableName); 123 | notify.other({ method: APIs.ON_EVENT, event: EVENTs.RULE_UPDATE, from: originalRule, target: existsRule }); 124 | resolve(rule); 125 | }; 126 | }; 127 | } else { 128 | // Create 129 | // Make sure it's not null - that makes indexeddb sad 130 | // @ts-ignore 131 | delete rule.id; 132 | const request = os.add(rule); 133 | request.onsuccess = (event) => { 134 | updateCache(tableName); 135 | // Give it the ID that was generated 136 | // @ts-ignore 137 | rule.id = event.target.result; 138 | notify.other({ method: APIs.ON_EVENT, event: EVENTs.RULE_UPDATE, from: null, target: rule }); 139 | resolve(rule); 140 | }; 141 | } 142 | }); 143 | }); 144 | } 145 | 146 | function remove(tableName: TABLE_NAMES, id: number): Promise { 147 | return new Promise((resolve) => { 148 | getDatabase().then((db) => { 149 | const tx = db.transaction([tableName], 'readwrite'); 150 | const os = tx.objectStore(tableName); 151 | const request = os.delete(Number(id)); 152 | request.onsuccess = () => { 153 | updateCache(tableName); 154 | notify.other({ method: APIs.ON_EVENT, event: EVENTs.RULE_DELETE, table: tableName, id: Number(id) }); 155 | // check common mark 156 | getLocal().get('common_rule').then((result) => { 157 | const key = `${tableName}-${id}`; 158 | if (Array.isArray(result.common_rule) && result.common_rule.includes(key)) { 159 | const newKeys = [...result.common_rule]; 160 | newKeys.splice(newKeys.indexOf(key), 1); 161 | getLocal().set({ 162 | common_rule: newKeys, 163 | }); 164 | } 165 | }); 166 | resolve(); 167 | }; 168 | }); 169 | }); 170 | } 171 | 172 | function get(type: TABLE_NAMES, options?: RuleFilterOptions) { 173 | // When browser is starting up, pass all requests 174 | const all = cache[type]; 175 | if (!all) { 176 | return null; 177 | } 178 | return options ? filter(all, options) : all; 179 | } 180 | 181 | function init() { 182 | setTimeout(() => { 183 | const queue: Array> = TABLE_NAMES_ARR.map((tableName) => updateCache(tableName)); 184 | Promise.all(queue).then(() => { 185 | if (TABLE_NAMES_ARR.some((tableName) => cache[tableName] === null)) { 186 | init(); 187 | } 188 | }); 189 | }); 190 | } 191 | 192 | init(); 193 | 194 | export default { 195 | get, 196 | filter, 197 | save, 198 | remove, 199 | updateCache, 200 | convertToBasicRule, 201 | }; 202 | -------------------------------------------------------------------------------- /src/pages/background/index.ts: -------------------------------------------------------------------------------- 1 | import createApiHandler from './api-handler'; 2 | import createRequestHandler from './request-handler'; 3 | import './upgrade'; 4 | 5 | if (typeof window !== 'undefined') { 6 | window.IS_BACKGROUND = true; 7 | } 8 | 9 | // 开始初始化 10 | createApiHandler(); 11 | createRequestHandler(); 12 | -------------------------------------------------------------------------------- /src/pages/background/upgrade.ts: -------------------------------------------------------------------------------- 1 | import browser from 'webextension-polyfill'; 2 | import * as storage from '@/share/core/storage'; 3 | import { TABLE_NAMES_ARR } from '@/share/core/constant'; 4 | import notify from '@/share/core/notify'; 5 | import { getDatabase } from './core/db'; 6 | 7 | // Upgrade 8 | const downloadHistory = localStorage.getItem('dl_history'); 9 | if (downloadHistory) { 10 | storage.getLocal().set({ dl_history: JSON.parse(downloadHistory) }); 11 | localStorage.removeItem('dl_history'); 12 | } 13 | 14 | // Put a version mark 15 | storage 16 | .getLocal() 17 | .get('version_mark') 18 | .then((v) => { 19 | const version = v.version_mark ? parseInt(v.version_mark, 10) : 0; 20 | if (!(version >= 1)) { 21 | storage.getLocal().set({ 22 | version_mark: 1, 23 | }); 24 | // Upgrade group 25 | const rebindRuleWithGroup = (group) => { 26 | return new Promise((resolve) => { 27 | const cacheQueue: Array> = []; 28 | function findGroup(type, id) { 29 | let result = browser.i18n.getMessage('ungrouped'); 30 | for (const k in group) { 31 | if (group[k].includes(`${type}-${id}`)) { 32 | result = k; 33 | break; 34 | } 35 | } 36 | return result; 37 | } 38 | TABLE_NAMES_ARR.forEach((k) => { 39 | getDatabase().then((db) => { 40 | const tx = db.transaction([k], 'readwrite'); 41 | const os = tx.objectStore(k); 42 | os.openCursor().onsuccess = (e) => { 43 | if (!e.target) { 44 | return; 45 | } 46 | const cursor = (e.target as any).result; 47 | if (cursor) { 48 | const s = cursor.value; 49 | s.id = cursor.key; 50 | if (typeof s.group === 'undefined') { 51 | s.group = findGroup(k, s.id); 52 | os.put(s); 53 | } 54 | cursor.continue(); 55 | } else { 56 | cacheQueue.push(notify.other({ method: 'updateCache', type: k })); 57 | } 58 | }; 59 | }); 60 | }); 61 | Promise.all(cacheQueue).then(resolve); 62 | }); 63 | }; 64 | 65 | const groups = localStorage.getItem('groups'); 66 | if (groups) { 67 | const g = JSON.parse(groups); 68 | localStorage.removeItem('groups'); 69 | rebindRuleWithGroup(g); 70 | } else { 71 | storage 72 | .getLocal() 73 | .get('groups') 74 | .then((r) => { 75 | if (r.groups !== undefined) { 76 | rebindRuleWithGroup(r.groups).then(() => storage.getLocal().remove('groups')); 77 | } else { 78 | const g = {}; 79 | g[browser.i18n.getMessage('ungrouped')] = []; 80 | rebindRuleWithGroup(g); 81 | } 82 | }); 83 | } 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /src/pages/background/utils.ts: -------------------------------------------------------------------------------- 1 | import { getActiveTab } from '@/share/core/utils'; 2 | import browser from 'webextension-polyfill'; 3 | 4 | interface OpenURLOptions { 5 | method?: string; 6 | url: string; 7 | active?: boolean; 8 | } 9 | 10 | export function openURL(options: OpenURLOptions) { 11 | delete options.method; 12 | return new Promise((resolve) => { 13 | const doCreate = () => browser.tabs.create(options).then(resolve); 14 | browser.tabs 15 | .query({ currentWindow: true, url: options.url }) 16 | .then((tabs) => { 17 | if (tabs.length) { 18 | browser.tabs 19 | .update(tabs[0].id, { 20 | active: true, 21 | }) 22 | .then(resolve) 23 | .catch(doCreate); 24 | } else { 25 | getActiveTab().then((tab) => { 26 | const url = tab.url || ''; 27 | // re-use an active new tab page 28 | // Firefox may have more than 1 newtab url, so check all 29 | const isNewTab = 30 | url.indexOf('about:newtab') === 0 || 31 | url.indexOf('about:home') === 0 || 32 | url.indexOf('chrome://newtab/') === 0; 33 | if (isNewTab) { 34 | browser.tabs 35 | .update(tab.id, options) 36 | .then(resolve) 37 | .catch(doCreate); 38 | } else { 39 | doCreate(); 40 | } 41 | }); 42 | } 43 | }) 44 | .catch(doCreate); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/pages/options/components/bool-radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup, withField } from '@douyinfe/semi-ui'; 2 | import type { OptionItem, RadioChangeEvent, RadioGroupProps } from '@douyinfe/semi-ui/lib/es/radio'; 3 | import React, { useMemo } from 'react'; 4 | 5 | interface BoolOptionItem extends Omit { 6 | value: boolean; 7 | } 8 | 9 | interface BoolRadioGroupProps extends Omit { 10 | value?: boolean; 11 | defaultValue?: boolean; 12 | onChange?: (v: boolean) => void; 13 | options?: BoolOptionItem[]; 14 | } 15 | 16 | const BoolRadioGroup = (props: BoolRadioGroupProps) => { 17 | const { value: valueProp, defaultValue: defaultValueProp, onChange: onChangeProp, options: optionsProp } = props; 18 | 19 | const options = useMemo(() => optionsProp?.map((x) => ({ ...x, value: x.value ? 'y' : 'n' })), [optionsProp]); 20 | 21 | const defaultValue = defaultValueProp ? 'y' : 'n'; 22 | // eslint-disable-next-line no-nested-ternary 23 | const value = typeof valueProp === 'boolean' ? valueProp ? 'y' : 'n' : undefined; 24 | 25 | const onChange = (e: RadioChangeEvent) => onChangeProp?.(e.target.value === 'y'); 26 | 27 | return ; 28 | }; 29 | 30 | export const BoolRadioGroupField = withField(BoolRadioGroup); 31 | export default BoolRadioGroup; 32 | -------------------------------------------------------------------------------- /src/pages/options/index.tsx: -------------------------------------------------------------------------------- 1 | import { Nav } from '@douyinfe/semi-ui'; 2 | import React, { useCallback, useEffect, useRef, useState } from 'react'; 3 | import { css } from '@emotion/css'; 4 | import { IconFolderOpen, IconHelpCircle, IconMenu, IconSetting } from '@douyinfe/semi-icons'; 5 | import { useGetState, useResponsive } from 'ahooks'; 6 | import { convertToRule } from '@/share/core/rule-utils'; 7 | import { t } from '@/share/core/utils'; 8 | import { prefs } from '@/share/core/prefs'; 9 | import type { Rule } from '@/share/core/types'; 10 | import SemiLocale from '@/share/components/semi-locale'; 11 | import isDarkMode from '@/share/pages/is-dark-mode'; 12 | import GroupSelect from './sections/group-select'; 13 | import ImportAndExportSection from './sections/import-and-export'; 14 | import OptionsSection from './sections/options'; 15 | import RulesSection from './sections/rules'; 16 | import Edit from './sections/rules/edit'; 17 | import type { OnSelectedData } from '@douyinfe/semi-ui/lib/es/navigation'; 18 | 19 | const Options = () => { 20 | const [editShow, setEditShow] = useState(false); 21 | const [editRule, setEditRule] = useState(); 22 | const [navCollapse, setNavCollapse, getNavCollapse] = useGetState(false); 23 | const [active, setActive, getActive] = useGetState('rules'); 24 | // 保存切换到帮助前是否为展开状态 25 | const isCollapsedRef = useRef(true); 26 | 27 | const responsive = useResponsive(); 28 | 29 | useEffect(() => { 30 | prefs.ready(() => { 31 | if (isDarkMode()) { 32 | document.body.setAttribute('theme-mode', 'dark'); 33 | } 34 | }); 35 | }, []); 36 | 37 | const handleSwitch = useCallback((data: OnSelectedData) => { 38 | const newActive = data.itemKey as string; 39 | if (newActive && newActive !== getActive()) { 40 | if (newActive === 'help') { 41 | isCollapsedRef.current = getNavCollapse(); 42 | } 43 | if (getActive() === 'help') { 44 | setNavCollapse(isCollapsedRef.current); 45 | } else { 46 | setNavCollapse(getNavCollapse() || newActive === 'help'); 47 | } 48 | setActive(newActive); 49 | window.scrollTo(0, 0); 50 | } 51 | }, []); 52 | 53 | const handleEditClose = useCallback(() => { 54 | setEditShow(false); 55 | setEditRule(undefined); 56 | }, []); 57 | 58 | const handleEdit = useCallback((rule?: Rule) => { 59 | setEditShow(true); 60 | setEditRule(rule ? convertToRule(rule) : undefined); 61 | }, []); 62 | 63 | useEffect(() => { 64 | // 小屏幕主动收起侧边栏 65 | if (!responsive.lg && getNavCollapse()) { 66 | setNavCollapse(false); 67 | } 68 | }, [responsive.lg]); 69 | 70 | return ( 71 | 72 |
.navbar { 78 | /* width: 240px; */ 79 | flex-grow: 0; 80 | flex-shrink: 0; 81 | height: 100vh; 82 | } 83 | 84 | > .main-content { 85 | flex-grow: 1; 86 | flex-shrink: 1; 87 | height: 100vh; 88 | overflow: auto; 89 | box-sizing: border-box; 90 | padding: 16px; 91 | background-color: var(--semi-color-fill-0); 92 | 93 | > .in-visible { 94 | display: none; 95 | } 96 | 97 | > section { 98 | > .semi-card { 99 | margin-bottom: 16px; 100 | } 101 | } 102 | } 103 | `} 104 | > 105 |