├── .cz-config.js ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── submit.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierrc.cjs ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── RECORD.md ├── __test__.html ├── assets ├── icon.png └── icon128x128.png ├── commitlint.config.js ├── index.d.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── __newtab.tsx ├── assets │ └── copy.svg ├── background │ ├── index.ts │ └── inject-script.ts ├── components │ ├── Footer.tsx │ └── Header.tsx ├── contents │ ├── event-open.ts │ ├── event │ │ └── index.ts │ ├── hooks │ │ ├── useGitHub.ts │ │ └── useScrollToTop.ts │ ├── index.tsx │ ├── link-go.ts │ ├── plasmo.ts │ ├── register │ │ └── keydownListen.ts │ ├── translate.ts │ ├── utils │ │ └── is.ts │ └── views │ │ └── Github │ │ ├── components │ │ ├── OnlineEditBtn │ │ │ └── index.tsx │ │ └── ScrollToTopBtn │ │ │ ├── arrow.tsx │ │ │ ├── index.css │ │ │ └── index.tsx │ │ └── index.tsx ├── enum │ └── commandEnum.ts ├── options │ └── index.tsx ├── popup │ ├── function-page │ │ └── index.tsx │ ├── index.tsx │ ├── tool-page │ │ └── index.tsx │ └── translation-page │ │ └── index.tsx ├── script │ ├── set-no-translate-node.ts │ └── translator-api.ts └── style │ └── index.css ├── tailwind.config.js └── tsconfig.json /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feat', name: 'feat: 一个新的特性' }, 4 | { value: 'fix', name: 'fix: 修复一个Bug' }, 5 | { value: 'docs', name: 'docs: 变更的只有文档' }, 6 | { value: 'style', name: 'style: 代码风格,格式修复' }, 7 | { 8 | value: 'refactor', 9 | name: 'refactor: 代码重构,(既不增加feature,也不是修复bug)', 10 | }, 11 | { value: 'perf', name: 'perf: 代码优化,改善性能' }, 12 | { value: 'test', name: 'test: 增加测试' }, 13 | { value: 'chore', name: 'chore: 变更构建流程或辅助工具' }, 14 | { value: 'revert', name: 'revert: 代码回退' }, 15 | { value: 'init', name: 'init: 项目初始化' }, 16 | { value: 'build', name: 'build: 变更项目构建或外部依赖' }, 17 | { value: 'wip', name: 'wip: 进行中的工作' }, 18 | ], 19 | 20 | scopes: [], 21 | allowTicketNumber: false, 22 | isTicketNumberRequired: false, 23 | ticketNumberPrefix: 'TICKET-', 24 | ticketNumberRegExp: '\\d{1,5}', 25 | 26 | // override the messages, defaults are as follows 27 | messages: { 28 | type: '选择一种你的提交类型:', 29 | scope: '选择一个scope (可选):', 30 | // used if allowCustomScopes is true 31 | customScope: 'Denote the SCOPE of this change:', 32 | subject: '简短说明(最多40个字):', 33 | body: '长说明,使用"|"换行(可选):\n', 34 | breaking: '非兼容性说明 (可选):\n', 35 | footer: '关联关闭的issue,例如:#12, #34(可选):\n', 36 | confirmCommit: '确定提交说明?', 37 | }, 38 | 39 | allowCustomScopes: true, 40 | allowBreakingChanges: ['feat', 'fix'], 41 | // skip any questions you want 42 | skipQuestions: ['scope', 'body', 'breaking'], 43 | // limit subject length 44 | subjectLimit: 100, 45 | // breaklineChar: '|', // It is supported for fields body and footer. 46 | // footerPrefix : 'ISSUES CLOSED:' 47 | // askForBreakingChangeFirst : true, // default is false 48 | 49 | // // it needs to match the value for field type. Eg.: 'fix' 50 | // scopeOverrides: { 51 | // fix: [{ name: 'merge' }, { name: 'style' }], 52 | // }, 53 | } 54 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # eslint ignore list 2 | .output 3 | .plasmo 4 | .vscode 5 | 6 | build/* 7 | node_modules 8 | package-lock.json 9 | yarn.lock 10 | .env 11 | .eslintrc.js 12 | postcss.config.js 13 | tailwind.config.js 14 | commitlint.config.js 15 | assets/* 16 | 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | 'airbnb-base', 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | ], 13 | overrides: [], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | ecmaVersion: 'latest', 17 | sourceType: 'module', 18 | project: './tsconfig.json', 19 | }, 20 | plugins: ['react', '@typescript-eslint', 'import'], 21 | settings: { 22 | // 处理 Unable to resolve path to module ‘xxx 23 | // 'import/resolver': { 24 | // // 默认使用根目录 tsconfig.json 25 | // typescript: { 26 | // // 从 @types 读取类型定义 27 | // alwaysTryTypes: true, 28 | // directory: './tsconfig.json', 29 | // }, 30 | // }, 31 | }, 32 | rules: { 33 | 'no-console': 'off', // 允许使用 console 34 | 'no-alert': 'off', // 允许使用 alert 35 | 'react/react-in-jsx-scope': 'off', // 关闭react必须引入的规则 36 | '@typescript-eslint/no-explicit-any': 'off', // 关闭any类型的规则 37 | '@typescript-eslint/no-unused-vars': 'off', // 关闭未使用变量的规则 38 | 'no-unsafe-optional-chaining': 'off', // 关闭不可使用可选链的规则 39 | 'import/extensions': 'off', // 关闭文件后缀名的规则 40 | 'import/no-unresolved': 'off', // 关闭文件路径的规则 41 | 'no-use-before-define': 'off', // 关闭变量使用前定义的规则 42 | 'import/prefer-default-export': 'off', // 关闭默认导出的规则 43 | 'no-unused-expressions': 'off', // 关闭未使用表达式的规则 (可以使用 && || 等) 44 | 'prefer-destructuring': 'off', // 关闭只能通过解构赋值的规则 45 | 'no-shadow': 'off', // TODO 关闭变量覆盖的规则,开启会导致 enum 声明的类型,抛出错误的提示 (暂时关闭) ❌ 46 | '@typescript-eslint/no-shadow': ['error'], // 开启变量覆盖的规则,上面关闭规则的补丁 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.2 18 | with: 19 | version: 7.11.0 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build and zip extension artifact 27 | run: pnpm build -- --zip 28 | - name: Browser Platform Publish 29 | uses: PlasmoHQ/bpp@v2 30 | with: 31 | keys: ${{ secrets.SUBMIT_KEYS }} 32 | artifact: build/chrome-mv3-prod.zip 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | #cache 12 | .turbo 13 | 14 | # misc 15 | .DS_Store 16 | *.pem 17 | 18 | # debug 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | .pnpm-debug.log* 23 | 24 | # local env files 25 | .env* 26 | 27 | out/ 28 | build/ 29 | dist/ 30 | 31 | # plasmo - https://www.plasmo.com 32 | .plasmo 33 | 34 | # bpp - http://bpp.browser.market/ 35 | keys.json 36 | 37 | # typescript 38 | .tsbuildinfo 39 | 40 | # ======== 41 | assets/google 42 | 43 | # Editor directories and files 44 | .idea 45 | .vscode 46 | *.suo 47 | *.ntvs* 48 | *.njsproj 49 | *.sln 50 | *.sw? -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | sharp_binary_host = "https://npmmirror.com/mirrors/sharp" 2 | sharp_libvips_binary_host = "https://npmmirror.com/mirrors/sharp-libvips" 3 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | module.exports = { 5 | plugins: [require.resolve("@plasmohq/prettier-plugin-sort-imports"), require.resolve("prettier-plugin-tailwindcss")], 6 | tailwindConfig: './tailwind.config.js', 7 | importOrder: ["^@plasmohq/(.*)$", "^~(.*)$", "^[./]"], 8 | importOrderSeparation: true, 9 | importOrderSortSpecifiers: true, 10 | 11 | tabWidth: 2, // 空格数 12 | useTabs: false, // 是否开启tab 13 | printWidth: 100, // 换行的宽度 14 | semi: false, // 是否在语句末尾打印分号 15 | singleQuote: true, // 是否使用单引号 16 | quoteProps: "as-needed", // 对象的key仅在需要时用引号 as-needed|consistent|preserve 17 | trailingComma: "all", // 多行时尽可能打印尾随逗号 |all|es5|none 18 | rangeStart: 0, // 每个文件格式化的范围是文件的全部内容 19 | bracketSpacing: true, // 对象文字中的括号之间打印空格 20 | jsxSingleQuote: true, // 在JSX中是否使用单引号 21 | bracketSameLine: false, // 将HTML元素的闭括号放在最后一行的末尾(不适用于自闭合元素)。 22 | arrowParens: "always", // 箭头函数,只有一个参数的时候,也需要括号 always|avoid 23 | htmlWhitespaceSensitivity: "ignore", // html中换行规则 css|strict|ignore,strict会强制在标签周围添加空格 24 | vueIndentScriptAndStyle: false, // vue中script与style里的第一条语句是否空格 25 | singleAttributePerLine: false, // 每行强制单个属性 26 | endOfLine: "lf", // 换行符 27 | proseWrap: "never", // 当超出print width时就折行 always|never|preserve .md文件? 28 | embeddedLanguageFormatting: "auto" 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "antd", 4 | "chromeos", 5 | "plasmo", 6 | "srclangs", 7 | "youdao" 8 | ], 9 | "editor.quickSuggestions": { 10 | "strings": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 0.0.5 (2022-12-04) 6 | 7 | ### Features 8 | 9 | - 添加行内和段落对比翻译 ([689c702](https://github.com/wangrongding/super-extensions/commit/689c7024adc8165422cf061896dd4d59653ad05b)) 10 | - 添加一些新的功能 ([63adb8a](https://github.com/wangrongding/super-extensions/commit/63adb8ab31b0897332553ffecc56d291de4d8e6b)) 11 | - 添加优化翻译和 github 在线编辑功能 ([3d17e53](https://github.com/wangrongding/super-extensions/commit/3d17e5371b5c40081739bba3a6ce6037229eb96c)) 12 | - 添加右键菜单和快捷键 ([aee3403](https://github.com/wangrongding/super-extensions/commit/aee3403b9f20f50b5a6b396891c307368990bfc4)) 13 | - 完成基本功能 ([10950cb](https://github.com/wangrongding/super-extensions/commit/10950cbead22766130c548e792bd61b354b66754)) 14 | - 完善 github 在线编辑功能 ([222cc60](https://github.com/wangrongding/super-extensions/commit/222cc60819229d6a17c294cd80206d86d29ec275)) 15 | - 完善 pupup 页面 ([fc099f0](https://github.com/wangrongding/super-extensions/commit/fc099f03124430228dd452f4c6ed6fa2d2cd5584)) 16 | - 优化 popup 弹窗样式 ([ff9faf9](https://github.com/wangrongding/super-extensions/commit/ff9faf95b6ef39eb613314b6c3220e08009d2110)) 17 | - 优化翻译逻辑,添加新页面 ([bf6db32](https://github.com/wangrongding/super-extensions/commit/bf6db32054eab0f10d955c0fa9e293ca9b770665)) 18 | - 优化了页面,添加了一些新的翻译功能 ([3fa3b26](https://github.com/wangrongding/super-extensions/commit/3fa3b26b3c6c627cdc6dcae79a50d240e8eff870)) 19 | 20 | ### Bug Fixes 21 | 22 | - 调整空字符的翻译 ([2f6294c](https://github.com/wangrongding/super-extensions/commit/2f6294cb3de553ae38a644484d8349b58c88401b)) 23 | - 取消一些事件的覆盖 ([6b699d9](https://github.com/wangrongding/super-extensions/commit/6b699d954d071dc7506250346aeaa2a61a830e04)) 24 | - 添加翻译服务连接失败的页面反馈 ([59cfc3f](https://github.com/wangrongding/super-extensions/commit/59cfc3f8913f42a808b88370ecc45a81880acb1a)) 25 | - 修复 github 模块中的功能 ([23e33d2](https://github.com/wangrongding/super-extensions/commit/23e33d2b39dc2f79c05ee6369884ebe74eb2a16c)) 26 | - 修复『解除用户拷贝的事件限制』的功能导致部分网页白屏的情况 ([655dba3](https://github.com/wangrongding/super-extensions/commit/655dba3da0108820596454ad3cc3d0270d146e6a)) 27 | - 修复对段落标签解析方式,针对内联元素翻译解析合并到父级 ([4200d11](https://github.com/wangrongding/super-extensions/commit/4200d11ffa540e1dc8f5f62a527691e890e34bf5)) 28 | - 修复样式问题 ([093eb09](https://github.com/wangrongding/super-extensions/commit/093eb095d98469b868428eef9f42dccc911fed46)) 29 | - 修复优化翻译的逻辑 ([3bb8d29](https://github.com/wangrongding/super-extensions/commit/3bb8d299b9c5a3f72cbce18f6370526519a90b40)) 30 | - 修改重复翻译的问题 ([aa6d3b8](https://github.com/wangrongding/super-extensions/commit/aa6d3b887127356a462b74e72344b6deb9ad78a7)) 31 | - 修改 chrome runtime 事件控制逻辑及测试页面 ([e22df78](https://github.com/wangrongding/super-extensions/commit/e22df78d4e9a38a35c7b231e3888d21c4d137ad7)) 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DevGo 2 | 3 | 这个项目包含一些提升效率的工具。用来在浏览器中帮助我们完成一些繁杂或者重复的工作,或者提升我们使用浏览器的阅读体验。 4 | 5 | 下载插件:👉🏻 [Chrome 应用商店的链接](https://chrome.google.com/webstore/detail/devgo/kcofdbjhicjdbmldlcffcijglkifnnjn) 6 | 7 | 使用文档:👉🏻 [DevGo 使用文档](https://fedtop.github.io/dev-go-docs) 8 | 9 | ## 贡献者们 10 | 11 | [Contributors](https://github.com/wangrongding/dev-go/graphs/contributors) 是 DevGo 的未来。 12 | 13 | 14 | 15 | 16 | 17 | ## 所有功能 18 | 19 | ### Done 20 | 21 | - [x] [翻译](#翻译) 22 | - [x] [优化浏览器中自带的翻译页面](#优化浏览器中自带的翻译页面) 23 | - [x] [github 添加在线编辑按钮](#github添加在线编辑按钮) 24 | - [x] [去除外链跳转的提示](#去除外链跳转的提示) 25 | - [x] [去除外链跳转的提示](#去除外链跳转的提示) 26 | - [x] [清除用户事件的限制](#清除用户事件的限制) 27 | 28 | ### Todo 29 | 30 | - [ ] P2P 传输文件 -(wip) 31 | - [ ] 保存页面为 PDF 32 | - [ ] 保存页面为 MarkDown 33 | - [ ] 浏览器代理 34 | - [ ] 图片处理工具 35 | - [ ] Json 格式化 36 | - [ ] 视频解析 37 | - [ ] Mock 数据 38 | - [ ] 番茄钟 39 | - [ ] 代办事项提醒 40 | - [ ] github 回到顶部 41 | 42 | 欢迎提 Issue 和 PR。共同完善这个插件集合。 43 | 44 | ## 功能介绍 45 | 46 | ### 翻译 47 | 48 | 查单词短句,可以通过快捷键 `Alt+Q`/`Option+Q` 或者点击插件图标打开该窗口。 49 | 50 | 51 | 52 | 翻译页面-通过中英文对照的形式阅读,在快速阅读页面的同时,也很好的解决了目前市面上翻译软件对专业词汇翻译不准确的问题。 53 | 54 | 可以通过快捷键 `Ctrl+Shift+E`/`Command+Shift+E` 快速翻译当前页面。 55 | 56 | 57 | 58 | 划词翻译、右键菜单翻译,这两天加上 59 | 60 | ### 优化浏览器中自带的翻译页面 61 | 62 | 所有站点过滤掉代码块等不需要翻译的元素,为 github 定制化过滤了不需要翻译的元素 63 | 64 | 优化浏览器中自带的翻译,标记了一些不该被翻译的元素(比如代码块,github 中一些导航),让浏览器自带的翻译在翻译页面时跳过被标记的标签。 65 | 66 | 67 | 68 | ### github 添加在线编辑按钮 69 | 70 | github 添加在线编辑按钮并且可以使用快捷键 ","直接进入。 方便快速使用 `1s` 查看代码,(为什么?因为 1s 比 通过 github 页面中快捷键"句号"调出的 github.dev 要快)。 71 | 72 | 73 | 74 | ### 去除外链跳转的提示 75 | 76 | 每次在知乎,掘金,简书...中打开外链,都有一个跳转提示,需要手动点击确定才能跳转,很难受,这里捕获后直接重定向到目标链接。 77 | 78 | 79 | 80 | ### 清除用户事件的限制 81 | 82 | 在一些网站中 copy 文本后常常后面附带一些版权信息等,很烦,清除了网站对用户行为进行了限制(比如右击菜单,选择文本,拷贝,剪切,键盘鼠标事件等) 83 | 84 | 85 | 86 | ## 开发 87 | 88 | ### 运行 89 | 90 | 首先,运行服务: 91 | 92 | ```bash 93 | npm run dev 94 | # or 95 | pnpm dev 96 | ``` 97 | 98 | 打开浏览器并加载适当的开发构建。例如,如果你正在为 chrome 浏览器开发,使用 manifest v3,使用:`build/chrome-mv3-dev`。 99 | 100 | ![](https://assets.fedtop.com/picbed/202210270156535.png) 101 | 102 | 你可以通过修改 `popup.tsx` 开始编辑弹出窗口。它应该在您进行更改时自动更新。要添加选项页面,只需添加一个 `options.tsx` 文件到项目的根,并导出一个默认的 react 组件。同样,要添加内容页,请添加 `content.ts` 文件到项目根目录,导入一些模块并执行一些逻辑,然后在浏览器上重新加载扩展。 103 | 104 | 进一步指导 👉🏻[plasmo docs](https://docs.plasmo.com/) 105 | 106 | ### 打包成 crx 文件 107 | 108 | 运行以下: 109 | 110 | ```sh 111 | npm run build 112 | # or 113 | pnpm build 114 | ``` 115 | 116 | 这将为您的扩展创建一个生产包,准备压缩并发布到商店。 117 | 118 | ### 提交到网上商店 119 | 120 | 部署 plasmo 扩展最简单的方法是使用内置的[bpp](https://bpp.browser.market) GitHub action 。但是,在使用此操作之前,请确保构建您的扩展并将第一个版本上传到存储中以建立基本凭证。然后,只需遵循 [此设置说明](https://docs.plasmo.com/workflows/submit),您就可以自动提交了! 121 | 122 | ## 赞赏 123 | 124 | 只需要点一个 Star⭐️ 支持我们~ 125 | 126 | 🌸Let's enjoy it! 127 | -------------------------------------------------------------------------------- /RECORD.md: -------------------------------------------------------------------------------- 1 | # 基于 Plasmo 搭建浏览器插件 2 | 3 | ## 创建项目 4 | 5 | ```bash 6 | pnpm create plasmo --with-src 7 | # or 8 | npx init-plasmo --with-src 9 | ``` 10 | 11 | ## 右键菜单 12 | 13 | 14 | 15 | ## 快捷键 16 | 17 | 18 | 19 | ## content-scripts 20 | 21 | 是 Chrome 插件中向页面注入脚本的一种形式。 content-scripts 能访问 DOM,但 不能访问绝大部分 `chrome.xxx.api`。 22 | 23 | 在 content-script 中的 CSS,用图片的方式 24 | 25 | 1 在 Mainfast 中,将图片的路径设置在访问的插件资源列表中,例如 26 | 27 | ``` 28 | "web_accessible_resources": ["images/*"], 29 | ``` 30 | 31 | 2 CSS 中用 `url(chrome-extension://__MSG_@@extension_id__/图片的路径)` 来写。如: 32 | 33 | ``` 34 | background-image: url(chrome-extension://__MSG_@@extension_id__/images/xx.png) 35 | ``` 36 | 37 | 当然,也可用 base64 的方式来做。 38 | 39 | ## 动态注入资源 40 | 41 | ### 注入 JS 42 | 43 | bacground 和 popup 的 js 不能直接访问 DOM。当通过在 bacground 或 popup 中 注入 js,动态注入的 js 可以访问 DOM。 44 | 45 | ``` 46 | // 注入文件 47 | chrome.tabs.executeScript(null, { 48 | file: "路径" 49 | }) 50 | 51 | // 注入代码 52 | chrome.tabs.executeScript(null, { 53 | code: 'document.body.style.backgroundColor="red"' 54 | }) 55 | ``` 56 | 57 | 需要在 `manifest.json` 中配置对应的配置: 58 | 59 | ``` 60 | "permissions": [ "tabs", "activeTab", "" ], 61 | ``` 62 | 63 | `""` 也可以改成, `"http://*/*", "https://*/*"` 64 | 65 | ## 事件 66 | 67 | 图标被点击 68 | 69 | ``` 70 | chrome.browserAction.onClicked.addListener(function() { 71 | 72 | }) 73 | ``` 74 | 75 | ## Popup 和 background 的通信 76 | 77 | popup 可以直接调用 background 中的 JS 方法,也可以直接访问 background 的 DOM: 78 | 79 | ``` 80 | // background.js 81 | function test() 82 | { 83 | alert('我是background!'); 84 | } 85 | 86 | // popup.js 87 | var bg = chrome.extension.getBackgroundPage(); 88 | bg.test(); // 访问bg的函数 89 | alert(bg.document.body.innerHTML) 90 | ``` 91 | 92 | background 调用 popup 的代码如下(前提是 popup 已经打开) 93 | 94 | ``` 95 | var views = chrome.extension.getViews({type:'popup'}); 96 | if(views.length > 0) { 97 | console.log(views[0].location.href); 98 | } 99 | ``` 100 | 101 | ## popup 和 background 向 content 发信息 102 | 103 | background.js 或者 popup.js 104 | 105 | ``` 106 | function sendMessageToContentScript(message, callback) 107 | { 108 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) 109 | { 110 | chrome.tabs.sendMessage(tabs[0].id, message, function(response) 111 | { 112 | if(callback) callback(response); 113 | }); 114 | }); 115 | } 116 | 117 | // cmd 是类型 118 | sendMessageToContentScript({cmd:'test', value:'你好,我是popup!'}, function(response) 119 | { 120 | console.log('来自content的回复:'+response); 121 | }) 122 | ``` 123 | 124 | content-script.js 接发信息 125 | 126 | ``` 127 | // 接收信息 128 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) 129 | { 130 | // console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension") 131 | // 根据不同类别做不同处理 132 | if(request.cmd == 'test') alert(request.value); 133 | sendResponse('我收到了你的消息!') 134 | }) 135 | 136 | // 发送信息 137 | chrome.runtime.sendMessage({ 138 | method: "xxx", // 调用函数 139 | cmd: 'xxx', // 执行命令。接受 140 | }, { // 发送的信息 141 | 142 | }, function(re) { 143 | sender() 144 | }) 145 | ``` 146 | 147 | 要注意的是:**chrome.runtime.sendMessage 的回调函数默认是同步的,而且超时后直接执行,返回 undefined,如果要异步执行,必须在处理函数中 return true。** 148 | 149 | ## 本地存储 150 | 151 | 推荐用`chrome.storage`而不是普通的`localStorage`。最重要的 2 点区别是: 152 | 153 | - chrome.storage 是针对插件全局的,即使你在 background 中保存的数据,在 content-script 也能获取到; 154 | - chrome.storage.sync 可以跟随当前登录用户自动同步,这台电脑修改的设置会自动同步到其它电脑,很方便,如果没有登录或者未联网则先保存到本地,等登录了再同步至网络; 155 | 156 | 需要声明 storage 权限,有 chrome.storage.sync 和 chrome.storage.local2 种方式可供选择,使用示例如下: 157 | 158 | ``` 159 | // 读取数据,第一个参数是指定要读取的key以及设置默认值 160 | chrome.storage.sync.get(['color', 'age'], function(items) { 161 | console.log(items.color, items.age); 162 | }); 163 | // 保存数据 164 | chrome.storage.sync.set({ color: 'blue', age: 18 }, function() { 165 | console.log('保存成功!'); 166 | }) 167 | ``` 168 | 169 | ## 发布到 Chrome 商店 170 | 171 | 在插件管理页有一个打包按钮,然后会生成 .crx 文件。npm 上也有打包的包。 172 | 173 | 首先,你需要一个 Chrome 开发者账号。如果你还没有,请访问[这里](https://chrome.google.com/webstore/developer/dashboard)。需要缴纳 5$ 的费用。而且这里很坑的是,我每一次填写信息绑定卡片的时候都会扣 1$的费用,选择香港,转大陆,信用卡,visa 等几种支持的。 174 | 175 | 然后,第一次上传扩展程序时,你需要手动把构建好的文件夹压缩成 zip 文件,然后上传到 Chrome 商店。 176 | 177 | ## chrome 插件开发本地调试 178 | 179 | - 调试 popup.html 页面:右键点击浏览器右上角插件图标,然后选择点击"审查弹出内容"。 180 | - 调试 background.html 或 background.js:在浏览器的扩展程序管理页面找到自己加载的扩展程序,然后点击"Service Worker"(MV2 则是"背景页")。 181 | - 调试 content_scripts.js:在注入的页面按 F12,和正常页面一样调试。 182 | - 调试 options.html 页面:右键点击浏览器右上角插件图标,然后选择点击"选项",打开插件的选项页,然后按 F12,和正常页面一样调试。 183 | 184 | ## chrome 插件的组成主要由以下部分组成: 185 | 186 | - manifest.json (配置文件,目前最新是 v3 版本) 187 | - popup (点击插件图标弹出的页面) 188 | - content script (插入到目标页面中执行的 JS) 189 | - background script (在浏览器后台 Service Workers 中运行的程序) 190 | - options (选项页面,可有可无) 191 | 192 | ## chrome 插件的能力 193 | 194 | 除了支持传统的一切 web API、JavaScript API 以外,chrome 插件额外支持以下 API(chrome.xxx): 195 | 196 | - bullet 浏览器窗口(chrome.window) 197 | - tab 标签(chrome.tabs) 198 | - 书签(chrome.bookmark) 199 | - 历史(chrome.history) 200 | - 下载(chrome.download) 201 | - 网络请求(chrome.webRequest) 202 | - 自定义右键菜单(chrome.contextMenus) 203 | - 开发者工具扩展(chrome.devtool) 204 | - 插件管理(chrome.extension) 205 | 206 | ## manifest v3 的主要特性 207 | 208 | - Service Workers 取代 background pages,使用 Service Workers,可对资源进行缓存,从而实现离线访问。 209 | - 网络请求调整,新增了一个 declarativeNetRequestAPI,允许插件修改及阻断网络请求。 210 | - 远程资源访问限制,禁止访问外部的 JavaScript 及 Wasm 文件,图片、音视频文件不受影响。 211 | - Promises 使用,可以愉快地使用 promise 了,包括 async/await。 212 | - manifest 文件的部分配置和 chrome API 做了部分调整。 213 | 214 | ## 注意事项 215 | 216 | - 在 MV3 中,由于 Service Workers 的机制,background 页中不支持使用 XMLHttpRequest,建议使用 fetch() 217 | - popup 可以直接调用 background 中的 JS 方法,也可以直接访问 background 的 DOM。 218 | - 在对 popup 页面审查元素的时候 popup 会被强制打开无法关闭,只有控制台关闭了才可以关闭 popup,原因很简单:如果 popup 关闭了控制台就没用了。 219 | - chrome.tabs.connect 或 chrome.tabs.sendMessage 不能用于与选项页面通信,选项页面可以使用 chrome.runtime.connect 和 chrome.runtime.sendMessage 与 background 页通信。 220 | - content script 文件中可以获取 web 页面的 DOM 并修改,content script 和原始页面共享 DOM,但是不共享 JS,JS 是相互隔离的,可以通过 window.postMessage 和 window.addEventListener 来实现二者消息通讯。 221 | - content script 不能发送跨域请求。???? 222 | - content script 文件中只能使用下面列出的 API: 223 | 224 | ```ts 225 | chrome.extension 226 | chrome.i18n 227 | chrome.runtime 228 | chrome.storage 229 | ``` 230 | 231 | ## Manifest V3 232 | 233 | - [升级到 V3 遇到的一些问题:](https://blog.csdn.net/lxm1353723767/article/details/127706101) 234 | - 235 | 236 | ``` 237 | { 238 | // 清单文件的版本,这个必须写,而且必须是2 239 | "manifest_version": 2, 240 | // 插件的名称 241 | "name": "demo", 242 | // 插件的版本 243 | "version": "1.0.0", 244 | // 插件描述 245 | "description": "简单的Chrome扩展demo", 246 | // 图标,一般偷懒全部用一个尺寸的也没问题 247 | "icons": 248 | { 249 | "16": "img/icon.png", 250 | "48": "img/icon.png", 251 | "128": "img/icon.png" 252 | }, 253 | // 会一直常驻的后台JS或后台页面 254 | "background": 255 | { 256 | // 2种指定方式,如果指定JS,那么会自动生成一个背景页 257 | "page": "background.html" 258 | //"scripts": ["js/background.js"] 259 | }, 260 | // 浏览器右上角图标设置,browser_action、page_action、app必须三选一 261 | "browser_action": 262 | { 263 | "default_icon": "img/icon.png", 264 | // 图标悬停时的标题,可选 265 | "default_title": "这是一个示例Chrome插件", 266 | "default_popup": "popup.html" 267 | }, 268 | // 当某些特定页面打开才显示的图标 269 | /*"page_action": 270 | { 271 | "default_icon": "img/icon.png", 272 | "default_title": "我是pageAction", 273 | "default_popup": "popup.html" 274 | },*/ 275 | // 需要直接注入页面的JS 276 | "content_scripts": 277 | [ 278 | { 279 | //"matches": ["http://*/*", "https://*/*"], 280 | // "" 表示匹配所有地址 281 | "matches": [""], 282 | // 多个JS按顺序注入 283 | "js": ["js/jquery-1.8.3.js", "js/content-script.js"], 284 | // JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式 285 | "css": ["css/custom.css"], 286 | // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle 287 | "run_at": "document_start" 288 | }, 289 | // 这里仅仅是为了演示content-script可以配置多个规则 290 | { 291 | "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"], 292 | "js": ["js/show-image-content-size.js"] 293 | } 294 | ], 295 | // 权限申请 296 | "permissions": 297 | [ 298 | "contextMenus", // 右键菜单 299 | "tabs", // 标签 300 | "notifications", // 通知 301 | "webRequest", // web请求 302 | "webRequestBlocking", 303 | "storage", // 插件本地存储 304 | "http://*/*", // 可以通过executeScript或者insertCSS访问的网站 305 | "https://*/*" // 可以通过executeScript或者insertCSS访问的网站 306 | ], 307 | // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的 308 | "web_accessible_resources": ["js/inject.js"], 309 | // 插件主页,这个很重要,不要浪费了这个免费广告位 310 | "homepage_url": "https://www.baidu.com", 311 | // 覆盖浏览器默认页面 312 | "chrome_url_overrides": 313 | { 314 | // 覆盖浏览器默认的新标签页 315 | "newtab": "newtab.html" 316 | }, 317 | // Chrome40以前的插件配置页写法 318 | "options_page": "options.html", 319 | // Chrome40以后的插件配置页写法,如果2个都写,新版Chrome只认后面这一个 320 | "options_ui": 321 | { 322 | "page": "options.html", 323 | // 添加一些默认的样式,推荐使用 324 | "chrome_style": true 325 | }, 326 | // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字 327 | "omnibox": { "keyword" : "go" }, 328 | // 默认语言 329 | "default_locale": "zh_CN", 330 | // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件 331 | "devtools_page": "devtools.html" 332 | } 333 | ``` 334 | 335 | ## 相关校验 336 | 337 | ### eslint 338 | 339 | 初始化 eslint 配置文件 340 | 341 | ```sh 342 | npx eslint --init 343 | ``` 344 | 345 | ```sh 346 | pn add eslint-config-airbnb-base eslint-plugin-import eslint-config-prettier -D 347 | ``` 348 | 349 | ![](https://assets.fedtop.com/picbed/202212041123165.png) 350 | 351 | ## 开发相关链接 352 | 353 | - [Plasmo 文档](https://www.plasmo.com/) 354 | - [谷歌开发文档](https://developer.chrome.com/docs/) 355 | - [Chrome 扩展开发文档](https://wizardforcel.gitbooks.io/chrome-doc/content/1.html) 356 | - [示例](https://github.com/GoogleChrome/chrome-extensions-samples) 357 | - [Chrome 插件开发教程](https://xieyufei.com/2021/11/09/Chrome-Plugin.html) 358 | - [Chrome 插件开发教程](https://blog.csdn.net/qq_34998786/article/details/121782426) 359 | - [手把手教你写扩展](https://juejin.cn/post/6844904077889912839) 360 | - [创建 Chrome 应用集](https://support.google.com/chrome/a/answer/2649489) 361 | - [Chrome 插件开发全攻略](https://github.com/sxei/chrome-plugin-demo) 362 | 363 | ### mv3 相关资料 364 | 365 | - [谷歌开发文档](https://developer.chrome.com/) 366 | - [Chrome 插件权限说明](https://developer.chrome.com/docs/extensions/mv3/declare_permissions/) 367 | 368 | ## 发布相关链接 369 | 370 | - [应用商店管理后台](https://chrome.google.com/webstore/devconsole) 371 | - [Workspace Cloud](https://workspace.google.com/) 372 | - [console.cloud.google](https://console.cloud.google.com/apis) 373 | - [Admin google](https://admin.google.com/) 374 | - [Google Cloud](https://cloud.google.com/) 375 | 376 | - [自动发布](https://www.jianshu.com/p/6c552290ccea) 377 | - https://github.com/PlasmoHQ/chrome-webstore-api/blob/main/token.md 378 | 379 | ## 一些注册教程 380 | 381 | - [注册扩展开发者](https://www.zhihu.com/column/p/27203832) 382 | - [](https://blog.csdn.net/ytlzq0228/article/details/105682567) 383 | - [土豪专用!Google 企业邮箱注册试用及详细使用教程(图文)](https://www.imhunk.com/how-to-apply-for-google-company-email-g-suite/) 384 | - [注册为 Chrome 应用商店开发者 5$](https://chrome.google.com/webstore/devconsole/register) 385 | - [创建 Play 管理中心开发者帐号 25$](https://play.google.com/console/u/0/signup) 386 | - [如何注册 Google Play 开发者账号(含收款设置)](https://juejin.cn/post/6844903829033484302) 387 | - [如何注册 Google Play 开发者账号(含收款设置)](https://juejin.cn/post/6907214824216723464) 388 | - [如何注册 visa 卡](https://www.bilibili.com/read/cv11596922) 389 | - [Google Play](https://pay.google.com/gp/w/u/0/home/signup) 390 | -------------------------------------------------------------------------------- /__test__.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 13 | 14 | 15 | 16 |

