├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── deploy-scripts.yml ├── .gitignore ├── .npmrc ├── .prettierrc.js ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── meta ├── blank.ts └── debug.ts ├── package.json ├── packages ├── captcha │ ├── README.md │ ├── captcha.user.js │ └── package.json ├── completion │ ├── README.md │ ├── completion.user.js │ └── package.json ├── copy-currency │ ├── README.md │ ├── meta.json │ ├── package.json │ └── src │ │ ├── global.d.ts │ │ ├── index.css │ │ ├── index.ts │ │ ├── register.ts │ │ └── utils.ts ├── copy │ ├── README.md │ ├── meta.json │ ├── package.json │ ├── src │ │ ├── constant │ │ │ └── event.ts │ │ ├── deploy.ts │ │ ├── index.ts │ │ ├── init.ts │ │ ├── modules │ │ │ ├── 17k.ts │ │ │ ├── aiyuke.ts │ │ │ ├── baiduwk.ts │ │ │ ├── bilibili.ts │ │ │ ├── boke112.ts │ │ │ ├── ciweimao.ts │ │ │ ├── cnitpm.ts │ │ │ ├── cnki.ts │ │ │ ├── common-e.ts │ │ │ ├── common-p.ts │ │ │ ├── common.ts │ │ │ ├── csdn.ts │ │ │ ├── diyifanwen.ts │ │ │ ├── doc88.ts │ │ │ ├── docin.ts │ │ │ ├── docqq.ts │ │ │ ├── edu30.ts │ │ │ ├── kdocs.ts │ │ │ ├── leetcode.ts │ │ │ ├── mbalib.ts │ │ │ ├── mihoyo.ts │ │ │ ├── qidian.ts │ │ │ ├── qq-slider.ts │ │ │ ├── qqbook.ts │ │ │ ├── sf.ts │ │ │ ├── uemeds.ts │ │ │ ├── utaten.ts │ │ │ ├── wk.ts │ │ │ ├── xiaohongshu.ts │ │ │ ├── youdao.ts │ │ │ ├── zhihu.ts │ │ │ └── zongheng.ts │ │ ├── styles │ │ │ ├── app.css │ │ │ └── style.css │ │ ├── types │ │ │ └── global.d.ts │ │ ├── utils │ │ │ ├── copy.ts │ │ │ ├── dom.ts │ │ │ ├── event.ts │ │ │ ├── helper.ts │ │ │ ├── instance.ts │ │ │ └── is.ts │ │ └── websites.ts │ └── tsconfig.json ├── expansion │ ├── README.md │ ├── expansion.user.js │ └── package.json ├── force-copy │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── locales │ │ │ ├── en │ │ │ │ └── messages.json │ │ │ └── zh_cn │ │ │ │ └── messages.json │ │ ├── misc │ │ │ ├── brick.png │ │ │ ├── favicon.dark.png │ │ │ └── poster.png │ │ ├── popup.html │ │ └── static │ │ │ ├── favicon.128.png │ │ │ └── favicon.png │ ├── rspack.config.js │ ├── script │ │ ├── files │ │ │ └── index.js │ │ ├── if-def │ │ │ ├── if-def.code-snippets │ │ │ ├── index.js │ │ │ └── package.json │ │ ├── manifest │ │ │ └── index.js │ │ ├── reload │ │ │ └── index.js │ │ ├── utils │ │ │ └── node.js │ │ └── wrapper │ │ │ └── index.js │ ├── src │ │ ├── bridge │ │ │ ├── content-inject │ │ │ │ ├── constant.ts │ │ │ │ ├── index.ts │ │ │ │ └── request.ts │ │ │ ├── content-worker │ │ │ │ ├── constant.ts │ │ │ │ ├── index.ts │ │ │ │ └── request.ts │ │ │ ├── popup-content │ │ │ │ ├── constant.ts │ │ │ │ ├── index.ts │ │ │ │ ├── request.ts │ │ │ │ └── response.ts │ │ │ ├── popup-worker │ │ │ │ ├── constant.ts │ │ │ │ ├── index.ts │ │ │ │ └── request.ts │ │ │ └── worker-content │ │ │ │ ├── constant.ts │ │ │ │ ├── index.ts │ │ │ │ └── request.ts │ │ ├── content │ │ │ ├── channel │ │ │ │ └── popup.ts │ │ │ ├── index.ts │ │ │ └── runtime │ │ │ │ ├── initialize.ts │ │ │ │ └── script.ts │ │ ├── inject │ │ │ ├── channel │ │ │ │ └── content.ts │ │ │ ├── index.ts │ │ │ ├── modules │ │ │ │ ├── basic.ts │ │ │ │ ├── doc88.ts │ │ │ │ ├── docin.ts │ │ │ │ ├── index.ts │ │ │ │ ├── qq-doc.ts │ │ │ │ ├── qq-ppt.ts │ │ │ │ ├── wenku.ts │ │ │ │ └── zhihu.ts │ │ │ ├── types │ │ │ │ ├── global.d.ts │ │ │ │ ├── state.ts │ │ │ │ └── website.ts │ │ │ └── utils │ │ │ │ ├── bus.ts │ │ │ │ ├── delay.ts │ │ │ │ ├── events.ts │ │ │ │ └── styles.ts │ │ ├── manifest │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── popup │ │ │ ├── components │ │ │ │ ├── app │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── console │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── footer │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ ├── header │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── tools │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ └── use-memoized-fn.ts │ │ │ ├── i18n │ │ │ │ ├── en.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── zh.ts │ │ │ ├── index.tsx │ │ │ └── utils │ │ │ │ └── badge.ts │ │ ├── types │ │ │ └── global.d.ts │ │ ├── utils │ │ │ ├── constant.ts │ │ │ ├── global.ts │ │ │ ├── is.ts │ │ │ ├── logger.ts │ │ │ ├── reload.ts │ │ │ └── types.ts │ │ └── worker │ │ │ ├── channel │ │ │ └── content.ts │ │ │ ├── index.ts │ │ │ ├── runtime │ │ │ ├── initialize.ts │ │ │ └── script.ts │ │ │ └── utils │ │ │ └── constant.ts │ └── tsconfig.json ├── site-director │ ├── README.md │ ├── meta.json │ ├── package.json │ └── src │ │ ├── index.ts │ │ ├── modules │ │ ├── csdn.ts │ │ ├── google.ts │ │ ├── jianshu.ts │ │ ├── juejin.ts │ │ ├── qq-mail.ts │ │ └── zhihu.ts │ │ ├── style.css │ │ ├── utils.ts │ │ └── websites.ts └── water-mark │ ├── README.md │ ├── meta.json │ ├── package.json │ ├── src │ ├── index.ts │ ├── modules │ │ └── basic.ts │ ├── types │ │ ├── global.d.ts │ │ └── website.ts │ └── utils │ │ ├── constant.ts │ │ ├── dom.ts │ │ └── styles.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── publish-scripts.sh ├── rollup.config.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["eslint:recommended", "plugin:prettier/recommended"], 3 | env: { 4 | browser: true, 5 | node: true, 6 | commonjs: true, 7 | es2021: true, 8 | }, 9 | parserOptions: { 10 | requireConfigFile: false, 11 | ecmaVersion: 2020, 12 | sourceType: "module", 13 | }, 14 | overrides: [ 15 | { 16 | files: ["*.ts"], 17 | parser: "@typescript-eslint/parser", 18 | plugins: ["@typescript-eslint"], 19 | extends: ["plugin:@typescript-eslint/recommended"], 20 | }, 21 | { 22 | files: ["*.tsx"], 23 | parser: "@typescript-eslint/parser", 24 | plugins: ["react", "react-hooks", "@typescript-eslint/eslint-plugin"], 25 | extends: ["plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], 26 | }, 27 | ], 28 | ignorePatterns: ["node_modules", "build", "dist", "coverage", "public"], 29 | rules: { 30 | "semi": "error", 31 | "quote-props": ["error", "consistent-as-needed"], 32 | "arrow-parens": ["error", "as-needed"], 33 | "no-var": "error", 34 | "prefer-const": "error", 35 | "no-console": "off", 36 | "@typescript-eslint/explicit-module-boundary-types": "off", 37 | "@typescript-eslint/ban-ts-ignore": "off", 38 | "@typescript-eslint/ban-ts-comment": "off", 39 | "@typescript-eslint/consistent-type-imports": "error", 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 问题描述 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## 项目所属 9 | 10 | 11 | 12 | - [ ] Copy 13 | - [ ] Force Copy 14 | - [ ] Copy Currency 15 | - [ ] Site Director 16 | - [ ] Water Mark 17 | - [ ] Captcha 18 | - [ ] Expansion 19 | - [ ] Completion 20 | - [ ] Unknown 21 | 22 | ## 相关链接 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## 预期表现 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/deploy-scripts.yml: -------------------------------------------------------------------------------- 1 | name: deploy-scripts gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | persist-credentials: false 17 | 18 | - name: install node-v16 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: '16.16.0' 22 | 23 | - name: install dependencies 24 | run: | 25 | node -v 26 | npm install -g pnpm@8.11.0 27 | pnpm --filter=\!force-copy --registry=https://registry.npmjs.org/ install 28 | 29 | - name: build project 30 | run: | 31 | pnpm run build:scripts 32 | 33 | - name: deploy 34 | uses: JamesIves/github-pages-deploy-action@releases/v3 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | BRANCH: gh-pages 38 | FOLDER: dist 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # base 2 | backup 3 | node_modules 4 | *.log 5 | .DS_Store 6 | .idea 7 | *.zip 8 | 9 | # dependencies 10 | node_modules 11 | .pnp 12 | .pnp.js 13 | 14 | # testing 15 | coverage 16 | 17 | # production 18 | build 19 | dist 20 | 21 | # misc 22 | .DS_Store 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com/ 2 | # strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "quoteProps": "preserve", 8 | "jsxSingleQuote": false, 9 | "trailingComma": "es5", 10 | "bracketSpacing": true, 11 | "jsxBracketSameLine": false, 12 | "arrowParens": "avoid", 13 | "requirePragma": false, 14 | "insertPragma": false, 15 | "proseWrap": "preserve", 16 | "htmlWhitespaceSensitivity": "ignore", 17 | "endOfLine": "lf", 18 | }; 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "rspack" 4 | ] 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Czy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TKScript 2 | 3 |

4 | GitHub 5 | 6 | GreasyFork 7 | 8 | FirefoxAddOns 9 | 10 | ChromeWebStore 11 | 12 | BLOG 13 |

14 | 15 | `GreaseMonkey`油猴脚本与`BrowserAddon`浏览器扩展,如果觉得不错,点个`star`吧 😁 16 | 17 | 18 | ## Install 19 | 20 | 使用油猴脚本需要首先安装`GreaseMonkey/TamperMonkey`扩展,版本库中有如下脚本,可以直接点击安装按钮进行安装,浏览器扩展程序直接在浏览器相关应用市场安装即可。 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 121 | 122 | 123 | 124 | 125 |
名称详情安装简介
文本选中复制详情 39 | 安装 40 | 41 | 备用 42 | 解除网站不允许复制的限制,需要适配新的网站可提issue
文本选中复制-通用详情 50 | 安装 51 | 52 | 备用 53 | 文本选中复制通用处理版本,具体使用方式请查阅详情。
Force Copy详情 61 | 安装 62 | 63 | 备用 64 | 浏览器扩展程序,集成文本选中复制与通用能力。
跳转链接直达详情 72 | 安装 73 | 74 | 备用 75 | 去掉确定跳转链接页面,用于谷歌、知乎、CSDN、简书等。
移除页面水印详情 83 | 安装 84 | 85 | 备用 86 | 移除常见网页的水印,请自行处理@match到匹配地址。
自动展开阅读全文详情 94 | 安装 95 | 96 | 备用 97 | 展开阅读全文,用于CSDN、知乎等。
强智教务验证码识别详情 105 | 安装 106 | 107 | 备用 108 | 自动填写强智的验证码,请自行处理@match到匹配地址。
阿里图标库HTTP详情 117 | 安装 118 | 119 | 备用 120 | 添加HTTP按钮,简化手动填写。
126 | 127 | ## Release 128 | 129 | 如果需要从源码构建脚本与扩展,请使用`pnpm`安装依赖,如果不需要这步操作,可以直接在上方表格点击安装按钮即可安装打包好的脚本与扩展。版本库中`dist`为脚本打包目录,其中仅`copy`、`site-director`、`copy-currency`脚本需`rollup`打包使用,其他脚本直接安装即可。浏览器扩展的打包位置为各自`monorepo package`下的`build`目录,调试开发过程需要主动指定浏览器加载目标位置。 130 | 131 | ```bash 132 | $ pnpm install 133 | $ pnpm run build 134 | $ pnpm run build:force-copy 135 | $ pnpm run build:force-copy:gecko 136 | ``` 137 | 138 | ## Contributors 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /meta/blank.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /meta/debug.ts: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name DEBUG 3 | // @description DEBUG MODE 4 | // @namespace https://github.com/WindrunnerMax/TKScript 5 | // @version 1.0.0 6 | // @author Czy 7 | // @match http://*/* 8 | // @match https://*/* 9 | // @license GPL License 10 | // @run-at document-end 11 | // @grant GM_notification 12 | // @require file:///Users/czy/Project/TKScript/dist/copy-currency.user.js 13 | // ==/UserScript== 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script", 3 | "version": "1.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/WindrunnerMax/TKScript" 7 | }, 8 | "scripts": { 9 | "dev": "npm run dev:scripts", 10 | "build": "npm run build:scripts", 11 | "dev:scripts": "rollup -c --watch", 12 | "build:scripts": "./publish-scripts.sh", 13 | "dev:force-copy": "pnpm --filter force-copy run dev", 14 | "build:force-copy": "pnpm --filter force-copy run build", 15 | "dev:force-copy:gecko": "pnpm --filter force-copy run dev:gecko", 16 | "build:force-copy:gecko": "pnpm --filter force-copy run build:gecko", 17 | "lint:es": "eslint --fix --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore .", 18 | "lint:ts": "pnpm --filter '*' run lint:ts" 19 | }, 20 | "author": "Czy", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "@rollup/plugin-commonjs": "20.0.0", 24 | "@rollup/plugin-node-resolve": "13.0.4", 25 | "@rollup/plugin-replace": "5.0.2", 26 | "@types/node": "20.8.0", 27 | "@typescript-eslint/eslint-plugin": "5.4.0", 28 | "@typescript-eslint/parser": "5.4.0", 29 | "cross-env": "7.0.3", 30 | "esbuild": "0.15.10", 31 | "eslint": "7.32.0", 32 | "eslint-config-prettier": "8.3.0", 33 | "eslint-plugin-prettier": "3.3.1", 34 | "eslint-plugin-react": "7.27.0", 35 | "eslint-plugin-react-hooks": "4.3.0", 36 | "postcss": "8.3.11", 37 | "prettier": "2.3.2", 38 | "rollup": "2.56.3", 39 | "rollup-plugin-esbuild": "4.10.1", 40 | "rollup-plugin-postcss": "4.0.1", 41 | "rollup-plugin-userscript-metablock": "0.3.1", 42 | "tslib": "2.6.2", 43 | "typescript": "4.6.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/captcha/README.md: -------------------------------------------------------------------------------- 1 | # 强智教务系统验证码识别 2 | 3 | 自动填写强智的验证码,请自行处理`@match`到需要匹配的网站。 4 | -------------------------------------------------------------------------------- /packages/captcha/captcha.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name SWVerifyCode 3 | // @namespace https://github.com/WindrunnerMax/TKScript 4 | // @version 1.1 5 | // @description try to take over the world! 6 | // @author Czy 7 | // @match *://jwgl.sdust.edu.cn/ 8 | // @match *://jwgl.sdust.edu.cn/jsxsd/ 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | /** 13 | * 配置账号密码信息(选填) 14 | */ 15 | const username = ""; //账号 16 | const password = ""; //密码 17 | const autologin = false; //自动登录 18 | 19 | const height = 22; 20 | const width = 62; 21 | const rgbThres = 150; 22 | const charMap = { 23 | 1: "111100111110000111110000111111100111111100111111100111111100111111100111111100111111100111110000001110000001", 24 | 2: "100000111000000011111111001111111001111111001111110011111000111110011111100111111001111111000000001000000001", 25 | 3: "100000111000000011111110001111111001111110011110000111110000011111110001111111001111110001100000011100000111", 26 | b: "001111111001111111001111111001000011000000001000111000001111100001111100001111100000111000000000001001000011", 27 | c: "111111111111111111111111111110000011100000011000111111001111111001111111001111111000111111100000011110000011", 28 | m: "111111111111111111111111111001000011000000000000111000001111001001111001001111001001111001001111001001111001", 29 | n: "111111111111111111111111111001100001001000000000011100000111100001111100001111100001111100001111100001111100", 30 | v: "111111111111111111111111111111111011001110011001110011001110011100100111100100111100100111110001111110001111", 31 | x: "111111111111111111111111111001110011001110011100100111110001111110001111110001111100100111001110011001110011", 32 | z: "111111111111111111111111111000000011000000011111100111111001111110011111100111111001111111000000011000000011", 33 | }; 34 | 35 | function binaryImage(ctx) { 36 | const imageData = ctx.getImageData(0, 0, width, height).data; 37 | const imgArr = []; 38 | for (let x = 0; x < width; ++x) { 39 | for (let y = 0; y < height; ++y) { 40 | if (!imgArr[y]) imgArr[y] = []; 41 | if (x === 0 || y === 0 || x === width - 1 || y === height - 1) { 42 | imgArr[y][x] = 1; 43 | continue; 44 | } 45 | const i = (y * width + x) * 4; 46 | if (imageData[i] < rgbThres && imageData[i + 1] < rgbThres && imageData[i + 2] < rgbThres) { 47 | imgArr[y][x] = 0; 48 | } else { 49 | imgArr[y][x] = 1; 50 | } 51 | } 52 | } 53 | return imgArr; 54 | } 55 | 56 | function removeByLine(imgArr) { 57 | const xCount = imgArr[0].length; 58 | const yCount = imgArr.length; 59 | for (let i = 1; i < yCount - 1; ++i) { 60 | for (let k = 1; k < xCount - 1; ++k) { 61 | if (imgArr[i][k] === 0) { 62 | const countOne = imgArr[i][k - 1] + imgArr[i][k + 1] + imgArr[i - 1][k] + imgArr[i + 1][k]; 63 | if (countOne > 2) imgArr[i][k] = 1; 64 | } 65 | } 66 | } 67 | return imgArr; 68 | } 69 | 70 | function cutImg(imgArr, arrX, arrY, n) { 71 | const imgArrArr = []; 72 | for (let i = 0; i < n; ++i) { 73 | const unitImg = []; 74 | for (let j = arrY[i][0]; j < arrY[i][1]; ++j) { 75 | if (!unitImg[j]) unitImg[j - arrY[i][0]] = []; 76 | for (let k = arrX[i][0]; k < arrX[i][1]; ++k) { 77 | unitImg[j - arrY[i][0]][k - arrX[i][0]] = imgArr[j][k]; 78 | } 79 | } 80 | imgArrArr.push(unitImg); 81 | } 82 | return imgArrArr; 83 | } 84 | 85 | function getString(imgArr) { 86 | let s = ""; 87 | imgArr.forEach(vx => { 88 | vx.forEach(vy => { 89 | s += vy; 90 | }); 91 | }); 92 | return s; 93 | } 94 | 95 | function comparedText(s1, s2) { 96 | let percent = 0; 97 | const n = s1.length; 98 | for (let i = 0; i < n; ++i) s1[i] === s2[i] ? ++percent : ""; 99 | return percent; 100 | } 101 | 102 | function matchCode(imgArrArr) { 103 | let record = ""; 104 | imgArrArr.forEach(imgArr => { 105 | let maxMatch = 0; 106 | let tempRecord = ""; 107 | for (const char in charMap) { 108 | const percent = comparedText(getString(imgArr), charMap[char]); 109 | if (percent > maxMatch) { 110 | maxMatch = percent; 111 | tempRecord = char; 112 | } 113 | } 114 | record += tempRecord; 115 | }); 116 | return record; 117 | } 118 | 119 | function showImg(imgArr) { 120 | let s = ""; 121 | imgArr.forEach(vx => { 122 | vx.forEach(vy => { 123 | s += vy; 124 | }); 125 | s += "\n"; 126 | }); 127 | console.log(s); 128 | } 129 | 130 | function main() { 131 | const img = document.getElementById("SafeCodeImg"); 132 | const canvas = document.createElement("canvas"); 133 | canvas.width = width; 134 | canvas.height = height; 135 | const ctx = canvas.getContext("2d"); 136 | ctx.drawImage(img, 0, 0); 137 | let imgArr = binaryImage(ctx); 138 | imgArr = removeByLine(imgArr); 139 | const imgArrArr = cutImg( 140 | imgArr, 141 | [ 142 | [4, 13], 143 | [14, 23], 144 | [24, 33], 145 | [34, 43], 146 | ], 147 | [ 148 | [4, 16], 149 | [4, 16], 150 | [4, 16], 151 | [4, 16], 152 | ], 153 | 4 154 | ); 155 | showImg(imgArr); 156 | const result = matchCode(imgArrArr); 157 | console.log("识别为:", result); 158 | document.getElementById("RANDOMCODE").value = result; 159 | document.getElementById("userAccount").value = username; 160 | document.getElementById("userPassword").value = password; 161 | if (autologin) document.getElementById("btnSubmit").click(); 162 | } 163 | 164 | (function () { 165 | "use strict"; 166 | const img = document.getElementById("SafeCodeImg"); 167 | img.onload = function () { 168 | main(); 169 | }; 170 | })(); 171 | -------------------------------------------------------------------------------- /packages/captcha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "captcha", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/completion/README.md: -------------------------------------------------------------------------------- 1 | # 阿里矢量图标库添加`HTTP`按钮 2 | 3 | 阿里矢量图标库添加`HTTP`按钮,简化手动填写。 4 | -------------------------------------------------------------------------------- /packages/completion/completion.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 阿里图标库CDN添加HTTP按钮 3 | // @namespace https://github.com/WindrunnerMax/TKScript 4 | // @version 1.0 5 | // @description try to take over the world! 6 | // @author You 7 | // @match https://www.iconfont.cn/manage/*manage_type=myprojects* 8 | // @grant none 9 | // ==/UserScript== 10 | 11 | function addProtocol() { 12 | const preTags = document.getElementsByTagName("pre"); 13 | const n = preTags.length; 14 | for (let i = 0; i < n; ++i) 15 | preTags[i].innerText = preTags[i].innerText.replace( 16 | /\/\/at.alicdn.com/g, 17 | "https://at.alicdn.com" 18 | ); 19 | } 20 | 21 | (function () { 22 | "use strict"; 23 | console.log(document.getElementsByClassName("block-bar")); 24 | const btn = document.createElement("div"); 25 | btn.style.position = "fixed"; 26 | btn.style.top = "35%"; 27 | btn.style.left = "10px"; 28 | btn.style.background = "#4C98F7"; 29 | btn.style.color = "#fff"; 30 | btn.style.height = "50px"; 31 | btn.style.width = "50px"; 32 | btn.style.cursor = "pointer"; 33 | btn.style["line-height"] = "50px"; 34 | btn.style["text-align"] = "center"; 35 | btn.style["border-radius"] = "50px"; 36 | btn.innerText = "HTTP"; 37 | const body = document.getElementsByTagName("body")[0]; 38 | btn.onclick = function () { 39 | addProtocol(); 40 | }; 41 | body.appendChild(btn); 42 | })(); 43 | -------------------------------------------------------------------------------- /packages/completion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "completion", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/copy-currency/README.md: -------------------------------------------------------------------------------- 1 | # 文本选中复制-通用版 2 | 文本选中复制通用版本,适用于大多数网站。 3 | 由于会匹配所有网站,为了减少对于正常网站的干扰,在打开网站之后,**需要自行在网站左下角找到淡蓝色块,鼠标悬浮之后,点击启动 解除复制限制 按钮**。 4 | 某度文库、道客某某等网站需要特殊处理,不能通用处理,请移步 文本选中复制 脚本。 5 | 6 | ![image](https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBK2RvQVE9PSIsImV4cCI6bnVsbCwicHVyIjoiYmxvYl9pZCJ9fQ==--1d03b44d54652721f61a2cc53354bedcc3c575cd/A36B8EFD-0044-4FF1-BBAF-F0C63B565747.png?locale=zh-CN) 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/copy-currency/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "default": "🔥🔥🔥文本选中复制(通用)🔥🔥🔥", 4 | "en": "Text Copy Universal", 5 | "zh": "🔥🔥🔥文本选中复制(通用)🔥🔥🔥" 6 | }, 7 | "namespace": "https://github.com/WindrunnerMax/TKScript", 8 | "version": "1.1.3", 9 | "description": { 10 | "default": "文本选中复制通用版本,适用于大多数网站", 11 | "en": "Text copy general version, suitable for most websites.", 12 | "zh": "文本选中复制通用版本,适用于大多数网站" 13 | }, 14 | "author": "Czy", 15 | "match": ["http://*/*", "https://*/*"], 16 | "supportURL": "https://github.com/WindrunnerMax/TKScript/issues", 17 | "license": "GPL License", 18 | "installURL": "https://github.com/WindrunnerMax/TKScript", 19 | "run-at": "document-end", 20 | "grant": ["GM_registerMenuCommand", "GM_unregisterMenuCommand", "GM_notification"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/copy-currency/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy-currency", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | }, 10 | "sideEffects": false, 11 | "files": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/copy-currency/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const unsafeWindow = any; 2 | 3 | declare function GM_registerMenuCommand(name: string, fn: () => void, index?: string): number; 4 | declare function GM_unregisterMenuCommand(id: number): void; 5 | -------------------------------------------------------------------------------- /packages/copy-currency/src/index.css: -------------------------------------------------------------------------------- 1 | .__copy-currency-container{ 2 | position: fixed; 3 | left: -150px; 4 | bottom: 0px; 5 | width: 150px; 6 | display: flex; 7 | flex-direction: column; 8 | border-radius: 3px; 9 | background-color: #4C98F7; 10 | transition: all .3s; 11 | z-index: 9999999999; 12 | } 13 | 14 | .__copy-currency-container::before{ 15 | content: ""; 16 | position: absolute; 17 | right: -6px; 18 | top: calc(50% - 10px); 19 | width: 20px; 20 | height: 20px; 21 | border-radius: 20px; 22 | background-color: #4C98F7; 23 | cursor: pointer; 24 | } 25 | 26 | .__copy-currency-container:hover{ 27 | left: 0; 28 | } 29 | 30 | .__copy-currency-container > .__copy-currency-button{ 31 | font-size: 12px !important; 32 | margin: 5px; 33 | cursor: pointer; 34 | color: #fff !important; 35 | border: 1px solid #fff; 36 | padding: 5px 3px 5px 4px; 37 | user-select: none; 38 | border-radius: 3px; 39 | z-index: 9999999999; 40 | text-align: center; 41 | } -------------------------------------------------------------------------------- /packages/copy-currency/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ControllerItem } from "./register"; 2 | import { BUTTON_STATUS, register, STORAGE_KEY_PREFIX } from "./register"; 3 | import { styles } from "./utils"; 4 | 5 | const stopNativePropagation = (event: Event) => event.stopPropagation(); 6 | const CONTROLLER_MAP: ControllerItem[] = [ 7 | { 8 | status: BUTTON_STATUS.CLOSE, 9 | storageKey: "selectstart-and-copy", 10 | openName: "✅ 启动解除复制限制", 11 | closeName: "❌ 关闭解除复制限制", 12 | openFunction: () => { 13 | window.addEventListener("selectstart", stopNativePropagation, true); 14 | window.addEventListener("copy", stopNativePropagation, true); 15 | styles.insertCSS( 16 | STORAGE_KEY_PREFIX + "selectstart-and-copy", 17 | "*{user-select: auto !important;-webkit-user-select: auto !important;}" 18 | ); 19 | }, 20 | closeFunction: () => { 21 | window.removeEventListener("selectstart", stopNativePropagation, true); 22 | window.removeEventListener("copy", stopNativePropagation, true); 23 | styles.removeCSS(STORAGE_KEY_PREFIX + "selectstart-and-copy"); 24 | }, 25 | }, 26 | { 27 | status: BUTTON_STATUS.CLOSE, 28 | storageKey: "contextmenu", 29 | openName: "✅ 启动解除右键限制", 30 | closeName: "❌ 关闭解除右键限制", 31 | openFunction: () => window.addEventListener("contextmenu", stopNativePropagation, true), 32 | closeFunction: () => window.removeEventListener("contextmenu", stopNativePropagation, true), 33 | }, 34 | { 35 | status: BUTTON_STATUS.CLOSE, 36 | storageKey: "keydown", 37 | openName: "✅ 启动解除键盘限制", 38 | closeName: "❌ 关闭解除键盘限制", 39 | openFunction: () => window.addEventListener("keydown", stopNativePropagation, true), 40 | closeFunction: () => window.removeEventListener("keydown", stopNativePropagation, true), 41 | }, 42 | ]; 43 | 44 | (function () { 45 | register(CONTROLLER_MAP); 46 | })(); 47 | -------------------------------------------------------------------------------- /packages/copy-currency/src/register.ts: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | const STORAGE_VALUE = { 4 | OPEN: "true", 5 | CLOSE: "false", 6 | }; 7 | export enum BUTTON_STATUS { 8 | OPEN, 9 | CLOSE, 10 | } 11 | export type ControllerItem = { 12 | status: BUTTON_STATUS; 13 | storageKey: string; 14 | openName: string; 15 | closeName: string; 16 | openFunction: () => void; 17 | closeFunction: () => void; 18 | }; 19 | export const STORAGE_KEY_PREFIX = "copy-currency--"; 20 | 21 | export const register = (controllers: ControllerItem[]): void => { 22 | const container = document.createElement("div"); 23 | container.className = "__copy-currency-container"; 24 | document.body.appendChild(container); 25 | controllers.forEach(controller => { 26 | const button = document.createElement("div"); 27 | button.className = "__copy-currency-button"; 28 | const localHookInfo = localStorage.getItem(STORAGE_KEY_PREFIX + controller.storageKey); 29 | // 初始加载会直接调用处理函数 状态置反 30 | controller.status = 31 | localHookInfo === STORAGE_VALUE.OPEN ? BUTTON_STATUS.CLOSE : BUTTON_STATUS.OPEN; 32 | const handler = () => { 33 | if (controller.status === BUTTON_STATUS.CLOSE) { 34 | controller.openFunction(); 35 | controller.status = BUTTON_STATUS.OPEN; 36 | button.textContent = controller.closeName; 37 | localStorage.setItem(STORAGE_KEY_PREFIX + controller.storageKey, STORAGE_VALUE.OPEN); 38 | } else { 39 | controller.closeFunction(); 40 | controller.status = BUTTON_STATUS.CLOSE; 41 | button.textContent = controller.openName; 42 | localStorage.setItem(STORAGE_KEY_PREFIX + controller.storageKey, STORAGE_VALUE.CLOSE); 43 | } 44 | }; 45 | handler(); 46 | button.addEventListener("click", handler); 47 | container.appendChild(button); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/copy-currency/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const styles = { 2 | insertCSS: (id: string, css: string): void => { 3 | const style = document.createElement("style"); 4 | style.id = id; 5 | style.innerText = css; 6 | const [body] = document.getElementsByTagName("body"); 7 | if (body) { 8 | body.appendChild(style); 9 | } else { 10 | window.addEventListener("DOMContentLoaded", () => document.body.appendChild(style)); 11 | } 12 | }, 13 | removeCSS: (id: string): void => { 14 | const style = document.getElementById(id); 15 | style && style.remove(); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/copy/README.md: -------------------------------------------------------------------------------- 1 | # 文本选中复制 2 | 3 | 解除网站不允许复制的限制,文本选中后点击复制按钮即可复制,主要用于 百度文库 道客巴巴 无忧考网 学习啦 蓬勃范文 思否社区 力扣 知乎 语雀 等等。 4 | 5 | 某些网站例如某度文库、道客某某等都不允许用户选中文本进行复制,作为一个搞前端的,就感觉离谱,文本都下载到本地了,还不让我复制,于是为了更好的学(复)习(制),实现了一个脚本去解决这些限制。 6 | 7 | ## 描述 8 | 首先来看一下效果图,依旧是以某度文库、道客某某为例展示效果,点击复制按钮即可成功复制。 9 | 10 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020062620515392.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 11 | 12 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200626205401136.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 13 | 14 | 脚本下载地址:[https://greasyfork.org/scripts/405130-文本选中复制](https://greasyfork.org/scripts/405130-%E6%96%87%E6%9C%AC%E9%80%89%E4%B8%AD%E5%A4%8D%E5%88%B6) 15 | Github:[https://github.com/WindrunnerMax/TKScript/tree/master/src/copy](https://github.com/WindrunnerMax/TKScript/tree/master/src/copy) 16 | 脚本主要支持 百度文库 道客巴巴 无忧考网 学习啦 蓬勃范文 中文本的复制 17 | 18 | ## 实现 19 | 在研究实现之前,可能需要知道下面的一些知识,后面的链接是我之前写过的一些博客: 20 | * 事件冒泡:[事件冒泡及阻止](https://github.com/WindrunnerMax/EveryDay/blob/master/JavaScript/%E4%BA%8B%E4%BB%B6%E5%86%92%E6%B3%A1%E5%8F%8A%E9%98%BB%E6%AD%A2.md) 21 | * 事件流模型:[JS事件流模型](https://github.com/WindrunnerMax/EveryDay/blob/master/JavaScript/JS%E4%BA%8B%E4%BB%B6%E6%B5%81%E6%A8%A1%E5%9E%8B.md) 22 | * 浏览器事件:[浏览器事件](https://github.com/WindrunnerMax/EveryDay/blob/master/Browser/%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BA%8B%E4%BB%B6.md) 23 | * `ES6`相关语法:[ES6新特性](https://github.com/WindrunnerMax/EveryDay/blob/master/JavaScript/ES6%E6%96%B0%E7%89%B9%E6%80%A7.md) 24 | * `jQuery`相关使用、`CSS`的基本语法、正则表达式、浏览器调试等 25 | 26 | ### 某度文库 27 | 在某度文库中直接右击检查元素的话,是能够直接看到文字的,可以直接在调试面板的`Elements`审查元素中复制,但是总是有些麻烦。如果在选中某度文库的东西会弹出他自行插入的复制按钮,无论是按`Ctrl+C`或者点击他的复制按钮都无效,除非开通一个`VIP`,本着白嫖的原则,且文本都下载到浏览器了还不让复制有点说不过去,于是首先研究一下他的`Event Listeners`。 28 | 29 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020062621305280.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 30 | 31 | 虽然通过移除一些`Event Listeners`确实能够达到使用`Ctrl+C`来实现复制的效果,但是浏览器并不提供获取所有事件监听的方法,无法移除对于匿名的事件处理函数,对于具名的事件处理函数也不容易获取,谷歌浏览器提供的`getEventListeners`方法也只能在`Console`中使用,在脚本中会出现找不到该方法的异常,于是替换了一种方案,通过自行实现一个复制按钮来规避某度文库对于按键以及复制事件的屏蔽,具体是通过动态地插入`Dom`实现一个按钮,然后使用`ClipboardJS`这个插件去实现复制,其他的操作都是一些细节的处理,例如阻止这个插入的按钮继续冒泡触发`onmouseup`事件等。 32 | 33 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020062621400978.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 34 | 35 | ### 道客某某 36 | 不得不说,这个真的是惊到我了,他的实现是将文本加密,然后解密文本,最后通过使用`Canvas`将文本绘制,拖动鼠标选中时其实只是通过事件监听动态的插入了一个淡蓝色的透明的`div`,看似是选中了,实际文本是并未选中的。 37 | 38 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200626214729628.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 39 | 40 | 见招拆招,既然文本都已经下载到了我本地,那么他的解密方式也必定在本地,于是我首先寻找的就是他对于加密的数据进行解密的代码,在浏览器中`debug`了很长时间,因为他对于代码有加密混淆压缩的行为,解密的相关代码比较混乱,并不太容易去复现,于是我换了一个思路,既然`VIP`是能够复制的,那么对于这个点击复制的按钮一定会有相应的事件处理函数,那么就寻找这个按钮绑定的事件处理函数,通过不断地`debug`我定位了一个加密的`Js`文件,虽然做了加密以及混淆但是将其解析并格式化之后在事件处理函数的部分不是特别影响阅读,此外他的混淆的变量名是动态生成的,所以要做的就是再次请求一遍这个`Js`首先将其解析生成一段字符串然后通过正则表达式匹配正确的变量名,从而实现文本的复制。 41 | 42 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200626220032743.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 43 | 44 | ### 其他 45 | 对于这一部分基本上都是通过监听一个`oncopy`事件去拦截复制操作,对于`DOM0`级模型直接将`oncopy`事件的处理函数指向一个空函数即可,对于`DOM2`级模型,前文提到无法在脚本中直接获取一个元素绑定的所有事件,通过观察这些网站的`Event Listeners`可以发现其绑定的`oncopy`事件都是绑定在`document`上的,而且都是冒泡模型,那么只需要阻止事件向上冒泡就能规避这些网站的`oncopy`事件的触发,实现方案就是在`body`上定义`oncopy`事件为一个空函数并阻止其向上冒泡。 46 | 47 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200626220824245.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNDEzNjcw,size_16,color_FFFFFF,t_70) 48 | 49 | ## Github 50 | 51 | ``` 52 | https://github.com/WindrunnerMax/TKScript 53 | ``` 54 | -------------------------------------------------------------------------------- /packages/copy/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "🔥🔥🔥文本选中复制🔥🔥🔥", 3 | "namespace": "https://github.com/WindrunnerMax/TKScript", 4 | "version": "6.2.9", 5 | "description": "解除网站不允许复制的限制,文本选中后点击复制按钮即可复制,主要用于 百度文库 道客巴巴 腾讯文档 豆丁网 无忧考网 学习啦 蓬勃范文 思否社区 力扣 知乎 语雀 等", 6 | "author": "Czy", 7 | "match": [ 8 | "*://wenku.baidu.com/view/*", 9 | "*://wenku.baidu.com/share/*", 10 | "*://wenku.baidu.com/link*", 11 | "*://wenku.baidu.com/aggs/*", 12 | "*://wenku.baidu.com/ndPureView/*", 13 | "*://www.51test.net/show/*", 14 | "*://www.xuexi.la/*", 15 | "*://www.xuexila.com/*", 16 | "*://www.cspengbo.com/*", 17 | "*://*.doc88.com/*", 18 | "*://segmentfault.com/*", 19 | "*://wk.baidu.com/view/*", 20 | "*://leetcode-cn.com/problems/*", 21 | "*://leetcode.cn/problems/*", 22 | "*://*.zhihu.com/*", 23 | "*://z.30edu.com.cn/*", 24 | "*://docs.qq.com/doc/*", 25 | "*://docs.qq.com/sheet/*", 26 | "*://docs.qq.com/slide/*", 27 | "*://boke112.com/post/*", 28 | "*://*.yuque.com/*", 29 | "*://www.commandlinux.com/*", 30 | "*://*.diyifanwen.com/*", 31 | "*://*.mbalib.com/*", 32 | "*://*.cnitpm.com/*", 33 | "*://bbs.mihoyo.com/ys/*", 34 | "*://*.ruiwen.com/*", 35 | "*://www.uemeds.cn/*", 36 | "*://www.oh100.com/*", 37 | "*://www.aiyuke.com/news/*", 38 | "*://www.fwsir.com/*", 39 | "*://www.wenxm.cn/*", 40 | "*://www.unjs.com/*", 41 | "*://www.ahsrst.cn/*", 42 | "*://*.yjbys.com/*", 43 | "*://*.qidian.com/*", 44 | "*://*.zongheng.com/*", 45 | "*://*.17k.com/*", 46 | "*://*.ciweimao.com/*", 47 | "*://book.qq.com/*", 48 | "*://*.360doc.com/content/*", 49 | "*://*.850500.com/news/*", 50 | "*://utaten.com/lyric/*", 51 | "*://*.jianbiaoku.com/*", 52 | "*://*.kt250.com/*", 53 | "*://www.kejudati.com/*", 54 | "*://*.xiaohongshu.com/discovery/*", 55 | "*://*.baibeike.com/*", 56 | "*://*.blog.csdn.net/*", 57 | "*://*.bilibili.com/read/*", 58 | "*://*.cnki.net/KXReader/*", 59 | "*://*.cnrencai.com/*", 60 | "*://*.kodiplayer.cn/*", 61 | "*://tongxiehui.net/*", 62 | "*://*.jianshu.com/p/*", 63 | "*://*.linovelib.com/novel/*", 64 | "*://*.wjx.cn/*", 65 | "*://*.wjx.top/*", 66 | "*://*.chazidian.com/*", 67 | "*://*.juejin.cn/post/*", 68 | "*://*.zgbk.com/ecph/*", 69 | "*://*.wenmi.com/article/*", 70 | "*://yuedu.baidu.com/*", 71 | "*://www.inrrp.com.cn/*", 72 | "*://fanyi.baidu.com/mtpe/*", 73 | "*://www.shubaoc.com/*", 74 | "*://blog.51cto.com/*", 75 | "*://www.ximalaya.com/*", 76 | "*://*.tianqi.com/*", 77 | "*://*.xiexiebang.com/*", 78 | "*://*.docin.com/*", 79 | "*://*.ddwk8.cn/*", 80 | "*://*.php.cn/*", 81 | "*://cooco.net.cn/*", 82 | "*://fanqienovel.com/*", 83 | "*://*.mobiletrain.org/*", 84 | "*://*.examcoo.com/*", 85 | "*://*.rrdynb.com/*", 86 | "*://*.fuwu7.com/*", 87 | "*://*.xiangqiqipu.com/*", 88 | "*://note.youdao.com/*", 89 | "*://*.163.com/*", 90 | "*://*.aipiaxi.com/*", 91 | "*://wenku.csdn.net/*", 92 | "*://www.kdocs.cn/*", 93 | "*://www.xiaoyuzhoufm.com/*", 94 | "*://*.mcmod.cn/*", 95 | "*://*.zsxq.com/", 96 | "*://*.volcengine.com/*", 97 | "*://*.lyrical-nonsense.com/*", 98 | "*://*.xueqiu.com/*", 99 | "*://*.php.cn/*", 100 | "*://*.51cto.com/*", 101 | "*://*.educoder.net/*" 102 | ], 103 | "supportURL": "https://github.com/WindrunnerMax/TKScript/issues", 104 | "license": "GPL License", 105 | "installURL": "https://github.com/WindrunnerMax/TKScript", 106 | "run-at": "document-start", 107 | "grant": ["unsafeWindow", "GM_xmlhttpRequest"], 108 | "connect": "res3.doc88.com" 109 | } 110 | -------------------------------------------------------------------------------- /packages/copy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | }, 10 | "scripts": { 11 | "lint:ts": "../../node_modules/typescript/bin/tsc --noEmit" 12 | }, 13 | "sideEffects": false, 14 | "files": [ 15 | "src" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/copy/src/constant/event.ts: -------------------------------------------------------------------------------- 1 | export const DOM_STAGE = { 2 | START: "document-start" as const, 3 | END: "document-end" as const, 4 | }; 5 | export const DOM_READY = "DOMContentLoaded"; 6 | export const DOM_LOADED = DOM_READY; 7 | export const PAGE_LOADED = "load"; 8 | export const MOUSE_UP = "mouseup"; 9 | export const MOUSE_DOWN = "mousedown"; 10 | export const MOUSE_MOVE = "mousemove"; 11 | export const COPY = "copy"; 12 | export const SELECT_START = "selectstart"; 13 | export const CONTEXT_MENU = "contextmenu"; 14 | export const KEY_DOWN = "keydown"; 15 | export const TOUCH_START = "touchstart"; 16 | export const TOUCH_MOVE = "touchmove"; 17 | export const TOUCH_END = "touchend"; 18 | export const FOCUS = "focus"; 19 | export const BLUR = "blur"; 20 | export const FOCUS_IN = "focusin"; 21 | export const FOCUS_OUT = "focusout"; 22 | export const PASTE = "paste"; 23 | -------------------------------------------------------------------------------- /packages/copy/src/deploy.ts: -------------------------------------------------------------------------------- 1 | import { DOM_READY, DOM_STAGE } from "./constant/event"; 2 | import type { CopyParams } from "./utils/copy"; 3 | import type { Website, WebsiteConfig } from "./websites"; 4 | import websites from "./websites"; 5 | 6 | let siteGetSelectedText: (() => CopyParams) | null = null; 7 | 8 | const initWebsite = (): WebsiteConfig => { 9 | let websiteConfig: WebsiteConfig = { 10 | initCopyEvent: true, 11 | runAt: DOM_STAGE.END, 12 | captureInstance: false, 13 | delay: 0, 14 | }; 15 | const mather = (regex: RegExp, website: Website) => { 16 | if (regex.test(window.location.href)) { 17 | if (website.config) { 18 | websiteConfig = Object.assign(websiteConfig, website.config); 19 | } 20 | if (websiteConfig.runAt === DOM_STAGE.END) { 21 | window.addEventListener(DOM_READY, () => website.init()); 22 | } else { 23 | website.init(); 24 | } 25 | if (website.getSelectedText) { 26 | siteGetSelectedText = website.getSelectedText; 27 | } 28 | return true; 29 | } 30 | return false; 31 | }; 32 | websites.some(website => mather(website.regexp, website)); 33 | return websiteConfig; 34 | }; 35 | 36 | const getSelectedText = (): CopyParams => { 37 | if (siteGetSelectedText) return siteGetSelectedText(); 38 | if (window.getSelection) return (window.getSelection() || "").toString(); 39 | if (document.getSelection) return (document.getSelection() || "").toString(); 40 | if (document.selection) return document.selection.createRange().text; 41 | return ""; 42 | }; 43 | 44 | export { initWebsite, getSelectedText }; 45 | -------------------------------------------------------------------------------- /packages/copy/src/index.ts: -------------------------------------------------------------------------------- 1 | import { initBaseEvent, initBaseStyle } from "./init"; 2 | import { initWebsite, getSelectedText } from "./deploy"; 3 | import instance from "./utils/instance"; 4 | import { isEmptyContent } from "./utils/copy"; 5 | import { MOUSE_UP } from "./constant/event"; 6 | 7 | (function () { 8 | const websiteConfig = initWebsite(); 9 | initBaseEvent(websiteConfig); 10 | initBaseStyle(); 11 | window.addEventListener( 12 | MOUSE_UP, 13 | e => { 14 | const handler = () => { 15 | const content = getSelectedText(); 16 | if (isEmptyContent(content)) { 17 | instance.hide(); 18 | return void 0; 19 | } 20 | instance.onCopy(content, e); 21 | }; 22 | websiteConfig.delay ? setTimeout(handler, websiteConfig.delay) : handler(); 23 | }, 24 | websiteConfig.captureInstance 25 | ); 26 | })(); 27 | -------------------------------------------------------------------------------- /packages/copy/src/init.ts: -------------------------------------------------------------------------------- 1 | import appStyle from "./styles/app.css"; 2 | import baseStyle from "./styles/style.css"; 3 | import { COPY, DOM_READY } from "./constant/event"; 4 | import type { WebsiteConfig } from "./websites"; 5 | import dom from "./utils/dom"; 6 | 7 | export const initBaseEvent = (websiteConfig: WebsiteConfig): void => { 8 | window.addEventListener(DOM_READY, () => { 9 | if (websiteConfig.initCopyEvent) { 10 | document.oncopy = e => e.stopPropagation(); 11 | document.body.oncopy = e => e.stopPropagation(); 12 | document.addEventListener(COPY, e => e.stopPropagation()); 13 | document.body.addEventListener(COPY, e => e.stopPropagation()); 14 | } 15 | }); 16 | }; 17 | 18 | export const initBaseStyle = (): void => { 19 | window.addEventListener(DOM_READY, () => { 20 | dom.append("head", ``); 21 | dom.append("head", ``); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/copy/src/modules/17k.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("17k"), 6 | init: () => { 7 | utils.hideButton(); 8 | utils.enableOnCopy(".readAreaBox .p"); 9 | }, 10 | }; 11 | 12 | export default website; 13 | -------------------------------------------------------------------------------- /packages/copy/src/modules/aiyuke.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp(".+aiyuke.com/news/.+"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | }, 10 | }; 11 | 12 | export default website; 13 | -------------------------------------------------------------------------------- /packages/copy/src/modules/bilibili.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("bilibili"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableOnCopyByCapture(); 9 | }, 10 | }; 11 | 12 | export default website; 13 | -------------------------------------------------------------------------------- /packages/copy/src/modules/boke112.ts: -------------------------------------------------------------------------------- 1 | import dom from "../utils/dom"; 2 | import utils from "../utils/event"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | regexp: new RegExp(".+://boke112.com/post/.+"), 7 | init: function () { 8 | utils.enableOnCopyByCapture(); 9 | const template = ` 10 | 21 | `; 22 | dom.append("head", template); 23 | }, 24 | }; 25 | 26 | export default website; 27 | -------------------------------------------------------------------------------- /packages/copy/src/modules/ciweimao.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("ciweimao"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | utils.enableOnCopy("#J_BookCnt"); 10 | utils.enableOnContextMenu("body"); 11 | utils.enableOnSelectStart("#J_BookCnt"); 12 | }, 13 | }; 14 | 15 | export default website; 16 | -------------------------------------------------------------------------------- /packages/copy/src/modules/cnitpm.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /cnitpm/, 6 | init: function () { 7 | utils.hideButton(); 8 | window.onload = () => { 9 | utils.removeAttributes("body", ["oncopy", "oncontextmenu", "onselectstart"]); 10 | }; 11 | }, 12 | }; 13 | 14 | export default website; 15 | -------------------------------------------------------------------------------- /packages/copy/src/modules/cnki.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("cnki"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableOnContextMenuByCapture(); 9 | utils.enableOnKeyDownByCapture(); 10 | utils.enableOnCopyByCapture(); 11 | }, 12 | }; 13 | 14 | export default website; 15 | -------------------------------------------------------------------------------- /packages/copy/src/modules/common-e.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp(["wjx", "fanyi\\.baidu", "tianqi", "rrdynb", "fuwu7"].join("|")), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | utils.enableOnCopyByCapture(); 10 | utils.enableOnKeyDownByCapture(); 11 | utils.enableOnSelectStartByCapture(); 12 | utils.enableOnContextMenuByCapture(); 13 | }, 14 | }; 15 | 16 | export default website; 17 | -------------------------------------------------------------------------------- /packages/copy/src/modules/common-p.ts: -------------------------------------------------------------------------------- 1 | import { DOM_STAGE } from "../constant/event"; 2 | import utils from "../utils/event"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | config: { 7 | runAt: DOM_STAGE.START, 8 | }, 9 | regexp: new RegExp(["examcoo"].join("|")), 10 | init: function () { 11 | utils.hideButton(); 12 | utils.enableUserSelectByCSS(); 13 | utils.enableOnCopyByCapture(); 14 | utils.enableOnKeyDownByCapture(); 15 | utils.enableOnSelectStartByCapture(); 16 | utils.enableOnContextMenuByCapture(); 17 | }, 18 | }; 19 | 20 | export default website; 21 | -------------------------------------------------------------------------------- /packages/copy/src/modules/common.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp( 6 | [ 7 | "commandlinux", 8 | "cnki", 9 | "ruiwen", 10 | "oh100", 11 | "fwsir", 12 | "wenxm", 13 | "unjs", 14 | "ahsrst", 15 | "yjbys", 16 | "360doc", 17 | "850500", 18 | "jianbiaoku", 19 | "kt250", 20 | "kejudati", 21 | "baibeike", 22 | "yuque", 23 | "cnrencai", 24 | "kodiplayer", 25 | "tongxiehui", 26 | "ndPureView", 27 | "jianshu", 28 | "linovelib", 29 | "chazidian", 30 | "juejin", 31 | "zgbk", 32 | "wenmi", 33 | "yuedu\\.baidu", 34 | "inrrp", 35 | "shubaoc", 36 | "51cto", 37 | "ximalaya", 38 | "xiexiebang", 39 | "ddwk8", 40 | "php\\.cn", 41 | "fanqienovel\\.com/reader", 42 | "cooco\\.net\\.cn", 43 | "mobiletrain", 44 | "xiangqiqipu", 45 | "m\\.163\\.com", 46 | "aipiaxi", 47 | "wenku\\.csdn\\.net", 48 | "xiaoyuzhoufm\\.com", 49 | "mcmod\\.cn", 50 | "zsxq\\.com", 51 | "volcengine\\.com", 52 | "lyrical-nonsense\\.com", 53 | "xueqiu\\.com", 54 | "php\\.cn", 55 | "51cto\\.com", 56 | "educoder\\.net", 57 | ].join("|") 58 | ), 59 | init: function () { 60 | utils.hideButton(); 61 | utils.enableUserSelectByCSS(); 62 | utils.enableOnCopyByCapture(); 63 | }, 64 | }; 65 | 66 | export default website; 67 | -------------------------------------------------------------------------------- /packages/copy/src/modules/csdn.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /csdn/, 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableOnCopyByCapture(); 9 | utils.enableUserSelectByCSS(); 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/copy/src/modules/diyifanwen.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /diyifanwen/, 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableOnCopyByCapture(); 9 | utils.enableOnKeyDownByCapture(); 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/copy/src/modules/doc88.ts: -------------------------------------------------------------------------------- 1 | import dom from "../utils/dom"; 2 | import type { Website } from "../websites"; 3 | 4 | /*! 5 | * 外部引用 static.doc88.com 声明 6 | * 此部分是在处理 doc88.com 才会加载的资源文件,此资源文件由该网站加载时提供 7 | */ 8 | 9 | let path = ""; 10 | const website: Website = { 11 | regexp: /.*doc88\.com\/.+/, 12 | init: () => { 13 | dom.append( 14 | "body", 15 | `` 16 | ); 17 | GM_xmlhttpRequest({ 18 | method: "GET", 19 | url: "https://res3.doc88.com/resources/js/modules/main-v2.min.js?v=3.55", 20 | onload: function (response) { 21 | const result = /\("#cp_textarea"\).val\(([\S]*?)\);/.exec(response.responseText); 22 | if (result) path = result[1]; 23 | }, 24 | }); 25 | window.addEventListener("load", () => { 26 | const cpFn = unsafeWindow.copyText.toString(); 27 | const fnResult = /'\+([\S]*?)\+"<\/textarea>/.exec(cpFn); 28 | if (fnResult) path = fnResult[1]; 29 | }); 30 | }, 31 | getSelectedText: (): string => { 32 | let select = unsafeWindow; 33 | path.split(".").forEach((v: string) => { 34 | select = select[v]; 35 | }); 36 | if (!select) { 37 | unsafeWindow.Config.vip = 1; 38 | unsafeWindow.Config.logined = 1; 39 | dom.remove("#copy-element-hide"); 40 | } 41 | return select; 42 | }, 43 | }; 44 | 45 | export default website; 46 | -------------------------------------------------------------------------------- /packages/copy/src/modules/docin.ts: -------------------------------------------------------------------------------- 1 | import { PAGE_LOADED } from "../constant/event"; 2 | import dom from "../utils/dom"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | regexp: new RegExp("docin.com/.*"), 7 | config: { 8 | initCopyEvent: false, 9 | captureInstance: true, 10 | delay: 100, 11 | }, 12 | init: function () { 13 | window.addEventListener(PAGE_LOADED, () => dom.query("#j_select")?.click()); 14 | dom.append("head", ""); 15 | }, 16 | getSelectedText: function () { 17 | if (unsafeWindow.docinReader && unsafeWindow.docinReader.st) { 18 | return unsafeWindow.docinReader.st; 19 | } 20 | return ""; 21 | }, 22 | }; 23 | 24 | export default website; 25 | -------------------------------------------------------------------------------- /packages/copy/src/modules/docqq.ts: -------------------------------------------------------------------------------- 1 | import { TEXT_HTML, TEXT_PLAIN } from "../utils/copy"; 2 | import { instance } from "../utils/instance"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | regexp: /.*docs\.qq\.com\/(doc)|(sheet)\/.+/, 7 | config: { 8 | initCopyEvent: false, 9 | captureInstance: true, 10 | delay: 100, 11 | }, 12 | init: function () { 13 | window.onload = () => { 14 | instance.disable(); 15 | }; 16 | }, 17 | getSelectedText: function () { 18 | // QQ Doc 19 | if (unsafeWindow.pad && unsafeWindow.pad.editor && !unsafeWindow.pad.editor.isCopyable()) { 20 | instance.enable(); 21 | const editor = unsafeWindow.pad.editor; 22 | if (editor.getCopyContent) { 23 | const content = editor.getCopyContent() || {}; 24 | const plainText: string = content.plain || ""; 25 | const htmlText: string = content.html || ""; 26 | return { 27 | [TEXT_PLAIN]: plainText, 28 | [TEXT_HTML]: htmlText, 29 | }; 30 | } else { 31 | editor._docEnv.copyable = true; 32 | editor.clipboardManager.copy(); 33 | const plainText: string = editor.clipboardManager.customClipboard.plain || ""; 34 | const htmlText: string = editor.clipboardManager.customClipboard.html || ""; 35 | editor._docEnv.copyable = false; 36 | return { 37 | [TEXT_PLAIN]: plainText, 38 | [TEXT_HTML]: htmlText, 39 | }; 40 | } 41 | } 42 | // QQ Sheet 43 | if ( 44 | unsafeWindow.SpreadsheetApp && 45 | unsafeWindow.SpreadsheetApp.permissions && 46 | unsafeWindow.SpreadsheetApp.permissions.sheetStatus && 47 | unsafeWindow.SpreadsheetApp.permissions.sheetStatus.canCopy === false && 48 | unsafeWindow.SpreadsheetApp.permissions.sheetStatus.canEdit && 49 | unsafeWindow.SpreadsheetApp.permissions.sheetStatus.canEdit() === false 50 | ) { 51 | instance.enable(); 52 | const SpreadsheetApp = unsafeWindow.SpreadsheetApp; 53 | const [selection] = SpreadsheetApp.view.getSelectionRanges(); 54 | if (selection) { 55 | const text: string[] = []; 56 | const { startColIndex, startRowIndex, endColIndex, endRowIndex } = selection; 57 | for (let i = startRowIndex; i <= endRowIndex; i++) { 58 | for (let k = startColIndex; k <= endColIndex; k++) { 59 | const cell = SpreadsheetApp.workbook.activeSheet.getCellDataAtPosition(i, k); 60 | if (!cell) continue; 61 | text.push(" ", cell.formattedValue?.value || cell.value || ""); 62 | } 63 | i !== endRowIndex && text.push("\n"); 64 | } 65 | const str = text.join(""); 66 | return /^\s*$/.test(str) ? "" : str; 67 | } 68 | } 69 | // Final 70 | return ""; 71 | }, 72 | }; 73 | 74 | export default website; 75 | 76 | // SpreadsheetApp.feature._copyPaste.copyPasteCache.onCopy({ 77 | // selectGridRange: SpreadsheetApp.view.getSelectionRanges()[0], 78 | // copyType: 0, 79 | // app: SpreadsheetApp.feature.app, 80 | // isSelectAll: undefined, 81 | // }); 82 | // SpreadsheetApp.sheetStatus.workbookStatus.status.setCanCopy(true); 83 | -------------------------------------------------------------------------------- /packages/copy/src/modules/edu30.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /.*30edu\.com\.cn\/.+/, 6 | init: function () { 7 | window.onload = () => { 8 | const iframes = document.getElementsByTagName("iframe"); 9 | if (iframes.length === 2) { 10 | const body = iframes[1].contentWindow?.document.querySelector("body"); 11 | body && utils.removeAttributes(body, ["oncopy", "oncontextmenu", "onselectstart"]); 12 | } 13 | }; 14 | }, 15 | }; 16 | 17 | export default website; 18 | -------------------------------------------------------------------------------- /packages/copy/src/modules/kdocs.ts: -------------------------------------------------------------------------------- 1 | import { DOM_STAGE } from "../constant/event"; 2 | import type { Website } from "../websites"; 3 | 4 | export const kdoc: Website = { 5 | config: { 6 | runAt: DOM_STAGE.START, 7 | }, 8 | regexp: new RegExp("kdocs"), 9 | init: function () { 10 | const patch = () => { 11 | unsafeWindow.APP && (unsafeWindow.APP.canCopy = () => true); 12 | }; 13 | if (unsafeWindow.APP) { 14 | patch(); 15 | } else { 16 | let APP: unknown = undefined; 17 | Object.defineProperty(unsafeWindow, "APP", { 18 | configurable: false, 19 | set: (value: unknown) => { 20 | APP = value; 21 | value && patch(); 22 | }, 23 | get: () => APP, 24 | }); 25 | } 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/copy/src/modules/leetcode.ts: -------------------------------------------------------------------------------- 1 | import { PAGE_LOADED } from "../constant/event"; 2 | import utils from "../utils/event"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | regexp: new RegExp("leetcode"), 7 | init: function () { 8 | utils.hideButton(); 9 | window.addEventListener(PAGE_LOADED, () => { 10 | utils.enableOnCopy("#lc-home"); 11 | utils.enableOnCopy("[data-layout-path='/ts0/t1']"); 12 | }); 13 | }, 14 | }; 15 | 16 | export default website; 17 | -------------------------------------------------------------------------------- /packages/copy/src/modules/mbalib.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /mbalib/, 6 | init: function () { 7 | window.onload = () => { 8 | utils.removeAttributes("fullScreenContainer", ["oncopy", "oncontextmenu", "onselectstart"]); 9 | }; 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/copy/src/modules/mihoyo.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp(".+bbs.mihoyo.com/.+"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableOnCopyByCapture(); 9 | utils.enableOnSelectStartByCapture(); 10 | utils.enableUserSelectByCSS(); 11 | }, 12 | }; 13 | 14 | export default website; 15 | -------------------------------------------------------------------------------- /packages/copy/src/modules/qidian.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("qidian"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | utils.enableOnCopy(".main-read-container"); 10 | utils.enableOnContextMenu(".main-read-container"); 11 | }, 12 | }; 13 | 14 | export default website; 15 | -------------------------------------------------------------------------------- /packages/copy/src/modules/qq-slider.ts: -------------------------------------------------------------------------------- 1 | import { instance } from "../utils/instance"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /.*docs\.qq\.com\/slide\/.+/, 6 | config: { 7 | initCopyEvent: false, 8 | captureInstance: true, 9 | runAt: "document-start", 10 | }, 11 | init: function () { 12 | let webpackJsonp = unsafeWindow.webpackJsonp; 13 | Object.defineProperty(unsafeWindow, "webpackJsonp", { 14 | get() { 15 | return webpackJsonp; 16 | }, 17 | set(newValue) { 18 | if (newValue.push.__HOOKED__) { 19 | return; 20 | } 21 | webpackJsonp = newValue; 22 | const originPush = webpackJsonp.push; 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | function push(...args: any[]) { 25 | const [, mods] = args[0]; 26 | for (const [key, fn] of Object.entries(mods)) { 27 | const stringifyFn = String(fn); 28 | if (/this\.shouldResponseCopy\(/.test(stringifyFn)) { 29 | const next = stringifyFn.replace(/this\.shouldResponseCopy\(/g, "(() => true)("); 30 | // .replace(/this.storeManager=/g, "window.storeManager=this.storeManager=") 31 | mods[key] = new Function(`return (${next})`)(); 32 | } 33 | } 34 | // @ts-expect-error this 35 | return originPush.call(this, ...args); 36 | } 37 | push.__HOOKED__ = 1; 38 | webpackJsonp.push = push; 39 | }, 40 | }); 41 | window.onload = () => { 42 | instance.disable(); 43 | }; 44 | }, 45 | }; 46 | 47 | export default website; 48 | -------------------------------------------------------------------------------- /packages/copy/src/modules/qqbook.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("book\\.qq"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableOnCopy("body"); 9 | utils.enableUserSelectByCSS(); 10 | utils.enableOnContextMenu("body"); 11 | utils.enableOnSelectStart("body"); 12 | }, 13 | }; 14 | 15 | export default website; 16 | -------------------------------------------------------------------------------- /packages/copy/src/modules/sf.ts: -------------------------------------------------------------------------------- 1 | import dom from "../utils/dom"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /.*segmentfault\.com\/.+/, 6 | init: function () { 7 | const body = dom.query("body"); 8 | if (body) { 9 | body.classList.add("_sf_adjust_body"); 10 | body.onclick = () => { 11 | body.style.paddingRight = "0"; 12 | }; 13 | } 14 | }, 15 | }; 16 | 17 | export default website; 18 | -------------------------------------------------------------------------------- /packages/copy/src/modules/uemeds.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp(".+www.uemeds.cn/.+"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | }, 10 | }; 11 | 12 | export default website; 13 | -------------------------------------------------------------------------------- /packages/copy/src/modules/utaten.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("utaten"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | utils.enableOnSelectStartByCapture(); 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/copy/src/modules/wk.ts: -------------------------------------------------------------------------------- 1 | import dom from "../utils/dom"; 2 | import utils from "../utils/event"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | regexp: /.*wk\.baidu\.com\/view\/.+/, 7 | init: function () { 8 | utils.hideButton(); 9 | utils.enableOnSelectStartByCapture(); 10 | window.onload = () => { 11 | dom.attr(".sf-edu-wenku-vw-container", "style", ""); 12 | }; 13 | }, 14 | }; 15 | 16 | export default website; 17 | -------------------------------------------------------------------------------- /packages/copy/src/modules/xiaohongshu.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("xiaohongshu"), 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | utils.enableOnKeyDownByCapture(); 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/copy/src/modules/youdao.ts: -------------------------------------------------------------------------------- 1 | import { MOUSE_DOWN, MOUSE_MOVE } from "../constant/event"; 2 | import utils, { stopNativePropagation } from "../utils/event"; 3 | import type { Website } from "../websites"; 4 | 5 | const website: Website = { 6 | config: { 7 | initCopyEvent: false, 8 | }, 9 | regexp: /note\.youdao\.com\/newEditorV1\/bulb\.html.*/, 10 | init: function () { 11 | utils.hideButton(); 12 | if (window.parent && window.parent.location.href.indexOf("ynoteshare") > -1) { 13 | utils.enableUserSelectByCSS(); 14 | document.addEventListener(MOUSE_DOWN, stopNativePropagation, true); 15 | document.addEventListener(MOUSE_MOVE, stopNativePropagation, true); 16 | } 17 | }, 18 | }; 19 | 20 | export default website; 21 | -------------------------------------------------------------------------------- /packages/copy/src/modules/zhihu.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: /.*zhihu\.com\/.*/, 6 | init: function () { 7 | utils.hideButton(); 8 | utils.enableUserSelectByCSS(); 9 | utils.enableOnCopyByCapture(); 10 | 11 | if (location.hostname === "zhuanlan.zhihu.com") { 12 | const removeFocalPointModal: MutationCallback = mutationsList => { 13 | for (const mutation of mutationsList) { 14 | const addedNodes = mutation.addedNodes; 15 | for (let i = 0; i < addedNodes.length; i++) { 16 | const target = addedNodes[i]; 17 | if (target.nodeType != 1) return void 0; 18 | if ( 19 | target instanceof HTMLDivElement && 20 | target.querySelector("[data-focus-scope-start]") 21 | ) { 22 | const element = target.querySelector("[data-focus-scope-start]"); 23 | element && 24 | element.parentElement && 25 | element.parentElement.textContent && 26 | element.parentElement.textContent.indexOf("立即登录/注册") > -1 && 27 | element.parentElement.parentElement && 28 | element.parentElement.parentElement.removeChild(element.parentElement); 29 | } 30 | } 31 | } 32 | }; 33 | const observer = new MutationObserver(removeFocalPointModal); 34 | observer.observe(document, { childList: true, subtree: true }); 35 | } 36 | }, 37 | }; 38 | 39 | export default website; 40 | 41 | // Force Focus `Login Button` // `focusout` Event Bubbles 42 | // https://github.com/adobe/react-spectrum/blob/e4bc3269fa41aa096700445c6bfa9c8620545e6a/packages/%40react-aria/focus/src/FocusScope.tsx#L179 43 | -------------------------------------------------------------------------------- /packages/copy/src/modules/zongheng.ts: -------------------------------------------------------------------------------- 1 | import utils from "../utils/event"; 2 | import type { Website } from "../websites"; 3 | 4 | const website: Website = { 5 | regexp: new RegExp("zongheng"), 6 | init: function () { 7 | utils.removeAttributes(".reader_box", ["style", "unselectable", "onselectstart"]); 8 | utils.removeAttributes(".reader_main", ["style", "unselectable", "onselectstart"]); 9 | utils.hideButton(); 10 | utils.enableOnKeyDown("body"); 11 | utils.enableUserSelectByCSS(); 12 | utils.enableOnCopy(".content"); 13 | utils.enableOnContextMenu("body"); 14 | utils.enableOnSelectStart(".content"); 15 | }, 16 | }; 17 | 18 | export default website; 19 | -------------------------------------------------------------------------------- /packages/copy/src/styles/app.css: -------------------------------------------------------------------------------- 1 | .__copy-button { 2 | width: 60px; 3 | height: 30px; 4 | background: #4C98F7; 5 | color: #fff; 6 | position: absolute; 7 | z-index: -1000; 8 | opacity: 0; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | border-radius: 3px; 13 | font-size: 13px; 14 | cursor: pointer; 15 | transition: opacity 0.3s; 16 | } -------------------------------------------------------------------------------- /packages/copy/src/styles/style.css: -------------------------------------------------------------------------------- 1 | div[id^=reader-helper], 2 | #sfModal, 3 | #select-tooltip{ 4 | display: none !important; 5 | } 6 | 7 | .modal-open{ 8 | overflow: auto !important; 9 | } 10 | 11 | ._sf_adjust_body{ 12 | padding-right: 0 !important; 13 | } -------------------------------------------------------------------------------- /packages/copy/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Document { 2 | selection: { 3 | createRange: () => { 4 | text: string; 5 | }; 6 | }; 7 | } 8 | 9 | declare function GM_xmlhttpRequest(params: { 10 | method: GET | POST; 11 | url: string; 12 | onload: (response: { status: number; responseText: string }) => void; 13 | }): void; 14 | 15 | declare const unsafeWindow = any; 16 | 17 | declare module "*.css" { 18 | const content: string; 19 | export default content; 20 | } 21 | -------------------------------------------------------------------------------- /packages/copy/src/utils/copy.ts: -------------------------------------------------------------------------------- 1 | import { COPY } from "../constant/event"; 2 | import { isString } from "./is"; 3 | 4 | type CopyData = { 5 | "text/plain": string; 6 | "text/html"?: string; 7 | } & Record; 8 | 9 | export type CopyParams = CopyData | string; 10 | 11 | export const TEXT_PLAIN = "text/plain"; 12 | export const TEXT_HTML = "text/html"; 13 | 14 | const execCopyCommand = (data: CopyData) => { 15 | const textarea = document.createElement("textarea"); 16 | const handler = (event: ClipboardEvent) => { 17 | event.preventDefault(); 18 | event.stopImmediatePropagation(); 19 | for (const [key, value] of Object.entries(data)) { 20 | event.clipboardData && event.clipboardData.setData(key, value); 21 | } 22 | }; 23 | textarea.addEventListener(COPY, handler, true); 24 | textarea.style.position = "fixed"; 25 | textarea.style.left = "-999999999px"; 26 | textarea.style.top = "-999999999px"; 27 | textarea.value = data[TEXT_PLAIN] || " "; 28 | document.body.appendChild(textarea); 29 | textarea.select(); 30 | document.execCommand("copy"); 31 | textarea.removeEventListener(COPY, handler); 32 | document.body.removeChild(textarea); 33 | }; 34 | 35 | export const isEmptyContent = (data: CopyParams): boolean => { 36 | if (!data) return true; 37 | return isString(data) ? !data : !data[TEXT_PLAIN]; 38 | }; 39 | 40 | export const copy = (data: CopyParams): boolean => { 41 | const params: CopyData = isString(data) ? { [TEXT_PLAIN]: data } : data; 42 | const plainText = params[TEXT_PLAIN]; 43 | if (!plainText) return false; 44 | if (navigator.clipboard && window.ClipboardItem) { 45 | const dataItems: Record = {}; 46 | for (const [key, value] of Object.entries(params)) { 47 | const blob = new Blob([value], { type: key }); 48 | dataItems[key] = blob; 49 | } 50 | navigator.clipboard.write([new ClipboardItem(dataItems)]).catch(() => { 51 | execCopyCommand(params); 52 | }); 53 | } else { 54 | execCopyCommand(params); 55 | } 56 | return true; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/copy/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "./is"; 2 | 3 | export const dom = { 4 | query: function (selector: string): null | HTMLElement { 5 | return document.querySelector(selector); 6 | }, 7 | attr: function (selector: string, attr: string, value: string): void { 8 | const dom = document.querySelector(selector); 9 | dom && dom.setAttribute(attr, value); 10 | }, 11 | append: function (selector: string, content: Element | string): HTMLDivElement { 12 | const container = document.createElement("div"); 13 | if (isString(content)) { 14 | container.innerHTML = content; 15 | } else { 16 | container.appendChild(content); 17 | } 18 | const targetDOM = document.querySelector(selector); 19 | targetDOM && targetDOM.append(container); 20 | return container; 21 | }, 22 | remove: function (selector: string): void { 23 | const targetDOM = document.querySelector(selector); 24 | targetDOM && targetDOM.remove(); 25 | }, 26 | }; 27 | 28 | export default dom; 29 | -------------------------------------------------------------------------------- /packages/copy/src/utils/event.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU, COPY, KEY_DOWN, PAGE_LOADED, SELECT_START } from "../constant/event"; 2 | import instance from "./instance"; 3 | import { isString } from "./is"; 4 | 5 | export const stopNativePropagation = (event: Event): void => { 6 | // event.stopImmediatePropagation(); // 即停且阻止该元素在此事件绑定之后的`on`同类事件触发 7 | event.stopPropagation(); // 阻止该元素继续冒泡后的`on`同类事件触发 8 | }; 9 | 10 | export const event = { 11 | hideButton: (): void => { 12 | instance.disable(); 13 | }, 14 | showButton: (): void => { 15 | instance.enable(); 16 | }, 17 | removeAttributes: (selector: string | HTMLElement, attr: string[] = []): void => { 18 | const dom = isString(selector) ? document.querySelector(selector) : selector; 19 | dom && attr.forEach(item => dom.removeAttribute(item)); 20 | }, 21 | enableUserSelectByCSS: (): void => { 22 | const css = "*{user-select: auto !important;-webkit-user-select: auto !important;}"; 23 | const style = document.createElement("style"); 24 | style.innerText = css; 25 | const head = document.getElementsByTagName("head")[0]; 26 | if (head) { 27 | head.appendChild(style); 28 | } else { 29 | window.addEventListener(PAGE_LOADED, () => 30 | document.getElementsByTagName("head")[0].appendChild(style) 31 | ); 32 | } 33 | }, 34 | enableOnSelectStart: (selector: string): void => { 35 | const dom = document.querySelector(selector); 36 | dom && dom.addEventListener(SELECT_START, stopNativePropagation); 37 | }, 38 | enableOnContextMenu: (selector: string): void => { 39 | const dom = document.querySelector(selector); 40 | dom && dom.addEventListener(CONTEXT_MENU, stopNativePropagation); 41 | }, 42 | enableOnCopy: (selector: string): void => { 43 | const dom = document.querySelector(selector); 44 | dom && dom.addEventListener(COPY, stopNativePropagation); 45 | }, 46 | enableOnKeyDown: (selector: string): void => { 47 | const dom = document.querySelector(selector) as HTMLDivElement; 48 | dom && 49 | dom.addEventListener(KEY_DOWN, (e: KeyboardEvent) => { 50 | if (e.key === "c" && e.ctrlKey) return e.stopPropagation(); 51 | }); 52 | }, 53 | enableOnSelectStartByCapture: (): void => { 54 | window.addEventListener(SELECT_START, stopNativePropagation, true); 55 | document.addEventListener(SELECT_START, stopNativePropagation, true); 56 | }, 57 | enableOnContextMenuByCapture: (): void => { 58 | window.addEventListener(CONTEXT_MENU, stopNativePropagation, true); 59 | document.addEventListener(CONTEXT_MENU, stopNativePropagation, true); 60 | }, 61 | enableOnCopyByCapture: (): void => { 62 | window.addEventListener(COPY, stopNativePropagation, true); 63 | document.addEventListener(COPY, stopNativePropagation, true); 64 | }, 65 | enableOnKeyDownByCapture: (): void => { 66 | document.addEventListener( 67 | KEY_DOWN, 68 | e => e.ctrlKey && e.key.toLocaleUpperCase() === "C" && e.stopPropagation(), 69 | true 70 | ); 71 | }, 72 | }; 73 | 74 | export default event; 75 | -------------------------------------------------------------------------------- /packages/copy/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-nocheck 3 | 4 | // =========================================================================== // 5 | 6 | /** 7 | * Scan Object 8 | */ 9 | const weakSet = new WeakSet(); 10 | const pathRouter = ["root"]; 11 | 12 | const deepScanObject = (origin, deep, maxDeep) => { 13 | if (deep > maxDeep) return; 14 | for (const item in origin) { 15 | try { 16 | const value = origin[item]; 17 | if (value && typeof value === "object") { 18 | if (weakSet.has(value)) continue; 19 | weakSet.add(value); 20 | pathRouter.push(item); 21 | deepScanObject(value, deep + 1, maxDeep); 22 | pathRouter.pop(); 23 | } else { 24 | const regexp = /以熔体流动速率/; 25 | if (regexp.test(item) || regexp.test(value)) { 26 | console.log(pathRouter.join("/") + "/" + item, "================", value); 27 | } 28 | } 29 | } catch (e) { 30 | console.warn(e); 31 | } 32 | } 33 | }; 34 | 35 | deepScanObject.toString = () => ""; 36 | console.log("start"); 37 | deepScanObject(window, 0, 10); 38 | console.log("finish"); 39 | 40 | // =========================================================================== // 41 | 42 | /** 43 | * Read Clipboard 44 | */ 45 | setTimeout(() => { 46 | navigator.clipboard.read().then(res => { 47 | for (const item of res) { 48 | const types = item.types; 49 | for (const type of types) { 50 | item.getType(type).then(data => { 51 | const reader = new FileReader(); 52 | reader.readAsText(data, "utf-8"); 53 | reader.onload = () => { 54 | console.info(type, reader.result); 55 | }; 56 | }); 57 | } 58 | } 59 | }); 60 | }, 2000); 61 | 62 | // =========================================================================== // 63 | 64 | /** 65 | * Remove Listeners 66 | */ 67 | const removeAllEventListeners = (element, ancestor = false) => { 68 | const listeners = getEventListeners(element); 69 | Object.keys(listeners).forEach(event => { 70 | listeners[event].forEach(listener => { 71 | element.removeEventListener(event, listener.listener, { 72 | capture: listener.useCapture, 73 | once: listener.once, 74 | passive: listener.passive, 75 | }); 76 | }); 77 | }); 78 | ancestor && element === document && window && removeAllEventListeners(window, ancestor); 79 | ancestor && 80 | element && 81 | element.parentNode && 82 | removeAllEventListeners(element.parentNode, ancestor); 83 | }; 84 | removeAllEventListeners($0, true); 85 | 86 | const elements = [...document.querySelectorAll("*")]; 87 | for (const element of elements) { 88 | element instanceof HTMLElement && removeAllEventListeners(element); 89 | } 90 | 91 | // =========================================================================== // 92 | 93 | /** 94 | * Remove Attributes 95 | */ 96 | const elements = [...document.querySelectorAll("*")]; 97 | for (const element of elements) { 98 | if (element instanceof HTMLElement) { 99 | const attrs = element.getAttributeNames(); 100 | attrs.forEach(item => element.removeAttribute(item)); 101 | } 102 | } 103 | 104 | // =========================================================================== // 105 | 106 | /** 107 | * Hook Function Call 108 | */ 109 | let index = 0; 110 | const log = window.console.log; 111 | (function reset() { 112 | index = 0; 113 | setTimeout(reset, 3000); 114 | })(); 115 | const native = [Array.prototype.slice, Object.prototype.toString, Object.prototype.hasOwnProperty]; 116 | Function.prototype.call = function (dynamic, ...args) { 117 | if ( 118 | !dynamic || 119 | typeof dynamic !== "object" || 120 | native.includes(this) || 121 | !args.length || 122 | dynamic.nodeType 123 | ) { 124 | return Reflect.apply(this, dynamic, args); 125 | } 126 | index++; 127 | if (index < 30) { 128 | log("__dynamic", dynamic); 129 | log("__args", args); 130 | log("__this", this); 131 | } 132 | return Reflect.apply(this, dynamic, args); 133 | }; 134 | 135 | // =========================================================================== // 136 | 137 | /** 138 | * Read Clipboard On Paste 139 | */ 140 | const input = document.createElement("input"); 141 | input.style.position = "fixed"; 142 | input.style.top = "100px"; 143 | input.style.right = "10px"; 144 | input.style.zIndex = "999999"; 145 | input.style.width = "200px"; 146 | input.placeholder = "Read Clipboard On Paste"; 147 | input.addEventListener("paste", event => { 148 | const clipboardData = event.clipboardData || window.clipboardData; 149 | for (const item of clipboardData.items) { 150 | console.log(`%c${item.type}`, "background-color: #165DFF; color: #fff; padding: 3px 5px;"); 151 | console.log(item.kind === "file" ? item.getAsFile() : clipboardData.getData(item.type)); 152 | } 153 | }); 154 | document.body.appendChild(input); 155 | 156 | // =========================================================================== // 157 | -------------------------------------------------------------------------------- /packages/copy/src/utils/instance.ts: -------------------------------------------------------------------------------- 1 | import type { CopyParams } from "./copy"; 2 | import { copy } from "./copy"; 3 | 4 | class Instance { 5 | private readonly id = "__copy"; 6 | private readonly className = "__copy-button"; 7 | private isReadyToHidden = false; 8 | private dom: null | HTMLDivElement = null; 9 | 10 | public enable = (): void => { 11 | const dom = this.getInstance(); 12 | dom.style.display = "flex"; 13 | }; 14 | 15 | public disable = (): void => { 16 | const dom = this.getInstance(); 17 | dom.style.display = "none"; 18 | }; 19 | 20 | public destroy = (): void => { 21 | const el = this.getInstance(); 22 | el.remove(); 23 | this.dom = null; 24 | }; 25 | 26 | private init = (name?: string) => { 27 | const container = document.createElement("div"); 28 | container.id = this.id; 29 | container.className = this.className; 30 | container.innerText = name || "复制"; 31 | container.addEventListener("mouseup", e => e.stopPropagation(), true); 32 | container.addEventListener("mousedown", e => e.stopPropagation(), true); 33 | this.dom = container; 34 | document.body.appendChild(this.dom); 35 | }; 36 | 37 | public getInstance = (): HTMLDivElement => { 38 | if (this.dom === null) { 39 | this.init(); 40 | } 41 | return this.dom; 42 | }; 43 | 44 | public show = (event: MouseEvent): void => { 45 | if (this.isReadyToHidden) return void 0; 46 | const dom = this.getInstance(); 47 | dom.style.left = `${event.pageX + 30}px`; 48 | dom.style.top = `${event.pageY}px`; 49 | dom.style.opacity = "1"; 50 | dom.style.zIndex = "1000"; 51 | }; 52 | 53 | public hide = (keep: number | false = 350): void => { 54 | const dom = this.getInstance(); 55 | dom.style.opacity = "0"; 56 | if (keep) { 57 | this.isReadyToHidden = true; 58 | setTimeout(() => { 59 | dom.style.zIndex = "-10000"; 60 | this.isReadyToHidden = false; 61 | }, keep); 62 | } 63 | }; 64 | 65 | public onCopy = (content: CopyParams, event: MouseEvent): void => { 66 | const dom = this.getInstance(); 67 | this.show(event); 68 | dom.onclick = () => { 69 | copy(content); 70 | this.hide(); 71 | }; 72 | }; 73 | } 74 | 75 | export const instance = new Instance(); 76 | 77 | export default instance; 78 | -------------------------------------------------------------------------------- /packages/copy/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const opt = Object.prototype.toString; 2 | 3 | export const isEmptyValue = (value: unknown): value is undefined | null => { 4 | return value === null || value === void 0; 5 | }; 6 | 7 | export function isObject(value: unknown): value is Record { 8 | return opt.call(value) === "[object Object]"; 9 | } 10 | 11 | export function isArray(value: unknown): value is unknown[] { 12 | return opt.call(value) === "[object Array]"; 13 | } 14 | 15 | export function isNumber(value: unknown): value is number { 16 | return opt.call(value) === "[object Number]"; 17 | } 18 | 19 | export function isPlainNumber(value: unknown): value is number { 20 | return /^(-|\+)?\d+(\.\d+)?$/.test(String(value)); 21 | } 22 | 23 | export function isString(value: unknown): value is string { 24 | return opt.call(value) === "[object String]"; 25 | } 26 | -------------------------------------------------------------------------------- /packages/copy/src/websites.ts: -------------------------------------------------------------------------------- 1 | import type { CopyParams } from "./utils/copy"; 2 | import doc88 from "./modules/doc88"; 3 | import sf from "./modules/sf"; 4 | import wk from "./modules/wk"; 5 | import zhihu from "./modules/zhihu"; 6 | import edu30 from "./modules/edu30"; 7 | import docqq from "./modules/docqq"; 8 | import qqSlider from "./modules/qq-slider"; 9 | import boke112 from "./modules/boke112"; 10 | import diyifanwen from "./modules/diyifanwen"; 11 | import mbalib from "./modules/mbalib"; 12 | import cnitpm from "./modules/cnitpm"; 13 | import mihoyo from "./modules/mihoyo"; 14 | import uemeds from "./modules/uemeds"; 15 | import aiyuke from "./modules/aiyuke"; 16 | import qidian from "./modules/qidian"; 17 | import zongheng from "./modules/zongheng"; 18 | import k17 from "./modules/17k"; 19 | import ciweimao from "./modules/ciweimao"; 20 | import qqbook from "./modules/qqbook"; 21 | import utaten from "./modules/utaten"; 22 | import baiduwk from "./modules/baiduwk"; 23 | import xiaohongshu from "./modules/xiaohongshu"; 24 | import leetcode from "./modules/leetcode"; 25 | import csdn from "./modules/csdn"; 26 | import bilibili from "./modules/bilibili"; 27 | import cnki from "./modules/cnki"; 28 | import docin from "./modules/docin"; 29 | import youdao from "./modules/youdao"; 30 | import common from "./modules/common"; 31 | import commonEnhance from "./modules/common-e"; 32 | import commonPreLoad from "./modules/common-p"; 33 | import { kdoc } from "./modules/kdocs"; 34 | 35 | export interface WebsiteConfig { 36 | initCopyEvent?: boolean; 37 | runAt?: "document-start" | "document-end"; 38 | captureInstance?: boolean; 39 | delay?: number; 40 | } 41 | export interface Website { 42 | config?: WebsiteConfig; 43 | regexp: RegExp; 44 | init: () => void; 45 | getSelectedText?: () => CopyParams; 46 | } 47 | 48 | const websites: Website[] = [ 49 | sf, 50 | wk, 51 | zhihu, 52 | edu30, 53 | qqSlider, 54 | docqq, 55 | boke112, 56 | diyifanwen, 57 | mbalib, 58 | cnitpm, 59 | mihoyo, 60 | uemeds, 61 | aiyuke, 62 | qidian, 63 | zongheng, 64 | k17, 65 | ciweimao, 66 | qqbook, 67 | utaten, 68 | baiduwk, 69 | xiaohongshu, 70 | doc88, 71 | leetcode, 72 | csdn, 73 | bilibili, 74 | cnki, 75 | docin, 76 | youdao, 77 | kdoc, 78 | common, 79 | commonEnhance, 80 | commonPreLoad, 81 | ]; 82 | 83 | export default websites; 84 | -------------------------------------------------------------------------------- /packages/copy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts", "./src/**/*.tsx"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/expansion/README.md: -------------------------------------------------------------------------------- 1 | # 自动展开阅读全文 2 | 3 | 展开阅读全文,用于`CSDN`、知乎。 4 | -------------------------------------------------------------------------------- /packages/expansion/expansion.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 自动阅读全文 3 | // @namespace https://github.com/WindrunnerMax/TKScript 4 | // @version 1.0 5 | // @description try to take over the world! 6 | // @author Czy 7 | // @match https://blog.csdn.net/* 8 | // @match https://www.zhihu.com/* 9 | // @grant none 10 | // ==/UserScript== 11 | 12 | function show(dom) { 13 | dom[0] ? dom[0].click() : ""; 14 | } 15 | 16 | (function () { 17 | "use strict"; 18 | 19 | /* CSDN 展开阅读全文 */ 20 | show(document.getElementsByClassName("btn-readmore")); 21 | 22 | /* 知乎 查看全部回答 */ 23 | setTimeout(() => { 24 | show(document.getElementsByClassName("QuestionMainAction ViewAll-QuestionMainAction")); 25 | }, 1000); 26 | })(); 27 | -------------------------------------------------------------------------------- /packages/expansion/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expansion", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/force-copy/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | build-gecko 3 | -------------------------------------------------------------------------------- /packages/force-copy/README.md: -------------------------------------------------------------------------------- 1 | # Force Copy 2 | 3 | Force Copy professionally circumvents limitations pertaining to Web Copying, Keyboard Usage, and Right-click Functionalities, facilitating an unhindered browsing experience. 4 | 5 | ## Install 6 | 7 | * [Firefox Add-Ons](https://addons.mozilla.org/en-US/firefox/addon/force-copy/) 8 | * [Chrome Web Store](https://chrome.google.com/webstore/detail/force-copy/cceclgeciefpanebkfkogecbjjchmico) 9 | * [GitHub Releases](https://github.com/WindrunnerMax/TKScript/releases) 10 | 11 | ## Release 12 | 13 | ```bash 14 | $ pnpm install 15 | $ pnpm run build # Chromium 16 | $ pnpm run build:gecko # Gecko 17 | ``` 18 | 19 | ## Notice 20 | 21 | 22 | 23 | All functions of the extension are turned off by default. When you need it, click the extension program and select the corresponding module to start it. 24 | 25 | ⭐ Start means that the domain name remains open. Even if the page is closed, it will still remain started the next time it is opened. 26 | ⭐ Once means that it will only take effect in the current Tab page. After closing the Tab page, it will remain closed the next time it is opened. Note that refreshing the page will not turn off the function. This is useful in many situations, especially in rich text editor websites. 27 | 28 | Open source address: https://github.com/WindrunnerMax/TKScript. 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /packages/force-copy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "force-copy", 3 | "version": "1.0.12", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "sideEffects": false, 7 | "scripts": { 8 | "dev": "cross-env NODE_ENV=development rspack build --watch", 9 | "build": "rm -rf build && rspack build", 10 | "dev:rollup": "cross-env NODE_ENV=development rollup -wc", 11 | "dev:gecko": "cross-env NODE_ENV=development PLATFORM=gecko rspack build --watch", 12 | "build:gecko": "rm -rf build-gecko && cross-env PLATFORM=gecko rspack build", 13 | "build:zip": "mkdir -p .zip && rm -f .zip/chromium.zip && cd build && zip -r ../.zip/chromium.zip .", 14 | "build:zip:gecko": "mkdir -p .zip && rm -f .zip/gecko.zip && cd build-gecko && zip -r ../.zip/gecko.zip .", 15 | "lint:ts": "../../node_modules/typescript/bin/tsc --noEmit" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/WindrunnerMax/TKScript" 20 | }, 21 | "dependencies": { 22 | "@arco-design/web-react": "2.50.0", 23 | "copy": "workspace: *", 24 | "copy-currency": "workspace: *", 25 | "laser-utils": "0.0.5-beta.0", 26 | "react": "17.0.2", 27 | "react-dom": "17.0.2" 28 | }, 29 | "devDependencies": { 30 | "@rspack/cli": "0.2.5", 31 | "@rspack/plugin-html": "0.2.5", 32 | "@types/chrome": "0.0.287", 33 | "@types/react": "17.0.2", 34 | "@types/react-dom": "17.0.2", 35 | "less": "3.0.0", 36 | "less-loader": "6.0.0", 37 | "sass": "1.52.3", 38 | "sass-loader": "13.3.2", 39 | "ts-node": "10.9.1" 40 | }, 41 | "lint-staged": { 42 | "*.{js,ts,jsx,tsx}": [ 43 | "eslint --fix" 44 | ], 45 | "*.{css,scss,less}": [ 46 | "stylelint --fix" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/force-copy/public/locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Force Copy" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/force-copy/public/locales/zh_cn/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "message": "Force Copy" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/force-copy/public/misc/brick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/TKScript/23f5b72f77c302f455d08d014c5a2b7a8e75b4a0/packages/force-copy/public/misc/brick.png -------------------------------------------------------------------------------- /packages/force-copy/public/misc/favicon.dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/TKScript/23f5b72f77c302f455d08d014c5a2b7a8e75b4a0/packages/force-copy/public/misc/favicon.dark.png -------------------------------------------------------------------------------- /packages/force-copy/public/misc/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/TKScript/23f5b72f77c302f455d08d014c5a2b7a8e75b4a0/packages/force-copy/public/misc/poster.png -------------------------------------------------------------------------------- /packages/force-copy/public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | POPUP 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/force-copy/public/static/favicon.128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/TKScript/23f5b72f77c302f455d08d014c5a2b7a8e75b4a0/packages/force-copy/public/static/favicon.128.png -------------------------------------------------------------------------------- /packages/force-copy/public/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/TKScript/23f5b72f77c302f455d08d014c5a2b7a8e75b4a0/packages/force-copy/public/static/favicon.png -------------------------------------------------------------------------------- /packages/force-copy/rspack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { FilesPlugin } = require("./script/files"); 3 | const { ReloadPlugin } = require("./script/reload"); 4 | const { ManifestPlugin } = require("./script/manifest"); 5 | const { WrapperCodePlugin } = require("./script/wrapper"); 6 | const { default: HtmlPlugin } = require("@rspack/plugin-html"); 7 | const { getUniqueId, IS_DEV, IS_GECKO } = require("./script/utils/node"); 8 | 9 | const folder = IS_GECKO ? "build-gecko" : "build"; 10 | const EVENT_TYPE = IS_DEV ? "EVENT_TYPE" : getUniqueId(); 11 | const INJECT_FILE = IS_DEV ? "INJECT_FILE" : getUniqueId(); 12 | 13 | process.env.EVENT_TYPE = EVENT_TYPE; 14 | process.env.INJECT_FILE = INJECT_FILE; 15 | process.env.PLATFORM = process.env.PLATFORM || "chromium"; 16 | 17 | /** 18 | * @type {import("@rspack/cli").Configuration} 19 | */ 20 | module.exports = { 21 | context: __dirname, 22 | entry: { 23 | popup: "./src/popup/index.tsx", 24 | content: "./src/content/index.ts", 25 | worker: "./src/worker/index.ts", 26 | [INJECT_FILE]: "./src/inject/index.ts", 27 | }, 28 | plugins: [ 29 | new HtmlPlugin({ 30 | filename: "popup.html", 31 | template: "./public/popup.html", 32 | inject: false, 33 | }), 34 | new FilesPlugin(), 35 | new ReloadPlugin(), 36 | new ManifestPlugin(), 37 | new WrapperCodePlugin(), 38 | ], 39 | resolve: { 40 | alias: { 41 | "@": path.resolve(__dirname, "./src"), 42 | }, 43 | }, 44 | builtins: { 45 | define: { 46 | "__DEV__": JSON.stringify(IS_DEV), 47 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), 48 | "process.env.PLATFORM": JSON.stringify(process.env.PLATFORM), 49 | "process.env.EVENT_TYPE": JSON.stringify(process.env.EVENT_TYPE), 50 | "process.env.INJECT_FILE": JSON.stringify(process.env.INJECT_FILE), 51 | }, 52 | pluginImport: [ 53 | { 54 | libraryName: "@arco-design/web-react", 55 | customName: "@arco-design/web-react/es/{{ member }}", 56 | style: true, 57 | }, 58 | ], 59 | }, 60 | module: { 61 | rules: [ 62 | { test: /\.svg$/, type: "asset" }, 63 | { 64 | test: /\.(m|module).scss$/, 65 | use: [{ loader: "sass-loader" }], 66 | type: "css/module", 67 | }, 68 | { 69 | test: /\.less$/, 70 | use: [ 71 | { 72 | loader: "less-loader", 73 | options: { 74 | lessOptions: { 75 | javascriptEnabled: true, 76 | importLoaders: true, 77 | localIdentName: "[name]__[hash:base64:5]", 78 | }, 79 | }, 80 | }, 81 | ], 82 | type: "css", 83 | }, 84 | { 85 | test: /\.(jsx?|tsx?)$/, 86 | use: [ 87 | { 88 | loader: "./script/if-def", 89 | options: { 90 | // debug: true, 91 | }, 92 | }, 93 | ], 94 | }, 95 | ], 96 | }, 97 | output: { 98 | publicPath: "/", 99 | filename: "[name].js", 100 | path: path.resolve(__dirname, folder), 101 | }, 102 | devtool: false, 103 | }; 104 | 105 | // https://www.rspack.dev/ 106 | -------------------------------------------------------------------------------- /packages/force-copy/script/files/index.js: -------------------------------------------------------------------------------- 1 | const thread = require("child_process"); 2 | const path = require("path"); 3 | const fs = require("fs/promises"); 4 | const { IS_GECKO } = require("../utils/node"); 5 | 6 | const exec = command => { 7 | return new Promise((resolve, reject) => { 8 | thread.exec(command, (err, stdout) => { 9 | if (err) reject(err); 10 | resolve(stdout); 11 | }); 12 | }); 13 | }; 14 | 15 | exports.FilesPlugin = class FilesPlugin { 16 | constructor() { 17 | const folder = IS_GECKO ? "build-gecko" : "build"; 18 | fs.mkdir(`${folder}/static`, { recursive: true }); 19 | fs.mkdir(`${folder}/_locales`, { recursive: true }); 20 | } 21 | 22 | apply(compiler) { 23 | compiler.hooks.make.tap("FilesPlugin", compilation => { 24 | const resources = path.resolve("public/static"); 25 | !compilation.contextDependencies.has(resources) && 26 | compilation.contextDependencies.add(resources); 27 | }); 28 | 29 | compiler.hooks.done.tapPromise("FilesPlugin", () => { 30 | const locales = path.resolve("public/locales"); 31 | const resources = path.resolve("public/static"); 32 | 33 | const folder = IS_GECKO ? "build-gecko" : "build"; 34 | const localesTarget = path.resolve(`${folder}/_locales`); 35 | const resourcesTarget = path.resolve(`${folder}/static`); 36 | 37 | return Promise.all([ 38 | exec(`cp -r ${locales}/* ${localesTarget}`), 39 | exec(`cp -r ${resources}/* ${resourcesTarget}`), 40 | ]); 41 | }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /packages/force-copy/script/if-def/if-def.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "if-def-code-snippet": { 3 | "scope": "javascript,typescript,javascriptreact,typescriptreact", 4 | "prefix": "#IFDEF", 5 | "body": ["// #IFDEF ${1:PLATFORM}", "$0", "// #ENDIF"], 6 | "description": "IFDEF --- ENDIF" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/force-copy/script/if-def/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | 4 | // 条件编译: process.env.PLATFORM 的深层次嵌套 5 | // #IFDEF #ENDIF: C/C++ 预处理指令 平台层面扩展性 6 | 7 | /** 8 | * @this {import("@rspack/core").LoaderContext} 9 | * @param {string} source 10 | * @returns {string} 11 | */ 12 | function IfDefineLoader(source) { 13 | // 检查参数配置 14 | /** @type {boolean} */ 15 | const debug = this.query.debug || false; 16 | /** @type {(string|RegExp)[]} */ 17 | const include = this.query.include || [path.resolve("src")]; 18 | /** @type {(string|RegExp)[]} */ 19 | const exclude = this.query.exclude || [/node_modules/]; 20 | /** @type {string} */ 21 | const envKey = this.query.platform || "PLATFORM"; 22 | 23 | // 过滤资源路径 24 | let hit = false; 25 | const resourcePath = this.resourcePath; 26 | for (const includeConfig of include) { 27 | const verified = 28 | includeConfig instanceof RegExp 29 | ? includeConfig.test(resourcePath) 30 | : resourcePath.startsWith(includeConfig); 31 | if (verified) { 32 | hit = true; 33 | break; 34 | } 35 | } 36 | for (const excludeConfig of exclude) { 37 | const verified = 38 | excludeConfig instanceof RegExp 39 | ? excludeConfig.test(resourcePath) 40 | : resourcePath.startsWith(excludeConfig); 41 | if (verified) { 42 | hit = false; 43 | break; 44 | } 45 | } 46 | if (debug && hit) { 47 | console.log("if-def-loader hit path", resourcePath); 48 | } 49 | if (!hit) return source; 50 | 51 | // 迭代时控制该行是否命中预处理条件 52 | const platform = (process.env[envKey] || "").toLowerCase(); 53 | let terser = false; 54 | let revised = false; 55 | let terserIndex = -1; 56 | /** @type {number[]} */ 57 | const stack = []; 58 | const lines = source.split("\n"); 59 | const target = lines.map((line, index) => { 60 | // 去掉首尾的空白 去掉行首注释符号与空白符(可选) 61 | const code = line.trim().replace(/^\/\/\s*/, ""); 62 | // 检查预处理指令起始 #IFDEF 只会置 true 63 | if (/^#IFDEF/.test(code)) { 64 | stack.push(index); 65 | // 如果是 true 继续即可 66 | if (terser) return ""; 67 | const match = code.replace("#IFDEF", "").trim(); 68 | const group = match.split("|").map(item => item.trim().toLowerCase()); 69 | if (group.indexOf(platform) === -1) { 70 | terser = true; 71 | revised = true; 72 | terserIndex = index; 73 | } 74 | return ""; 75 | } 76 | // 检查预处理指令结束 #IFDEF 只会置 false 77 | if (/^#ENDIF$/.test(code)) { 78 | const index = stack.pop(); 79 | // 额外的 #ENDIF 忽略 80 | if (index === undefined) return ""; 81 | if (index === terserIndex) { 82 | terser = false; 83 | terserIndex = -1; 84 | } 85 | return ""; 86 | } 87 | // 如果命中预处理条件则擦除 88 | if (terser) return ""; 89 | return line; 90 | }); 91 | 92 | // 测试文件复写 93 | if (debug && revised) { 94 | // rm -rf ./**/*.log 95 | console.log("if-def-loader revise path", resourcePath); 96 | fs.writeFile(resourcePath + ".log", target.join("\n"), () => null); 97 | } 98 | 99 | // 返回处理结果 100 | return target.join("\n"); 101 | } 102 | 103 | module.exports = IfDefineLoader; 104 | -------------------------------------------------------------------------------- /packages/force-copy/script/if-def/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "if-def-processor", 3 | "version": "1.0.3", 4 | "files": [ 5 | "index.js", 6 | "*.code-snippets" 7 | ], 8 | "main": "index.js", 9 | "sideEffects": false, 10 | "author": "Czy", 11 | "license": "MIT", 12 | "description": "Rspack/Webpack if-def predefined syntax loader", 13 | "scripts": { 14 | "publish:main": "npm publish --registry=https://registry.npmjs.org/", 15 | "publish:test": "npm publish --registry=https://registry.npmjs.org/ --dry-run" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/WindrunnerMax/TKScript/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/WindrunnerMax/TKScript" 23 | }, 24 | "homepage": "https://github.com/WindrunnerMax/TKScript", 25 | "keywords": [ 26 | "if-def", 27 | "rspack", 28 | "loader" 29 | ], 30 | "dependencies": {}, 31 | "devDependencies": {} 32 | } 33 | -------------------------------------------------------------------------------- /packages/force-copy/script/manifest/index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const tsNode = require("ts-node"); 3 | const fs = require("fs"); 4 | const { promisify, IS_GECKO } = require("../utils/node"); 5 | 6 | const writeFile = promisify(fs.writeFile); 7 | 8 | exports.ManifestPlugin = class ManifestPlugin { 9 | constructor() { 10 | tsNode.register(); 11 | this.manifest = path.resolve(`src/manifest/index.ts`); 12 | } 13 | 14 | apply(compiler) { 15 | compiler.hooks.make.tap("ManifestPlugin", compilation => { 16 | const manifest = this.manifest; 17 | !compilation.fileDependencies.has(manifest) && compilation.fileDependencies.add(manifest); 18 | }); 19 | 20 | compiler.hooks.done.tapPromise("ManifestPlugin", () => { 21 | delete require.cache[require.resolve(this.manifest)]; 22 | const manifest = require(this.manifest); 23 | const version = require(path.resolve("package.json")).version; 24 | manifest.version = version; 25 | const folder = IS_GECKO ? "build-gecko" : "build"; 26 | return writeFile(path.resolve(`${folder}/manifest.json`), JSON.stringify(manifest, null, 2)); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/force-copy/script/reload/index.js: -------------------------------------------------------------------------------- 1 | const { IS_DEV } = require("../utils/node"); 2 | const WebSocketServer = require("ws").Server; 3 | 4 | /** 5 | * @type {import("ws").WebSocket | null} 6 | */ 7 | let wsClient = null; 8 | 9 | exports.ReloadPlugin = class ReloadPlugin { 10 | constructor() { 11 | if (IS_DEV) { 12 | try { 13 | const server = new WebSocketServer({ port: 3333 }); 14 | server.on("connection", client => { 15 | wsClient && wsClient.close(); 16 | wsClient = client; 17 | console.log("Client Connected"); 18 | }); 19 | } catch (error) { 20 | console.log("Auto Reload Server Error", error); 21 | } 22 | } 23 | } 24 | apply(compiler) { 25 | compiler.hooks.afterDone.tap("ReloadPlugin", () => { 26 | wsClient && wsClient.send("reload-app"); 27 | }); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /packages/force-copy/script/utils/node.js: -------------------------------------------------------------------------------- 1 | const IS_DEV = process.env.NODE_ENV === "development"; 2 | const IS_PROD = process.env.NODE_ENV === "production"; 3 | const IS_GECKO = process.env.PLATFORM === "gecko"; 4 | const IS_CHROMIUM = process.env.PLATFORM ? process.env.PLATFORM === "chromium" : true; 5 | const CHARTS = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm0123456789"; 6 | 7 | exports.IS_DEV = IS_DEV; 8 | exports.IS_PROD = IS_PROD; 9 | exports.IS_GECKO = IS_GECKO; 10 | exports.IS_CHROMIUM = IS_CHROMIUM; 11 | 12 | const getUniqueId = (len = 10) => { 13 | const chars = new Array(len - 1).fill(""); 14 | return ( 15 | CHARTS[Math.floor(Math.random() * 52)] + 16 | chars.map(() => CHARTS[Math.floor(Math.random() * CHARTS.length)]).join("") 17 | ); 18 | }; 19 | 20 | const promisify = fn => { 21 | return (...args) => 22 | new Promise((resolve, reject) => { 23 | fn(...args, (err, res) => { 24 | if (err) reject(err); 25 | else resolve(res); 26 | }); 27 | }); 28 | }; 29 | 30 | exports.promisify = promisify; 31 | exports.getUniqueId = getUniqueId; 32 | -------------------------------------------------------------------------------- /packages/force-copy/script/wrapper/index.js: -------------------------------------------------------------------------------- 1 | const { IS_CHROMIUM } = require("../utils/node"); 2 | const crypto = require("crypto"); 3 | 4 | exports.WrapperCodePlugin = class WrapperCodePlugin { 5 | constructor(options) { 6 | this.options = options || {}; 7 | } 8 | apply(compiler) { 9 | if (IS_CHROMIUM) return void 0; 10 | compiler.hooks.emit.tapAsync("WrapperCodePlugin", (compilation, done) => { 11 | const injectKey = process.env.INJECT_FILE + ".js"; 12 | const injectFile = compilation.assets[injectKey]; 13 | const workerKey = "worker.js"; 14 | const workerFile = compilation.assets[workerKey]; 15 | if (injectFile) { 16 | // 处理 Inject Script 17 | const buffer = injectFile.source(); 18 | const code = buffer.toString("utf-8"); 19 | const source = `window.${process.env.INJECT_FILE}=function(){${code}}`; 20 | compilation.assets[injectKey] = { 21 | source() { 22 | return source; 23 | }, 24 | size() { 25 | return this.source().length; 26 | }, 27 | }; 28 | // 处理 Worker Script 29 | if (workerFile) { 30 | const workerBuffer = workerFile.source(); 31 | const workerCode = workerBuffer.toString("utf-8"); 32 | const hash = crypto.createHash("sha256"); 33 | hash.update(`;(function(){${code}})();`); 34 | const hashed = hash.digest("base64"); 35 | const nonCSP = workerCode.replace("${CSP-HASH}", hashed); 36 | compilation.assets[workerKey] = { 37 | source() { 38 | return nonCSP; 39 | }, 40 | size() { 41 | return this.source().length; 42 | }, 43 | }; 44 | } 45 | } 46 | done(); 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/content-inject/constant.ts: -------------------------------------------------------------------------------- 1 | export const MARK = "CI" as const; 2 | export const EVENT_TYPE = process.env.EVENT_TYPE || "EVENT_TYPE_CI"; 3 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/content-inject/index.ts: -------------------------------------------------------------------------------- 1 | import { decodeJSON, encodeJSON, isUndefined } from "laser-utils"; 2 | import type { CIRequestType } from "./request"; 3 | import { CONTENT_TO_INJECT_REQUEST } from "./request"; 4 | import { EVENT_TYPE, MARK } from "./constant"; 5 | 6 | export class CIBridge { 7 | public static readonly REQUEST = CONTENT_TO_INJECT_REQUEST; 8 | 9 | static postToInject(data: CIRequestType) { 10 | window.dispatchEvent(new CustomEvent(EVENT_TYPE, { detail: encodeJSON(data) })); 11 | } 12 | 13 | static onContentMessage(cb: (data: CIRequestType) => void) { 14 | const handler = (event: CustomEvent) => { 15 | const data = decodeJSON(event.detail); 16 | if (data && data.type && !isUndefined(data.payload)) { 17 | cb(data); 18 | } 19 | }; 20 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 21 | // @ts-ignore 22 | window.addEventListener(EVENT_TYPE, handler); 23 | return () => { 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-ignore 26 | window.removeEventListener(EVENT_TYPE, handler); 27 | }; 28 | } 29 | 30 | static isCIRequestType(data: CIRequestType): data is CIRequestType { 31 | return data && data.type && data.type.endsWith(`__${MARK}__`); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/content-inject/request.ts: -------------------------------------------------------------------------------- 1 | import type { Reflex, Object } from "@/utils/types"; 2 | import { MARK } from "./constant"; 3 | 4 | const CI_REQUEST_ENUM = [ 5 | "COPY_TYPE", 6 | "KEYBOARD_TYPE", 7 | "CONTEXT_MENU_TYPE", 8 | "DEBUG_MOUSE_EVENT", 9 | "DEBUG_FOCUS_EVENT", 10 | "DEBUG_EDITABLE", 11 | "DEBUG_PASTE", 12 | ] as const; 13 | 14 | export const CONTENT_TO_INJECT_REQUEST = CI_REQUEST_ENUM.reduce( 15 | (acc, cur) => ({ ...acc, [cur]: `__${cur}__${MARK}__` }), 16 | {} as { [K in typeof CI_REQUEST_ENUM[number]]: `__${K}__${typeof MARK}__` } 17 | ); 18 | 19 | export const CI_EXECUTION_ENUM = { 20 | START: "START", 21 | CLOSE: "CLOSE", 22 | } as const; 23 | 24 | export type CIExecutionType = Object.Values; 25 | 26 | export type EventMap = { 27 | [CONTENT_TO_INJECT_REQUEST.COPY_TYPE]: CIExecutionType; 28 | [CONTENT_TO_INJECT_REQUEST.KEYBOARD_TYPE]: CIExecutionType; 29 | [CONTENT_TO_INJECT_REQUEST.CONTEXT_MENU_TYPE]: CIExecutionType; 30 | [CONTENT_TO_INJECT_REQUEST.DEBUG_MOUSE_EVENT]: null; 31 | [CONTENT_TO_INJECT_REQUEST.DEBUG_FOCUS_EVENT]: null; 32 | [CONTENT_TO_INJECT_REQUEST.DEBUG_EDITABLE]: null; 33 | [CONTENT_TO_INJECT_REQUEST.DEBUG_PASTE]: null; 34 | }; 35 | 36 | export type CIRequestType = Reflex.Tuple; 37 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/content-worker/constant.ts: -------------------------------------------------------------------------------- 1 | export const MARK = "CW" as const; 2 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/content-worker/index.ts: -------------------------------------------------------------------------------- 1 | import { cross } from "@/utils/global"; 2 | import type { CWRequestType } from "./request"; 3 | import { CONTENT_TO_WORKER_REQUEST } from "./request"; 4 | import { MARK } from "./constant"; 5 | 6 | export class CWBridge { 7 | public static readonly REQUEST = CONTENT_TO_WORKER_REQUEST; 8 | 9 | static async postToWorker(data: CWRequestType) { 10 | return new Promise(resolve => { 11 | if (cross.runtime.id) { 12 | cross.runtime.sendMessage(data).then(resolve); 13 | } else { 14 | resolve(null); 15 | } 16 | }); 17 | } 18 | 19 | static onContentMessage(cb: (data: CWRequestType, sender: chrome.runtime.MessageSender) => null) { 20 | const handler = ( 21 | message: CWRequestType, 22 | sender: chrome.runtime.MessageSender, 23 | sendResponse: (response?: null) => void 24 | ) => { 25 | const rtn = cb(message, sender); 26 | sendResponse(rtn || null); 27 | }; 28 | cross.runtime.onMessage.addListener(handler); 29 | return () => { 30 | cross.runtime.onMessage.removeListener(handler); 31 | }; 32 | } 33 | 34 | static isCWRequestType(data: CWRequestType): data is CWRequestType { 35 | return data && data.type && data.type.endsWith(`__${MARK}__`); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/content-worker/request.ts: -------------------------------------------------------------------------------- 1 | import type { Reflex } from "@/utils/types"; 2 | import { MARK } from "./constant"; 3 | 4 | const CW_REQUEST_TYPE = ["RELOAD", "SET_BADGE"] as const; 5 | export const CONTENT_TO_WORKER_REQUEST = CW_REQUEST_TYPE.reduce( 6 | (acc, cur) => ({ ...acc, [cur]: `__${cur}__${MARK}__` }), 7 | {} as { [K in typeof CW_REQUEST_TYPE[number]]: `__${K}__${typeof MARK}__` } 8 | ); 9 | 10 | type EventMap = { 11 | [CONTENT_TO_WORKER_REQUEST.RELOAD]: null; 12 | [CONTENT_TO_WORKER_REQUEST.SET_BADGE]: number; 13 | }; 14 | 15 | export type CWRequestType = Reflex.Tuple; 16 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-content/constant.ts: -------------------------------------------------------------------------------- 1 | export const MARK = "PC" as const; 2 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-content/index.ts: -------------------------------------------------------------------------------- 1 | import { cross } from "@/utils/global"; 2 | import { isEmptyValue } from "laser-utils"; 3 | import { logger } from "@/utils/logger"; 4 | import { URL_MATCH } from "@/utils/constant"; 5 | import type { PCRequestType } from "./request"; 6 | import { POPUP_TO_CONTENT_REQUEST } from "./request"; 7 | import type { PCResponseType } from "./response"; 8 | import { MARK } from "./constant"; 9 | 10 | export class PCBridge { 11 | public static readonly REQUEST = POPUP_TO_CONTENT_REQUEST; 12 | 13 | static async postToContent(data: PCRequestType) { 14 | return new Promise(resolve => { 15 | cross.tabs 16 | .query({ active: true, currentWindow: true }) 17 | .then(tabs => { 18 | const tab = tabs[0]; 19 | const tabId = tab && tab.id; 20 | const tabURL = tab && tab.url; 21 | if (tabURL && !URL_MATCH.some(match => new RegExp(match).test(tabURL))) { 22 | resolve(null); 23 | return void 0; 24 | } 25 | if (!isEmptyValue(tabId)) { 26 | cross.tabs.sendMessage(tabId, data).then(resolve); 27 | } else { 28 | resolve(null); 29 | } 30 | }) 31 | .catch(error => { 32 | logger.warning("Send Message Error", error); 33 | }); 34 | }); 35 | } 36 | 37 | static onPopupMessage(cb: (data: PCRequestType) => void | PCResponseType) { 38 | const handler = ( 39 | request: PCRequestType, 40 | _: chrome.runtime.MessageSender, 41 | sendResponse: (response: PCResponseType | null) => void 42 | ) => { 43 | const response = cb(request); 44 | response && response.type === request.type && sendResponse(response); 45 | }; 46 | cross.runtime.onMessage.addListener(handler); 47 | return () => { 48 | cross.runtime.onMessage.removeListener(handler); 49 | }; 50 | } 51 | 52 | static isPCRequestType(data: PCRequestType): data is PCRequestType { 53 | return data && data.type && data.type.endsWith(`__${MARK}__`); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-content/request.ts: -------------------------------------------------------------------------------- 1 | import type { Reflex } from "@/utils/types"; 2 | import { MARK } from "./constant"; 3 | 4 | const PC_REQUEST_TYPE = [ 5 | "COPY_TYPE", 6 | "KEYBOARD_TYPE", 7 | "CONTEXT_MENU_TYPE", 8 | "QUERY_STATE", 9 | "DEBUG_MOUSE_EVENT", 10 | "DEBUG_FOCUS_EVENT", 11 | "DEBUG_EDITABLE", 12 | "DEBUG_PASTE_EVENT", 13 | ] as const; 14 | 15 | export const POPUP_TO_CONTENT_REQUEST = PC_REQUEST_TYPE.reduce( 16 | (acc, cur) => ({ ...acc, [cur]: `__${cur}__${MARK}__` }), 17 | {} as { [K in typeof PC_REQUEST_TYPE[number]]: `__${K}__${typeof MARK}__` } 18 | ); 19 | 20 | type EventMap = { 21 | [POPUP_TO_CONTENT_REQUEST.COPY_TYPE]: { checked: boolean; once: boolean }; 22 | [POPUP_TO_CONTENT_REQUEST.KEYBOARD_TYPE]: { checked: boolean; once: boolean }; 23 | [POPUP_TO_CONTENT_REQUEST.CONTEXT_MENU_TYPE]: { checked: boolean; once: boolean }; 24 | [POPUP_TO_CONTENT_REQUEST.QUERY_STATE]: null; 25 | [POPUP_TO_CONTENT_REQUEST.DEBUG_MOUSE_EVENT]: null; 26 | [POPUP_TO_CONTENT_REQUEST.DEBUG_FOCUS_EVENT]: null; 27 | [POPUP_TO_CONTENT_REQUEST.DEBUG_EDITABLE]: null; 28 | [POPUP_TO_CONTENT_REQUEST.DEBUG_PASTE_EVENT]: null; 29 | }; 30 | 31 | export type PCRequestType = Reflex.Tuple; 32 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-content/response.ts: -------------------------------------------------------------------------------- 1 | import type { Reflex, Object } from "@/utils/types"; 2 | import type { POPUP_TO_CONTENT_REQUEST } from "./request"; 3 | 4 | export const PC_QUERY_STATE_ENUM = { 5 | COPY: "COPY", 6 | MENU: "MENU", 7 | KEYBOARD: "KEYBOARD", 8 | COPY_ONCE: "COPY_ONCE", 9 | MENU_ONCE: "MENU_ONCE", 10 | KEYBOARD_ONCE: "KEYBOARD_ONCE", 11 | } as const; 12 | 13 | export type PCQueryStateType = Object.Values; 14 | 15 | type EventMap = { 16 | [POPUP_TO_CONTENT_REQUEST.QUERY_STATE]: { [K in PCQueryStateType]: boolean }; 17 | }; 18 | 19 | export type PCResponseType = Reflex.Tuple; 20 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-worker/constant.ts: -------------------------------------------------------------------------------- 1 | export const MARK = "PW" as const; 2 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-worker/index.ts: -------------------------------------------------------------------------------- 1 | import { cross } from "@/utils/global"; 2 | import type { PWRequestType } from "./request"; 3 | import { POPUP_TO_WORKER_REQUEST } from "./request"; 4 | import { MARK } from "./constant"; 5 | 6 | export class PWBridge { 7 | public static readonly REQUEST = POPUP_TO_WORKER_REQUEST; 8 | 9 | static async postToWorker(data: PWRequestType) { 10 | return new Promise(resolve => { 11 | if (cross.runtime.id) { 12 | cross.runtime.sendMessage(data).then(resolve); 13 | } else { 14 | resolve(null); 15 | } 16 | }); 17 | } 18 | 19 | static onPopupMessage(cb: (data: PWRequestType, sender: chrome.runtime.MessageSender) => null) { 20 | const handler = ( 21 | message: PWRequestType, 22 | sender: chrome.runtime.MessageSender, 23 | sendResponse: (response?: null) => void 24 | ) => { 25 | const rtn = cb(message, sender); 26 | sendResponse(rtn || null); 27 | }; 28 | cross.runtime.onMessage.addListener(handler); 29 | return () => { 30 | cross.runtime.onMessage.removeListener(handler); 31 | }; 32 | } 33 | 34 | static isPWRequestType(data: PWRequestType): data is PWRequestType { 35 | return data && data.type && data.type.endsWith(`__${MARK}__`); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/popup-worker/request.ts: -------------------------------------------------------------------------------- 1 | import type { Reflex } from "@/utils/types"; 2 | import { MARK } from "./constant"; 3 | 4 | const PW_REQUEST_TYPE = ["RELOAD", "__"] as const; 5 | export const POPUP_TO_WORKER_REQUEST = PW_REQUEST_TYPE.reduce( 6 | (acc, cur) => ({ ...acc, [cur]: `__${cur}__${MARK}__` }), 7 | {} as { [K in typeof PW_REQUEST_TYPE[number]]: `__${K}__${typeof MARK}__` } 8 | ); 9 | 10 | export type PWRequestMap = { 11 | [POPUP_TO_WORKER_REQUEST.RELOAD]: null; 12 | [POPUP_TO_WORKER_REQUEST.__]: null; 13 | }; 14 | 15 | export type PWRequestType = Reflex.Tuple; 16 | 17 | export type PWRequestArgs = Reflex.Array; 18 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/worker-content/constant.ts: -------------------------------------------------------------------------------- 1 | export const MARK = "WC" as const; 2 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/worker-content/index.ts: -------------------------------------------------------------------------------- 1 | import { isEmptyValue } from "laser-utils"; 2 | 3 | import { URL_MATCH } from "@/utils/constant"; 4 | import { cross } from "@/utils/global"; 5 | import { logger } from "@/utils/logger"; 6 | import type { WCRequestType } from "./request"; 7 | import { POPUP_TO_CONTENT_REQUEST } from "./request"; 8 | import { MARK } from "./constant"; 9 | 10 | export class WCBridge { 11 | public static readonly REQUEST = POPUP_TO_CONTENT_REQUEST; 12 | 13 | static async postToContent(data: WCRequestType) { 14 | return new Promise(resolve => { 15 | cross.tabs 16 | .query({ active: true, currentWindow: true }) 17 | .then(tabs => { 18 | const tab = tabs[0]; 19 | const tabId = tab && tab.id; 20 | const tabURL = tab && tab.url; 21 | if (tabURL && !URL_MATCH.some(match => new RegExp(match).test(tabURL))) { 22 | resolve(null); 23 | return void 0; 24 | } 25 | if (!isEmptyValue(tabId)) { 26 | cross.tabs.sendMessage(tabId, data).then(resolve); 27 | } else { 28 | resolve(null); 29 | } 30 | }) 31 | .catch(error => { 32 | logger.warning("Send Message Error", error); 33 | }); 34 | }); 35 | } 36 | 37 | static onWorkerMessage(cb: (data: WCRequestType) => void | null) { 38 | const handler = ( 39 | message: WCRequestType, 40 | _: chrome.runtime.MessageSender, 41 | sendResponse: (response?: void) => void 42 | ) => { 43 | const rtn = cb(message); 44 | rtn && sendResponse(rtn); 45 | }; 46 | cross.runtime.onMessage.addListener(handler); 47 | return () => { 48 | cross.runtime.onMessage.removeListener(handler); 49 | }; 50 | } 51 | 52 | static isWCRequestType(data: WCRequestType): data is WCRequestType { 53 | return data && data.type && data.type.endsWith(`__${MARK}__`); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/force-copy/src/bridge/worker-content/request.ts: -------------------------------------------------------------------------------- 1 | import type { Reflex } from "@/utils/types"; 2 | import { MARK } from "./constant"; 3 | 4 | const WC_REQUEST_TYPE = ["DATA", "__"] as const; 5 | export const POPUP_TO_CONTENT_REQUEST = WC_REQUEST_TYPE.reduce( 6 | (acc, cur) => ({ ...acc, [cur]: `__${cur}__${MARK}__` }), 7 | {} as { [K in typeof WC_REQUEST_TYPE[number]]: `__${K}__${typeof MARK}__` } 8 | ); 9 | 10 | type EventMap = { 11 | [POPUP_TO_CONTENT_REQUEST.DATA]: string; 12 | }; 13 | 14 | export type WCRequestType = Reflex.Tuple; 15 | -------------------------------------------------------------------------------- /packages/force-copy/src/content/channel/popup.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import { isInIframe } from "@/utils/is"; 3 | import { logger } from "@/utils/logger"; 4 | import { Storage } from "laser-utils"; 5 | import { CI_EXECUTION_ENUM } from "@/bridge/content-inject/request"; 6 | import type { PCRequestType } from "@/bridge/popup-content/request"; 7 | import { POPUP_TO_CONTENT_REQUEST } from "@/bridge/popup-content/request"; 8 | import { CIBridge } from "@/bridge/content-inject"; 9 | import { CONTENT_TO_INJECT_REQUEST } from "@/bridge/content-inject/request"; 10 | import { PC_QUERY_STATE_ENUM } from "@/bridge/popup-content/response"; 11 | import { PCBridge } from "@/bridge/popup-content"; 12 | 13 | export const onPopupMessage = (data: PCRequestType) => { 14 | logger.info("Content Receive Popup Message", location.host, data); 15 | switch (data.type) { 16 | case POPUP_TO_CONTENT_REQUEST.COPY_TYPE: { 17 | CIBridge.postToInject({ 18 | type: CONTENT_TO_INJECT_REQUEST.COPY_TYPE, 19 | payload: data.payload.checked ? CI_EXECUTION_ENUM.START : CI_EXECUTION_ENUM.CLOSE, 20 | }); 21 | if (!data.payload.once) { 22 | Storage.local.set(COPY_TYPE, data.payload.checked); 23 | } else { 24 | Storage.session.set(COPY_TYPE, data.payload.checked); 25 | } 26 | break; 27 | } 28 | case POPUP_TO_CONTENT_REQUEST.KEYBOARD_TYPE: { 29 | CIBridge.postToInject({ 30 | type: CONTENT_TO_INJECT_REQUEST.KEYBOARD_TYPE, 31 | payload: data.payload.checked ? CI_EXECUTION_ENUM.START : CI_EXECUTION_ENUM.CLOSE, 32 | }); 33 | if (!data.payload.once) { 34 | Storage.local.set(KEYBOARD_TYPE, data.payload.checked); 35 | } else { 36 | Storage.session.set(KEYBOARD_TYPE, data.payload.checked); 37 | } 38 | break; 39 | } 40 | case POPUP_TO_CONTENT_REQUEST.CONTEXT_MENU_TYPE: { 41 | CIBridge.postToInject({ 42 | type: CONTENT_TO_INJECT_REQUEST.CONTEXT_MENU_TYPE, 43 | payload: data.payload.checked ? CI_EXECUTION_ENUM.START : CI_EXECUTION_ENUM.CLOSE, 44 | }); 45 | if (!data.payload.once) { 46 | Storage.local.set(CONTEXT_MENU_TYPE, data.payload.checked); 47 | } else { 48 | Storage.session.set(CONTEXT_MENU_TYPE, data.payload.checked); 49 | } 50 | break; 51 | } 52 | case POPUP_TO_CONTENT_REQUEST.QUERY_STATE: { 53 | if (isInIframe) break; 54 | return { 55 | type: data.type, 56 | payload: { 57 | [PC_QUERY_STATE_ENUM.COPY]: !!Storage.local.get(COPY_TYPE), 58 | [PC_QUERY_STATE_ENUM.MENU]: !!Storage.local.get(CONTEXT_MENU_TYPE), 59 | [PC_QUERY_STATE_ENUM.KEYBOARD]: !!Storage.local.get(KEYBOARD_TYPE), 60 | [PC_QUERY_STATE_ENUM.COPY_ONCE]: !!Storage.session.get(COPY_TYPE), 61 | [PC_QUERY_STATE_ENUM.MENU_ONCE]: !!Storage.session.get(CONTEXT_MENU_TYPE), 62 | [PC_QUERY_STATE_ENUM.KEYBOARD_ONCE]: !!Storage.session.get(KEYBOARD_TYPE), 63 | }, 64 | }; 65 | } 66 | case PCBridge.REQUEST.DEBUG_MOUSE_EVENT: { 67 | CIBridge.postToInject({ 68 | type: CIBridge.REQUEST.DEBUG_MOUSE_EVENT, 69 | payload: null, 70 | }); 71 | break; 72 | } 73 | case PCBridge.REQUEST.DEBUG_FOCUS_EVENT: { 74 | CIBridge.postToInject({ 75 | type: CIBridge.REQUEST.DEBUG_FOCUS_EVENT, 76 | payload: null, 77 | }); 78 | break; 79 | } 80 | case PCBridge.REQUEST.DEBUG_EDITABLE: { 81 | CIBridge.postToInject({ 82 | type: CIBridge.REQUEST.DEBUG_EDITABLE, 83 | payload: null, 84 | }); 85 | break; 86 | } 87 | case PCBridge.REQUEST.DEBUG_PASTE_EVENT: { 88 | CIBridge.postToInject({ 89 | type: CIBridge.REQUEST.DEBUG_PASTE, 90 | payload: null, 91 | }); 92 | break; 93 | } 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /packages/force-copy/src/content/index.ts: -------------------------------------------------------------------------------- 1 | import { PCBridge } from "@/bridge/popup-content"; 2 | import { onReceiveReloadMsg } from "../utils/reload"; 3 | import { onPopupMessage } from "./channel/popup"; 4 | import { LOG_LEVEL, logger } from "@/utils/logger"; 5 | import { initializeWorker } from "./runtime/initialize"; 6 | import { isInIframe } from "@/utils/is"; 7 | import { importInjectScript } from "./runtime/script"; 8 | 9 | (() => { 10 | if (__DEV__) { 11 | !isInIframe && onReceiveReloadMsg(); 12 | logger.setLevel(LOG_LEVEL.INFO); 13 | } 14 | logger.info("Content Script Loaded"); 15 | importInjectScript(); 16 | !isInIframe && initializeWorker(); 17 | PCBridge.onPopupMessage(onPopupMessage); 18 | })(); 19 | -------------------------------------------------------------------------------- /packages/force-copy/src/content/runtime/initialize.ts: -------------------------------------------------------------------------------- 1 | import { CWBridge } from "@/bridge/content-worker"; 2 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 3 | import { Storage } from "laser-utils"; 4 | 5 | export const initializeWorker = () => { 6 | Promise.resolve().then(() => { 7 | const badge = [ 8 | Storage.local.get(COPY_TYPE), 9 | Storage.local.get(CONTEXT_MENU_TYPE), 10 | Storage.local.get(KEYBOARD_TYPE), 11 | Storage.session.get(COPY_TYPE), 12 | Storage.session.get(CONTEXT_MENU_TYPE), 13 | Storage.session.get(KEYBOARD_TYPE), 14 | ].filter(Boolean).length; 15 | CWBridge.postToWorker({ type: CWBridge.REQUEST.SET_BADGE, payload: badge }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/force-copy/src/content/runtime/script.ts: -------------------------------------------------------------------------------- 1 | export const importInjectScript = () => { 2 | const fn = window[process.env.INJECT_FILE as unknown as number] as unknown as () => void; 3 | // #IFDEF GECKO 4 | if (fn && document instanceof XMLDocument === false) { 5 | const script = document.createElementNS("http://www.w3.org/1999/xhtml", "script"); 6 | script.setAttribute("type", "text/javascript"); 7 | // 这里的内容需要跟 WrapperCodePlugin 的 HASH 计算保持一致 8 | const code = `;(${fn.toString()})();`; 9 | script.innerText = code; 10 | document.documentElement.appendChild(script); 11 | // 在这里仅移除 script 标签, 但不会删除 window 上的属性 12 | // 保证注入重试, inject 幂等且 content 处于隔离环境不会受到影响 13 | script.remove(); 14 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts 15 | const unsafeWindow = window.wrappedJSObject; 16 | const signal = process.env.EVENT_TYPE; 17 | // 此时说明页面中的脚本没有被注入 尝试以 blob 的形式注入 18 | if (unsafeWindow && !unsafeWindow[signal]) { 19 | const blob = new Blob([code], { type: "application/javascript" }); 20 | const url = URL.createObjectURL(blob); 21 | const script = document.createElementNS("http://www.w3.org/1999/xhtml", "script"); 22 | script.setAttribute("type", "text/javascript"); 23 | // 实际上这里是异步的引入 不能完全保证 document_start 的时机 24 | (script).src = url; 25 | document.documentElement.appendChild(script); 26 | script.onload = () => { 27 | script.remove(); 28 | // 如果仍然不存在 尝试在 Content Script 中执行 29 | // 在 Content Script 中执行可以保证 DOM 事件类型的处理 30 | !unsafeWindow[signal] && fn(); 31 | }; 32 | script.onerror = () => { 33 | script.remove(); 34 | !unsafeWindow[signal] && fn(); 35 | }; 36 | } 37 | } 38 | // #ENDIF 39 | }; 40 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/channel/content.ts: -------------------------------------------------------------------------------- 1 | import type { WebSite } from "../types/website"; 2 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 3 | import { logger } from "@/utils/logger"; 4 | import { DOM_STAGE } from "copy/src/constant/event"; 5 | import type { CIRequestType } from "@/bridge/content-inject/request"; 6 | import { CI_EXECUTION_ENUM, CONTENT_TO_INJECT_REQUEST } from "@/bridge/content-inject/request"; 7 | import { CIBridge } from "@/bridge/content-inject"; 8 | import { EventBus, EVENTS_ENUM } from "../utils/bus"; 9 | import { stopNativePropagation } from "../utils/events"; 10 | 11 | export const onContentMessage = (handler: WebSite) => { 12 | return (data: CIRequestType) => { 13 | logger.info("Inject Receive Content Message", location.host, data); 14 | switch (data.type) { 15 | case CONTENT_TO_INJECT_REQUEST.COPY_TYPE: { 16 | data.payload === CI_EXECUTION_ENUM.START 17 | ? handler.start(COPY_TYPE, DOM_STAGE.END) 18 | : handler.close(COPY_TYPE); 19 | break; 20 | } 21 | case CONTENT_TO_INJECT_REQUEST.KEYBOARD_TYPE: { 22 | data.payload === CI_EXECUTION_ENUM.START 23 | ? handler.start(KEYBOARD_TYPE, DOM_STAGE.END) 24 | : handler.close(KEYBOARD_TYPE); 25 | break; 26 | } 27 | case CONTENT_TO_INJECT_REQUEST.CONTEXT_MENU_TYPE: { 28 | data.payload === CI_EXECUTION_ENUM.START 29 | ? handler.start(CONTEXT_MENU_TYPE, DOM_STAGE.END) 30 | : handler.close(CONTEXT_MENU_TYPE); 31 | break; 32 | } 33 | case CIBridge.REQUEST.DEBUG_MOUSE_EVENT: { 34 | EventBus.on(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, stopNativePropagation); 35 | EventBus.on(EVENTS_ENUM.MOUSE_UP_CAPTURE, stopNativePropagation); 36 | EventBus.on(EVENTS_ENUM.MOUSE_MOVE_CAPTURE, stopNativePropagation); 37 | EventBus.on(EVENTS_ENUM.TOUCH_START_CAPTURE, stopNativePropagation); 38 | EventBus.on(EVENTS_ENUM.TOUCH_MOVE_CAPTURE, stopNativePropagation); 39 | EventBus.on(EVENTS_ENUM.TOUCH_END_CAPTURE, stopNativePropagation); 40 | break; 41 | } 42 | case CIBridge.REQUEST.DEBUG_FOCUS_EVENT: { 43 | EventBus.on(EVENTS_ENUM.FOCUS_CAPTURE, stopNativePropagation); 44 | EventBus.on(EVENTS_ENUM.BLUR_CAPTURE, stopNativePropagation); 45 | break; 46 | } 47 | case CIBridge.REQUEST.DEBUG_EDITABLE: { 48 | document.body.contentEditable = "true"; 49 | break; 50 | } 51 | case CIBridge.REQUEST.DEBUG_PASTE: { 52 | EventBus.on(EVENTS_ENUM.PASTE_CAPTURE, stopNativePropagation); 53 | break; 54 | } 55 | } 56 | }; 57 | }; 58 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/index.ts: -------------------------------------------------------------------------------- 1 | import type { State } from "./types/state"; 2 | import { Storage } from "laser-utils"; 3 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 4 | import { websites } from "./modules"; 5 | import { initBaseEvents } from "./utils/events"; 6 | import { CIBridge } from "@/bridge/content-inject"; 7 | import { onContentMessage } from "./channel/content"; 8 | import { LOG_LEVEL, logger } from "@/utils/logger"; 9 | import { DOM_STAGE } from "copy/src/constant/event"; 10 | 11 | (async (): Promise => { 12 | if (__DEV__) { 13 | logger.setLevel(LOG_LEVEL.INFO); 14 | } 15 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 16 | // @ts-ignore 17 | if (window[process.env.EVENT_TYPE]) { 18 | logger.info("Inject Script Already Loaded"); 19 | return void 0; 20 | } 21 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 22 | // @ts-ignore 23 | window[process.env.EVENT_TYPE] = true; 24 | logger.info("Inject Script Loaded"); 25 | // 初始化事件 26 | initBaseEvents(); 27 | // 初始化状态 28 | const state: State = { 29 | COPY: !!Storage.local.get(COPY_TYPE) || !!Storage.session.get(COPY_TYPE), 30 | KEYBOARD: 31 | !!Storage.local.get(KEYBOARD_TYPE) || !!Storage.session.get(KEYBOARD_TYPE), 32 | CONTEXT_MENU: 33 | !!Storage.local.get(CONTEXT_MENU_TYPE) || 34 | !!Storage.session.get(CONTEXT_MENU_TYPE), 35 | }; 36 | const handler = websites.find(item => item.regexp.test(location.href)) || websites.slice(-1)[0]; 37 | if (!handler) return void 0; 38 | handler.init && handler.init(state); 39 | state.COPY && handler.start(COPY_TYPE, DOM_STAGE.START); 40 | state.KEYBOARD && handler.start(KEYBOARD_TYPE, DOM_STAGE.START); 41 | state.CONTEXT_MENU && handler.start(CONTEXT_MENU_TYPE, DOM_STAGE.START); 42 | CIBridge.onContentMessage(onContentMessage(handler)); 43 | })(); 44 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/basic.ts: -------------------------------------------------------------------------------- 1 | import { styles } from "copy-currency/src/utils"; 2 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 3 | import type { WebSite } from "../types/website"; 4 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 5 | import { onCopyKeyboardHandler, stopNativePropagation } from "../utils/events"; 6 | import { STYLE_ID, AUTO_USER_SELECT, AUTO_SELECTION } from "../utils/styles"; 7 | 8 | export const Basic: WebSite = { 9 | regexp: /.*/, 10 | start(type) { 11 | if (type === COPY_TYPE) { 12 | styles.insertCSS(STYLE_ID, AUTO_USER_SELECT + AUTO_SELECTION); 13 | EventBus.on(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 14 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 15 | EventBus.on(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 16 | EventBus.on(EVENTS_ENUM.TOUCH_START_CAPTURE, stopNativePropagation); 17 | } else if (type === KEYBOARD_TYPE) { 18 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 19 | } else if (type === CONTEXT_MENU_TYPE) { 20 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 21 | } 22 | }, 23 | close(type) { 24 | if (type === COPY_TYPE) { 25 | styles.removeCSS(STYLE_ID); 26 | EventBus.off(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 27 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 28 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 29 | EventBus.off(EVENTS_ENUM.TOUCH_START_CAPTURE, stopNativePropagation); 30 | } else if (type === KEYBOARD_TYPE) { 31 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 32 | } else if (type === CONTEXT_MENU_TYPE) { 33 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 34 | } 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/doc88.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import type { WebSite } from "../types/website"; 3 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 4 | import { styles } from "copy-currency/src/utils"; 5 | import { onCopyKeyboardHandler, stopNativePropagation } from "../utils/events"; 6 | import instance from "copy/src/utils/instance"; 7 | import { ALLOW_PAINT, AUTO_USER_SELECT, COPY_BUTTON_STYLE, STYLE_ID } from "../utils/styles"; 8 | import { logger } from "@/utils/logger"; 9 | import { isString } from "laser-utils"; 10 | import { delayExecute } from "../utils/delay"; 11 | import { PAGE_LOADED } from "copy/src/constant/event"; 12 | 13 | let isMouseDown = false; 14 | let preSelectedText = ""; 15 | let curSelectedText = ""; 16 | 17 | const onMouseDownCapture = () => { 18 | isMouseDown = true; 19 | instance.hide(false); 20 | }; 21 | const onMouseUpCapture = () => { 22 | isMouseDown = false; 23 | }; 24 | const init = () => { 25 | try { 26 | // @ts-expect-error global 27 | window.Config && (window.Config.vip = 1) && (window.Config.logined = 1); 28 | const hook = () => { 29 | // @ts-expect-error global 30 | const api = window.Core.Annotation.api; 31 | const keys = Object.keys(api).filter(key => isString(api[key])); 32 | const duplication: Record = {}; 33 | keys.forEach(key => { 34 | duplication[key] = api[key]; 35 | Object.defineProperty(api, key, { 36 | set: function (value) { 37 | duplication[key] = value; 38 | !isMouseDown && (curSelectedText = value); 39 | !isMouseDown && logger.info("TEXT SETTER", value); 40 | return true; 41 | }, 42 | get: function () { 43 | return duplication[key]; 44 | }, 45 | }); 46 | }); 47 | }; 48 | // @ts-expect-error global 49 | if (window.Core) { 50 | hook(); 51 | // @ts-expect-error global 52 | } else if (window.Core === null) { 53 | delayExecute(PAGE_LOADED).then(hook); 54 | } else { 55 | let Core: unknown = undefined; 56 | Object.defineProperty(window, "Core", { 57 | configurable: true, 58 | set: (value: unknown) => { 59 | Core = value; 60 | value && hook(); 61 | }, 62 | get: () => Core, 63 | }); 64 | } 65 | } catch (error) { 66 | logger.warning("INIT TEXT ERROR", error); 67 | } 68 | }; 69 | 70 | const onMouseUp = (event: MouseEvent) => { 71 | logger.info("SELECT", curSelectedText); 72 | if (curSelectedText && preSelectedText !== curSelectedText) { 73 | instance.onCopy(curSelectedText, event); 74 | instance.show(event); 75 | } 76 | preSelectedText = curSelectedText; 77 | }; 78 | 79 | export const Doc88: WebSite = { 80 | regexp: /www\.doc88\.com/, 81 | start(type) { 82 | if (type === COPY_TYPE) { 83 | init(); 84 | styles.insertCSS( 85 | STYLE_ID, 86 | AUTO_USER_SELECT + ALLOW_PAINT + COPY_BUTTON_STYLE + "#left-menu{display:none !important;} " 87 | ); 88 | EventBus.on(EVENTS_ENUM.MOUSE_UP_BUBBLE, onMouseUp); 89 | EventBus.on(EVENTS_ENUM.MOUSE_UP_CAPTURE, onMouseUpCapture); 90 | EventBus.on(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDownCapture); 91 | EventBus.on(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 92 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 93 | EventBus.on(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 94 | } else if (type === KEYBOARD_TYPE) { 95 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 96 | } else if (type === CONTEXT_MENU_TYPE) { 97 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 98 | } 99 | }, 100 | close(type) { 101 | if (type === COPY_TYPE) { 102 | instance.destroy(); 103 | styles.removeCSS(STYLE_ID); 104 | EventBus.off(EVENTS_ENUM.MOUSE_UP_BUBBLE, onMouseUp); 105 | EventBus.off(EVENTS_ENUM.MOUSE_UP_CAPTURE, onMouseUpCapture); 106 | EventBus.off(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDownCapture); 107 | EventBus.off(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 108 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 109 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 110 | } else if (type === KEYBOARD_TYPE) { 111 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 112 | } else if (type === CONTEXT_MENU_TYPE) { 113 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 114 | } 115 | }, 116 | }; 117 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/docin.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import type { WebSite } from "../types/website"; 3 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 4 | import { styles } from "copy-currency/src/utils"; 5 | import { onCopyKeyboardHandler, stopNativePropagation } from "../utils/events"; 6 | import instance from "copy/src/utils/instance"; 7 | import { ALLOW_PAINT, AUTO_USER_SELECT, COPY_BUTTON_STYLE, STYLE_ID } from "../utils/styles"; 8 | import { logger } from "@/utils/logger"; 9 | import { delayExecute } from "../utils/delay"; 10 | import { PAGE_LOADED } from "copy/src/constant/event"; 11 | 12 | let preSelectedText = ""; 13 | let curSelectedText = ""; 14 | 15 | const init = () => { 16 | const el = document.querySelector("#j_select"); 17 | el && el.click(); 18 | }; 19 | 20 | const onMouseDown = () => { 21 | instance.hide(false); 22 | }; 23 | 24 | const onMouseUp = (event: MouseEvent) => { 25 | const handler = () => { 26 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 27 | // @ts-ignore 28 | curSelectedText = window.docinReader?.st || ""; 29 | logger.info("SELECT", curSelectedText); 30 | if (curSelectedText && preSelectedText !== curSelectedText) { 31 | instance.onCopy(curSelectedText, event); 32 | } 33 | preSelectedText = curSelectedText; 34 | }; 35 | setTimeout(handler, 100); 36 | }; 37 | 38 | export const DocIn: WebSite = { 39 | regexp: /www\.docin\.com/, 40 | start(type) { 41 | if (type === COPY_TYPE) { 42 | delayExecute(PAGE_LOADED).then(init); 43 | styles.insertCSS( 44 | STYLE_ID, 45 | AUTO_USER_SELECT + ALLOW_PAINT + COPY_BUTTON_STYLE + ".copy-tips{display:none !important;} " 46 | ); 47 | EventBus.on(EVENTS_ENUM.MOUSE_UP_CAPTURE, onMouseUp); 48 | EventBus.on(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDown); 49 | EventBus.on(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 50 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 51 | EventBus.on(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 52 | } else if (type === KEYBOARD_TYPE) { 53 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 54 | } else if (type === CONTEXT_MENU_TYPE) { 55 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 56 | } 57 | }, 58 | close(type) { 59 | if (type === COPY_TYPE) { 60 | instance.destroy(); 61 | styles.removeCSS(STYLE_ID); 62 | EventBus.off(EVENTS_ENUM.MOUSE_UP_CAPTURE, onMouseUp); 63 | EventBus.off(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDown); 64 | EventBus.off(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 65 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 66 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 67 | } else if (type === KEYBOARD_TYPE) { 68 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 69 | } else if (type === CONTEXT_MENU_TYPE) { 70 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 71 | } 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/index.ts: -------------------------------------------------------------------------------- 1 | import type { WebSite } from "../types/website"; 2 | import { Basic } from "./basic"; 3 | import { Doc88 } from "./doc88"; 4 | import { DocIn } from "./docin"; 5 | import { QQDoc } from "./qq-doc"; 6 | import { QQPpt } from "./qq-ppt"; 7 | import { Wenku } from "./wenku"; 8 | import { Zhihu } from "./zhihu"; 9 | 10 | export const websites: WebSite[] = [Wenku, Doc88, QQDoc, DocIn, Zhihu, QQPpt, Basic]; 11 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/qq-doc.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import type { WebSite } from "../types/website"; 3 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 4 | import { styles } from "copy-currency/src/utils"; 5 | import { onCopyKeyboardHandler, stopNativePropagation } from "../utils/events"; 6 | import instance from "copy/src/utils/instance"; 7 | import { ALLOW_PAINT, AUTO_USER_SELECT, COPY_BUTTON_STYLE, STYLE_ID } from "../utils/styles"; 8 | import { logger } from "@/utils/logger"; 9 | import { TEXT_HTML, TEXT_PLAIN } from "copy/src/utils/copy"; 10 | 11 | const onMouseDown = () => { 12 | instance.hide(false); 13 | }; 14 | 15 | const onMouseUp = (event: MouseEvent) => { 16 | // QQ Doc 17 | if (window.pad && window.pad.editor && !window.pad.editor.isCopyable()) { 18 | const editor = window.pad.editor; 19 | if (editor.getCopyContent) { 20 | const content = editor.getCopyContent() || {}; 21 | const plainText: string | undefined = content.plain; 22 | const htmlText: string = content.html || ""; 23 | logger.info("SELECT", plainText); 24 | if (plainText) { 25 | return instance.onCopy({ [TEXT_PLAIN]: plainText, [TEXT_HTML]: htmlText }, event); 26 | } 27 | } 28 | } 29 | // QQ Sheet 30 | if ( 31 | window.SpreadsheetApp && 32 | window.SpreadsheetApp.permissions && 33 | window.SpreadsheetApp.permissions.sheetStatus && 34 | window.SpreadsheetApp.permissions.sheetStatus.canCopy === false && 35 | window.SpreadsheetApp.permissions.sheetStatus.canEdit && 36 | window.SpreadsheetApp.permissions.sheetStatus.canEdit() === false 37 | ) { 38 | const SpreadsheetApp = window.SpreadsheetApp; 39 | const [selection] = SpreadsheetApp.view.getSelectionRanges(); 40 | if (selection) { 41 | const text: string[] = []; 42 | const { startColIndex, startRowIndex, endColIndex, endRowIndex } = selection; 43 | for (let i = startRowIndex; i <= endRowIndex; i++) { 44 | for (let k = startColIndex; k <= endColIndex; k++) { 45 | const cell = SpreadsheetApp.workbook.activeSheet.getCellDataAtPosition(i, k); 46 | if (!cell) continue; 47 | text.push(" ", cell.formattedValue?.value || cell.value || ""); 48 | } 49 | i !== endRowIndex && text.push("\n"); 50 | } 51 | const str = text.join(""); 52 | logger.info("SELECT", str); 53 | return instance.onCopy(/^\s*$/.test(str) ? "" : str, event); 54 | } 55 | } 56 | // Final 57 | return instance.hide(false); 58 | }; 59 | 60 | export const QQDoc: WebSite = { 61 | regexp: /docs?\.(weixin\.)?qq\.com\/(doc)|(sheet)\/.+/, 62 | start(type) { 63 | if (type === COPY_TYPE) { 64 | styles.insertCSS(STYLE_ID, AUTO_USER_SELECT + ALLOW_PAINT + COPY_BUTTON_STYLE); 65 | EventBus.on(EVENTS_ENUM.MOUSE_UP_BUBBLE, onMouseUp); 66 | EventBus.on(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDown); 67 | EventBus.on(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 68 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 69 | EventBus.on(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 70 | } else if (type === KEYBOARD_TYPE) { 71 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 72 | } else if (type === CONTEXT_MENU_TYPE) { 73 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 74 | } 75 | }, 76 | close(type) { 77 | if (type === COPY_TYPE) { 78 | instance.destroy(); 79 | styles.removeCSS(STYLE_ID); 80 | EventBus.off(EVENTS_ENUM.MOUSE_UP_BUBBLE, onMouseUp); 81 | EventBus.off(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDown); 82 | EventBus.off(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 83 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 84 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 85 | } else if (type === KEYBOARD_TYPE) { 86 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 87 | } else if (type === CONTEXT_MENU_TYPE) { 88 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 89 | } 90 | }, 91 | }; 92 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/qq-ppt.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import type { WebSite } from "../types/website"; 3 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 4 | import { styles } from "copy-currency/src/utils"; 5 | import { stopNativePropagation } from "../utils/events"; 6 | import instance from "copy/src/utils/instance"; 7 | import { ALLOW_PAINT, AUTO_USER_SELECT, STYLE_ID } from "../utils/styles"; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | const win: any = window; 11 | 12 | export const QQPpt: WebSite = { 13 | regexp: /docs?\.(weixin\.)?qq\.com\/slide\/.+/, 14 | init() { 15 | let webpackJsonp = win.webpackJsonp; 16 | Object.defineProperty(win, "webpackJsonp", { 17 | get() { 18 | return webpackJsonp; 19 | }, 20 | set(newValue) { 21 | if (newValue.push.__HOOKED__) { 22 | return; 23 | } 24 | webpackJsonp = newValue; 25 | const originPush = webpackJsonp.push; 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | function push(...args: any[]) { 28 | const [, mods] = args[0]; 29 | for (const [key, fn] of Object.entries(mods)) { 30 | const stringifyFn = String(fn); 31 | if (/this\.shouldResponseCopy\(/.test(stringifyFn)) { 32 | const payload = 33 | "((...args) => window.__CAN_COPY === undefined " + 34 | "? this.shouldResponseCopy(...args) " + 35 | ": window.__CAN_COPY)("; 36 | const next = stringifyFn.replace(/this\.shouldResponseCopy\(/g, payload); 37 | mods[key] = new Function(`return (${next})`)(); 38 | } 39 | } 40 | // @ts-expect-error this 41 | return originPush.call(this, ...args); 42 | } 43 | push.__HOOKED__ = 1; 44 | webpackJsonp.push = push; 45 | }, 46 | }); 47 | }, 48 | start(type) { 49 | if (type === COPY_TYPE) { 50 | win.__CAN_COPY = true; 51 | styles.insertCSS(STYLE_ID, AUTO_USER_SELECT + ALLOW_PAINT); 52 | } else if (type === KEYBOARD_TYPE) { 53 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 54 | } else if (type === CONTEXT_MENU_TYPE) { 55 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 56 | } 57 | }, 58 | close(type) { 59 | if (type === COPY_TYPE) { 60 | win.__CAN_COPY = undefined; 61 | instance.destroy(); 62 | styles.removeCSS(STYLE_ID); 63 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 64 | } else if (type === KEYBOARD_TYPE) { 65 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 66 | } else if (type === CONTEXT_MENU_TYPE) { 67 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/wenku.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import type { WebSite } from "../types/website"; 3 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 4 | import { styles } from "copy-currency/src/utils"; 5 | import { onCopyKeyboardHandler, stopNativePropagation } from "../utils/events"; 6 | import instance from "copy/src/utils/instance"; 7 | import { ALLOW_PAINT, AUTO_USER_SELECT, COPY_BUTTON_STYLE, STYLE_ID } from "../utils/styles"; 8 | import { logger } from "@/utils/logger"; 9 | 10 | let preSelectedText = ""; 11 | let curSelectedText = ""; 12 | 13 | const onMouseDown = () => { 14 | instance.hide(false); 15 | }; 16 | const onMouseUp = (event: MouseEvent) => { 17 | try { 18 | const elements = document.querySelectorAll("#app > div"); 19 | for (const item of elements) { 20 | // @ts-expect-error __vue__ 21 | const vue = item.__vue__; 22 | if (vue) { 23 | const text = vue.$store.getters["readerPlugin/selectedTextTrim"]; 24 | text && (curSelectedText = text); 25 | break; 26 | } 27 | } 28 | } catch (error) { 29 | logger.warning("GET TEXT FAIL", error); 30 | } 31 | if (!curSelectedText) { 32 | const result = /查看全部包含“([\s\S]*?)”的文档/.exec(document.body.innerHTML); 33 | result && result[1] && (curSelectedText = result[1]); 34 | } 35 | logger.info("SELECT", curSelectedText); 36 | if (curSelectedText && preSelectedText !== curSelectedText) { 37 | instance.onCopy(curSelectedText, event); 38 | } 39 | preSelectedText = curSelectedText; 40 | }; 41 | 42 | export const Wenku: WebSite = { 43 | regexp: /wenku\.baidu\.com/, 44 | start(type) { 45 | if (type === COPY_TYPE) { 46 | // instance.init("Copy"); 47 | styles.insertCSS( 48 | STYLE_ID, 49 | AUTO_USER_SELECT + 50 | ALLOW_PAINT + 51 | COPY_BUTTON_STYLE + 52 | "#reader-helper{display:none !important;} " 53 | ); 54 | EventBus.on(EVENTS_ENUM.MOUSE_UP_BUBBLE, onMouseUp); 55 | EventBus.on(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDown); 56 | EventBus.on(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 57 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 58 | EventBus.on(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 59 | } else if (type === KEYBOARD_TYPE) { 60 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 61 | } else if (type === CONTEXT_MENU_TYPE) { 62 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 63 | } 64 | }, 65 | close(type) { 66 | if (type === COPY_TYPE) { 67 | instance.destroy(); 68 | styles.removeCSS(STYLE_ID); 69 | EventBus.off(EVENTS_ENUM.MOUSE_UP_BUBBLE, onMouseUp); 70 | EventBus.off(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, onMouseDown); 71 | EventBus.off(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 72 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 73 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 74 | } else if (type === KEYBOARD_TYPE) { 75 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 76 | } else if (type === CONTEXT_MENU_TYPE) { 77 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 78 | } 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/modules/zhihu.ts: -------------------------------------------------------------------------------- 1 | import { CONTEXT_MENU_TYPE, COPY_TYPE, KEYBOARD_TYPE } from "@/utils/constant"; 2 | import type { WebSite } from "../types/website"; 3 | import { EVENTS_ENUM, EventBus } from "../utils/bus"; 4 | import { styles } from "copy-currency/src/utils"; 5 | import { onCopyKeyboardHandler, stopNativePropagation } from "../utils/events"; 6 | import { ALLOW_PAINT, AUTO_USER_SELECT, STYLE_ID } from "../utils/styles"; 7 | import { delayExecute } from "../utils/delay"; 8 | import { DOM_LOADED } from "copy/src/constant/event"; 9 | 10 | const init = () => { 11 | if (location.hostname === "zhuanlan.zhihu.com") { 12 | const elements = document.querySelectorAll("[data-focus-scope-start]"); 13 | elements.forEach(node => { 14 | if ((node.textContent || "").indexOf("立即登录/注册") > -1) { 15 | node.parentElement ? node.parentElement.remove() : node.remove(); 16 | } 17 | }); 18 | const removeFocalPointModal: MutationCallback = mutationsList => { 19 | for (const mutation of mutationsList) { 20 | const addedNodes = mutation.addedNodes; 21 | for (let i = 0; i < addedNodes.length; i++) { 22 | const target = addedNodes[i]; 23 | if (target.nodeType != 1) return void 0; 24 | if ( 25 | target instanceof HTMLDivElement && 26 | target.querySelector("[data-focus-scope-start]") 27 | ) { 28 | const element = target.querySelector("[data-focus-scope-start]"); 29 | element && 30 | element.parentElement && 31 | (element.parentElement.textContent || "").indexOf("立即登录/注册") > -1 && 32 | element.parentElement.parentElement && 33 | element.parentElement.parentElement.removeChild(element.parentElement); 34 | } 35 | } 36 | } 37 | }; 38 | const observer = new MutationObserver(removeFocalPointModal); 39 | observer.observe(document, { childList: true, subtree: true }); 40 | } 41 | }; 42 | 43 | export const Zhihu: WebSite = { 44 | regexp: /zhuanlan\.zhihu\.com/, 45 | start(type) { 46 | if (type === COPY_TYPE) { 47 | delayExecute(DOM_LOADED).then(init); 48 | styles.insertCSS(STYLE_ID, AUTO_USER_SELECT + ALLOW_PAINT); 49 | EventBus.on(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 50 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 51 | EventBus.on(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 52 | } else if (type === KEYBOARD_TYPE) { 53 | EventBus.on(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 54 | } else if (type === CONTEXT_MENU_TYPE) { 55 | EventBus.on(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 56 | } 57 | }, 58 | close(type) { 59 | if (type === COPY_TYPE) { 60 | styles.removeCSS(STYLE_ID); 61 | EventBus.off(EVENTS_ENUM.COPY_CAPTURE, stopNativePropagation); 62 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, onCopyKeyboardHandler); 63 | EventBus.off(EVENTS_ENUM.SELECT_START_CAPTURE, stopNativePropagation); 64 | } else if (type === KEYBOARD_TYPE) { 65 | EventBus.off(EVENTS_ENUM.KEY_BOARD_CAPTURE, stopNativePropagation); 66 | } else if (type === CONTEXT_MENU_TYPE) { 67 | EventBus.off(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, stopNativePropagation); 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare interface Window { 3 | pad?: any; 4 | wrappedJSObject?: any; 5 | SpreadsheetApp?: any; 6 | } 7 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/types/state.ts: -------------------------------------------------------------------------------- 1 | export type State = { 2 | COPY: boolean; 3 | KEYBOARD: boolean; 4 | CONTEXT_MENU: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/types/website.ts: -------------------------------------------------------------------------------- 1 | import type { ActionType } from "@/utils/constant"; 2 | import type { State } from "./state"; 3 | import type { DOM_STAGE } from "copy/src/constant/event"; 4 | import type { Object } from "@/utils/types"; 5 | 6 | export type WebSite = { 7 | /** 链接匹配正则 */ 8 | regexp: RegExp; 9 | /** 初始化必须执行 */ 10 | init?: (state: State) => void; 11 | /** 启动时执行 初始启动/被动启动 */ 12 | start: (type: ActionType, when: Object.Values) => void; 13 | /** 关闭时执行 被动关闭 */ 14 | close: (type: ActionType) => void; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/utils/bus.ts: -------------------------------------------------------------------------------- 1 | import type { ActionType } from "@/utils/constant"; 2 | import { EventBus as AbstractEventBus } from "laser-utils"; 3 | 4 | const EVENTS_TYPE = [ 5 | "MOUSE_UP_CAPTURE", 6 | "MOUSE_DOWN_CAPTURE", 7 | "MOUSE_UP_BUBBLE", 8 | "MOUSE_DOWN_BUBBLE", 9 | "DOM_LOADED", 10 | "PAGE_LOADED", 11 | "OPEN_ACTION", 12 | "CLOSE_ACTION", 13 | "OPEN_ONCE_ACTION", 14 | "COPY_CAPTURE", 15 | "KEY_BOARD_CAPTURE", 16 | "CONTEXT_MENU_CAPTURE", 17 | "SELECT_START_CAPTURE", 18 | "TOUCH_START_CAPTURE", 19 | "FOCUS_CAPTURE", 20 | "BLUR_CAPTURE", 21 | "MOUSE_MOVE_CAPTURE", 22 | "TOUCH_MOVE_CAPTURE", 23 | "TOUCH_END_CAPTURE", 24 | "PASTE_CAPTURE", 25 | ] as const; 26 | 27 | export const EVENTS_ENUM = EVENTS_TYPE.reduce( 28 | (acc, cur) => ({ ...acc, [cur]: `__${cur}__` }), 29 | {} as { [K in typeof EVENTS_TYPE[number]]: `__${K}__` } 30 | ); 31 | 32 | interface EventBusParams { 33 | [EVENTS_ENUM.MOUSE_UP_CAPTURE]: MouseEvent; 34 | [EVENTS_ENUM.MOUSE_DOWN_CAPTURE]: MouseEvent; 35 | [EVENTS_ENUM.MOUSE_UP_BUBBLE]: MouseEvent; 36 | [EVENTS_ENUM.MOUSE_DOWN_BUBBLE]: MouseEvent; 37 | [EVENTS_ENUM.MOUSE_MOVE_CAPTURE]: MouseEvent; 38 | [EVENTS_ENUM.DOM_LOADED]: Event; 39 | [EVENTS_ENUM.PAGE_LOADED]: Event; 40 | [EVENTS_ENUM.OPEN_ACTION]: ActionType; 41 | [EVENTS_ENUM.CLOSE_ACTION]: ActionType; 42 | [EVENTS_ENUM.OPEN_ONCE_ACTION]: ActionType; 43 | [EVENTS_ENUM.COPY_CAPTURE]: Event; 44 | [EVENTS_ENUM.KEY_BOARD_CAPTURE]: KeyboardEvent; 45 | [EVENTS_ENUM.CONTEXT_MENU_CAPTURE]: Event; 46 | [EVENTS_ENUM.SELECT_START_CAPTURE]: Event; 47 | [EVENTS_ENUM.TOUCH_START_CAPTURE]: TouchEvent; 48 | [EVENTS_ENUM.FOCUS_CAPTURE]: FocusEvent; 49 | [EVENTS_ENUM.BLUR_CAPTURE]: FocusEvent; 50 | [EVENTS_ENUM.TOUCH_MOVE_CAPTURE]: TouchEvent; 51 | [EVENTS_ENUM.TOUCH_END_CAPTURE]: TouchEvent; 52 | [EVENTS_ENUM.PASTE_CAPTURE]: ClipboardEvent; 53 | } 54 | 55 | declare module "laser-utils/dist/es/event-bus" { 56 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 57 | interface EventBusType extends EventBusParams {} 58 | } 59 | 60 | export const EventBus = new AbstractEventBus(); 61 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/utils/delay.ts: -------------------------------------------------------------------------------- 1 | import { DOM_READY, PAGE_LOADED } from "copy/src/constant/event"; 2 | import { EVENTS_ENUM, EventBus } from "./bus"; 3 | 4 | export const delayExecute = ( 5 | when: typeof DOM_READY | typeof PAGE_LOADED = PAGE_LOADED, 6 | delayMax: number | false = 6000 7 | ) => { 8 | const delayWithEvent = new Promise(r => { 9 | const resolve = () => r(); 10 | if (when === DOM_READY) { 11 | if (document.readyState !== "loading") { 12 | resolve(); 13 | } else { 14 | EventBus.once(EVENTS_ENUM.DOM_LOADED, resolve); 15 | } 16 | return void 0; 17 | } 18 | if (document.readyState === "complete") { 19 | resolve(); 20 | } else { 21 | EventBus.once(EVENTS_ENUM.PAGE_LOADED, resolve); 22 | } 23 | }); 24 | const delayWithTimeout = delayMax && new Promise(resolve => setTimeout(resolve, delayMax)); 25 | return Promise.race([delayWithEvent, delayWithTimeout].filter(Boolean)); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BLUR, 3 | CONTEXT_MENU, 4 | COPY, 5 | DOM_READY, 6 | FOCUS, 7 | FOCUS_IN, 8 | FOCUS_OUT, 9 | KEY_DOWN, 10 | MOUSE_DOWN, 11 | MOUSE_MOVE, 12 | MOUSE_UP, 13 | PAGE_LOADED, 14 | SELECT_START, 15 | TOUCH_END, 16 | TOUCH_MOVE, 17 | TOUCH_START, 18 | } from "copy/src/constant/event"; 19 | import { EVENTS_ENUM, EventBus } from "./bus"; 20 | 21 | export const initBaseEvents = () => { 22 | window.addEventListener(MOUSE_UP, e => EventBus.emit(EVENTS_ENUM.MOUSE_UP_BUBBLE, e)); 23 | window.addEventListener(MOUSE_DOWN, e => EventBus.emit(EVENTS_ENUM.MOUSE_DOWN_BUBBLE, e)); 24 | window.addEventListener(MOUSE_UP, e => EventBus.emit(EVENTS_ENUM.MOUSE_UP_CAPTURE, e), true); 25 | window.addEventListener(MOUSE_DOWN, e => EventBus.emit(EVENTS_ENUM.MOUSE_DOWN_CAPTURE, e), true); 26 | window.addEventListener(COPY, e => EventBus.emit(EVENTS_ENUM.COPY_CAPTURE, e), true); 27 | window.addEventListener(KEY_DOWN, e => EventBus.emit(EVENTS_ENUM.KEY_BOARD_CAPTURE, e), true); 28 | window.addEventListener( 29 | CONTEXT_MENU, 30 | e => EventBus.emit(EVENTS_ENUM.CONTEXT_MENU_CAPTURE, e), 31 | true 32 | ); 33 | window.addEventListener( 34 | SELECT_START, 35 | e => EventBus.emit(EVENTS_ENUM.SELECT_START_CAPTURE, e), 36 | true 37 | ); 38 | window.addEventListener( 39 | TOUCH_START, 40 | e => EventBus.emit(EVENTS_ENUM.TOUCH_START_CAPTURE, e), 41 | true 42 | ); 43 | window.addEventListener(DOM_READY, e => EventBus.emit(EVENTS_ENUM.DOM_LOADED, e), true); 44 | window.addEventListener(PAGE_LOADED, e => EventBus.emit(EVENTS_ENUM.PAGE_LOADED, e), true); 45 | window.addEventListener(FOCUS, e => EventBus.emit(EVENTS_ENUM.FOCUS_CAPTURE, e), true); 46 | window.addEventListener(FOCUS_IN, e => EventBus.emit(EVENTS_ENUM.FOCUS_CAPTURE, e), true); 47 | window.addEventListener(BLUR, e => EventBus.emit(EVENTS_ENUM.BLUR_CAPTURE, e), true); 48 | window.addEventListener(FOCUS_OUT, e => EventBus.emit(EVENTS_ENUM.FOCUS_CAPTURE, e), true); 49 | window.addEventListener(MOUSE_MOVE, e => EventBus.emit(EVENTS_ENUM.MOUSE_MOVE_CAPTURE, e), true); 50 | window.addEventListener(TOUCH_MOVE, e => EventBus.emit(EVENTS_ENUM.TOUCH_MOVE_CAPTURE, e), true); 51 | window.addEventListener(TOUCH_END, e => EventBus.emit(EVENTS_ENUM.TOUCH_END_CAPTURE, e), true); 52 | }; 53 | 54 | export const stopNativePropagation = (event: Event) => event.stopImmediatePropagation(); 55 | 56 | export const onCopyKeyboardHandler = (e: KeyboardEvent) => { 57 | if ((e.ctrlKey || e.metaKey) && e.key.toLocaleUpperCase() === "C") { 58 | stopNativePropagation(e); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /packages/force-copy/src/inject/utils/styles.ts: -------------------------------------------------------------------------------- 1 | export const STYLE_ID = "__FORCE_COPY_STYLE__"; 2 | 3 | export const COPY_BUTTON_STYLE = 4 | ".__copy-button {width: 60px; height: 30px; background: #4C98F7;color: #fff;position: absolute;" + 5 | "z-index: -1000;opacity: 0;display: flex;justify-content: center;align-items: center;border-radius: 3px;" + 6 | "font-size: 13px;cursor: pointer;transition: opacity 0.3s;} "; 7 | 8 | // https://developer.mozilla.org/zh-CN/docs/Web/CSS/user-select 9 | export const AUTO_USER_SELECT = 10 | "*, html body * :not(input):not(textarea)" + 11 | "{user-select: auto !important;-webkit-user-select: auto !important;} "; 12 | 13 | // https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media 14 | export const ALLOW_PAINT = "@media print{body{display:block !important;}} "; 15 | 16 | // https://developer.mozilla.org/en-US/docs/Web/CSS/unset 17 | // https://developer.mozilla.org/zh-CN/docs/Web/CSS/::selection 18 | // https://stackoverflow.com/questions/33448213/how-to-reset-background-color-of-selection 19 | export const AUTO_SELECTION = 20 | ":not(input):not(textarea)::selection {" + 21 | " color: inherit !important;" + 22 | " background-color: #BEDAFF !important;" + 23 | " background-color: highlight !important;" + 24 | "} " + 25 | ":not(input):not(textarea)::-moz-selection {" + 26 | " color: inherit !important;" + 27 | " background-color: #BEDAFF !important;" + 28 | " background-color: highlight !important;" + 29 | "} "; 30 | -------------------------------------------------------------------------------- /packages/force-copy/src/manifest/index.ts: -------------------------------------------------------------------------------- 1 | import type { Manifest } from "./types"; 2 | import { __URL_MATCH__ } from "./types"; 3 | 4 | // Chromium 5 | const __MANIFEST__: Manifest = { 6 | manifest_version: 3, 7 | name: "Force Copy", 8 | version: "0.0.0", 9 | description: "Force Copy Everything", 10 | default_locale: "en", 11 | icons: { 12 | 32: "static/favicon.128.png", 13 | 96: "static/favicon.128.png", 14 | 128: "static/favicon.128.png", 15 | }, 16 | action: { 17 | default_popup: "popup.html", 18 | default_icon: "static/favicon.128.png", 19 | }, 20 | content_scripts: [ 21 | { 22 | matches: [...__URL_MATCH__], 23 | js: ["./content.js"], 24 | run_at: "document_start", 25 | all_frames: true, 26 | }, 27 | ], 28 | web_accessible_resources: [ 29 | { 30 | resources: ["static/*", process.env.INJECT_FILE + ".js"], 31 | matches: [""], 32 | }, 33 | ], 34 | background: { 35 | service_worker: "worker.js", 36 | }, 37 | host_permissions: [...__URL_MATCH__], 38 | permissions: ["activeTab", "tabs", "scripting", "management"], 39 | minimum_chrome_version: "88.0", 40 | }; 41 | 42 | // Gecko 43 | if (process.env.PLATFORM === "gecko") { 44 | __MANIFEST__.manifest_version = 2; 45 | __MANIFEST__.browser_action = __MANIFEST__.action; 46 | __MANIFEST__.content_scripts = [ 47 | { 48 | matches: [...__URL_MATCH__], 49 | js: [process.env.INJECT_FILE + ".js", "./content.js"], 50 | run_at: "document_start", 51 | all_frames: true, 52 | }, 53 | ]; 54 | __MANIFEST__.background = { 55 | scripts: [__MANIFEST__.background.service_worker as string], 56 | }; 57 | __MANIFEST__.permissions = ["activeTab", "tabs", "webRequest", "management", ...__URL_MATCH__]; 58 | __MANIFEST__.browser_specific_settings = { 59 | gecko: { strict_min_version: "91.1.0" }, 60 | gecko_android: { strict_min_version: "91.1.0" }, 61 | }; 62 | 63 | delete __MANIFEST__.action; 64 | delete __MANIFEST__.host_permissions; 65 | delete __MANIFEST__.minimum_chrome_version; 66 | delete __MANIFEST__.web_accessible_resources; 67 | } 68 | 69 | module.exports = __MANIFEST__; 70 | -------------------------------------------------------------------------------- /packages/force-copy/src/manifest/types.ts: -------------------------------------------------------------------------------- 1 | export type Manifest = { 2 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json 3 | manifest_version: number; 4 | name: string; 5 | version: string; 6 | description: string; 7 | default_locale: string; 8 | icons: Record; 9 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts 10 | content_scripts?: { 11 | matches: string[]; 12 | js: string[]; 13 | run_at: string; 14 | all_frames: boolean; 15 | }[]; 16 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions 17 | permissions: string[]; 18 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background 19 | background: { 20 | service_worker?: string; 21 | scripts?: string[]; 22 | }; 23 | // ====== Chromium ====== 24 | action?: { 25 | default_popup: string; 26 | default_icon: string; 27 | }; 28 | web_accessible_resources?: { 29 | resources: string[]; 30 | matches: string[]; 31 | }[]; 32 | host_permissions?: string[]; 33 | minimum_chrome_version?: string; 34 | // ====================== 35 | // ====== Gecko ====== 36 | browser_action?: { 37 | default_popup: string; 38 | default_icon: string; 39 | }; 40 | browser_specific_settings?: { 41 | gecko: { 42 | strict_min_version: string; 43 | }; 44 | gecko_android: { 45 | strict_min_version: string; 46 | }; 47 | }; 48 | // =================== 49 | }; 50 | 51 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns 52 | export const __URL_MATCH__ = ["https://*/*", "http://*/*", "file://*/*"]; 53 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/app/index.module.scss: -------------------------------------------------------------------------------- 1 | body { 2 | user-select: none; 3 | background-color: var(--color-bg-3); 4 | } 5 | 6 | .container { 7 | padding: 10px; 8 | width: 270px; 9 | color: var(--color-text-1); 10 | } 11 | 12 | .hr { 13 | margin: 5px 0; 14 | height: 1px; 15 | background-color: var(--color-border-3); 16 | width: 100%; 17 | } 18 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/app/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import styles from "./index.module.scss"; 3 | import { cs } from "laser-utils"; 4 | import { I18n } from "../../i18n"; 5 | import { cross } from "@/utils/global"; 6 | import { Console } from "../console"; 7 | import { Header } from "../header"; 8 | import { Footer } from "../footer"; 9 | 10 | // https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags 11 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n 12 | const i18n = new I18n(cross.i18n.getUILanguage()); 13 | 14 | export const App: FC = () => { 15 | return ( 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/console/index.module.scss: -------------------------------------------------------------------------------- 1 | .console { 2 | position: relative; 3 | > div { 4 | margin: 10px 5px; 5 | display: flex; 6 | align-items: center; 7 | } 8 | } 9 | 10 | .switch { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/console/index.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from "react"; 2 | import { useLayoutEffect, useState } from "react"; 3 | import styles from "./index.module.scss"; 4 | import { PCBridge } from "@/bridge/popup-content"; 5 | import type { I18n } from "../../i18n"; 6 | import { setBadgeNumber as setNextBadgeNumber } from "../../utils/badge"; 7 | import { PC_QUERY_STATE_ENUM } from "@/bridge/popup-content/response"; 8 | import { Grid, Switch } from "@arco-design/web-react"; 9 | 10 | const Row = Grid.Row; 11 | const Col = Grid.Col; 12 | 13 | export const Console: FC<{ 14 | i18n: I18n; 15 | }> = props => { 16 | const { i18n } = props; 17 | const [copyState, setCopyState] = useState(false); 18 | const [copyStateOnce, setCopyStateOnce] = useState(false); 19 | const [menuState, setMenuState] = useState(false); 20 | const [menuStateOnce, setMenuStateOnce] = useState(false); 21 | const [keydownState, setKeydownState] = useState(false); 22 | const [keydownStateOnce, setKeydownStateOnce] = useState(false); 23 | 24 | const onSwitchChange = ( 25 | type: 26 | | typeof PCBridge.REQUEST.COPY_TYPE 27 | | typeof PCBridge.REQUEST.KEYBOARD_TYPE 28 | | typeof PCBridge.REQUEST.CONTEXT_MENU_TYPE, 29 | checked: boolean, 30 | once = false 31 | ) => { 32 | PCBridge.postToContent({ type: type, payload: { checked, once } }); 33 | setNextBadgeNumber(checked); 34 | }; 35 | 36 | useLayoutEffect(() => { 37 | const mapper: Record = { 38 | [PC_QUERY_STATE_ENUM.COPY]: setCopyState, 39 | [PC_QUERY_STATE_ENUM.MENU]: setMenuState, 40 | [PC_QUERY_STATE_ENUM.KEYBOARD]: setKeydownState, 41 | [PC_QUERY_STATE_ENUM.COPY_ONCE]: setCopyStateOnce, 42 | [PC_QUERY_STATE_ENUM.MENU_ONCE]: setMenuStateOnce, 43 | [PC_QUERY_STATE_ENUM.KEYBOARD_ONCE]: setKeydownStateOnce, 44 | }; 45 | PCBridge.postToContent({ 46 | type: PCBridge.REQUEST.QUERY_STATE, 47 | payload: null, 48 | }).then(res => { 49 | if (res && res.type === PCBridge.REQUEST.QUERY_STATE) { 50 | for (const [key, value] of Object.entries(res.payload)) { 51 | const handler = mapper[key]; 52 | handler && handler(value); 53 | } 54 | } 55 | }); 56 | }, []); 57 | 58 | return ( 59 |
60 | 61 | 62 | {i18n.t("Captain.Modules")} 63 | 64 | 65 | {i18n.t("Captain.Start")} 66 | 67 | 68 | {i18n.t("Captain.Once")} 69 | 70 | 71 | 72 | 73 | {i18n.t("Operation.Copy")} 74 | 75 | 76 | { 80 | setCopyState(v); 81 | onSwitchChange(PCBridge.REQUEST.COPY_TYPE, v); 82 | }} 83 | /> 84 | 85 | 86 | { 90 | setCopyStateOnce(v); 91 | onSwitchChange(PCBridge.REQUEST.COPY_TYPE, v, true); 92 | }} 93 | /> 94 | 95 | 96 | 97 | 98 | {i18n.t("Operation.Keyboard")} 99 | 100 | 101 | { 105 | setKeydownState(v); 106 | onSwitchChange(PCBridge.REQUEST.KEYBOARD_TYPE, v); 107 | }} 108 | /> 109 | 110 | 111 | { 115 | setKeydownStateOnce(v); 116 | onSwitchChange(PCBridge.REQUEST.KEYBOARD_TYPE, v, true); 117 | }} 118 | /> 119 | 120 | 121 | 122 | 123 | {i18n.t("Operation.ContextMenu")} 124 | 125 | 126 | { 130 | setMenuState(v); 131 | onSwitchChange(PCBridge.REQUEST.CONTEXT_MENU_TYPE, v); 132 | }} 133 | /> 134 | 135 | 136 | { 140 | setMenuStateOnce(v); 141 | onSwitchChange(PCBridge.REQUEST.CONTEXT_MENU_TYPE, v, true); 142 | }} 143 | /> 144 | 145 | 146 |
147 | ); 148 | }; 149 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/footer/index.module.scss: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 3px 5px 0; 3 | display: flex; 4 | align-items: center; 5 | font-size: 12px; 6 | justify-content: end; 7 | 8 | > a, 9 | div { 10 | cursor: pointer; 11 | color: var(--color-text-1); 12 | margin-left: 7px; 13 | display: flex; 14 | align-items: center; 15 | text-decoration: none; 16 | 17 | > svg { 18 | margin-right: 3px; 19 | } 20 | } 21 | 22 | :global { 23 | .arco-icon-question-circle, 24 | .arco-icon-experiment, 25 | .arco-icon-refresh { 26 | font-size: 14px; 27 | } 28 | 29 | .arco-icon-question-circle, 30 | .arco-icon-refresh { 31 | margin-left: -2px; 32 | } 33 | 34 | .arco-icon-experiment { 35 | margin-right: 2px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./index.module.scss"; 2 | import type { FC } from "react"; 3 | import type { I18n } from "@/popup/i18n"; 4 | import { 5 | IconExperiment, 6 | IconGithub, 7 | IconQuestionCircle, 8 | IconRefresh, 9 | } from "@arco-design/web-react/icon"; 10 | import { cross } from "@/utils/global"; 11 | import { Trigger } from "@arco-design/web-react"; 12 | import { Tools } from "../tools"; 13 | 14 | export const Footer: FC<{ 15 | i18n: I18n; 16 | }> = props => { 17 | const { i18n } = props; 18 | 19 | return ( 20 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/header/index.module.scss: -------------------------------------------------------------------------------- 1 | .captain { 2 | align-items: center; 3 | display: flex; 4 | 5 | img { 6 | height: 20px; 7 | margin-right: 5px; 8 | width: 20px; 9 | } 10 | 11 | padding-bottom: 3px; 12 | } 13 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { cs } from "laser-utils"; 2 | import styles from "./index.module.scss"; 3 | import type { FC } from "react"; 4 | import type { I18n } from "@/popup/i18n"; 5 | 6 | export const Header: FC<{ 7 | i18n: I18n; 8 | }> = props => { 9 | const { i18n } = props; 10 | 11 | return ( 12 |
13 | 14 | {i18n.t("Title")} 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/tools/index.module.scss: -------------------------------------------------------------------------------- 1 | $box-shadow: 0 0 4px var(--color-border-2); 2 | 3 | .container { 4 | background-color: var(--color-bg-4); 5 | padding: 10px; 6 | 7 | border: 1px solid var(--color-border-2); 8 | border-radius: 4px; 9 | box-shadow: $box-shadow; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/components/tools/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Grid } from "@arco-design/web-react"; 2 | import styles from "./index.module.scss"; 3 | import type { FC } from "react"; 4 | import type { I18n } from "@/popup/i18n"; 5 | import { PCBridge } from "@/bridge/popup-content"; 6 | 7 | const GridItem = Grid.GridItem; 8 | 9 | export const Tools: FC<{ 10 | i18n: I18n; 11 | }> = props => { 12 | const { i18n } = props; 13 | 14 | return ( 15 |
16 | 17 | 18 | 29 | 30 | 31 | 42 | 43 | 44 | 55 | 56 | 57 | 68 | 69 | 70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/hooks/use-memoized-fn.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from "react"; 2 | 3 | type noop = (this: unknown, ...args: unknown[]) => never; 4 | 5 | type PickFunction = ( 6 | this: ThisParameterType, 7 | ...args: Parameters 8 | ) => ReturnType; 9 | 10 | export const useMemoizedFn = (fn: T) => { 11 | const fnRef = useRef(fn); 12 | 13 | // why not write `fnRef.current = fn`? 14 | // https://github.com/alibaba/hooks/issues/728 15 | fnRef.current = useMemo(() => fn, [fn]); 16 | 17 | const memoizedFn = useRef>(); 18 | if (!memoizedFn.current) { 19 | memoizedFn.current = function (this, ...args) { 20 | return fnRef.current.apply(this, args); 21 | }; 22 | } 23 | 24 | return memoizedFn.current as T; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/i18n/en.ts: -------------------------------------------------------------------------------- 1 | export const en = { 2 | Title: "Force Copy", 3 | Captain: { 4 | Modules: "Modules", 5 | Start: "Start", 6 | Once: "Once", 7 | }, 8 | Operation: { 9 | Copy: "Copy", 10 | Keyboard: "Keyboard", 11 | ContextMenu: "ContextMenu", 12 | }, 13 | Information: { 14 | Tools: "Tools", 15 | GitHub: "GitHub", 16 | Help: "Help", 17 | Reload: "Reload", 18 | }, 19 | Tools: { 20 | MouseEvent: "MouseEvent", 21 | FocusEvent: "FocusEvent", 22 | Editable: "Editable", 23 | PasteEvent: "PasteEvent", 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "laser-utils"; 2 | import type { ConfigBlock, I18nTypes } from "./types"; 3 | import { en } from "./en"; 4 | import { zh } from "./zh"; 5 | 6 | const cache: Record = {}; 7 | 8 | export class I18n { 9 | private config: I18nTypes; 10 | private language: string; 11 | constructor(language: string) { 12 | this.language = language.toLowerCase(); 13 | this.config = I18n.getFullConfig(this.language); 14 | } 15 | 16 | public t = (key: keyof I18nTypes, defaultValue = "") => { 17 | return this.config[key] || defaultValue || key; 18 | }; 19 | 20 | public get p(): typeof en { 21 | if (this.language.startsWith("zh")) { 22 | return zh; 23 | } 24 | return en; 25 | } 26 | 27 | private static getFullConfig = (lang: string) => { 28 | if (cache[lang]) return cache[lang]; 29 | let config; 30 | if (lang.startsWith("zh")) { 31 | config = this.generateFlattenConfig(zh); 32 | } else { 33 | config = this.generateFlattenConfig(en); 34 | } 35 | cache[lang] = config; 36 | return config; 37 | }; 38 | 39 | private static generateFlattenConfig = (config: ConfigBlock): I18nTypes => { 40 | const target: Record = {}; 41 | const dfs = (obj: ConfigBlock, prefix: string[]) => { 42 | for (const [key, value] of Object.entries(obj)) { 43 | if (isString(value)) { 44 | target[[...prefix, key].join(".")] = value; 45 | } else { 46 | dfs(value, [...prefix, key]); 47 | } 48 | } 49 | }; 50 | dfs(config, []); 51 | return target as I18nTypes; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/i18n/types.ts: -------------------------------------------------------------------------------- 1 | import type { en } from "./en"; 2 | 3 | export type DefaultI18nConfig = typeof en; 4 | 5 | export type ConfigBlock = { 6 | [key: string]: string | ConfigBlock; 7 | }; 8 | 9 | type FlattenKeys = Key extends string 10 | ? T[Key] extends ConfigBlock 11 | ? `${Key}.${FlattenKeys}` 12 | : `${Key}` 13 | : never; 14 | 15 | export type I18nTypes = Record, string>; 16 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/i18n/zh.ts: -------------------------------------------------------------------------------- 1 | import { en } from "./en"; 2 | import type { DefaultI18nConfig } from "./types"; 3 | 4 | export const zh: DefaultI18nConfig = { 5 | Title: "Force Copy", 6 | Captain: { 7 | Modules: "模块", 8 | Start: "启动", 9 | Once: "仅本次", 10 | }, 11 | Operation: { 12 | Copy: "解除复制限制", 13 | Keyboard: "解除键盘限制", 14 | ContextMenu: "解除右键限制", 15 | }, 16 | Information: en.Information, 17 | Tools: en.Tools, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom"; 2 | import { App } from "./components/app"; 3 | import { LOG_LEVEL, logger } from "@/utils/logger"; 4 | 5 | if (__DEV__) { 6 | logger.setLevel(LOG_LEVEL.INFO); 7 | } 8 | 9 | const darkThemeMatch = window.matchMedia("(prefers-color-scheme: dark)"); 10 | if (darkThemeMatch.matches) { 11 | document.body.setAttribute("arco-theme", "dark"); 12 | } 13 | 14 | ReactDOM.render(, document.getElementById("root")); 15 | -------------------------------------------------------------------------------- /packages/force-copy/src/popup/utils/badge.ts: -------------------------------------------------------------------------------- 1 | import { cross } from "@/utils/global"; 2 | 3 | export const setBadgeNumber = (checked: boolean) => { 4 | cross.tabs 5 | .query({ active: true, currentWindow: true }) 6 | .then(tabs => { 7 | const tab = tabs[0]; 8 | const tabId = tab && tab.id; 9 | return tabId; 10 | }) 11 | .then(tabId => { 12 | if (tabId) { 13 | let action: typeof cross.action | typeof cross.browserAction = cross.action; 14 | // #IFDEF GECKO 15 | action = cross.browserAction; 16 | // #ENDIF 17 | action.getBadgeText({ tabId }).then(text => { 18 | const badge = Number(text) || 0; 19 | const next = badge + (checked ? 1 : -1); 20 | action.setBadgeText({ text: next <= 0 ? "" : String(next), tabId }); 21 | action.setBadgeBackgroundColor({ color: "#4e5969", tabId }); 22 | }); 23 | } 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/force-copy/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.scss" { 2 | const content: Record; 3 | export default content; 4 | } 5 | 6 | declare const __DEV__: boolean; 7 | declare const browser: typeof chrome | undefined; 8 | 9 | declare namespace NodeJS { 10 | interface ProcessEnv { 11 | NODE_ENV: "development" | "production"; 12 | PLATFORM: "chromium" | "gecko"; 13 | EVENT_TYPE: string; 14 | INJECT_FILE: string; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/force-copy/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const COPY_TYPE = "__COPY__"; 2 | export const KEYBOARD_TYPE = "__KEYBOARD_TYPE__"; 3 | export const CONTEXT_MENU_TYPE = "__CONTEXT_MENU_TYPE__"; 4 | export const URL_MATCH = ["https://*/*", "http://*/*", "file://*/*"]; 5 | 6 | export type ActionType = typeof COPY_TYPE | typeof KEYBOARD_TYPE | typeof CONTEXT_MENU_TYPE; 7 | -------------------------------------------------------------------------------- /packages/force-copy/src/utils/global.ts: -------------------------------------------------------------------------------- 1 | let env = chrome; 2 | 3 | // #IFDEF GECKO 4 | if (typeof browser !== "undefined") { 5 | env = browser; 6 | } 7 | // #ENDIF 8 | 9 | export const cross = env; 10 | -------------------------------------------------------------------------------- /packages/force-copy/src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export const isInIframe = self !== top; 2 | -------------------------------------------------------------------------------- /packages/force-copy/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | const _global: typeof globalThis = (() => { 2 | if (typeof globalThis !== "undefined") return globalThis; // ES2020 3 | if (typeof window !== "undefined") return window; // Browser 4 | if (typeof global !== "undefined") return global; // Node 5 | if (typeof self !== "undefined") return self; // Worker 6 | return Function("return this")(); // Fallback 7 | })(); 8 | 9 | export const log = _global.console.log; 10 | export const warn = _global.console.warn; 11 | export const error = _global.console.error; 12 | export const trace = _global.console.trace; 13 | export const LOG_LEVEL = { INFO: 0, WARNING: 1, ERROR: 2, DISABLE: 3 }; 14 | 15 | class Logger { 16 | public constructor(private level: number) {} 17 | 18 | public setLevel(level: typeof LOG_LEVEL[keyof typeof LOG_LEVEL]) { 19 | this.level = level; 20 | } 21 | 22 | public info(...args: unknown[]) { 23 | if (this.level <= LOG_LEVEL.INFO) { 24 | log("FC Log:", ...args); 25 | } 26 | } 27 | 28 | public trace(...args: unknown[]) { 29 | if (this.level <= LOG_LEVEL.INFO) { 30 | trace("FC Trace:", ...args); 31 | } 32 | } 33 | 34 | public warning(...args: unknown[]) { 35 | if (this.level <= LOG_LEVEL.WARNING) { 36 | warn("FC Warning:", ...args); 37 | } 38 | } 39 | 40 | public error(...args: unknown[]) { 41 | if (this.level <= LOG_LEVEL.ERROR) { 42 | error("FC Error:", ...args); 43 | } 44 | } 45 | } 46 | 47 | export const logger = new Logger(LOG_LEVEL.DISABLE); 48 | -------------------------------------------------------------------------------- /packages/force-copy/src/utils/reload.ts: -------------------------------------------------------------------------------- 1 | import { CWBridge } from "@/bridge/content-worker"; 2 | import { cross } from "./global"; 3 | import { logger } from "./logger"; 4 | 5 | export const RELOAD_APP = "RELOAD_APP"; 6 | 7 | export const onReceiveReloadMsg = () => { 8 | if (__DEV__) { 9 | try { 10 | const ws = new WebSocket("ws://localhost:3333"); 11 | // 收到消息即重载 12 | ws.onmessage = () => { 13 | try { 14 | CWBridge.postToWorker({ type: CWBridge.REQUEST.RELOAD, payload: null }); 15 | } catch (error) { 16 | logger.warning("SEND MESSAGE ERROR", error); 17 | } 18 | }; 19 | } catch (error) { 20 | logger.warning("CONNECT ERROR", error); 21 | } 22 | } 23 | }; 24 | 25 | export const reloadApp = (msg: unknown) => { 26 | if (__DEV__ && msg === RELOAD_APP) { 27 | cross.runtime.reload(); 28 | logger.warning("RELOAD SUCCESS"); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /packages/force-copy/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | 3 | export namespace Object { 4 | export type KeyType = string | number | symbol; 5 | 6 | export type Keys> = keyof T; 7 | 8 | export type Values> = T[keyof T]; 9 | } 10 | 11 | export namespace Array { 12 | export type Values = T[number]; 13 | } 14 | 15 | export namespace String { 16 | export type Map = { [P in T]: P }; 17 | } 18 | 19 | export namespace Reflex { 20 | type _Array> = T extends string 21 | ? [type: T, payload: unknown extends M[T] ? null : M[T]] 22 | : never; 23 | 24 | export type Array> = _Array, M>; 25 | 26 | type _Map> = { 27 | [P in T]: { type: P; payload: unknown extends M[P] ? never : M[P] }; 28 | }; 29 | 30 | export type Tuple> = Object.Values<_Map, M>>; 31 | } 32 | -------------------------------------------------------------------------------- /packages/force-copy/src/worker/channel/content.ts: -------------------------------------------------------------------------------- 1 | import { CWBridge } from "@/bridge/content-worker"; 2 | import type { CWRequestType } from "@/bridge/content-worker/request"; 3 | import { cross } from "@/utils/global"; 4 | import { logger } from "@/utils/logger"; 5 | import { RELOAD_APP, reloadApp } from "@/utils/reload"; 6 | 7 | export const onContentMessage = (data: CWRequestType, sender: chrome.runtime.MessageSender) => { 8 | logger.info("Worker Receive Content Message", data); 9 | switch (data.type) { 10 | case CWBridge.REQUEST.RELOAD: { 11 | reloadApp(RELOAD_APP); 12 | break; 13 | } 14 | case CWBridge.REQUEST.SET_BADGE: { 15 | const { payload } = data; 16 | if (payload && sender.tab && sender.tab.id) { 17 | const tabId = sender.tab.id; 18 | let action: typeof cross.action | typeof cross.browserAction = cross.action; 19 | // #IFDEF GECKO 20 | action = cross.browserAction; 21 | // #ENDIF 22 | action.setBadgeText({ text: payload.toString(), tabId }); 23 | action.setBadgeBackgroundColor({ color: "#4e5969", tabId }); 24 | } 25 | break; 26 | } 27 | } 28 | return null; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/force-copy/src/worker/index.ts: -------------------------------------------------------------------------------- 1 | import { LOG_LEVEL, logger } from "@/utils/logger"; 2 | import { importWorkerScript } from "./runtime/script"; 3 | import { CWBridge } from "@/bridge/content-worker"; 4 | import { onContentMessage } from "./channel/content"; 5 | import { initializeEvents } from "./runtime/initialize"; 6 | 7 | (() => { 8 | if (__DEV__) { 9 | logger.setLevel(LOG_LEVEL.INFO); 10 | } 11 | initializeEvents(); 12 | importWorkerScript(); 13 | CWBridge.onContentMessage(onContentMessage); 14 | })(); 15 | -------------------------------------------------------------------------------- /packages/force-copy/src/worker/runtime/initialize.ts: -------------------------------------------------------------------------------- 1 | import { URL_MATCH } from "@/utils/constant"; 2 | import { cross } from "@/utils/global"; 3 | import { logger } from "@/utils/logger"; 4 | import { NOOP } from "../utils/constant"; 5 | 6 | export const initializeEvents = () => { 7 | const onSetup = () => { 8 | logger.info("Register Inject Scripts On Setup"); 9 | cross.tabs.query({}).then(tabs => { 10 | for (const tab of tabs) { 11 | const tabId = tab && tab.id; 12 | const tabURL = tab && tab.url; 13 | if (!tabId || !tabURL) continue; 14 | if (!URL_MATCH.some(match => new RegExp(match).test(tabURL))) { 15 | continue; 16 | } 17 | // #IFDEF CHROMIUM 18 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/executeScript 19 | cross.scripting.executeScript({ 20 | target: { tabId: tabId, allFrames: true }, 21 | files: ["content.js"], 22 | injectImmediately: true, 23 | }); 24 | cross.scripting.executeScript({ 25 | target: { tabId: tabId, allFrames: true }, 26 | files: [process.env.INJECT_FILE + ".js"], 27 | injectImmediately: true, 28 | world: "MAIN", 29 | }); 30 | // #ENDIF 31 | // #IFDEF GECKO 32 | // https://developer.mozilla.org/zh-CN/docs/Mozilla/Add-ons/WebExtensions/API/tabs/executeScript 33 | cross.tabs 34 | .executeScript(tabId, { 35 | allFrames: true, 36 | file: process.env.INJECT_FILE + ".js", 37 | }) 38 | .catch(NOOP); 39 | cross.tabs 40 | .executeScript(tabId, { 41 | allFrames: true, 42 | file: "content.js", 43 | }) 44 | .catch(NOOP); 45 | // #ENDIF 46 | } 47 | }); 48 | }; 49 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled 50 | cross.runtime.onInstalled.addListener(onSetup); 51 | // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/management/onEnabled 52 | cross.management.onEnabled.addListener(onSetup); 53 | }; 54 | -------------------------------------------------------------------------------- /packages/force-copy/src/worker/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const CODE_PREFIX = [ 2 | `if (window["${process.env.INJECT_FILE}"] && document instanceof XMLDocument === false) {`, 3 | ` const script = document.createElementNS("http://www.w3.org/1999/xhtml", "script");`, 4 | ` script.setAttribute("type", "text/javascript");`, 5 | ` const code = \`;(\${window["${process.env.INJECT_FILE}"].toString()})();\`;`, 6 | ].join("\n"); 7 | 8 | export const CODE_SUFFIX = [ 9 | ` document.documentElement.appendChild(script);`, 10 | ` script.remove();`, 11 | `}`, 12 | ].join("\n"); 13 | 14 | export const NOOP = () => null; 15 | -------------------------------------------------------------------------------- /packages/force-copy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | } 7 | }, 8 | "include": ["./src/**/*.ts", "./src/**/*.tsx"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/site-director/README.md: -------------------------------------------------------------------------------- 1 | # 跳转链接直达 2 | 3 | 跳转链接直达,自动跳转链接的中间页面,用于谷歌、知乎、`CSDN`、简书。 4 | -------------------------------------------------------------------------------- /packages/site-director/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "🔥🔥🔥跳转链接直达🔥🔥🔥", 3 | "namespace": "https://github.com/WindrunnerMax/TKScript", 4 | "version": "1.3.3", 5 | "description": "跳转链接直达,去掉确定跳转链接页面,用于谷歌、知乎、CSDN、简书", 6 | "author": "Czy", 7 | "match": [ 8 | "*://*.google.com/*", 9 | "*://*.google.com.cn/*", 10 | "*://*.google.com.hk/*", 11 | "*://link.zhihu.com/*", 12 | "*://link.csdn.net/*", 13 | "*://link.juejin.cn/*", 14 | "*://www.jianshu.com/go-wild/*", 15 | "*://mail.qq.com/cgi-bin/readtemplate/*" 16 | ], 17 | "license": "MIT License", 18 | "supportURL": "https://github.com/WindrunnerMax/TKScript/issues", 19 | "installURL": "https://github.com/WindrunnerMax/TKScript", 20 | "grant": ["unsafeWindow", "GM_xmlhttpRequest"], 21 | "run-at": "document-start" 22 | } 23 | -------------------------------------------------------------------------------- /packages/site-director/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "site-director", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/site-director/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "./websites"; 2 | import websites from "./websites"; 3 | 4 | ((): void => { 5 | const mather = (regex: RegExp, website: Website) => { 6 | if (regex.test(window.location.href)) { 7 | website.init(); 8 | return true; 9 | } 10 | return false; 11 | }; 12 | websites.some(website => mather(website.regexp, website)); 13 | })(); 14 | -------------------------------------------------------------------------------- /packages/site-director/src/modules/csdn.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../websites"; 2 | 3 | const website: Website = { 4 | regexp: /csdn/, 5 | init: function () { 6 | const result = /.*link.csdn.net\/\?target=(.*)/.exec(location.href); 7 | if (result) { 8 | const url = decodeURIComponent(result[1]); 9 | if (url) { 10 | console.log(url); 11 | location.href = url; 12 | } 13 | } 14 | }, 15 | }; 16 | 17 | export default website; 18 | -------------------------------------------------------------------------------- /packages/site-director/src/modules/google.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../websites"; 2 | 3 | const website: Website = { 4 | regexp: /google/, 5 | init: function () { 6 | const isScholar = window.location.host.startsWith("scholar"); 7 | const selector = isScholar ? "#gs_bdy_ccl .gs_rt a" : "#res a"; 8 | document.addEventListener("DOMContentLoaded", () => 9 | document.querySelectorAll(selector).forEach(item => item.setAttribute("target", "_blank")) 10 | ); 11 | }, 12 | }; 13 | 14 | export default website; 15 | -------------------------------------------------------------------------------- /packages/site-director/src/modules/jianshu.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../websites"; 2 | 3 | const website: Website = { 4 | regexp: /jianshu/, 5 | init: function () { 6 | const result = /.*jianshu.com\/go-wild.*url=(.*)/.exec(location.href); 7 | if (result) { 8 | const url = decodeURIComponent(result[1]); 9 | if (url) { 10 | location.href = url; 11 | } 12 | } 13 | }, 14 | }; 15 | 16 | export default website; 17 | -------------------------------------------------------------------------------- /packages/site-director/src/modules/juejin.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../websites"; 2 | 3 | const website: Website = { 4 | regexp: /link\.juejin/, 5 | init: function () { 6 | const result = new URL(location.href).searchParams.get("target"); 7 | if (result) { 8 | location.href = decodeURIComponent(result); 9 | } 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/site-director/src/modules/qq-mail.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../websites"; 2 | 3 | const website: Website = { 4 | regexp: /mail\.qq/, 5 | init: function () { 6 | const result = new URL(location.href).searchParams.get("gourl"); 7 | if (result) { 8 | location.href = decodeURIComponent(result); 9 | } 10 | }, 11 | }; 12 | 13 | export default website; 14 | -------------------------------------------------------------------------------- /packages/site-director/src/modules/zhihu.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../websites"; 2 | 3 | const website: Website = { 4 | regexp: /link\.zhihu/, 5 | init: function () { 6 | const result = /.*link.zhihu.com\/\?target=(.*)/.exec(location.href); 7 | if (result) { 8 | const url = decodeURIComponent(result[1]); 9 | if (url) { 10 | console.log(url); 11 | location.href = url; 12 | } 13 | } 14 | }, 15 | }; 16 | 17 | export default website; 18 | -------------------------------------------------------------------------------- /packages/site-director/src/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WindRunnerMax/TKScript/23f5b72f77c302f455d08d014c5a2b7a8e75b4a0/packages/site-director/src/style.css -------------------------------------------------------------------------------- /packages/site-director/src/utils.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | directByBlockEvent: function (event: Event): void { 3 | event.stopPropagation(); 4 | event.preventDefault(); 5 | }, 6 | directByCapture: function (el: Element): void { 7 | el.addEventListener("click", e => this.directByBlockEvent(e)); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/site-director/src/websites.ts: -------------------------------------------------------------------------------- 1 | import google from "./modules/google"; 2 | import zhihu from "./modules/zhihu"; 3 | import csdn from "./modules/csdn"; 4 | import jianshu from "./modules/jianshu"; 5 | import qqMail from "./modules/qq-mail"; 6 | import juejin from "./modules/juejin"; 7 | 8 | export interface Website { 9 | regexp: RegExp; 10 | init: () => void; 11 | } 12 | 13 | const websites: Website[] = [google, zhihu, csdn, jianshu, qqMail, juejin]; 14 | 15 | export default websites; 16 | -------------------------------------------------------------------------------- /packages/water-mark/README.md: -------------------------------------------------------------------------------- 1 | # 移除页面水印 2 | 3 | 独立适配模式,移除常见网页的水印。 4 | -------------------------------------------------------------------------------- /packages/water-mark/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": { 3 | "default": "🔥🔥🔥移除页面水印🔥🔥🔥", 4 | "en": "Remove Page Watermark", 5 | "zh": "🔥🔥🔥移除页面水印🔥🔥🔥" 6 | }, 7 | "namespace": "https://github.com/WindrunnerMax/TKScript", 8 | "version": "1.0.7", 9 | "description": { 10 | "default": "移除常见网页的水印", 11 | "en": "Remove watermarks from common web pages", 12 | "zh": "移除常见网页的水印" 13 | }, 14 | "author": "Czy", 15 | "match": ["http://*/*", "https://*/*"], 16 | "supportURL": "https://github.com/WindrunnerMax/TKScript/issues", 17 | "license": "GPL License", 18 | "installURL": "https://github.com/WindrunnerMax/TKScript", 19 | "run-at": "document-start", 20 | "grant": ["GM_addStyle", "unsafeWindow"] 21 | } 22 | -------------------------------------------------------------------------------- /packages/water-mark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "water-mark", 3 | "version": "1.0.0", 4 | "author": "Czy", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/WindrunnerMax/TKScript" 9 | }, 10 | "scripts": { 11 | "lint:ts": "../../node_modules/typescript/bin/tsc --noEmit" 12 | }, 13 | "sideEffects": false, 14 | "files": [ 15 | "src" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/water-mark/src/index.ts: -------------------------------------------------------------------------------- 1 | import { basic } from "./modules/basic"; 2 | import type { Website } from "./types/website"; 3 | 4 | const websites: Website[] = [basic]; 5 | const web = websites.find(item => item.regexp.test(location.href)); 6 | web && web.init(); 7 | -------------------------------------------------------------------------------- /packages/water-mark/src/modules/basic.ts: -------------------------------------------------------------------------------- 1 | import type { Website } from "../types/website"; 2 | import { FALLBACK_CLASS, OPACITY_BACKGROUND_PROPERTY, OPACITY_PROPERTY } from "../utils/constant"; 3 | import { inspectWaterMarkDOM } from "../utils/dom"; 4 | import { injectCSSEarly } from "../utils/styles"; 5 | 6 | export const basic: Website = { 7 | regexp: /.*/, 8 | init: () => { 9 | const observer = MutationObserver.prototype.observe; 10 | MutationObserver.prototype.observe = function (target, options) { 11 | inspectWaterMarkDOM(target); 12 | target.childNodes.forEach(inspectWaterMarkDOM); 13 | observer.call(this, target, options); 14 | }; 15 | const _MutationObserver = MutationObserver; 16 | const getMutationCallback = (callback: MutationCallback) => { 17 | return (records: MutationRecord[], observer: MutationObserver) => { 18 | let isMatchedWaterMarkDOM = false; 19 | for (const record of records) { 20 | if (inspectWaterMarkDOM(record.target) && !isMatchedWaterMarkDOM) { 21 | isMatchedWaterMarkDOM = true; 22 | } 23 | } 24 | !isMatchedWaterMarkDOM && callback(records, observer); 25 | }; 26 | }; 27 | unsafeWindow.MutationObserver = class extends _MutationObserver { 28 | constructor(callback: MutationCallback) { 29 | super(getMutationCallback(callback)); 30 | } 31 | }; 32 | const PRESET_CLASSES = [`.${FALLBACK_CLASS}`].join(","); 33 | injectCSSEarly(`${PRESET_CLASSES}{${OPACITY_PROPERTY}}`); 34 | const PRESET_BACKGROUND = [ 35 | `div[id*="watermark"]`, 36 | `div[id*="WaterMark"]`, 37 | `div[id*="Watermark"]`, 38 | `div[class*="watermark"]`, 39 | `div[class*="WaterMark"]`, 40 | `div[class*="Watermark"]`, 41 | `div[style*="pointer-events"][style*="background: url"]`, 42 | `div[style*="pointer-events"][style*="background-image: url"]`, 43 | `div[style*="pointer-events"][style*="background:url"]`, 44 | `div[style*="pointer-events"][style*="background-image:url"]`, 45 | ].join(","); 46 | injectCSSEarly(`${PRESET_BACKGROUND}{${OPACITY_BACKGROUND_PROPERTY}}`); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/water-mark/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare const unsafeWindow = any; 2 | 3 | declare module "*.css" { 4 | const content: string; 5 | export default content; 6 | } 7 | 8 | declare const GM_addStyle: (css: string) => void; 9 | -------------------------------------------------------------------------------- /packages/water-mark/src/types/website.ts: -------------------------------------------------------------------------------- 1 | export type Website = { 2 | regexp: RegExp; 3 | init: () => void; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/water-mark/src/utils/constant.ts: -------------------------------------------------------------------------------- 1 | export const FALLBACK_CLASS = "__WATERMARK__"; 2 | 3 | export const OPACITY_PROPERTY = [ 4 | "opacity: 0 !important;", 5 | "visibility: hidden !important;", 6 | "transform: translate(-999999px, -999999px) !important;", 7 | ].join(""); 8 | 9 | export const OPACITY_BACKGROUND_PROPERTY = [ 10 | "background: transparent !important;", 11 | "background-color: transparent !important;", 12 | "background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0)) !important;", 13 | ].join(""); 14 | 15 | export const NOOP = () => null; 16 | -------------------------------------------------------------------------------- /packages/water-mark/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | import { FALLBACK_CLASS } from "./constant"; 2 | 3 | export const inspectWaterMarkDOM = (node: Node) => { 4 | if (node instanceof HTMLElement === false) { 5 | return false; 6 | } 7 | if (node.classList.contains(FALLBACK_CLASS)) { 8 | return true; 9 | } 10 | if (!node.hasAttribute("style") || node.style.pointerEvents !== "none") { 11 | return false; 12 | } 13 | if (node instanceof HTMLImageElement && node.src && node.src.startsWith("data:")) { 14 | !node.classList.contains(FALLBACK_CLASS) && node.classList.add(FALLBACK_CLASS); 15 | return true; 16 | } 17 | if (node.style.background.startsWith("url") || node.style.backgroundImage.startsWith("url")) { 18 | !node.classList.contains(FALLBACK_CLASS) && node.classList.add(FALLBACK_CLASS); 19 | return true; 20 | } 21 | return false; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/water-mark/src/utils/styles.ts: -------------------------------------------------------------------------------- 1 | import { styles } from "../../../copy-currency/src/utils"; 2 | 3 | export const injectCSSEarly = (css: string) => { 4 | if (typeof GM_addStyle === "function") { 5 | GM_addStyle(css); 6 | return void 0; 7 | } 8 | const style = document.createElement("style"); 9 | style.innerText = css; 10 | const head = document.head; 11 | if (head) { 12 | head.appendChild(style); 13 | return void 0; 14 | } 15 | const html = document.documentElement; 16 | if (html) { 17 | html.appendChild(style); 18 | return void 0; 19 | } 20 | styles.insertCSS(String(Math.random()), css); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/water-mark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src/**/*.ts", "./src/**/*.tsx"] 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" -------------------------------------------------------------------------------- /publish-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | npx rollup -c 5 | cp ./packages/expansion/expansion.user.js ./dist/expansion.user.js 6 | cp ./packages/captcha/captcha.user.js ./dist/captcha.user.js 7 | cp ./packages/completion/completion.user.js ./dist/completion.user.js 8 | cp ./README.md ./dist/README.md 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import postcss from "rollup-plugin-postcss"; 2 | import esbuild from "rollup-plugin-esbuild"; 3 | import metablock from "rollup-plugin-userscript-metablock"; 4 | import path from "path"; 5 | import replace from "@rollup/plugin-replace"; 6 | 7 | const buildConfig = { 8 | replace: { 9 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), 10 | "process.env.CHANNEL": JSON.stringify(process.env.CHANNEL), 11 | "preventAssignment": true, 12 | }, 13 | postcss: { 14 | minimize: true, 15 | extensions: [".css"], 16 | }, 17 | esbuild: { 18 | exclude: [/node_modules/], 19 | sourceMap: false, 20 | target: "es2015", 21 | minify: false, 22 | charset: "utf8", 23 | tsconfig: path.resolve(__dirname, "tsconfig.json"), 24 | }, 25 | }; 26 | 27 | const scriptConfig = [ 28 | { 29 | name: "Copy", 30 | meta: { 31 | input: "./meta/blank.ts", 32 | output: "./dist/meta/copy.meta.js", 33 | metaFile: "./packages/copy/meta.json", 34 | }, 35 | script: { 36 | input: "./packages/copy/src/index.ts", 37 | output: "./dist/copy.user.js", 38 | injectCss: false, 39 | }, 40 | }, 41 | { 42 | name: "CopyCurrency", 43 | meta: { 44 | input: "./meta/blank.ts", 45 | output: "./dist/meta/copy-currency.meta.js", 46 | metaFile: "./packages/copy-currency/meta.json", 47 | }, 48 | script: { 49 | input: "./packages/copy-currency/src/index.ts", 50 | output: "./dist/copy-currency.user.js", 51 | }, 52 | }, 53 | { 54 | name: "SiteDirector", 55 | meta: { 56 | input: "./meta/blank.ts", 57 | output: "./dist/meta/site-director.meta.js", 58 | metaFile: "./packages/site-director/meta.json", 59 | }, 60 | script: { 61 | input: "./packages/site-director/src/index.ts", 62 | output: "./dist/site-director.user.js", 63 | injectCss: false, 64 | }, 65 | }, 66 | { 67 | name: "WaterMark", 68 | meta: { 69 | input: "./meta/blank.ts", 70 | output: "./dist/meta/water-mark.meta.js", 71 | metaFile: "./packages/water-mark/meta.json", 72 | }, 73 | script: { 74 | input: "./packages/water-mark/src/index.ts", 75 | output: "./dist/water-mark.user.js", 76 | injectCss: false, 77 | }, 78 | }, 79 | ]; 80 | 81 | export default [ 82 | ...scriptConfig.map(item => ({ 83 | input: item.meta.input, 84 | output: { 85 | file: item.meta.output, 86 | format: "es", 87 | name: item.name + "Meta", 88 | }, 89 | plugins: [metablock({ file: item.meta.metaFile })], 90 | })), 91 | ...scriptConfig.map(item => ({ 92 | input: item.script.input, 93 | output: { 94 | file: item.script.output, 95 | format: "iife", 96 | name: item.name + "Module", 97 | }, 98 | plugins: [ 99 | replace({ ...buildConfig.replace }), 100 | postcss({ ...buildConfig.postcss, inject: item.script.injectCss }), 101 | esbuild(buildConfig.esbuild), 102 | // terser({ format: { comments: true } }), 103 | metablock({ file: item.meta.metaFile }), 104 | ], 105 | })), 106 | ]; 107 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "module": "esnext", 6 | "strict": true, 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true, 13 | "strictNullChecks": true, 14 | "downlevelIteration": true, 15 | "declaration": false, 16 | "jsx": "react-jsx", 17 | "newLine": "lf", 18 | "paths": { 19 | "copy/*": ["./packages/copy/*"], 20 | "copy-currency/*": ["./packages/copy-currency/*"] 21 | } 22 | }, 23 | "include": ["packages/*/src"], 24 | "exclude": ["node_modules"], 25 | "ts-node": { 26 | "compilerOptions": { 27 | "module": "commonjs" 28 | } 29 | } 30 | } 31 | --------------------------------------------------------------------------------