├── .vscode └── settings.json ├── assets ├── icon.png ├── 423down.svg ├── nodeseek.svg ├── shimo.svg ├── telegram.svg ├── juejin.svg ├── curseforge.svg ├── oschina.svg ├── chinaz.svg ├── aliyun.svg ├── docsqq.svg ├── gitee.svg ├── kookapp.svg ├── latexstudio.svg ├── linkedin.svg ├── youtube.svg ├── mpweixin.svg ├── zhihu.svg ├── 360doc.svg ├── sspai.svg ├── qcc.svg ├── logonews.svg ├── steamcommunity.svg ├── tianyancha.svg ├── yuque.svg ├── douban.svg ├── developersweixin.svg ├── tencent.svg ├── baidutieba.svg ├── 51cto.svg ├── nowcoder.svg ├── bilibili.svg ├── weibo.svg ├── uisdc.svg ├── mailqq.svg ├── icon.svg ├── acgrip.svg ├── jianshu.svg ├── afdian.svg ├── leetcode.svg ├── baike.svg ├── weixin110.svg ├── coolapk.svg ├── gamercomtw.svg ├── hellogithub.svg ├── csdn.svg ├── bookmarkearth.svg ├── blzxteam.svg ├── yunpanziyuan.svg ├── duowan.svg ├── instagram.svg ├── infoq.svg └── qq.svg ├── screenshots ├── theme1.png ├── theme2.png ├── theme3.png └── theme4.png ├── .npmrc ├── postcss.config.js ├── utils ├── index.ts ├── favicons.ts └── pure.ts ├── tsconfig.json ├── .gitignore ├── .prettierrc.mjs ├── popup.tsx ├── components ├── Img.tsx ├── Modal.tsx ├── hooks.tsx └── Icons.tsx ├── tailwind.config.js ├── contents ├── chrome-bridge.ts └── go.ts ├── .github └── workflows │ └── submit.yml ├── LICENSE ├── locales ├── en │ └── messages.json └── zh_CN │ └── messages.json ├── package.json ├── README.md ├── README.en-US.md ├── tabs ├── Settings.tsx └── Sidepanel.tsx ├── tailwind.less └── background.ts /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dolov/chrome-QuickGo/HEAD/assets/icon.png -------------------------------------------------------------------------------- /screenshots/theme1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dolov/chrome-QuickGo/HEAD/screenshots/theme1.png -------------------------------------------------------------------------------- /screenshots/theme2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dolov/chrome-QuickGo/HEAD/screenshots/theme2.png -------------------------------------------------------------------------------- /screenshots/theme3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dolov/chrome-QuickGo/HEAD/screenshots/theme3.png -------------------------------------------------------------------------------- /screenshots/theme4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dolov/chrome-QuickGo/HEAD/screenshots/theme4.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | sharp_binary_host=https://npmmirror.com/mirrors/sharp 3 | sharp_libvips_binary_host=https://npmmirror.com/mirrors/sharp-libvips -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('postcss').ProcessOptions} 3 | */ 4 | module.exports = { 5 | plugins: { 6 | tailwindcss: {}, 7 | autoprefixer: {} 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import { parse as tldtsParse } from "tldts" 2 | 3 | export function getDomain(url: string, hostname = false) { 4 | if (!url) return 5 | const parmas = tldtsParse(url) 6 | if (hostname) { 7 | return parmas.hostname 8 | } 9 | return parmas.domain 10 | } 11 | -------------------------------------------------------------------------------- /assets/423down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "plasmo/templates/tsconfig.base", 3 | "exclude": [ 4 | "node_modules" 5 | ], 6 | "include": [ 7 | ".plasmo/index.d.ts", 8 | "./**/*.ts", 9 | "./**/*.tsx" 10 | ], 11 | "compilerOptions": { 12 | "paths": { 13 | "~*": [ 14 | "./*" 15 | ] 16 | }, 17 | "baseUrl": "." 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | *.pem 15 | 16 | # debug 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | .pnpm-debug.log* 21 | 22 | # local env files 23 | .env*.local 24 | 25 | .env 26 | 27 | out/ 28 | build/ 29 | dist/ 30 | 31 | # plasmo 32 | .plasmo 33 | 34 | # typescript 35 | .tsbuildinfo 36 | 37 | .test.js 38 | .server.js -------------------------------------------------------------------------------- /.prettierrc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | export default { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "none", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 14 | importOrder: [ 15 | "", // Node.js built-in modules 16 | "", // Imports not matched by other special words or groups. 17 | "", // Empty line 18 | "^@plasmo/(.*)$", 19 | "", 20 | "^@plasmohq/(.*)$", 21 | "", 22 | "^~(.*)$", 23 | "", 24 | "^[./]" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /popup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import "~tailwind.less" 4 | 5 | export interface popupProps {} 6 | 7 | const popup: React.FC = (props) => { 8 | const {} = props 9 | return ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | ) 20 | } 21 | 22 | export default popup 23 | -------------------------------------------------------------------------------- /components/Img.tsx: -------------------------------------------------------------------------------- 1 | import icon from "data-base64:~assets/icon.svg" 2 | import React from "react" 3 | 4 | export interface ImgProps extends React.ImgHTMLAttributes {} 5 | 6 | const Img: React.FC = (props) => { 7 | const { src, className, ...rProps } = props 8 | const [imageUrl, setImageUrl] = React.useState(src) 9 | 10 | React.useEffect(() => { 11 | if (src) { 12 | setImageUrl(src) 13 | return 14 | } 15 | setImageUrl(icon) 16 | }, [src]) 17 | 18 | const onError = () => { 19 | setImageUrl(icon) 20 | } 21 | 22 | return ( 23 | 24 | ) 25 | } 26 | 27 | export default Img 28 | -------------------------------------------------------------------------------- /assets/nodeseek.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/shimo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/telegram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/juejin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/curseforge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/oschina.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | mode: "jit", 4 | darkMode: "class", 5 | content: ["./**/*.tsx"], 6 | daisyui: { 7 | themes: [ 8 | "light", 9 | "dark", 10 | "cupcake", 11 | "bumblebee", 12 | "emerald", 13 | "corporate", 14 | "synthwave", 15 | "retro", 16 | "cyberpunk", 17 | "valentine", 18 | "halloween", 19 | "garden", 20 | "forest", 21 | "aqua", 22 | "lofi", 23 | "pastel", 24 | "fantasy", 25 | "wireframe", 26 | "black", 27 | "luxury", 28 | "dracula", 29 | "cmyk", 30 | "autumn", 31 | "business", 32 | "acid", 33 | "lemonade", 34 | "night", 35 | "coffee", 36 | "winter", 37 | "dim", 38 | "nord", 39 | "sunset" 40 | ] 41 | }, 42 | plugins: [require("daisyui")] 43 | } 44 | -------------------------------------------------------------------------------- /assets/chinaz.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /assets/aliyun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /assets/docsqq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/gitee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /contents/chrome-bridge.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoCSConfig } from "plasmo" 2 | 3 | import { Storage } from "@plasmohq/storage" 4 | 5 | import { StorageKeys, type RuleProps } from "~utils/pure" 6 | 7 | export const config: PlasmoCSConfig = { 8 | matches: [""], 9 | all_frames: false, 10 | run_at: "document_start" 11 | } 12 | 13 | const storage = new Storage() 14 | 15 | window.addEventListener("message", (event) => { 16 | if (event.source !== window) return // 避免处理其他来源的消息 17 | const { type, data } = event.data 18 | if (!type) return 19 | if (type === "QuickGo::GET_RULES_FROM_INJECTED_SCRIPT") { 20 | storage.get>(StorageKeys.RULES).then((data) => { 21 | const newData = data || {} 22 | window.postMessage( 23 | { type: "QuickGo::GET_RULES_FROM_CONTENT_SCRIPT", data: newData }, 24 | "*" 25 | ) 26 | }) 27 | } 28 | 29 | if (type === "QuickGo::SET_RULES_FROM_INJECTED_SCRIPT") { 30 | storage.set(StorageKeys.RULES, data) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /.github/workflows/submit.yml: -------------------------------------------------------------------------------- 1 | name: "Submit to Web Store" 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - name: Cache pnpm modules 11 | uses: actions/cache@v3 12 | with: 13 | path: ~/.pnpm-store 14 | key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} 15 | restore-keys: | 16 | ${{ runner.os }}- 17 | - uses: pnpm/action-setup@v2.2.4 18 | with: 19 | version: latest 20 | run_install: true 21 | - name: Use Node.js 16.x 22 | uses: actions/setup-node@v3.4.1 23 | with: 24 | node-version: 16.x 25 | cache: "pnpm" 26 | - name: Build the extension 27 | run: pnpm build 28 | - name: Package the extension into a zip artifact 29 | run: pnpm package 30 | - name: Browser Platform Publish 31 | uses: PlasmoHQ/bpp@v3 32 | with: 33 | keys: ${{ secrets.SUBMIT_KEYS }} 34 | artifact: build/chrome-mv3-prod.zip 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [shisongyan] 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. -------------------------------------------------------------------------------- /assets/kookapp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/latexstudio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /assets/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /assets/mpweixin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/zhihu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/360doc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "QuickGo", 4 | "description": "" 5 | }, 6 | "extensionDescription": { 7 | "message": "Say goodbye to annoying safe redirects, and enjoy smoother access.", 8 | "description": "" 9 | }, 10 | "confirm": { 11 | "message": "Confirm", 12 | "description": "" 13 | }, 14 | "cancel": { 15 | "message": "Cancel", 16 | "description": "" 17 | }, 18 | "actions_setting": { 19 | "message": "Settings", 20 | "description": "" 21 | }, 22 | "actions_issues": { 23 | "message": "Feature Requests & Bug Reports", 24 | "description": "" 25 | }, 26 | "create": { 27 | "message": "Create", 28 | "description": "" 29 | }, 30 | "disabled": { 31 | "message": "Disable", 32 | "description": "" 33 | }, 34 | "enable": { 35 | "message": "Enable", 36 | "description": "" 37 | }, 38 | "delete": { 39 | "message": "Delete", 40 | "description": "" 41 | }, 42 | "settings_theme": { 43 | "message": "Theme", 44 | "description": "" 45 | }, 46 | "existed": { 47 | "message": "Existed", 48 | "description": "" 49 | }, 50 | "placeholder_url": { 51 | "message": "Type url here", 52 | "description": "" 53 | }, 54 | "placeholder_parameter": { 55 | "message": "Type redirect parameter name here", 56 | "description": "" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /assets/sspai.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/qcc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "QuickGo 外链直达 — 无感知自动跳过*乎、*书、掘*金、C**N、少数派、Gi**e 等网站的安全中心跳转限制", 4 | "description": "" 5 | }, 6 | "extensionDescription": { 7 | "message": "你是不是经常在*乎、*书、掘*金、C**N、少数派、Gi**e等网站上点击外链,却被拦截到“安全中心”,还得再点一次才能继续?太麻烦了!QuickGo 帮你自动绕过安全跳转,无感知直达目标页面,节省时间,浏览体验更丝滑!想去哪就去哪,畅行无阻!时时刻刻快人一步!", 8 | "description": "" 9 | }, 10 | "confirm": { 11 | "message": "确定", 12 | "description": "" 13 | }, 14 | "cancel": { 15 | "message": "取消", 16 | "description": "" 17 | }, 18 | "actions_setting": { 19 | "message": "设置", 20 | "description": "" 21 | }, 22 | "actions_issues": { 23 | "message": "功能申请 & 问题报告", 24 | "description": "" 25 | }, 26 | "create": { 27 | "message": "创建", 28 | "description": "" 29 | }, 30 | "disabled": { 31 | "message": "禁用", 32 | "description": "" 33 | }, 34 | "enable": { 35 | "message": "启用", 36 | "description": "" 37 | }, 38 | "delete": { 39 | "message": "删除", 40 | "description": "" 41 | }, 42 | "settings_theme": { 43 | "message": "主题配置", 44 | "description": "" 45 | }, 46 | "existed": { 47 | "message": "已存在", 48 | "description": "" 49 | }, 50 | "placeholder_url": { 51 | "message": "请输入网址", 52 | "description": "" 53 | }, 54 | "placeholder_parameter": { 55 | "message": "请输入 url 中的重定向参数", 56 | "description": "" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "quickgo", 3 | "displayName": "__MSG_extensionName__", 4 | "version": "3.4.0", 5 | "description": "__MSG_extensionDescription__", 6 | "author": "Dolov. ", 7 | "scripts": { 8 | "dev": "plasmo dev", 9 | "build": "plasmo build && plasmo package", 10 | "package": "plasmo package" 11 | }, 12 | "dependencies": { 13 | "@plasmohq/storage": "^1.11.0", 14 | "classnames": "^2.5.1", 15 | "daisyui": "^4.12.8", 16 | "dayjs": "^1.11.11", 17 | "plasmo": "0.88.0", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0", 20 | "tldts": "^6.1.85" 21 | }, 22 | "devDependencies": { 23 | "@ianvs/prettier-plugin-sort-imports": "4.1.1", 24 | "@types/chrome": "0.0.258", 25 | "@types/dom-view-transitions": "^1.0.4", 26 | "@types/node": "20.11.5", 27 | "@types/react": "18.2.48", 28 | "@types/react-dom": "18.2.18", 29 | "autoprefixer": "^10.4.19", 30 | "postcss": "^8.4.38", 31 | "prettier": "3.2.4", 32 | "tailwindcss": "^3.4.4", 33 | "typescript": "5.3.3" 34 | }, 35 | "manifest": { 36 | "default_locale": "zh_CN", 37 | "permissions": [ 38 | "sidePanel", 39 | "webNavigation", 40 | "contextMenus", 41 | "tabs" 42 | ], 43 | "side_panel": { 44 | "default_path": "tabs/Sidepanel.html" 45 | }, 46 | "host_permissions": [ 47 | "https://*/*" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /assets/logonews.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/steamcommunity.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/tianyancha.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/yuque.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | -------------------------------------------------------------------------------- /assets/douban.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/developersweixin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 17 | 18 | 19 | 22 | 25 | -------------------------------------------------------------------------------- /assets/tencent.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import classnames from "classnames" 2 | import React from "react" 3 | 4 | export interface ModalProps { 5 | visible: boolean 6 | onClose: () => void 7 | children: React.ReactNode 8 | title?: React.ReactNode 9 | onOk?: () => void 10 | okButtonProps?: Record 11 | } 12 | 13 | const Modal: React.FC = (props) => { 14 | const { children, visible, onClose, title, onOk, okButtonProps } = props 15 | const { disabled } = okButtonProps || {} 16 | 17 | const id = React.useMemo(() => { 18 | return `modal-${Date.now()}` 19 | }, []) 20 | 21 | React.useEffect(() => { 22 | const dialog: HTMLDialogElement = document.querySelector(`#${id}`) 23 | dialog.addEventListener("cancel", () => { 24 | onClose() 25 | }) 26 | }, []) 27 | 28 | React.useEffect(() => { 29 | const dialog: HTMLDialogElement = document.querySelector(`#${id}`) 30 | if (visible) { 31 | dialog.showModal() 32 | return 33 | } 34 | dialog.close() 35 | }, [visible]) 36 | 37 | const handleOk = () => { 38 | if (onOk) onOk() 39 | } 40 | 41 | return ( 42 | 43 |
44 |