Test content

17 |

18 | our answer could be improved with additional supporting information. Please edit to add 19 | further details, such as citations or documentation, so that others can confirm that your 20 | answer is correct. You can find more information on how to write good answers in the help 21 | center. – Community Bot 22 |

23 |
24 | This is a piece of test text, which will include some tags, such as 25 | link 26 | 、 27 | strong 28 | 、 29 | em 30 | 、 31 | del 32 | 、 33 | ins 34 | 、 35 | sub 36 | 、 37 | sup 38 | 、 39 | mark 40 | 、 41 | small 42 | 、 43 | big 44 | 、 45 | u 46 | 、 47 | s 48 | 、 49 | q 50 | 、 51 | cite 52 | 、 53 | abbr 54 | 、 55 | dfn 56 | 、 57 | 58 | 、 59 | var 60 | 、 61 | samp 62 | 、 63 | kbd 64 | 、 65 | i 66 | 、 67 | b 68 | 、 69 | span 70 | The expectation is that these tags will not be understood as a node alone, but together with 71 | the parent node. 72 | 73 |
74 | 75 | But for block-level elements, it will be understood as a separate node 76 |
77 | this is a 78 | div 79 | element 80 |
81 |

82 | this is a 83 | p 84 | element 85 |

86 |

this is a h1 element

