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 |
--------------------------------------------------------------------------------
/assets/leetcode.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/baike.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 | # QuickGo
6 |
7 | 
8 | 
9 | 
10 | 
11 | 
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 | 
53 | 
54 | 
55 | 
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 | [](https://star-history.com/#Dolov/chrome-QuickGo&Date)
71 |
--------------------------------------------------------------------------------
/assets/weixin110.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/assets/coolapk.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.en-US.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 | # QuickGo
6 |
7 | 
8 | 
9 | 
10 | 
11 | 
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 | 
47 | 
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 |
38 |
--------------------------------------------------------------------------------
/assets/hellogithub.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/csdn.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/bookmarkearth.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/blzxteam.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/assets/duowan.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
19 |
20 | )
21 | }
22 |
23 | export function StreamlineEmojisBug(props: React.SVGProps) {
24 | const { className, ...restProps } = props
25 |
26 | return (
27 |
28 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------