{title}

45 |
{children}
46 |
47 | 50 | 57 |
58 |
59 |
60 | ) 61 | } 62 | 63 | export default Modal 64 | -------------------------------------------------------------------------------- /assets/baidutieba.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /assets/51cto.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/nowcoder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | -------------------------------------------------------------------------------- /assets/bilibili.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /assets/weibo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/uisdc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 10 | 13 | -------------------------------------------------------------------------------- /assets/mailqq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | -------------------------------------------------------------------------------- /assets/icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 22 | 36 | -------------------------------------------------------------------------------- /assets/acgrip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 14 | 16 | 18 | 20 | 23 | 26 | -------------------------------------------------------------------------------- /contents/go.ts: -------------------------------------------------------------------------------- 1 | import type { PlasmoCSConfig } from "plasmo" 2 | 3 | import { getMergedRules, type RuleProps } from "~utils/pure" 4 | 5 | export const config: PlasmoCSConfig = { 6 | matches: [""], 7 | all_frames: false, 8 | run_at: "document_end", 9 | world: "MAIN" 10 | } 11 | 12 | const getRules = async (): Promise<{ 13 | rules: RuleProps[] 14 | ruleMap: Record 15 | }> => { 16 | return new Promise((resolve) => { 17 | const handleMessage = (event) => { 18 | if (event.data.type === "QuickGo::GET_RULES_FROM_CONTENT_SCRIPT") { 19 | window.removeEventListener("message", handleMessage) 20 | const ruleMap = event.data.data 21 | const rules = getMergedRules(ruleMap) 22 | resolve({ 23 | rules, 24 | ruleMap 25 | }) 26 | } 27 | } 28 | 29 | window.addEventListener("message", handleMessage) 30 | window.postMessage({ type: "QuickGo::GET_RULES_FROM_INJECTED_SCRIPT" }, "*") 31 | }) 32 | } 33 | 34 | const handleNavigation = async () => { 35 | const { origin, hostname, pathname } = window.location 36 | if (!hostname || origin === "chrome://newtab") return 37 | 38 | const { rules, ruleMap } = await getRules() 39 | 40 | const currentUrl = pathname ? `${hostname}${pathname}` : hostname 41 | 42 | const rule = rules.find((i) => { 43 | if (!i.matchUrl) return false 44 | 45 | let pattern = i.matchUrl 46 | .replace(/\./g, "\\.") // 转义 `.` 47 | .replace(/\(\*\)/g, ".*") // `(*)` 替换为 `.*`,匹配任意内容 48 | 49 | // 允许 `www.` 可选,并允许末尾可选的 `/` 50 | pattern = `^(www\\.)?${pattern}/?$` 51 | 52 | const regex = new RegExp(pattern) 53 | 54 | return regex.test(currentUrl) 55 | }) 56 | 57 | if (!rule) return 58 | 59 | const { id, disabled, runAtContent } = rule 60 | if (disabled || !runAtContent) return 61 | 62 | if (typeof rule.redirect === "function") { 63 | const updater = () => { 64 | const { count, ...restProps } = ruleMap[id] || {} 65 | const newRuleMap = { 66 | ...ruleMap, 67 | [id]: { 68 | ...restProps, 69 | count: count || 0, 70 | updateAt: Date.now() 71 | } 72 | } 73 | 74 | return () => { 75 | newRuleMap[id].count = newRuleMap[id].count + 1 76 | newRuleMap[id].updateAt = Date.now() 77 | window.postMessage( 78 | { type: "QuickGo::SET_RULES_FROM_INJECTED_SCRIPT", data: newRuleMap }, 79 | "*" 80 | ) 81 | } 82 | } 83 | 84 | const update = updater() 85 | rule.redirect(update) 86 | return 87 | } 88 | } 89 | 90 | handleNavigation() 91 | -------------------------------------------------------------------------------- /assets/jianshu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/afdian.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 8 | 11 | 14 | -------------------------------------------------------------------------------- /assets/leetcode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/baike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # QuickGo 6 | 7 | ![GitHub commit activity](https://img.shields.io/github/commit-activity/y/dolov/chrome-QuickGo) 8 | ![Chrome Web Store Users](https://img.shields.io/chrome-web-store/users/homllehcipjgpbpepcojhgcpfdopjhml) 9 | ![Chrome Web Store Version](https://img.shields.io/chrome-web-store/v/homllehcipjgpbpepcojhgcpfdopjhml) 10 | ![Chrome Web Store Stars](https://img.shields.io/chrome-web-store/stars/homllehcipjgpbpepcojhgcpfdopjhml) 11 | ![GitHub](https://img.shields.io/github/license/dolov/chrome-QuickGo) 12 | 13 |
14 | 15 |
16 | 17 | Chrome 商店安装 18 | 19 | 20 | Edge 商店安装 21 | 22 |

QuickGo 外链直达 — 无感知自动跳过知乎、简书、掘金、CSDN、少数派、Gitee 等 50+ 网站的安全中心跳转限制。

23 | 24 | [English](https://github.com/Dolov/chrome-QuickGo/blob/main/README.en-US.md) | 简体中文 25 |
26 | 27 | ### 🚀 功能亮点 28 | 29 | 你是否在知乎、CSDN、掘金、简书等网站上点击外链时,总是被拦截到“安全中心”,还得多点一次才能继续?是不是觉得太麻烦了?😩 30 | 31 | 别担心,**QuickGo** 🏎️ 来帮你!它能 **自动绕过繁琐跳转,无感直达目标页面**,让你的浏览体验更加丝滑,省时又省心!💨 **想去哪就去哪,畅行无阻!快人一步,从此告别多余点击!** 🎯 32 | 33 | ✨ **核心功能**: 34 | 35 | - ⚡ **极速跳转**,使用更快的 onCreated & onBeforeNavigate API! 36 | - 📦 **即装即用**,支持知乎、简书、掘金、CSDN、少数派、Gitee 等 50+ 网站的自动跳转! 37 | - 🎨 **精美 UI**,多款主题随心切换,打造个性化浏览体验! 38 | - ✏️ **自定义规则**,支持手动添加未适配网站,让你自由掌控跳转路径! 39 | - 🖱️ **极简操作**,无需复杂设置,安装即生效,顺畅直达目标! 40 | - 📊 **智能统计**,支持展示快捷跳转次数,数据尽在掌握! 41 | 42 | ### 🛠️ 自定义规则指南 43 | 44 | 轻松绕过安全跳转,只需简单几步!👇 45 | 46 | 1️⃣ 当某个站点出现安全跳转时,**点击扩展图标**,页面右侧将弹出扩展窗口。 47 | 2️⃣ 点击 **“创建”** 按钮,打开设置弹窗。 48 | 3️⃣ 在弹窗中输入 **当前网站地址**(通常会自动填充,无需手动输入)。 49 | 4️⃣ 在弹窗中输入 **重定向参数名**,可观察地址栏(通常会自动填充,若未填充,可手动查找,常见名称如 `target` 或 `url`)。 50 | 5️⃣ **保存并刷新页面**,立即生效,畅通无阻! 🚀 51 | 52 | ![img](./screenshots/theme1.png) 53 | ![img](./screenshots/theme2.png) 54 | ![img](./screenshots/theme3.png) 55 | ![img](./screenshots/theme4.png) 56 | 57 | ### 🎉 欢迎使用 QuickGo 58 | 59 | 如果在使用过程中遇到问题,或者有新功能的需求,欢迎在 **issues** 中反馈,我们会及时跟进!🚀 60 | 61 | ### 🛠️ 开发 & 构建指南 62 | 63 | 1️⃣ **安装 Node.js** 👉 [下载地址](https://nodejs.org/en/download/package-manager) 64 | 2️⃣ **安装依赖**:`npm i` 65 | 3️⃣ **构建项目**:`npm build` 66 | 4️⃣ **打包扩展**:`npm package` 67 | 68 | 💡 快速上手,贡献你的想法,让 QuickGo 变得更强大!🎯 69 | 70 | [![Star History Chart](https://api.star-history.com/svg?repos=Dolov/chrome-QuickGo&type=Date)](https://star-history.com/#Dolov/chrome-QuickGo&Date) 71 | -------------------------------------------------------------------------------- /assets/weixin110.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | -------------------------------------------------------------------------------- /assets/coolapk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.en-US.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # QuickGo 6 | 7 | ![GitHub commit activity](https://img.shields.io/github/commit-activity/y/dolov/chrome-QuickGo) 8 | ![Chrome Web Store Users](https://img.shields.io/chrome-web-store/users/homllehcipjgpbpepcojhgcpfdopjhml) 9 | ![Chrome Web Store Version](https://img.shields.io/chrome-web-store/v/homllehcipjgpbpepcojhgcpfdopjhml) 10 | ![Chrome Web Store Stars](https://img.shields.io/chrome-web-store/stars/homllehcipjgpbpepcojhgcpfdopjhml) 11 | ![GitHub](https://img.shields.io/github/license/dolov/chrome-QuickGo) 12 | 13 |
14 | 15 |
16 | 17 | [Chrome Web Store](https://chromewebstore.google.com/detail/quickgo/homllehcipjgpbpepcojhgcpfdopjhml) QuickGo Direct Links — Automatically bypass security redirects on Zhihu, Jianshu, Juejin, CSDN, SSPAI, Gitee, and more. 18 | 19 | English | [Simplified Chinese](https://github.com/Dolov/chrome-QuickGo/blob/main/README.md) 20 | 21 |
22 | 23 | ### 🚀 Features 24 | 25 | Do you find it frustrating when clicking external links on Zhihu, CSDN, Juejin, Jianshu, and similar sites, only to be redirected to a "Security Center" page before proceeding? 😩 26 | 27 | No worries! **QuickGo** 🏎️ is here to help! It **automatically bypasses these annoying security redirects and takes you directly to your target page**—making your browsing experience smoother, faster, and hassle-free! 💨 **Go anywhere instantly without extra clicks!** 🎯 28 | 29 | ✨ **Key Features**: 30 | 31 | - 📦 **Ready to use** — Supports instant redirection on Zhihu, Jianshu, Juejin, CSDN, SSPAI, Gitee, and more. No extra steps required! 32 | - 🎨 **Beautiful UI** — Multiple themes available for a personalized browsing experience! 33 | - ✏️ **Custom Rules** — Manually add support for sites that aren’t yet included! 34 | - 🖱️ **Simple & Efficient** — No complex setup needed. Install and enjoy seamless browsing! 🚀 35 | 36 | ### 🛠️ Custom Rules Guide 37 | 38 | Easily bypass security redirects in just a few steps!👇 39 | 40 | 1️⃣ When a security redirect occurs on a site, **click the extension icon**, and a settings window will appear on the right side of the page. 41 | 2️⃣ Click the **"Create"** button to open a settings pop-up. 42 | 3️⃣ Enter the **current website address** in the pop-up (usually auto-filled, so no manual input needed). 43 | 4️⃣ Enter the **redirection parameter name**, which can be found in the URL (usually auto-filled as well. If not, check the address bar—common values are `target` or `url`). 44 | 5️⃣ **Save and refresh the page** for instant effect! 🚀 45 | 46 | ![img](./screenshots/theme1.png) 47 | ![img](./screenshots/theme2.png) 48 | 49 | ### 🎉 Welcome to QuickGo 50 | 51 | If you encounter any issues or have feature requests, feel free to submit feedback in **issues**. We’ll be happy to assist you! 🚀 52 | 53 | ### 🛠️ Development & Build Guide 54 | 55 | 1️⃣ **Install Node.js** 👉 [Download here](https://nodejs.org/en/download/package-manager) 56 | 2️⃣ **Install dependencies**: `npm i` 57 | 3️⃣ **Build the project**: `npm build` 58 | 4️⃣ **Package the extension**: `npm package` 59 | 60 | 💡 Get started quickly and contribute your ideas to make QuickGo even better! 🎯 61 | -------------------------------------------------------------------------------- /assets/gamercomtw.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 14 | 15 | 37 | 38 | -------------------------------------------------------------------------------- /assets/hellogithub.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/csdn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/bookmarkearth.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | -------------------------------------------------------------------------------- /assets/blzxteam.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | -------------------------------------------------------------------------------- /tabs/Settings.tsx: -------------------------------------------------------------------------------- 1 | import classnames from "classnames" 2 | import React from "react" 3 | 4 | import { useThemeChange } from "~components/hooks" 5 | 6 | import "~tailwind.less" 7 | 8 | const themes = [ 9 | "light", 10 | "dark", 11 | "cupcake", 12 | "bumblebee", 13 | "emerald", 14 | "corporate", 15 | "synthwave", 16 | "retro", 17 | "cyberpunk", 18 | "valentine", 19 | "halloween", 20 | "garden", 21 | "forest", 22 | "aqua", 23 | "lofi", 24 | "pastel", 25 | "fantasy", 26 | "wireframe", 27 | "black", 28 | "luxury", 29 | "dracula", 30 | "cmyk", 31 | "autumn", 32 | "business", 33 | "acid", 34 | "lemonade", 35 | "night", 36 | "coffee", 37 | "winter", 38 | "dim", 39 | "nord", 40 | "sunset" 41 | ] 42 | 43 | document.title = `${chrome.i18n.getMessage("extensionName")}` 44 | 45 | const ThemeList = (props) => { 46 | const { value, onChange } = props 47 | 48 | return ( 49 |
50 | {themes.map((item) => { 51 | const checked = value === item 52 | return ( 53 |
onChange(item)} 56 | className={classnames("overflow-hidden rounded-lg item-border", { 57 | "item-border-active": checked 58 | })}> 59 |
62 |
63 |
{" "} 64 |
65 |
66 |
{item}
67 |
70 |
71 |
72 | A 73 |
74 |
75 |
76 |
77 | A 78 |
79 |
80 |
81 |
82 | A 83 |
84 |
85 |
86 |
87 | A 88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | ) 96 | })} 97 |
98 | ) 99 | } 100 | 101 | export interface SettingProps {} 102 | 103 | const Setting: React.FC = (props) => { 104 | const {} = props 105 | 106 | const [theme, setTheme] = useThemeChange() 107 | 108 | return ( 109 |
110 |
111 | 112 |
113 | {chrome.i18n.getMessage("settings_theme")} 114 |
115 |
116 | 117 |
118 |
119 |
120 | ) 121 | } 122 | 123 | export default Setting 124 | -------------------------------------------------------------------------------- /tailwind.less: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | #__plasmo { 6 | @apply p-4; 7 | height: 100vh; 8 | background-image: repeating-linear-gradient( 9 | 45deg, 10 | var(--fallback-b1, oklch(var(--b1))), 11 | var(--fallback-b1, oklch(var(--b1))) 13px, 12 | var(--fallback-b2, oklch(var(--b2))) 13px, 13 | var(--fallback-b2, oklch(var(--b2))) 14px 14 | ); 15 | } 16 | 17 | @layer components { 18 | .item-border { 19 | @apply border-base-content/20 hover:border-base-content/40 border outline outline-2 outline-offset-2 outline-transparent; 20 | } 21 | 22 | .item-border-active { 23 | @apply !outline-base-content; 24 | } 25 | 26 | .ellipsis { 27 | @apply overflow-ellipsis overflow-hidden whitespace-nowrap; 28 | } 29 | 30 | .h-center { 31 | @apply flex items-center; 32 | } 33 | 34 | .x-center { 35 | @apply flex justify-center; 36 | } 37 | 38 | .center { 39 | @apply flex justify-center items-center; 40 | } 41 | 42 | .active { 43 | background-color: var(--fallback-n, oklch(var(--n) / var(--tw-bg-opacity))); 44 | color: var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity))); 45 | --tw-bg-opacity: 1; 46 | --tw-text-opacity: 1; 47 | } 48 | 49 | .no-scrollbar::-webkit-scrollbar { 50 | @apply hidden; 51 | } 52 | } 53 | 54 | @bounceIn: ~"bubble-bounceIn"; 55 | @bounceOut: ~"bubble-bounceOut"; 56 | 57 | @keyframes @bounceIn { 58 | from, 59 | 20%, 60 | 40%, 61 | 60%, 62 | 80%, 63 | to { 64 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 65 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 66 | } 67 | 68 | 0% { 69 | opacity: 0; 70 | -webkit-transform: scale3d(0.3, 0.3, 0.3); 71 | transform: scale3d(0.3, 0.3, 0.3); 72 | } 73 | 74 | 20% { 75 | -webkit-transform: scale3d(1.1, 1.1, 1.1); 76 | transform: scale3d(1.1, 1.1, 1.1); 77 | } 78 | 79 | 40% { 80 | -webkit-transform: scale3d(0.9, 0.9, 0.9); 81 | transform: scale3d(0.9, 0.9, 0.9); 82 | } 83 | 84 | 60% { 85 | opacity: 1; 86 | -webkit-transform: scale3d(1.03, 1.03, 1.03); 87 | transform: scale3d(1.03, 1.03, 1.03); 88 | } 89 | 90 | 80% { 91 | -webkit-transform: scale3d(0.97, 0.97, 0.97); 92 | transform: scale3d(0.97, 0.97, 0.97); 93 | } 94 | 95 | to { 96 | opacity: 1; 97 | -webkit-transform: scale3d(1, 1, 1); 98 | transform: scale3d(1, 1, 1); 99 | } 100 | } 101 | 102 | @keyframes @bounceOut { 103 | 20% { 104 | -webkit-transform: scale3d(0.9, 0.9, 0.9); 105 | transform: scale3d(0.9, 0.9, 0.9); 106 | } 107 | 108 | 50%, 109 | 55% { 110 | opacity: 1; 111 | -webkit-transform: scale3d(1.1, 1.1, 1.1); 112 | transform: scale3d(1.1, 1.1, 1.1); 113 | } 114 | 115 | to { 116 | opacity: 0; 117 | -webkit-transform: scale3d(0.3, 0.3, 0.3); 118 | transform: scale3d(0.3, 0.3, 0.3); 119 | } 120 | } 121 | 122 | .bubble { 123 | z-index: 999; 124 | position: relative; 125 | 126 | &-surround { 127 | position: absolute; 128 | top: 50%; 129 | left: 50%; 130 | margin-top: var(--offset-size); 131 | margin-left: var(--offset-size); 132 | display: none; 133 | z-index: -1; 134 | 135 | &.animate { 136 | display: block; 137 | } 138 | 139 | &.animate.visible { 140 | animation: @bounceIn 0.3s; 141 | } 142 | 143 | &.animate.hidden { 144 | animation: @bounceOut 0.3s forwards; 145 | } 146 | } 147 | 148 | &-sub { 149 | top: var(--offset-size); 150 | left: var(--offset-size); 151 | position: absolute; 152 | border-radius: 50%; 153 | display: flex; 154 | align-items: center; 155 | justify-content: center; 156 | 157 | &:hover { 158 | box-shadow: 159 | 0 2px 10px 0 var(--color), 160 | 0 2px 8px 0 var(--color); 161 | } 162 | } 163 | 164 | &:hover { 165 | box-shadow: 166 | 0 2px 10px 0 var(--color), 167 | 0 2px 8px 0 var(--color); 168 | } 169 | 170 | &.attach { 171 | transition: 0.3s; 172 | 173 | &.right:hover { 174 | border-radius: 50% 0 0 50%; 175 | } 176 | 177 | &.left:hover { 178 | border-radius: 0 50% 50% 0; 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /assets/yunpanziyuan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/duowan.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | -------------------------------------------------------------------------------- /components/hooks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { useStorage } from "@plasmohq/storage/hook" 4 | 5 | import { ga, GaEvents, StorageKeys } from "~utils/pure" 6 | 7 | export const useBoolean = (defaultValue = false) => { 8 | const [value, setValue] = React.useState(defaultValue) 9 | const valueRef = React.useRef() 10 | valueRef.current = value 11 | const toggle = (value?: boolean) => { 12 | const isBoolean = typeof value === "boolean" 13 | valueRef.current = isBoolean ? value : !valueRef.current 14 | setValue(valueRef.current) 15 | } 16 | return [value, toggle, valueRef] as const 17 | } 18 | 19 | export const useRefState = (defaultValue?: T) => { 20 | const [value, setValue] = React.useState(defaultValue) 21 | const valueRef = React.useRef(value) 22 | valueRef.current = value 23 | 24 | const setChangeValue = (nextValue: T) => { 25 | setValue(nextValue) 26 | } 27 | 28 | return [valueRef.current, setChangeValue, valueRef] as const 29 | } 30 | 31 | export const useUpdateEffect = ( 32 | effect: React.EffectCallback, 33 | deps?: React.DependencyList 34 | ) => { 35 | const isMounted = React.useRef(false) 36 | 37 | React.useEffect(() => { 38 | if (!isMounted.current) { 39 | isMounted.current = true 40 | return 41 | } 42 | return effect() 43 | }, deps) 44 | } 45 | 46 | export interface Options { 47 | trigger?: string 48 | defaultValue?: T 49 | valuePropName?: string 50 | defaultValuePropName?: string 51 | } 52 | 53 | export interface Props { 54 | [key: string]: any 55 | } 56 | 57 | /** 58 | * 在某些组件开发时,我们需要组件的状态即可以自己管理,也可以被外部控制,`useControllableValue` 就是帮你管理这种状态的 Hook 59 | * @param {any} props 60 | * @param {Options} options 61 | * @returns [state, setState] 62 | * @example 63 | * const [controllableValue, setControllableValue] = useControllableValue({ 64 | * focus: true, 65 | * onFocusChange: () => {...}, 66 | * }, { 67 | * trigger: 'onFocusChange', 68 | * valuePropName: 'focus' 69 | * }) 70 | * 71 | * const [controllableValue, setControllableValue] = useControllableValue({ 72 | * value: 123, 73 | * onChange: () => {...}, 74 | * }) 75 | */ 76 | export function useControllableValue( 77 | props: Props = {}, 78 | options: Options = {} 79 | ) { 80 | const { 81 | defaultValue: innerDefaultValue, 82 | trigger = "onChange", 83 | valuePropName = "value", 84 | defaultValuePropName = "defaultValue" 85 | } = options 86 | 87 | /** 目标状态值 */ 88 | const value = props[valuePropName] as T 89 | 90 | /** 目标状态默认值 */ 91 | const defaultValue = (props[defaultValuePropName] as T) ?? innerDefaultValue 92 | 93 | /** 初始化内部状态 */ 94 | const [innerValue, setInnerValue] = React.useState(() => { 95 | /** 优先取 props 中的目标状态值 */ 96 | if (value !== undefined) { 97 | return value 98 | } 99 | /** 其次取 defaultValue */ 100 | if (defaultValue !== undefined) { 101 | if (typeof defaultValue === "function") { 102 | return defaultValue() 103 | } 104 | return defaultValue 105 | } 106 | return undefined 107 | }) 108 | 109 | /** 优先使用外部状态值,其实使用内部状态值 */ 110 | const mergedValue = value !== undefined ? value : innerValue 111 | 112 | const triggerChange = (newValue: T, ...args: any[]) => { 113 | setInnerValue(newValue) 114 | if ( 115 | mergedValue !== newValue && 116 | /** 目标状态回调函数,props[trigger] 可以避免 this 丢失 */ 117 | typeof props[trigger] === "function" 118 | ) { 119 | props[trigger](newValue, ...args) 120 | } 121 | } 122 | 123 | /** 124 | * 同步非第一次的外部 undefined 状态至内部 125 | */ 126 | const firstRenderRef = React.useRef(true) 127 | React.useEffect(() => { 128 | if (firstRenderRef.current) { 129 | firstRenderRef.current = false 130 | return 131 | } 132 | 133 | if (value === undefined) { 134 | setInnerValue(value) 135 | } 136 | }, [value]) 137 | 138 | return [mergedValue, triggerChange] as const 139 | } 140 | 141 | export const useThemeChange = () => { 142 | const [settings, setSettings] = useStorage<{ 143 | theme?: string 144 | }>(StorageKeys.SETTINGS, { 145 | theme: "light" 146 | }) 147 | 148 | const { theme } = settings 149 | 150 | const setTheme = (theme: string) => { 151 | ga(GaEvents.SETTING_THEME, { 152 | value: theme 153 | }) 154 | setSettings({ 155 | ...settings, 156 | theme 157 | }) 158 | } 159 | 160 | React.useEffect(() => { 161 | if (!theme) return 162 | const html = document.querySelector("html") 163 | html.setAttribute("data-theme", theme) 164 | }, [theme]) 165 | 166 | return [theme, setTheme] 167 | } 168 | -------------------------------------------------------------------------------- /utils/favicons.ts: -------------------------------------------------------------------------------- 1 | import cto51 from "data-base64:~assets/51cto.svg" 2 | import doc360 from "data-base64:~assets/360doc.svg" 3 | import down423 from "data-base64:~assets/423down.svg" 4 | import acgrip from "data-base64:~assets/acgrip.svg" 5 | import afdian from "data-base64:~assets/afdian.svg" 6 | import aliyun from "data-base64:~assets/aliyun.svg" 7 | import baidutieba from "data-base64:~assets/baidutieba.svg" 8 | import baike from "data-base64:~assets/baike.svg" 9 | import bilibili from "data-base64:~assets/bilibili.svg" 10 | import blzxteam from "data-base64:~assets/blzxteam.svg" 11 | import bookmarkearth from "data-base64:~assets/bookmarkearth.svg" 12 | import chinaz from "data-base64:~assets/chinaz.svg" 13 | import coolapk from "data-base64:~assets/coolapk.svg" 14 | import csdn from "data-base64:~assets/csdn.svg" 15 | import curseforge from "data-base64:~assets/curseforge.svg" 16 | import developersweixin from "data-base64:~assets/developersweixin.svg" 17 | import docsqq from "data-base64:~assets/docsqq.svg" 18 | import douban from "data-base64:~assets/douban.svg" 19 | import duowan from "data-base64:~assets/duowan.svg" 20 | import gamercomtw from "data-base64:~assets/gamercomtw.svg" 21 | import gcores from "data-base64:~assets/gcores.svg" 22 | import gitee from "data-base64:~assets/gitee.svg" 23 | import hellogithub from "data-base64:~assets/hellogithub.svg" 24 | import infoq from "data-base64:~assets/infoq.svg" 25 | import instagram from "data-base64:~assets/instagram.svg" 26 | import jianshu from "data-base64:~assets/jianshu.svg" 27 | import juejin from "data-base64:~assets/juejin.svg" 28 | import kookapp from "data-base64:~assets/kookapp.svg" 29 | import latexstudio from "data-base64:~assets/latexstudio.svg" 30 | import leetcode from "data-base64:~assets/leetcode.svg" 31 | import linkedin from "data-base64:~assets/linkedin.svg" 32 | import logonews from "data-base64:~assets/logonews.svg" 33 | import mailqq from "data-base64:~assets/mailqq.svg" 34 | import mpweixin from "data-base64:~assets/mpweixin.svg" 35 | import nodeseek from "data-base64:~assets/nodeseek.svg" 36 | import nowcoder from "data-base64:~assets/nowcoder.svg" 37 | import oschina from "data-base64:~assets/oschina.svg" 38 | import qcc from "data-base64:~assets/qcc.svg" 39 | import qq from "data-base64:~assets/qq.svg" 40 | import shimo from "data-base64:~assets/shimo.svg" 41 | import sspai from "data-base64:~assets/sspai.svg" 42 | import steamcommunity from "data-base64:~assets/steamcommunity.svg" 43 | import telegram from "data-base64:~assets/telegram.svg" 44 | import tencent from "data-base64:~assets/tencent.svg" 45 | import tianyancha from "data-base64:~assets/tianyancha.svg" 46 | import uisdc from "data-base64:~assets/uisdc.svg" 47 | import weibo from "data-base64:~assets/weibo.svg" 48 | import weixin110 from "data-base64:~assets/weixin110.svg" 49 | import youtube from "data-base64:~assets/youtube.svg" 50 | import yunpanziyuan from "data-base64:~assets/yunpanziyuan.svg" 51 | import yuque from "data-base64:~assets/yuque.svg" 52 | import zhihu from "data-base64:~assets/zhihu.svg" 53 | 54 | export const domainFaviconMap = { 55 | "zhihu.com": zhihu, 56 | "juejin.cn": juejin, 57 | "jianshu.com": jianshu, 58 | "gitee.com": gitee, 59 | "csdn.net": csdn, 60 | "sspai.com": sspai, 61 | "afdian.com": afdian, 62 | "blzxteam.com": blzxteam, 63 | "chinaz.com": chinaz, 64 | "coolapk.com": coolapk, 65 | "curseforge.com": curseforge, 66 | "aliyun.com": aliyun, 67 | "douban.com": douban, 68 | "bilibili.com": bilibili, 69 | "gamer.com.tw": gamercomtw, 70 | "gcores.com": gcores, 71 | "hellogithub.com": hellogithub, 72 | "infoq.cn": infoq, 73 | "kookapp.cn": kookapp, 74 | "latexstudio.net": latexstudio, 75 | "leetcode.cn": leetcode, 76 | "linkedin.com": linkedin, 77 | "logonews.cn": logonews, 78 | "oschina.net": oschina, 79 | "qcc.com": qcc, 80 | "docs.qq.com": docsqq, 81 | "360doc.cn": doc360, 82 | "instagram.com": instagram, 83 | "mail.qq.com": mailqq, 84 | "wx.mail.qq.com": qq, 85 | "shimo.im": shimo, 86 | "steamcommunity.com": steamcommunity, 87 | "t.me": telegram, 88 | "tencent.com": tencent, 89 | "tianyancha.com": tianyancha, 90 | "tieba.baidu.com": baidutieba, 91 | "yuque.com": yuque, 92 | "youtube.com": youtube, 93 | "duowan.com": duowan, 94 | "weibo.cn": weibo, 95 | "bookmarkearth.cn": bookmarkearth, 96 | "yunpanziyuan.xyz": yunpanziyuan, 97 | "51cto.com": cto51, 98 | "developers.weixin.qq.com": developersweixin, 99 | "uisdc.com": uisdc, 100 | "nowcoder.com": nowcoder, 101 | "nodeseek.com": nodeseek, 102 | "baike.com": baike, 103 | "acgrip.com": acgrip, 104 | "weixin110.qq.com": weixin110, 105 | "mp.weixin.qq.com": mpweixin, 106 | "423down.com": down423 107 | } 108 | -------------------------------------------------------------------------------- /assets/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 44 | 46 | 48 | 50 | 53 | 55 | -------------------------------------------------------------------------------- /background.ts: -------------------------------------------------------------------------------- 1 | import { Storage } from "@plasmohq/storage" 2 | 3 | import { 4 | ga, 5 | GaEvents, 6 | getMergedRules, 7 | StorageKeys, 8 | type RuleProps 9 | } from "~utils/pure" 10 | 11 | const storage = new Storage() 12 | 13 | // 初始化侧边栏行为 14 | chrome.sidePanel 15 | .setPanelBehavior({ openPanelOnActionClick: true }) 16 | .catch((error) => console.error(error)) 17 | 18 | // 定义右键菜单项 19 | interface MenuItem extends chrome.contextMenus.CreateProperties { 20 | action?(tab: chrome.tabs.Tab): void 21 | } 22 | 23 | const menuList: MenuItem[] = [ 24 | { 25 | id: "issues", 26 | title: "功能申请 && 问题反馈", 27 | contexts: ["action"], 28 | action() { 29 | chrome.tabs.create({ 30 | url: "https://github.com/Dolov/chrome-QuickGo/issues" 31 | }) 32 | } 33 | }, 34 | { 35 | id: "settings", 36 | title: "个性化设置", 37 | contexts: ["action"], 38 | action() { 39 | chrome.tabs.create({ url: "./tabs/Settings.html" }) 40 | } 41 | } 42 | ] 43 | 44 | // 创建右键菜单 45 | function createContextMenus(menuList: MenuItem[]) { 46 | menuList.forEach(({ action, ...menuProps }) => { 47 | chrome.contextMenus.create(menuProps) 48 | }) 49 | } 50 | 51 | // 监听右键菜单点击事件 52 | function setupContextMenuListeners(menuList: MenuItem[]) { 53 | chrome.contextMenus.onClicked.addListener((info, tab) => { 54 | const menu = menuList.find((item) => item.id === info.menuItemId) 55 | menu?.action?.(tab) 56 | }) 57 | } 58 | 59 | // 监听页面导航事件 60 | function setupNavigationListeners() { 61 | const handleNavigation = async (url, tabId) => { 62 | const urlObj = new URL(url) 63 | const { origin, hostname, pathname, searchParams } = urlObj 64 | if (!hostname || origin === "chrome://newtab") return 65 | 66 | const data = 67 | (await storage.get>(StorageKeys.RULES)) || {} 68 | const dataSource: RuleProps[] = getMergedRules(data) 69 | const currentUrl = pathname ? `${hostname}${pathname}` : hostname 70 | 71 | const item = dataSource.find((i) => { 72 | const matchUrlVariants = [ 73 | i.matchUrl, 74 | `${i.matchUrl}/`, 75 | `www.${i.matchUrl}`, 76 | `www.${i.matchUrl}/` 77 | ] 78 | return matchUrlVariants.includes(currentUrl) 79 | }) 80 | 81 | if (!item || item.disabled || item.runAtContent) return 82 | 83 | if (typeof item.redirect === "function") return 84 | 85 | const redirectKeys = Array.isArray(item.redirect) 86 | ? item.redirect 87 | : [item.redirect] 88 | 89 | let redirectUrl = redirectKeys 90 | .map((key) => searchParams.get(key)) 91 | .find(Boolean) 92 | 93 | if (!redirectUrl) return 94 | 95 | if (item.formatter) { 96 | redirectUrl = item.formatter(redirectUrl) 97 | } 98 | 99 | const decodeUrl = decodeURIComponent(redirectUrl) 100 | if (!decodeUrl.includes("://")) return 101 | 102 | ga(GaEvents.REDIRECT) 103 | 104 | const { id, count } = item 105 | const newData = { 106 | ...data, 107 | [id]: { 108 | ...data[id], 109 | count: (count || 0) + 1, 110 | updateAt: Date.now() 111 | } 112 | } 113 | 114 | chrome.tabs.update(tabId, { url: decodeUrl }) 115 | storage.set(StorageKeys.RULES, newData) 116 | } 117 | 118 | // 刷新页面时 onBeforeNavigate > onCommitted, onBeforeNavigate 无需等待 TTFB 119 | // 在终端中点击链接打开时 onCreated > (onBeforeNavigate === onUpdated), onCreated 无需等待 TTFB 120 | // 点击 a 标签打开新标签页 onCreated(无 url) > onBeforeNavigate > onUpdated, onCreated、onBeforeNavigate 都无需等待 TTFB 121 | 122 | // chrome.webNavigation.onCommitted.addListener((details) => { 123 | // if (details.transitionType === "reload") { 124 | // console.log("legacy:onCommitted: ", Date.now(), details.url) 125 | // handleNavigation(details.url, details.tabId) 126 | // } 127 | // }) 128 | 129 | const ignoreUrls = [ 130 | "about:blank", 131 | "chrome://flags/", 132 | "chrome://newtab/", 133 | "chrome://history/", 134 | "chrome://settings/", 135 | "chrome://bookmarks/", 136 | "chrome://downloads/", 137 | "chrome://extensions/", 138 | "chrome://new-tab-page/" 139 | ] 140 | 141 | // 302 后会触发 onUpdated 不会触发 onBeforeNavigate 142 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 143 | const { status, url } = changeInfo 144 | if (status === "loading" && url && !ignoreUrls.includes(url)) { 145 | handleNavigation(changeInfo.url, tabId) 146 | } 147 | }) 148 | 149 | chrome.webNavigation.onBeforeNavigate.addListener((details) => { 150 | const { url, tabId } = details 151 | if (!url) return 152 | if (ignoreUrls.includes(url)) return 153 | handleNavigation(url, tabId) 154 | }) 155 | 156 | chrome.tabs.onCreated.addListener((tab) => { 157 | const { id, pendingUrl } = tab 158 | if (!pendingUrl) return 159 | if (ignoreUrls.includes(pendingUrl)) return 160 | handleNavigation(pendingUrl, id) 161 | }) 162 | } 163 | 164 | // 初始化 165 | function init() { 166 | createContextMenus(menuList) 167 | setupContextMenuListeners(menuList) 168 | setupNavigationListeners() 169 | } 170 | 171 | init() 172 | -------------------------------------------------------------------------------- /assets/infoq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/qq.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function MaterialSymbolsSettings(props: React.SVGProps) { 4 | const { className, ...restProps } = props 5 | 6 | return ( 7 | 8 | 14 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export function StreamlineEmojisBug(props: React.SVGProps) { 24 | const { className, ...restProps } = props 25 | 26 | return ( 27 | 28 | 34 | 41 | 45 | 49 | 53 | 57 | 64 | 71 | 76 | 80 | 84 | 91 | 95 | 99 | 106 | 110 | 114 | 121 | 125 | 129 | 136 | 140 | 144 | 151 | 155 | 159 | 166 | 170 | 174 | 181 | 185 | 189 | 196 | 200 | 204 | 211 | 215 | 219 | 223 | 227 | 234 | 241 | 245 | 249 | 256 | 260 | 264 | 271 | 275 | 279 | 286 | 290 | 294 | 301 | 305 | 309 | 316 | 317 | 318 | ) 319 | } 320 | -------------------------------------------------------------------------------- /tabs/Sidepanel.tsx: -------------------------------------------------------------------------------- 1 | import classnames from "classnames" 2 | import React from "react" 3 | 4 | import { useStorage } from "@plasmohq/storage/hook" 5 | 6 | import { useThemeChange } from "~components/hooks" 7 | import { MaterialSymbolsSettings, StreamlineEmojisBug } from "~components/Icons" 8 | import Img from "~components/Img" 9 | import Modal from "~components/Modal" 10 | import { domainFaviconMap } from "~utils/favicons" 11 | import { getDomain } from "~utils/index" 12 | import { 13 | ga, 14 | GaEvents, 15 | getDocumentTitle, 16 | getMergedRules, 17 | StorageKeys, 18 | type RuleProps 19 | } from "~utils/pure" 20 | 21 | import "~tailwind.less" 22 | 23 | export interface SidepanelProps {} 24 | 25 | const Sidepanel: React.FC = (props) => { 26 | const {} = props 27 | 28 | useThemeChange() 29 | const editRef = React.useRef() 30 | const [peekData, setPeekData] = React.useState(null) 31 | const [peekVisible, setPeekVisible] = React.useState(false) 32 | const [createVisible, setCreateVisible] = React.useState(false) 33 | const [rules, setRules] = useStorage>( 34 | StorageKeys.RULES, 35 | {} 36 | ) 37 | // console.log("rules: ", rules) 38 | 39 | // React.useEffect(() => { 40 | // setRules({}) 41 | // }, []) 42 | 43 | const dataSource = React.useMemo(() => { 44 | return getMergedRules(rules) 45 | }, [rules]) 46 | 47 | React.useEffect(() => { 48 | if (createVisible) return 49 | editRef.current = null 50 | }, [createVisible]) 51 | 52 | const handleCreate = () => { 53 | ga(GaEvents.CREATE) 54 | editRef.current = null 55 | setCreateVisible(true) 56 | } 57 | 58 | const handlePeek = (item: RuleProps) => { 59 | if (item === peekData) { 60 | setPeekData(null) 61 | setPeekVisible(false) 62 | return 63 | } 64 | setPeekData(item) 65 | setPeekVisible(true) 66 | } 67 | 68 | const handleEdit = (item: RuleProps) => { 69 | if (item.isDefault) return 70 | ga(GaEvents.ITEM_EDIT) 71 | editRef.current = item 72 | setCreateVisible(true) 73 | } 74 | 75 | const handleClickUrl = (e, item: RuleProps) => { 76 | if (!item.homePage) return 77 | e.stopPropagation() 78 | chrome.tabs.create({ 79 | url: item.homePage 80 | }) 81 | } 82 | 83 | const handleDelete = (item: RuleProps) => { 84 | if (item.isDefault) return 85 | ga(GaEvents.ITEM_DELETE) 86 | const newRules = { ...rules } 87 | delete newRules[item.id] 88 | setRules(newRules) 89 | } 90 | 91 | const handleDisable = (item: RuleProps) => { 92 | ga(GaEvents.ITEM_DISABLE) 93 | setRules({ 94 | ...rules, 95 | [item.id]: { 96 | ...rules[item.id], 97 | disabled: !item.disabled 98 | } 99 | }) 100 | } 101 | 102 | const handleCreateSave = (item: RuleProps) => { 103 | ga(GaEvents.CREATE_SAVE) 104 | const { id, ...restProps } = item 105 | let updateAt = restProps.updateAt 106 | if (!editRef.current) { 107 | updateAt = Date.now() 108 | } 109 | setRules({ 110 | ...rules, 111 | [id]: { 112 | ...(restProps as RuleProps), 113 | updateAt 114 | } 115 | }) 116 | setCreateVisible(false) 117 | } 118 | 119 | return ( 120 |
121 |
122 | {dataSource.map((item) => { 123 | const { id } = item 124 | return ( 125 | 134 | ) 135 | })} 136 |
137 | 138 | 139 | setCreateVisible(false)} 145 | /> 146 |
147 | ) 148 | } 149 | 150 | export default Sidepanel 151 | 152 | const Card = (props) => { 153 | const { 154 | item, 155 | handlePeek, 156 | handleEdit, 157 | handleDelete, 158 | handleDisable, 159 | handleClickUrl 160 | } = props 161 | 162 | const { matchUrl, disabled, isDefault, count, hostIcon, title, homePage } = 163 | item as RuleProps 164 | const domain = getDomain(matchUrl, hostIcon) 165 | const favicon = domainFaviconMap[domain] 166 | const iconUrl = 167 | favicon || `https://www.faviconextractor.com/favicon/${domain}` 168 | 169 | return ( 170 |
handleEdit(item)} 173 | className={classnames( 174 | "alert flex mb-2 justify-between overflow-hidden py-3 group", 175 | { 176 | "bg-base-300": disabled, 177 | "hover:shadow-xl": !disabled 178 | } 179 | )}> 180 |
181 |
182 | handlePeek(item)} 185 | className={classnames( 186 | "w-full h-full rounded-md object--contain cursor-pointer", 187 | { 188 | "filter grayscale": disabled 189 | } 190 | )} 191 | /> 192 |
193 | handleClickUrl(e, item)}> 200 | {title || matchUrl} 201 | 202 |
203 |
e.stopPropagation()}> 206 | 207 | handleDisable(item)} 212 | /> 213 | {!isDefault && ( 214 | 219 | )} 220 |
221 |
222 | ) 223 | } 224 | 225 | const Count = (props) => { 226 | const { count, className } = props 227 | 228 | if (!count) return null 229 | 230 | return ( 231 |
236 | {count.toLocaleString()} 237 |
238 | ) 239 | } 240 | 241 | const CreateFormModal = (props) => { 242 | const { visible, onClose, onOk, editData, dataSource } = props 243 | const [form, setForm] = React.useState>({ 244 | title: "", 245 | homePage: "", 246 | redirect: "", 247 | matchUrl: "" 248 | }) 249 | const [existed, setExisted] = React.useState(false) 250 | const redirectRef = React.useRef(null) 251 | const create = !editData 252 | 253 | React.useEffect(() => { 254 | if (!create) return 255 | const existed = dataSource.find((i) => i.matchUrl === form.matchUrl) 256 | setExisted(!!existed) 257 | }, [create, form.matchUrl]) 258 | 259 | /** 编辑 */ 260 | React.useEffect(() => { 261 | if (!editData) return 262 | setForm(editData) 263 | }, [editData]) 264 | 265 | /** 新建 */ 266 | React.useEffect(() => { 267 | if (!visible) return 268 | if (editData) return 269 | redirectRef.current?.focus?.() 270 | chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { 271 | const activeTab = tabs[0] 272 | const activeTabUrl = activeTab.url 273 | if (["chrome://extensions/", "chrome://newtab/"].includes(activeTabUrl)) { 274 | return 275 | } 276 | if (!activeTabUrl) return 277 | const { hostname, pathname, searchParams } = new URL(activeTabUrl) 278 | const url = pathname ? `${hostname}${pathname}` : hostname 279 | 280 | const newForm: Partial = { 281 | ...form, 282 | matchUrl: url 283 | } 284 | if (searchParams.get("target")) { 285 | newForm.redirect = searchParams.get("target") 286 | } 287 | if (searchParams.get("url")) { 288 | newForm.redirect = searchParams.get("url") 289 | } 290 | const title = await getDocumentTitle() 291 | if (title) { 292 | newForm.title = title 293 | } 294 | 295 | const domain = getDomain(url) 296 | if (domain) { 297 | newForm.homePage = `https://${domain}` 298 | } 299 | 300 | setForm(newForm) 301 | }) 302 | }, [visible]) 303 | 304 | const { matchUrl, redirect } = form 305 | 306 | const handleOk = () => { 307 | if (!matchUrl || !redirect) return 308 | const id = editData?.id || `${Date.now()}` 309 | if (onOk) onOk({ id, ...form }) 310 | setForm({ matchUrl: "", redirect: "", title: "", homePage: "" }) 311 | } 312 | 313 | const disabled = !matchUrl || !redirect || existed 314 | 315 | return ( 316 | 321 |
322 |
323 | 335 | 347 | 368 | 381 |
382 |
383 |
384 | ) 385 | } 386 | 387 | const Actions = (props) => { 388 | const { handleCreate } = props 389 | 390 | const handleIssue = () => { 391 | ga(GaEvents.ACTIONS_ISSUE) 392 | 393 | chrome.tabs.create({ 394 | url: "https://github.com/Dolov/chrome-QuickGo/issues" 395 | }) 396 | } 397 | 398 | const handleSetting = () => { 399 | ga(GaEvents.ACTIONS_SETTING) 400 | chrome.tabs.create({ 401 | url: "tabs/Settings.html" 402 | }) 403 | } 404 | 405 | return ( 406 |
407 | 412 |
413 |
416 | 419 |
420 |
423 | 428 |
429 |
430 |
431 | ) 432 | } 433 | 434 | const Peek: React.FC<{ visible: boolean; data: RuleProps }> = (props) => { 435 | const { visible, data } = props 436 | if (!visible) return null 437 | 438 | const { homePage } = data 439 | return ( 440 |
441 |
442 |
{homePage}
443 |
444 |
445 | 446 |
447 |
448 | ) 449 | } 450 | -------------------------------------------------------------------------------- /utils/pure.ts: -------------------------------------------------------------------------------- 1 | export enum StorageKeys { 2 | RULES = "RULES", 3 | SETTINGS = "SETTINGS" 4 | } 5 | 6 | export interface RuleProps extends BaseRuleProps { 7 | id: string 8 | // 规则生效的次数 9 | count?: number 10 | disabled?: boolean 11 | // 是否是默认数据,用于区分用户自定义数据,不可删除,不可编辑 12 | isDefault?: boolean 13 | } 14 | 15 | export interface BaseRuleProps { 16 | matchUrl: string 17 | redirect: string | string[] | ((callback) => void) 18 | title?: string 19 | // 使用 hostname 的图标 20 | hostIcon?: boolean 21 | // 在 contentjs 中生效 22 | runAtContent?: boolean 23 | // 更新时间 24 | updateAt?: number 25 | // 主页 26 | homePage?: string 27 | // 格式化函数 28 | formatter?: (url: string) => string 29 | } 30 | 31 | const defaultRuleMap: Record = { 32 | // https://link.zhihu.com/?target=https%3A//manus.im/ 33 | zhihu: { 34 | title: "知乎 - 有问题,就会有答案", 35 | homePage: "https://www.zhihu.com/", 36 | matchUrl: "link.zhihu.com", 37 | redirect: "target" 38 | }, 39 | // https://link.juejin.cn/?target=https%3A%2F%2Fcodesandbox.io%2Fp%2Fsandbox%2Fxwxsv6 40 | juejin: { 41 | title: "稀土掘金", 42 | homePage: "https://juejin.cn/", 43 | matchUrl: "link.juejin.cn", 44 | redirect: "target" 45 | }, 46 | // https://links.jianshu.com/go?to=https%3A%2F%2Fdbarobin.com%2F2017%2F01%2F24%2Fgithub-acceleration-best-practices%2F 47 | jianshu: { 48 | title: "简书 - 创作你的创作", 49 | homePage: "https://www.jianshu.com/", 50 | matchUrl: "links.jianshu.com/go", 51 | redirect: "to" 52 | }, 53 | // https://www.jianshu.com/go-wild?ac=2&url=https%3A%2F%2Fwww.runoob.com%2Fjs%2Fjs-intro.html 54 | jianshu2: { 55 | title: "简书 - 创作你的创作", 56 | homePage: "https://www.jianshu.com/", 57 | matchUrl: "jianshu.com/go-wild", 58 | redirect: "url" 59 | }, 60 | // https://gitee.com/link?target=https%3A%2F%2Fnano.hyperf.wiki 61 | gitee: { 62 | title: "Gitee - 基于 Git 的代码托管和研发协作平台", 63 | homePage: "https://gitee.com/", 64 | matchUrl: "gitee.com/link", 65 | redirect: "target" 66 | }, 67 | // https://link.csdn.net/?from_id=145825938&target=https%3A%2F%2Fgithub.com%2Fyour-repo%2Fcompression-template 68 | csdn: { 69 | title: "CSDN - 专业开发者社区", 70 | homePage: "https://www.csdn.net/", 71 | matchUrl: "link.csdn.net", 72 | redirect: "target" 73 | }, 74 | // https://sspai.com/link?target=https%3A%2F%2Fwww.digitalocean.com%2Fcommunity%2Ftools%2Fnginx%3Fglobal.app.lang%3DzhCN 75 | sspai: { 76 | title: "少数派 - 高效工作,品质生活", 77 | homePage: "https://sspai.com/", 78 | matchUrl: "sspai.com/link", 79 | redirect: "target" 80 | }, 81 | // https://afdian.com/link?target=https%3A%2F%2Flarkcommunity.feishu.cn%2Fbase%2FM2gsbZmBtaHyagsOtbrca2c2nvh 82 | afdian: { 83 | title: "爱发电 · 连接创作者与粉丝的会员制平台", 84 | homePage: "https://afdian.com/", 85 | matchUrl: "afdian.com/link", 86 | redirect: "target" 87 | }, 88 | // https://www.baike.com/redirect_link?url=https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fgithub-builds-a-search-engine-for-code-from-scratch-in-rust%2F&collect_params=%7B%22doc_title%22%3A%22github%22%2C%22doc_id%22%3A%227239981009876418592%22%2C%22version_id%22%3A%227473689138244403212%22%2C%22reference_type%22%3A%22web%22%2C%22link%22%3A%22https%3A%2F%2Fwww.zdnet.com%2Farticle%2Fgithub-builds-a-search-engine-for-code-from-scratch-in-rust%2F%22%2C%22author%22%3A%22%22%2C%22title%22%3A%22GitHubbuiltanewsearchengineforcode%27fromscratch%27inRust%22%2C%22reference_tag%22%3A%22%22%2C%22source_name%22%3A%22zdnet%22%2C%22publish_date%22%3A%22%22%2C%22translator%22%3A%22%22%2C%22volume%22%3A%22%22%2C%22period%22%3A%22%22%2C%22page%22%3A%22%22%2C%22doi%22%3A%22%22%2C%22version%22%3A%22%22%2C%22publish_area%22%3A%22%22%2C%22publisher%22%3A%22%22%2C%22book_number%22%3A%22%22%7D 89 | baike: { 90 | title: "快懂百科", 91 | homePage: "https://www.baike.com/", 92 | matchUrl: "baike.com/redirect_link", 93 | redirect: "url" 94 | }, 95 | // https://www.chinaz.com/go.shtml?url=https://mp.weixin.qq.com/s/vhv4Eb5XoA2d4LKRqVRQag 96 | chinaz: { 97 | title: "站长之家 - 站长资讯-我们致力于为中文网站提供动力!", 98 | homePage: "https://www.chinaz.com/", 99 | matchUrl: "chinaz.com/go.shtml", 100 | redirect: "url" 101 | }, 102 | coolapk: { 103 | title: "酷安 - 分享美好科技生活", 104 | homePage: "https://www.coolapk.com/", 105 | matchUrl: "coolapk.com/link", 106 | redirect: "target" 107 | }, 108 | // https://www.curseforge.com/linkout?remoteUrl=https%253a%252f%252fwww.complementary.dev%252fshaders%252f%2523download-section 109 | curseforge: { 110 | title: "CurseForge - Mods & Addons Leading Community", 111 | homePage: "https://www.curseforge.com/", 112 | matchUrl: "curseforge.com/linkout", 113 | redirect: "remoteUrl" 114 | }, 115 | // https://developer.aliyun.com/redirect?target=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 116 | developeraliyun: { 117 | title: "阿里云开发者社区-云计算社区-阿里云", 118 | homePage: "https://developer.aliyun.com/", 119 | matchUrl: "developer.aliyun.com/redirect", 120 | redirect: "target" 121 | }, 122 | // https://www.douban.com/link2/?url=http%3A%2F%2Fwww.truecrypt.org%2F&link2key=c2b1b99b0b 123 | douban: { 124 | title: "豆瓣", 125 | homePage: "https://www.douban.com/", 126 | matchUrl: "douban.com/link2", 127 | redirect: "url" 128 | }, 129 | // https://game.bilibili.com/linkfilter/?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 130 | bilibili: { 131 | title: "bilibili游戏丨你的幻想世界", 132 | homePage: "https://game.bilibili.com/", 133 | matchUrl: "game.bilibili.com/linkfilter", 134 | redirect: "url" 135 | }, 136 | // https://ref.gamer.com.tw/redir.php?url=http%3A%2F%2Fsunderfolk.com%2F 137 | gamer: { 138 | title: "巴哈姆特電玩資訊站", 139 | homePage: "https://www.gamer.com.tw/", 140 | matchUrl: "ref.gamer.com.tw/redir.php", 141 | redirect: "url" 142 | }, 143 | // https://www.gcores.com/link?target=https%3A%2F%2Fals.rjsy313.com%2F 144 | gcores: { 145 | title: "机核 GCORES", 146 | homePage: "https://www.gcores.com/", 147 | matchUrl: "gcores.com/link", 148 | redirect: "target" 149 | }, 150 | // https://hellogithub.com/periodical/statistics/click?target=https%3A%2F%2Fals.rjsy313.com%2F 151 | hellogithub: { 152 | title: "有趣的开源社区 - HelloGitHub", 153 | homePage: "https://hellogithub.com/", 154 | matchUrl: "hellogithub.com/periodical/statistics/click", 155 | redirect: "target" 156 | }, 157 | // https://xie.infoq.cn/link?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzg5MjU0NTI5OQ%3D%3D%26mid%3D2247604333%26idx%3D1%26sn%3D4021da1c6fb035906fd747487bbb8a23%26scene%3D21%23wechat_redirect 158 | xieinfoq: { 159 | title: "InfoQ 写作社区-专业技术博客社区", 160 | homePage: "https://xie.infoq.cn/", 161 | matchUrl: "xie.infoq.cn/link", 162 | redirect: "target" 163 | }, 164 | // https://www.infoq.cn/link?target=https%3A%2F%2Fsloanreview.mit.edu%2Farticle%2Fmanaging-the-bots-that-are-managing-the-business%2F 165 | infoq: { 166 | title: "InfoQ - 促进软件开发及相关领域知识与创新的传播-极客邦", 167 | homePage: "https://www.infoq.cn/", 168 | matchUrl: "infoq.cn/link", 169 | redirect: "target" 170 | }, 171 | // https://www.kookapp.cn/go-wild.html?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 172 | kookapp: { 173 | title: "KOOK,一个好用的语音沟通工具 - 官方网站", 174 | homePage: "https://www.kookapp.cn/", 175 | matchUrl: "kookapp.cn/go-wild.html", 176 | redirect: "url" 177 | }, 178 | // https://ask.latexstudio.net/go/index?url=https%3A%2F%2Fgithub.com%2Fzepinglee%2Fciteproc-lua 179 | latexstudio: { 180 | title: "LaTeX问答", 181 | homePage: "https://ask.latexstudio.net/", 182 | matchUrl: "ask.latexstudio.net/go/index", 183 | redirect: "url" 184 | }, 185 | // https://leetcode.cn/link/?target=https%3A%2F%2Fjobs.mihoyo.com%2Fm%2F%3FsharePageId%3D77920%26recommendationCode%3DGZRRW%26isRecommendation%3Dtrue%23%2Fcampus%2Fposition 186 | leetcode: { 187 | title: "力扣 (LeetCode) 全球极客挚爱的技术成长平台", 188 | homePage: "https://leetcode.cn/", 189 | matchUrl: "leetcode.cn/link", 190 | redirect: "target" 191 | }, 192 | linkedin: { 193 | title: "领英 - 人人都在领英", 194 | homePage: "https://www.linkedin.com/", 195 | matchUrl: "linkedin.com/safety/go", 196 | redirect: "url" 197 | }, 198 | // https://link.logonews.cn/?url=http%3A%2F%2Fsunderfolk.com%2F 199 | logonews: { 200 | title: "标志情报局 - 全球LOGO新闻和品牌设计趋势平台", 201 | homePage: "https://logonews.cn/", 202 | matchUrl: "link.logonews.cn", 203 | redirect: "url" 204 | }, 205 | // https://www.nodeseek.com/jump?to=https%3A%2F%2Fblogverse.cn 206 | nodeseek: { 207 | title: "开发者社区 · 运维实践 · 开源技术交流", 208 | homePage: "https://www.nodeseek.com/", 209 | matchUrl: "nodeseek.com/jump", 210 | redirect: "to" 211 | }, 212 | // https://hd.nowcoder.com/link.html?target=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 213 | nowcoder: { 214 | title: 215 | "牛客网 - 找工作神器|笔试题库|面试经验|实习招聘内推,求职就业一站解决_牛客网", 216 | homePage: "https://www.nowcoder.com/", 217 | matchUrl: "hd.nowcoder.com/link.html", 218 | redirect: "target" 219 | }, 220 | // https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 221 | oschina: { 222 | title: "OSCHINA - 中文开源技术交流社区", 223 | homePage: "https://www.oschina.net/", 224 | matchUrl: "oschina.net/action/GoToLink", 225 | redirect: "url" 226 | }, 227 | // https://www.qcc.com/web/transfer-link?link=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 228 | qcc: { 229 | title: "企查查 - 查企业_查老板_查风险_企业信息查询系统", 230 | homePage: "https://www.qcc.com/", 231 | matchUrl: "qcc.com/web/transfer-link", 232 | redirect: "link" 233 | }, 234 | // https://docs.qq.com/scenario/link.html?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 235 | docsqq: { 236 | title: "腾讯文档-官方网站-支持多人在线编辑Word、Excel和PPT文档", 237 | homePage: "https://docs.qq.com/", 238 | matchUrl: "docs.qq.com/scenario/link.html", 239 | redirect: "url", 240 | hostIcon: true 241 | }, 242 | // https://www.360doc.cn/outlink.html?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 243 | "360doc": { 244 | title: "360doc个人图书馆", 245 | homePage: "https://www.360doc.cn/", 246 | matchUrl: "360doc.cn/outlink.html", 247 | redirect: "url" 248 | }, 249 | // https://www.instagram.com/linkshim/?u=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 250 | instagram: { 251 | title: "Instagram", 252 | homePage: "https://www.instagram.com/", 253 | matchUrl: "instagram.com/linkshim", 254 | redirect: "u" 255 | }, 256 | // https://mail.qq.com/cgi-bin/readtemplate?gourl=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 257 | mailqq: { 258 | title: "腾讯企业邮箱", 259 | homePage: "https://mail.qq.com/", 260 | matchUrl: "mail.qq.com/cgi-bin/readtemplate", 261 | redirect: "gourl", 262 | hostIcon: true 263 | }, 264 | // https://wx.mail.qq.com/xmspamcheck/xmsafejump?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 265 | wxmailqq: { 266 | title: "腾讯企业邮箱", 267 | homePage: "https://mail.qq.com/", 268 | matchUrl: "wx.mail.qq.com/xmspamcheck/xmsafejump", 269 | redirect: "url", 270 | hostIcon: true 271 | }, 272 | // https://shimo.im/outlink/black?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 273 | shimo: { 274 | title: 275 | "石墨文档官网-在线协同办公系统平台,支持云端多人在线协作文档,表格,幻灯片", 276 | homePage: "https://shimo.im/", 277 | matchUrl: "shimo.im/outlink/black", 278 | redirect: "url" 279 | }, 280 | // https://shimo.im/outlink/gray?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 281 | shimo2: { 282 | title: 283 | "石墨文档官网-在线协同办公系统平台,支持云端多人在线协作文档,表格,幻灯片", 284 | homePage: "https://shimo.im/", 285 | matchUrl: "shimo.im/outlink/gray", 286 | redirect: "url" 287 | }, 288 | // https://steamcommunity.com/linkfilter?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 289 | steamcommunity: { 290 | title: "Steam 社区", 291 | homePage: "https://steamcommunity.com/", 292 | matchUrl: "steamcommunity.com/linkfilter", 293 | redirect: "url" 294 | }, 295 | telegram: { 296 | title: "Telegram Messenger", 297 | homePage: "https://telegram.org/", 298 | matchUrl: "t.me/iv", 299 | redirect: "url" 300 | }, 301 | // https://cloud.tencent.com/developer/tools/blog-entry?target=https%3A%2F%2Fgit-scm.com%2Fbook%2Fzh%2Fv2%2Fch00%2F_commit_status&objectId=1434763&objectType=1&isNewArticle=undefined 302 | cloudtencent: { 303 | title: "腾讯云开发者社区-腾讯云", 304 | homePage: "https://cloud.tencent.com/", 305 | matchUrl: "cloud.tencent.com/developer/tools/blog-entry", 306 | redirect: "target" 307 | }, 308 | // https://www.tianyancha.com/security?target=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 309 | tianyancha: { 310 | title: 311 | "天眼查-商业查询平台_企业信息查询_公司查询_工商查询_企业信用信息系统", 312 | homePage: "https://www.tianyancha.com/", 313 | matchUrl: "tianyancha.com/security", 314 | redirect: "target" 315 | }, 316 | // https://tieba.baidu.com/mo/q/checkurl?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 317 | tiebabaidu: { 318 | hostIcon: true, 319 | title: "百度贴吧——全球领先的中文社区", 320 | homePage: "https://tieba.baidu.com/", 321 | matchUrl: "tieba.baidu.com/mo/q/checkurl", 322 | redirect: "url" 323 | }, 324 | // https://link.uisdc.com/?redirect=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 325 | uisdc: { 326 | title: 327 | "优设网官网 - UISDC - 国内专业设计师平台 - 看设计文章,学AIGC教程,找灵感素材,尽在优设网!", 328 | homePage: "https://www.uisdc.com/", 329 | matchUrl: "link.uisdc.com", 330 | redirect: "redirect" 331 | }, 332 | // https://developers.weixin.qq.com/community/middlepage/href?href=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 333 | developersweixin: { 334 | title: "微信开发者社区", 335 | hostIcon: true, 336 | homePage: "https://developers.weixin.qq.com/", 337 | matchUrl: "developers.weixin.qq.com/community/middlepage/href", 338 | redirect: "href" 339 | }, 340 | // https://www.yuque.com/r/goto?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 341 | yuque: { 342 | title: "语雀,为每一个人提供优秀的文档和知识库工具", 343 | homePage: "https://www.yuque.com/", 344 | matchUrl: "yuque.com/r/goto", 345 | redirect: "url" 346 | }, 347 | // https://www.youtube.com/redirect?q=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 348 | youtube: { 349 | title: "YouTube", 350 | homePage: "https://www.youtube.com/", 351 | matchUrl: "youtube.com/redirect", 352 | redirect: "q" 353 | }, 354 | // http://redir.yy.duowan.com/warning.php?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 355 | duowan: { 356 | title: "多玩游戏网", 357 | homePage: "https://www.duowan.com/", 358 | matchUrl: "redir.yy.duowan.com/warning.php", 359 | redirect: "url" 360 | }, 361 | // https://weibo.cn/sinaurl?toasturl=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 362 | // https://weibo.cn/sinaurl?url=https%3A%2F%2Fwww.aliwork.com%2Fo%2Fcursor 363 | weibo: { 364 | title: "微博", 365 | homePage: "https://weibo.cn/", 366 | matchUrl: "weibo.cn/sinaurl", 367 | redirect: ["toasturl", "url", "u"] 368 | }, 369 | // https://blzxteam.com/gowild.htm?url=https_3A_2F_2Fjq_2eqq_2ecom_2F_3F_5fwv_3D1027_26k_3D1ywspCt0&u=31468&fr=https_3A_2F_2Fblzxteam_2ecom_2Fthread_2d479_2ehtm 370 | blzxteam: { 371 | title: "碧蓝之星_深海迷航社区", 372 | homePage: "https://blzxteam.com/", 373 | matchUrl: "blzxteam.com/gowild.htm", 374 | redirect() { 375 | const url = document 376 | .querySelector("div._2VEbEOHfDtVWiQAJxSIrVi_0") 377 | .getAttribute("title") 378 | window.location.href = url 379 | }, 380 | runAtContent: true 381 | }, 382 | // https://www.yunpanziyuan.xyz/gowild.htm?url=https_3A_2F_2Fpan_2equark_2ecn_2Fs_2Fdee48eed51d7 383 | // https://www.yunpanziyuan.xyz/thread-522696.htm 384 | yunpanziyuan: { 385 | title: "云盘资源网最新地址发布页", 386 | homePage: "https://www.yunpanziyuan.xyz/", 387 | matchUrl: "yunpanziyuan.xyz/gowild.htm", 388 | runAtContent: true, 389 | redirect(updateLog) { 390 | updateLog() 391 | const url = document.querySelector("div.url_div").getAttribute("title") 392 | window.location.href = url 393 | } 394 | }, 395 | // https://bbs.acgrip.com/thread-5675-1-1.html 396 | acgrip: { 397 | title: "Anime字幕论坛 - Powered by Discuz!", 398 | homePage: "https://bbs.acgrip.com/", 399 | matchUrl: "bbs.acgrip.com/(*)", 400 | runAtContent: true, 401 | redirect(updateLog) { 402 | document.querySelectorAll("a").forEach((elem) => { 403 | if ( 404 | elem.href && 405 | elem.href.startsWith("http") && 406 | !elem.href.includes(window.location.host) 407 | ) { 408 | elem.addEventListener("click", (event) => { 409 | event.preventDefault() 410 | updateLog() 411 | // @ts-ignore 412 | window.hideMenu("fwin_dialog", "dialog") 413 | window.open(elem.href, "_blank") 414 | }) 415 | } 416 | }) 417 | } 418 | }, 419 | // https://www.bookmarkearth.cn/view/863157e793d711edb9f55254005bdbf9 420 | bookmarkearth: { 421 | title: "书签地球-中国首家浏览器书签共享搜索引擎平台", 422 | homePage: "https://www.bookmarkearth.cn/", 423 | matchUrl: "bookmarkearth.cn/view/(*)", 424 | runAtContent: true, 425 | redirect(updateLog) { 426 | updateLog() 427 | window.location.replace(document.querySelector("p.link").innerHTML) 428 | } 429 | }, 430 | // https://blog.51cto.com/transfer?https://cloud.tencent.com/product/lke?from_column=20421&from=20421 431 | "51cto": { 432 | title: "技术成就梦想51CTO-中国知名的数字化人才学习平台和技术社区", 433 | homePage: "https://51cto.com/", 434 | matchUrl: "blog.51cto.com/transfer", 435 | runAtContent: true, 436 | redirect(updateLog) { 437 | updateLog() 438 | window.location.href = window.location.href.replace( 439 | "https://blog.51cto.com/transfer?", 440 | "" 441 | ) 442 | } 443 | }, 444 | weixin110: { 445 | title: "微信安全中心 - 安全连接一切", 446 | homePage: "https://weixin110.qq.com/", 447 | hostIcon: true, 448 | matchUrl: 449 | "weixin110.qq.com/cgi-bin/mmspamsupport-bin/newredirectconfirmcgi", 450 | runAtContent: true, 451 | redirect(updateLog) { 452 | updateLog() 453 | const element: HTMLParagraphElement = document.querySelector( 454 | "body > div > div.weui-msg__text-area > div > div > div:nth-child(1) > p" 455 | ) 456 | if (!element) return 457 | window.location.href = element.innerText 458 | } 459 | }, 460 | mpweixinqq: { 461 | title: "微信公众号", 462 | hostIcon: true, 463 | homePage: "https://mp.weixin.qq.com", 464 | matchUrl: "mp.weixin.qq.com/s/(*)", 465 | runAtContent: true, 466 | redirect(updateLog) { 467 | const elements = document.querySelectorAll( 468 | "#js_content > section a[data-linktype='2']" 469 | ) 470 | if (!elements.length) return 471 | 472 | elements.forEach((elem) => { 473 | const cloned = elem.cloneNode(true) as HTMLAnchorElement 474 | cloned.setAttribute("data-s-source", "quickgo") 475 | cloned.addEventListener("click", (event) => { 476 | updateLog() 477 | window.open(cloned.href, "_blank") 478 | }) 479 | 480 | elem.replaceWith(cloned) 481 | }) 482 | } 483 | }, 484 | down423: { 485 | title: "423down - 免费资源分享平台", 486 | homePage: "https://423down.com/", 487 | matchUrl: "423down.com/go.php", 488 | redirect: "url", 489 | formatter: atobPolyfill 490 | } 491 | } 492 | 493 | export enum GaEvents { 494 | CREATE = "create", 495 | CREATE_SAVE = "create_save", 496 | ITEM_EDIT = "item_edit", 497 | ITEM_DISABLE = "item_disable", 498 | ITEM_DELETE = "item_delete", 499 | REDIRECT = "redirect", 500 | ACTIONS_ISSUE = "actions_issues", 501 | ACTIONS_SETTING = "actions_setting", 502 | SETTING_THEME = "setting_theme" 503 | } 504 | 505 | export function formatDateTime() { 506 | const now = new Date() 507 | 508 | const year = now.getFullYear() 509 | const month = String(now.getMonth() + 1).padStart(2, "0") 510 | const day = String(now.getDate()).padStart(2, "0") 511 | 512 | const hours = String(now.getHours()).padStart(2, "0") 513 | const minutes = String(now.getMinutes()).padStart(2, "0") 514 | const seconds = String(now.getSeconds()).padStart(2, "0") 515 | 516 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` 517 | } 518 | 519 | async function getOrCreateClientId() { 520 | const result = await chrome.storage.local.get("clientId") 521 | let clientId = result.clientId 522 | if (!clientId) { 523 | // Generate a unique client ID, the actual value is not relevant 524 | clientId = self.crypto.randomUUID() 525 | await chrome.storage.local.set({ clientId }) 526 | } 527 | return clientId 528 | } 529 | 530 | export const ga = async (name, params?: Record) => { 531 | const GA_ENDPOINT = "https://www.google-analytics.com/mp/collect" 532 | const MEASUREMENT_ID = process.env.PLASMO_PUBLIC_MEASUREMENT_ID 533 | const API_SECRET = process.env.PLASMO_PUBLIC_API_SECRET 534 | 535 | fetch( 536 | `${GA_ENDPOINT}?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, 537 | { 538 | method: "POST", 539 | body: JSON.stringify({ 540 | client_id: await getOrCreateClientId(), 541 | events: [ 542 | { 543 | name, 544 | params: { 545 | time: formatDateTime(), 546 | ...params 547 | } 548 | } 549 | ] 550 | }) 551 | } 552 | ) 553 | } 554 | 555 | const getDefaultRules = (): RuleProps[] => { 556 | return Object.keys(defaultRuleMap).map((id) => { 557 | const rule = defaultRuleMap[id] 558 | return { 559 | ...rule, 560 | id, 561 | isDefault: true 562 | } 563 | }) 564 | } 565 | 566 | export const getMergedRules = ( 567 | storageData: Record = {} 568 | ): RuleProps[] => { 569 | const defaultRules = getDefaultRules() 570 | 571 | const mergedRules = defaultRules.map((defaultRule) => { 572 | const id = defaultRule.id 573 | if (storageData[id]) { 574 | return { 575 | id, 576 | isDefault: true, 577 | ...defaultRule, 578 | ...storageData[id] 579 | } 580 | } 581 | return defaultRule 582 | }) 583 | 584 | for (const id in storageData) { 585 | if (!defaultRules.some((rule) => rule.id === id)) { 586 | mergedRules.push({ id, ...storageData[id] }) 587 | } 588 | } 589 | 590 | return mergedRules.sort((a, b) => (b.updateAt || 0) - (a.updateAt || 0)) 591 | } 592 | 593 | export const getDocumentTitle = async (): Promise => { 594 | return new Promise((resolve, reject) => { 595 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 596 | if (!tabs.length || !tabs[0].id) { 597 | reject(new Error("No active tab found")) 598 | return 599 | } 600 | 601 | chrome.scripting.executeScript( 602 | { 603 | target: { tabId: tabs[0].id }, 604 | func: () => document.title 605 | }, 606 | (results) => { 607 | if (chrome.runtime.lastError) { 608 | reject(new Error(chrome.runtime.lastError.message)) 609 | return 610 | } 611 | 612 | if (results && results[0]?.result) { 613 | resolve(results[0].result) 614 | } else { 615 | resolve("") 616 | } 617 | } 618 | ) 619 | }) 620 | }) 621 | } 622 | 623 | export function atobPolyfill(input) { 624 | if (typeof window !== "undefined" && window.atob) { 625 | return window.atob(input) 626 | } 627 | 628 | const chars = 629 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=" 630 | 631 | let str = input.replace(/=+$/, "") 632 | if (str.length % 4 === 1) { 633 | throw new Error( 634 | '"atob" failed: The string to be decoded is not correctly encoded.' 635 | ) 636 | } 637 | 638 | let output = "" 639 | let bc = 0, 640 | bs, 641 | buffer, 642 | idx = 0 643 | 644 | while ((buffer = str.charAt(idx++))) { 645 | buffer = chars.indexOf(buffer) 646 | if (buffer === -1) continue 647 | 648 | bs = bc % 4 ? bs * 64 + buffer : buffer 649 | if (bc++ % 4) { 650 | output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))) 651 | } 652 | } 653 | 654 | return output 655 | } 656 | --------------------------------------------------------------------------------