87 |
    88 |
  • this is a li element
  • 89 |
90 |
91 |
this is a dt element
92 |
this is a dd element
93 |
94 | 95 |
96 | 97 | There are also some special tags, such as 98 | br 99 | , 100 | hr 101 | should also be understood as a sign of the end of a paragraph. here is a br tag: 102 |
103 | here is a hr tag: 104 |
105 | They will both be understood as the end of a paragraph. 106 |
107 |

Test list and link

108 |
    109 |
  • 110 | You can find more information on 111 | test link 112 | how to write good answers in the help center 113 |
  • 114 |
  • 115 | You can find more information on 116 | test link 117 | how to write good answers in the help center 118 |
  • 119 |
  • 120 | You can find more information on 121 | test link 122 | how to write good answers in the help center 123 |
  • 124 |
125 |

Test code

126 |
127 |       
128 |         const test = 'test';
129 |         console.log(test);
130 |       
131 |       
132 |         const a = 123; 
133 |         function test (){
134 |           let b=333;
135 |         }
136 |       
137 |     
138 |

Test table

139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
TestTestTest
TestTestTest
155 | 156 | 157 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedtop/dev-go/86e759baaf722c9ddc262bca276a9eeedf5810f7/assets/icon.png -------------------------------------------------------------------------------- /assets/icon128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fedtop/dev-go/86e759baaf722c9ddc262bca276a9eeedf5810f7/assets/icon128x128.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['cz'] } 2 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV: 'development' | 'production' 4 | 5 | PLASMO_PUBLIC_SHIP_NAME?: string 6 | PLASMO_PUBLIC_SHIELD_FREQUENCY?: string 7 | 8 | PLASMO_PUBLIC_SITE_URL?: string 9 | } 10 | } 11 | 12 | declare module '*.svg' { 13 | const content: any 14 | export default content 15 | } 16 | 17 | interface Window { 18 | customProp: { 19 | world: string 20 | coolNumber: number 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev-go", 3 | "displayName": "DevGo", 4 | "version": "0.0.6", 5 | "description": "A super unbeatable browser extension plugin for developers.", 6 | "author": "荣顶", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build", 10 | "build:zip": "plasmo build -- --zip", 11 | "package": "plasmo package", 12 | "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,scss,less,html,json,md}\"", 13 | "eslint": "eslint \"./**/*{.js,.ts,.jsx,.tsx}\" --fix", 14 | "prepare": "husky install", 15 | "release": "standard-version", 16 | "cz": "cz" 17 | }, 18 | "lint-staged": { 19 | "*.{js,ts,jsx,tsx,css,sass,scss,less,md,json,html}": [ 20 | "prettier --write" 21 | ], 22 | "*.{ts,js,tsx,jsx}": [ 23 | "eslint" 24 | ] 25 | }, 26 | "config": { 27 | "commitizen": { 28 | "path": "node_modules/cz-customizable" 29 | } 30 | }, 31 | "dependencies": { 32 | "antd": "^5.0.3", 33 | "lodash": "^4.17.21", 34 | "react": "18.2.0", 35 | "react-dom": "18.2.0" 36 | }, 37 | "devDependencies": { 38 | "@commitlint/cli": "^17.8.1", 39 | "@commitlint/config-conventional": "^17.8.1", 40 | "@commitlint/cz-commitlint": "^17.8.1", 41 | "@plasmohq/prettier-plugin-sort-imports": "3.5.4", 42 | "@types/chrome": "0.0.198", 43 | "@types/node": "18.11.0", 44 | "@types/react": "18.0.21", 45 | "@types/react-dom": "18.0.6", 46 | "@typescript-eslint/eslint-plugin": "^5.45.0", 47 | "@typescript-eslint/parser": "^5.45.0", 48 | "autoprefixer": "^10.4.13", 49 | "commitizen": "^4.2.5", 50 | "commitlint-config-cz": "^0.13.3", 51 | "cz-customizable": "^7.0.0", 52 | "eslint": "^8.0.1", 53 | "eslint-config-airbnb-base": "^15.0.0", 54 | "eslint-config-prettier": "^8.5.0", 55 | "eslint-plugin-import": "^2.26.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "eslint-plugin-react": "^7.31.11", 58 | "husky": "^8.0.2", 59 | "lint-staged": "^13.0.4", 60 | "plasmo": "^0.84.0", 61 | "postcss": "^8.4.19", 62 | "prettier": "2.8.0", 63 | "prettier-plugin-tailwindcss": "^0.2.0", 64 | "standard-version": "^9.5.0", 65 | "tailwindcss": "^3.2.4", 66 | "typescript": "*" 67 | }, 68 | "homepage": "https://github.com/wangrongding", 69 | "manifest": { 70 | "homepage_url": "https://github.com/wangrongding", 71 | "permissions": [ 72 | "contextMenus", 73 | "scripting" 74 | ], 75 | "commands": { 76 | "_execute_action": { 77 | "suggested_key": { 78 | "default": "Alt+Q", 79 | "windows": "Alt+Q", 80 | "mac": "Alt+Q", 81 | "chromeos": "Alt+Q", 82 | "linux": "Alt+Q" 83 | } 84 | }, 85 | "inline-translate": { 86 | "suggested_key": { 87 | "default": "Ctrl+Shift+E", 88 | "windows": "Ctrl+Shift+E", 89 | "chromeos": "Ctrl+Shift+E", 90 | "linux": "Ctrl+Shift+E", 91 | "mac": "Command+Shift+E" 92 | }, 93 | "description": "inline-translate" 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/__newtab.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import './style.css' 4 | 5 | function IndexNewtab() { 6 | const [data, setData] = useState('') 7 | 8 | return ( 9 |
17 |

18 | Welcome to your Plasmo Extension! 19 |

20 | setData(e.target.value)} value={data} /> 21 |
22 | ) 23 | } 24 | 25 | export default IndexNewtab 26 | -------------------------------------------------------------------------------- /src/assets/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { googleTrans, testGoogleTrans } from '~script/translator-api' 2 | // import injectGoogleTranslate from 'raw:../../assets/google/injection.js' 3 | // console.log('😀😀', injectGoogleTranslate) // chrome-extension:///image..png 4 | 5 | // 监听 message 事件 6 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 7 | const { type, text } = message 8 | 9 | if (type === 'test') { 10 | // 测试翻译服务 11 | testGoogleTrans().then((res) => { 12 | sendResponse(res) 13 | }) 14 | } else { 15 | // 通过谷歌翻译 api 翻译文本 16 | googleTrans(text).then((res) => { 17 | res && sendResponse({ text: res }) 18 | return true 19 | }) 20 | } 21 | // 等待响应保持通道打开 22 | return true 23 | }) 24 | 25 | // 通知 contents 中的 translate.ts 翻译页面 26 | const translatePage = async (type) => { 27 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => { 28 | chrome.tabs.sendMessage(tabs[0].id, { type }) 29 | }) 30 | } 31 | 32 | // 监听右键菜单点击事件 33 | chrome.contextMenus.onClicked.addListener((info, tab) => { 34 | translatePage('translate-inline') 35 | // if (info.menuItemId === 'trans1') 36 | }) 37 | 38 | // 监听命令执行事件 39 | chrome.commands.onCommand.addListener((command) => { 40 | console.log(`Command: ${command}`) 41 | translatePage('translate-inline') 42 | }) 43 | 44 | // 创建右键菜单 45 | chrome.contextMenus.create({ 46 | id: 'inline-translate', 47 | title: '对比翻译', 48 | }) 49 | 50 | // 用户首次安装插件时执行一次,后面不会再重新执行。(除非用户重新安装插件) 51 | chrome.runtime.onInstalled.addListener(() => { 52 | console.log('onInstalled') 53 | // 打开使用帮助 54 | if (process.env.NODE_ENV !== 'development') { 55 | chrome.tabs.create({ url: 'https://github.com/wangrongding/dev-go#devgo' }) 56 | } 57 | }) 58 | 59 | // 监听tab页面加载状态,添加处理事件 60 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 61 | // 设置判断条件,页面加载完成才添加事件,否则会导致事件重复添加触发多次 62 | if (changeInfo.status === 'complete') { 63 | // console.log('🚀🚀🚀 / onUpdated', changeInfo) 64 | // chrome.scripting 65 | // .executeScript({ 66 | // target: { tabId }, 67 | // // files: ['./inject-script.js'], 68 | // // files: [injectGoogleTranslate], 69 | // files: ['https//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit'], 70 | // // https://docs.plasmo.com/browser-extension/import#raw 71 | // }) 72 | // .then(() => { 73 | // console.log('🚀🚀🚀 / inject-script') 74 | // }) 75 | // .catch((err) => console.log(err)) 76 | } 77 | }) 78 | 79 | // 当前选项卡发生变化时触发 80 | chrome.tabs.onActivated.addListener((activeInfo) => { 81 | console.log('🚀🚀🚀 / onActivated', activeInfo) 82 | }) 83 | -------------------------------------------------------------------------------- /src/background/inject-script.ts: -------------------------------------------------------------------------------- 1 | const windowChanger = (): void => { 2 | const anotherFunc = (): number => 42 3 | // Here's an example where we can reference the window object 4 | // and add a new property to it 5 | window.customProp = { 6 | world: 'from injected content script', 7 | coolNumber: anotherFunc(), 8 | // you can call other functions from the injected script 9 | // but they must be declared inside the injected function 10 | // or be present in the global scope 11 | } 12 | 13 | // Here's an example where we show you can reference the DOM 14 | // This console.log will show within the tab you injected into 15 | console.log(document.getElementsByTagName('html')) 16 | } 17 | 18 | export default windowChanger 19 | 20 | // https://www.w3schools.com/howto/howto_google_translate.asp 21 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |

4 | 使用快捷键 Alt+Q 快速切换该面板 5 | 10 | Github 🌸 11 | 12 |

13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Radio } from 'antd' 2 | 3 | interface Props { 4 | active: string 5 | setActive: (active: string) => void 6 | pages: { key: string; name: string }[] 7 | } 8 | 9 | export default function Header({ active, setActive, pages }: Props) { 10 | return ( 11 |
12 |

13 | 🤖 {process.env.PLASMO_PUBLIC_SHIP_NAME} 14 |

15 | | 16 | { 19 | setActive(e.target.value) 20 | }} 21 | buttonStyle='solid' 22 | size='small' 23 | > 24 | {pages.map((page) => ( 25 | 26 | {page.name} 27 | 28 | ))} 29 | 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/contents/event-open.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoContentScript } from 'plasmo' 2 | 3 | export const config: PlasmoContentScript = { 4 | matches: [''], 5 | run_at: 'document_end', 6 | } 7 | 8 | window.addEventListener('DOMContentLoaded', () => { 9 | eventOpen() 10 | }) 11 | 12 | const eventList = [ 13 | 'copy', 14 | 'cut', 15 | // 'contextmenu', 16 | // 'selectstart', 17 | // 'mousedown', 18 | // 'mouseup', 19 | // 'mousemove', 20 | // 'keydown', 21 | // 'keypress', 22 | // 'keyup', 23 | ] 24 | 25 | function t(e) { 26 | e.stopPropagation() 27 | e.stopImmediatePropagation && e.stopImmediatePropagation() 28 | } 29 | // 清除网站中被开发者限制的用户行为 30 | export default function eventOpen() { 31 | eventList.forEach((event) => { 32 | document.documentElement.addEventListener(event, t, { capture: !0 }) 33 | }) 34 | } 35 | 36 | document 37 | .querySelectorAll( 38 | // 不能用*,会导致部分网站无法正常copy 39 | 'div,p,span,a,ul,li,ol,h1,h2,h3,h4,article,section,header,footer,aside,nav,main,a', 40 | ) 41 | .forEach((element: HTMLElement) => { 42 | if (window.getComputedStyle(element, null).getPropertyValue('user-select')) { 43 | // TODO,+ important 会导致部分网页白屏 44 | // element.style.setProperty('user-select', 'text', 'important') 45 | element.style.setProperty('user-select', 'text') 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /src/contents/event/index.ts: -------------------------------------------------------------------------------- 1 | import { isGithubCodePage } from '../utils/is' 2 | 3 | function toGithub1s() { 4 | if (!isGithubCodePage()) { 5 | return 6 | } 7 | 8 | window.open(`${`https://github1s.com${window.location.pathname}`}`) 9 | } 10 | 11 | export { toGithub1s } 12 | -------------------------------------------------------------------------------- /src/contents/hooks/useGitHub.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react' 2 | 3 | import { isGithubCodePage } from '../utils/is' 4 | import registerKeyDownListen from '../register/keydownListen' 5 | 6 | const useGitHub = () => { 7 | useEffect(() => { 8 | registerKeyDownListen() 9 | }, []) 10 | 11 | return { 12 | isCodePage: isGithubCodePage(), 13 | } 14 | } 15 | 16 | export { useGitHub } 17 | -------------------------------------------------------------------------------- /src/contents/hooks/useScrollToTop.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { throttle } from 'lodash' 3 | 4 | const useScrollToTop = (): [boolean, () => void] => { 5 | const [isTop, setTop] = useState(true) 6 | 7 | const scrollToTop = () => { 8 | try { 9 | setTimeout(() => { 10 | window.scroll({ 11 | top: 0, 12 | left: 0, 13 | behavior: 'smooth', 14 | }) 15 | }, 0) 16 | } catch (error) { 17 | window.scrollTo(0, 0) 18 | } 19 | } 20 | 21 | useEffect(() => { 22 | const handleScroll = throttle(() => { 23 | setTop(!(window.scrollY > 0)) 24 | }, 100) 25 | 26 | window.addEventListener('scroll', handleScroll, { passive: true }) 27 | return () => window.removeEventListener('scroll', handleScroll) 28 | }, []) 29 | 30 | return [isTop, scrollToTop] 31 | } 32 | 33 | export default useScrollToTop 34 | -------------------------------------------------------------------------------- /src/contents/index.tsx: -------------------------------------------------------------------------------- 1 | export default function FunctionPage() { 2 | // TODO 使用组件库的提示 3 | // const [messageApi, contextHolder] = message.useMessage() 4 | function tip(msg: string) { 5 | alert(msg) 6 | } 7 | 8 | chrome.runtime.onMessage.addListener((message, sender, res) => { 9 | console.log('🚀🚀🚀 / message', message) 10 | const { type } = message 11 | switch (type) { 12 | case 'tip': 13 | tip(message.msg) 14 | break 15 | default: 16 | break 17 | } 18 | }) 19 | return <> 20 | } 21 | -------------------------------------------------------------------------------- /src/contents/link-go.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoContentScript } from 'plasmo' 2 | 3 | export const config: PlasmoContentScript = { 4 | matches: [''], 5 | run_at: 'document_start', 6 | } 7 | 8 | window.addEventListener('DOMContentLoaded', () => { 9 | linkGo() 10 | }) 11 | 12 | const rules = [ 13 | { 14 | name: '掘金', 15 | match: /link.juejin.cn/, 16 | sign: 'target=', 17 | }, 18 | { 19 | name: '知乎', 20 | match: /link.zhihu.com/, 21 | sign: 'target=', 22 | }, 23 | { 24 | name: '简书', 25 | match: /jianshu.com\/go-wild/, 26 | sign: 'target=', 27 | }, 28 | ] 29 | 30 | // 获取外链的链接地址,然后直接跳转,而不是点击按钮触发网站跳转 31 | export default function linkGo() { 32 | const url = document.documentURI 33 | let targetUrl = '' 34 | rules.forEach((rule) => { 35 | if (rule.match.test(url)) { 36 | targetUrl = url.split(rule.sign)[1] 37 | } 38 | }) 39 | if (targetUrl) { 40 | targetUrl = decodeURIComponent(targetUrl) 41 | window.location.replace(targetUrl) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/contents/plasmo.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoContentScript } from 'plasmo' 2 | 3 | export const config: PlasmoContentScript = { 4 | matches: ['https://www.plasmo.com/*'], 5 | } 6 | window.addEventListener('load', () => { 7 | console.log('content script loaded') 8 | 9 | document.body.style.background = 'pink' 10 | }) 11 | -------------------------------------------------------------------------------- /src/contents/register/keydownListen.ts: -------------------------------------------------------------------------------- 1 | import { keyCodeEnums } from '../../enum/commandEnum' 2 | import { toGithub1s } from '../event' 3 | 4 | const keyCodeEventMap: Record void> = { 5 | [keyCodeEnums.Comma]: toGithub1s, 6 | } 7 | 8 | const keyDownListen = () => { 9 | window.addEventListener('keydown', (e) => { 10 | const isHasSetKey = keyCodeEnums.Comma === e.code; 11 | 12 | if (!isHasSetKey) return 13 | 14 | keyCodeEventMap[e.code]() 15 | }) 16 | } 17 | 18 | export default keyDownListen 19 | -------------------------------------------------------------------------------- /src/contents/translate.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoContentScript } from 'plasmo' 2 | 3 | import { passTransClass, passTransNode, setNotranslateNode } from '~script/set-no-translate-node' 4 | 5 | export const config: PlasmoContentScript = { 6 | matches: [''], 7 | run_at: 'document_start', 8 | } 9 | 10 | interface TranslateElements { 11 | elements: NodeListOf 12 | tag: keyof HTMLElementTagNameMap 13 | } 14 | 15 | chrome.runtime.onMessage.addListener((message, sender, res) => { 16 | const { type } = message 17 | console.log('🚀🚀🚀 / type', type) 18 | switch (type) { 19 | case 'translate-inline': 20 | // 测试连接性 21 | testConnection() 22 | // 翻译所有的标签 23 | loopTransNode(document.body) 24 | break 25 | case 'translate-paragraph': 26 | // 测试连接性 27 | testConnection() 28 | paragraphTrans() 29 | break 30 | default: 31 | break 32 | } 33 | }) 34 | 35 | // 测试连接性 36 | function testConnection() { 37 | chrome.runtime.sendMessage({ type: 'test' }, (res) => { 38 | if (!res) { 39 | alert('Google翻译服务不稳定!请检查您的网络,大陆的朋友需要翻墙。') 40 | } 41 | }) 42 | } 43 | 44 | // 要过滤的标签 45 | const passTransList = [ 46 | 'html', 47 | 'head', 48 | 'meta', 49 | 'title', 50 | 'body', 51 | 'script', 52 | 'style', 53 | 'link', 54 | 'code', 55 | ].concat(passTransNode) 56 | // 要过滤的 class 名 57 | const passTransClassList = ['translated', ...passTransClass] 58 | 59 | // 过滤标签 60 | function filterTagsFn(tag): HTMLElement | null { 61 | if (tag?.nodeType === 3) return tag 62 | // 过滤掉在过滤标签中的标签 63 | if ( 64 | tag?.nodeType === 1 && 65 | !passTransList.includes(tag?.tagName?.toLowerCase()) && 66 | [...tag?.classList].every((item) => !passTransClassList.includes(item)) 67 | ) { 68 | return tag 69 | } 70 | return null 71 | } 72 | 73 | // 递归处理所有的标签 74 | function loopTransNode(element) { 75 | // 当子元素为空时,中断 76 | if (element.childNodes.length === 0) return 77 | 78 | // 获取所有的子节点 79 | const childrenList = Array.from(element?.childNodes, filterTagsFn).filter((item) => item) 80 | 81 | // 存储需要翻译的文本 82 | let translateText = '' 83 | let lastElement: HTMLElement = null 84 | 85 | // 遍历所有的子节点, 需要翻译的元素 86 | childrenList.forEach((tag) => { 87 | if (hasTransTextNode(tag)) { 88 | // 不需要翻译的元素 89 | if (!hasTranslate(tag.textContent)) return 90 | 91 | // 拼接需要翻译的文本 92 | translateText += tag.textContent 93 | lastElement = tag 94 | } else { 95 | // 进入到这里证明一个段落已经结束,开始翻译 96 | sentenceTrans(translateText, lastElement) 97 | translateText = '' 98 | lastElement = null 99 | 100 | // 递归处理非内联元素或者文本节点 101 | loopTransNode(tag) 102 | } 103 | }) 104 | 105 | // 最后一个段落的翻译 106 | sentenceTrans(translateText, lastElement) 107 | } 108 | 109 | // 判断节点是否是段落 110 | function hasTransTextNode(element: HTMLElement) { 111 | if (element == null) return false 112 | 113 | // 如果是文本节点,直接返回 114 | if (element.nodeType === 3) { 115 | return true 116 | } 117 | 118 | // 换行元素过滤掉 119 | const lineBreakList = ['BR', 'HR'] 120 | if (lineBreakList.includes(element.tagName)) { 121 | return false 122 | } 123 | 124 | // display 属性为 inline/inline-block 的元素是符合条件的 125 | const { display } = window.getComputedStyle(element) 126 | if (display === 'inline' || display === 'inline-block') { 127 | return true 128 | } 129 | 130 | return false 131 | } 132 | 133 | // 判断是否需要翻译,只检测中文 134 | function hasTranslate(str: string) { 135 | if (!str) return false 136 | 137 | return !/[\u4e00-\u9fa5]/.test(str) 138 | } 139 | 140 | // 发送翻译请求 141 | function sentenceTrans(text: string, insetBefore: HTMLElement) { 142 | if (text.trim() === '') return 143 | 144 | chrome.runtime.sendMessage({ text: text.replace(/[\r\n]/g, ''), type: 'translate' }, (res) => { 145 | insertTransResult(insetBefore, res.text) 146 | }) 147 | } 148 | 149 | // 段落对比翻译 150 | function paragraphTrans() { 151 | // 需要翻译的元素 152 | const translateElements: TranslateElements[] = [ 153 | { 154 | elements: document.querySelectorAll( 155 | 'h1:not(.translated),h2:not(.translated),h3:not(.translated),h4:not(.translated),h5:not(.translated),h6:not(.translated)', 156 | ), 157 | tag: 'p', 158 | }, 159 | { 160 | elements: document.querySelectorAll('p:not(.translated)'), 161 | tag: 'p', 162 | }, 163 | { 164 | elements: document.querySelectorAll('li:not(.translated)'), 165 | tag: 'li', 166 | }, 167 | ] 168 | 169 | // 遍历需要翻译的元素 170 | translateElements.forEach(({ elements, tag }) => { 171 | // 遍历所有的元素 172 | elements.forEach((item) => { 173 | // 给所有的元素添加翻译标识 174 | item.classList.add('translated') 175 | // // 如果文本中全是中文或空,不翻译 176 | // if (!item.innerText || /^[\u4e00-\u9fa5]+$/.test(item.innerText)) return 177 | // 发送翻译请求 178 | chrome.runtime.sendMessage({ text: item.innerText, type: 'translate' }, (res) => { 179 | // 插入翻译后的文本到元素中 180 | insertTransResult(item, res.text, tag) 181 | }) 182 | }) 183 | }) 184 | } 185 | 186 | // 插入翻译结果 187 | export function insertTransResult(node: HTMLElement, transResult: string, resultTag?: string) { 188 | // 如何返回值中不包含中文或者为空时候,不插入到页面中 189 | if (!transResult || !/[\u4e00-\u9fa5]/.test(transResult)) return 190 | // 如果本文开头包含中文标点符号,去除 191 | const text = transResult.replace(/^[,。?!:;、]/, '') 192 | // 插入翻译后的文本到元素中 193 | const transNode = document.createElement(resultTag || 'font') 194 | transNode.className = 'translated' 195 | transNode.style.cssText = ` 196 | color:red; 197 | padding: 0 4px; 198 | font-size: 14px; 199 | ` 200 | transNode.innerText = text 201 | node.parentNode?.insertBefore(transNode, node.nextSibling) 202 | const parent = node.parentNode as HTMLElement 203 | if (parent?.nodeType === 1) { 204 | parent?.classList.add('translated') 205 | } 206 | } 207 | 208 | // // 页面上所有的DOM,样式表,脚本,图片都已经加载完成时 209 | // window.onload = () => { 210 | // // 优化浏览器自带的页面翻译,设置不自动翻译的元素 211 | // setNotranslateNode() 212 | // } 213 | 214 | // 仅当DOM加载完成时 215 | window.addEventListener('DOMContentLoaded', () => { 216 | setNotranslateNode() 217 | }) 218 | -------------------------------------------------------------------------------- /src/contents/utils/is.ts: -------------------------------------------------------------------------------- 1 | export const isGithubCodePage = (): boolean => { 2 | const url = window.location.href 3 | const fileNavigationInstance = document.querySelectorAll('.file-navigation') 4 | const isHasReadme = Boolean(document.getElementById('readme')) 5 | const isHasBlob = url.includes('/blob/') 6 | const isHasTree = url.includes('/tree/') 7 | const res = fileNavigationInstance.length > 0 || isHasBlob || isHasTree || isHasReadme 8 | 9 | return res 10 | } 11 | 12 | export const isHasWindow = (): boolean => { 13 | return Boolean(window) 14 | } 15 | -------------------------------------------------------------------------------- /src/contents/views/Github/components/OnlineEditBtn/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | 3 | interface OnlineEditBtnProps { 4 | onClick?: () => void 5 | title: string 6 | } 7 | 8 | const OnlineEditBtn: FC = (props) => { 9 | const { onClick, title } = props 10 | 11 | return ( 12 | 32 | ) 33 | } 34 | 35 | export default OnlineEditBtn 36 | -------------------------------------------------------------------------------- /src/contents/views/Github/components/ScrollToTopBtn/arrow.tsx: -------------------------------------------------------------------------------- 1 | import React, { SVGProps } from 'react' 2 | 3 | export function MaterialSymbolsArrowUpward(props: SVGProps) { 4 | return ( 5 | 6 | 10 | 11 | ) 12 | } 13 | export default MaterialSymbolsArrowUpward 14 | -------------------------------------------------------------------------------- /src/contents/views/Github/components/ScrollToTopBtn/index.css: -------------------------------------------------------------------------------- 1 | .scrollToTopBtn { 2 | color: var(--color-fg-muted); 3 | padding: 3px 5px 0; 4 | cursor: pointer; 5 | font-size: 16px; 6 | background: var(--color-btn-bg); 7 | position: fixed; 8 | right: 50px; 9 | bottom: 80px; 10 | border: 1px solid var(--color-btn-border); 11 | border-radius: 6px; 12 | } 13 | .scrollToTopBtn:hover { 14 | /* todo */ 15 | } 16 | -------------------------------------------------------------------------------- /src/contents/views/Github/components/ScrollToTopBtn/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react' 2 | import Arrow from './arrow' 3 | // import './index.css' 4 | 5 | interface ScrollToTopBtnProps { 6 | onClick: () => void 7 | } 8 | 9 | const ScrollToTopBtn: FC = (props) => { 10 | const { onClick } = props 11 | 12 | return ( 13 |
30 | 31 |
32 | ) 33 | } 34 | 35 | export default ScrollToTopBtn 36 | -------------------------------------------------------------------------------- /src/contents/views/Github/index.tsx: -------------------------------------------------------------------------------- 1 | import type { PlasmoContentScript } from 'plasmo' 2 | 3 | import { useGitHub } from '../../hooks/useGitHub' 4 | import useScrollToTop from '../../hooks/useScrollToTop' 5 | import OnlineEditBtn from './components/OnlineEditBtn' 6 | import ScrollToTopBtn from './components/ScrollToTopBtn' 7 | 8 | export const config: PlasmoContentScript = { 9 | matches: ['https://github.com/*/*'], 10 | run_at: 'document_start', 11 | } 12 | 13 | export default function FunctionPage() { 14 | const { isCodePage } = useGitHub() 15 | const [isTopPage, setToTop] = useScrollToTop() 16 | return ( 17 | <> 18 | {isCodePage && } 19 | {isCodePage && !isTopPage && } 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/enum/commandEnum.ts: -------------------------------------------------------------------------------- 1 | // 2 | export enum keyCodeEnums { 3 | /** 逗号的 按键 */ 4 | Comma = 'Comma', 5 | } 6 | -------------------------------------------------------------------------------- /src/options/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | function IndexOptions() { 4 | const [data, setData] = useState('') 5 | 6 | return ( 7 | <> 8 |
14 |

这里是 DevGo 的选项页

15 | setData(e.target.value)} value={data} /> 16 |

17 | 插件使用 Plasmo 开发 18 |

19 |
20 | 21 | ) 22 | } 23 | 24 | export default IndexOptions 25 | -------------------------------------------------------------------------------- /src/popup/function-page/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'antd' 2 | import React from 'react' 3 | 4 | const onChange = (checked: boolean) => { 5 | console.log(`switch to ${checked}`) 6 | } 7 | 8 | export default function FunctionPage() { 9 | return ( 10 |
11 |
12 | {' '} 13 | 14 |
15 |

功能页,开发中...( Tools page wip...)

16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | import Footer from '~components/Footer' 4 | import Header from '~components/Header' 5 | import FunctionPage from '~popup/function-page' 6 | import ToolsPage from '~popup/tool-page' 7 | import TranslatePage from '~popup/translation-page' 8 | 9 | import 'antd/dist/reset.css' 10 | import '~style/index.css' 11 | 12 | const pages = [ 13 | { 14 | key: 'translate', 15 | name: '翻译页', 16 | }, 17 | { 18 | key: 'tools', 19 | name: '工具页', 20 | }, 21 | { 22 | key: 'function', 23 | name: '功能页', 24 | }, 25 | ] 26 | export default function IndexPopup() { 27 | const [active, setActive] = useState('translate') 28 | // const [active, setActive] = useState('tools') 29 | 30 | return ( 31 |
32 | {/* 头部 */} 33 |
34 |
35 | {/* 各个标签页 */} 36 |
37 | {/* 翻译页 */} 38 | {active === 'translate' && } 39 | {/* 工具页 */} 40 | {active === 'tools' && } 41 | {/* 功能页 */} 42 | {active === 'function' && } 43 |
44 |
45 | {/* 页脚 */} 46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/popup/tool-page/index.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'antd' 2 | import React from 'react' 3 | 4 | const onChange = (checked: boolean) => { 5 | console.log(`switch to ${checked}`) 6 | } 7 | 8 | export default function ToolsPage() { 9 | return ( 10 |
11 |
摘录本页连接到 MarkDown
12 |

工具页,开发中...( Tools page wip...)

13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/popup/translation-page/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Select } from 'antd' 2 | import { useEffect, useRef, useState } from 'react' 3 | 4 | import CopyIcon from '~assets/copy.svg' 5 | import { YoudaoTransRes, youdaoTrans } from '~script/translator-api' 6 | 7 | // 翻译页面 8 | const translatePage = (type): void => { 9 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => { 10 | chrome.tabs.sendMessage(tabs[0].id, { type }) 11 | }) 12 | } 13 | // 开发中 14 | const wip = (): void => { 15 | chrome.tabs.query({ currentWindow: true, active: true }, (tabs) => { 16 | chrome.tabs.sendMessage(tabs[0].id, { type: 'tip', msg: '开发中...' }) 17 | }) 18 | } 19 | 20 | const { TextArea } = Input 21 | function TranslatePage() { 22 | const [text, setText] = useState('') 23 | const [result, setResult] = useState({} as YoudaoTransRes) 24 | const [loading, setLoading] = useState('') 25 | 26 | // 翻译 27 | const translate = async () => { 28 | setLoading('trans-loading') 29 | const res = await youdaoTrans(text) 30 | setResult(res) 31 | setLoading('') 32 | } 33 | 34 | // 朗读 35 | const readText = async (url?: string) => { 36 | if (!url) { 37 | const msg = new SpeechSynthesisUtterance() 38 | msg.text = result.translation[0] || 'hello world' // 你要朗读的文本 39 | msg.lang = 'zh-CN' // 语言 40 | msg.volume = 1 // 音量,范围 0 ~ 1 41 | msg.rate = 0.8 // 语速,范围 0.1 ~ 10 42 | msg.pitch = 1 // 音调,范围 0 ~ 2 43 | speechSynthesis.speak(msg) // 朗读 44 | return 45 | } 46 | const audio = new Audio(url) 47 | audio.play() 48 | } 49 | 50 | // TODO ↓ 51 | // YouTube 视频翻译 52 | // 整页翻译 53 | 54 | // react 页面加载完成时,输入框自动获取焦点 55 | const input = useRef(null) 56 | useEffect(() => { 57 | if (input.current) { 58 | input.current.focus() 59 | } 60 | // input && input.current.focus() 61 | }, []) 62 | 63 | return ( 64 |
65 |
66 |