├── .editorconfig ├── .github └── workflows │ └── submit.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── LICENSE ├── README.md ├── assets └── icon.png ├── build └── userscript-prod │ └── my-extension.user.js ├── manual-installation.md ├── package.json ├── pnpm-lock.yaml ├── scripts ├── bookmarklet │ ├── build.mjs │ └── watch.mjs ├── common.mjs ├── module │ ├── build.mjs │ └── watch.mjs └── userscript │ ├── banner.txt │ ├── build.mjs │ └── watch.mjs ├── src ├── background.ts ├── content.scss ├── content.ts ├── messages │ ├── en.ts │ ├── index.ts │ └── zh-cn.ts ├── options.tsx └── popup.tsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | #cache 13 | .turbo 14 | 15 | # misc 16 | .DS_Store 17 | *.pem 18 | 19 | # debug 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | .pnpm-debug.log* 24 | 25 | # local env files 26 | .env* 27 | 28 | out/ 29 | build/ 30 | dist/ 31 | 32 | # plasmo - https://www.plasmo.com 33 | .plasmo 34 | 35 | # bpp - http://bpp.browser.market/ 36 | keys.json 37 | 38 | # typescript 39 | .tsbuildinfo 40 | tsconfig.tsbuildinfo 41 | 42 | ._* 43 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | LICENSE 3 | .plasmo/** 4 | build/chrome-*/** 5 | build/firefox-*/** 6 | build/bookmarklet-*/** 7 | build/*-dev/** 8 | *.min.js 9 | pnpm-lock.yaml 10 | tsconfig.json 11 | tsconfig.tsbuildinfo 12 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Options} 3 | */ 4 | module.exports = { 5 | printWidth: 80, 6 | tabWidth: 2, 7 | useTabs: false, 8 | semi: false, 9 | singleQuote: false, 10 | trailingComma: "es5", 11 | bracketSpacing: true, 12 | bracketSameLine: true, 13 | plugins: [require.resolve("@plasmohq/prettier-plugin-sort-imports")], 14 | importOrder: ["^@plasmohq/(.*)$", "^~(.*)$", "^[./]"], 15 | importOrderSeparation: true, 16 | importOrderSortSpecifiers: true, 17 | overrides: [ 18 | { 19 | files: "src/messages/*.ts", 20 | options: { 21 | printWidth: 9999, 22 | }, 23 | }, 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pipecraft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Browser Extension Starter and Userscript Starter 2 | 3 | ## Features 4 | 5 | - One codebase for Chrome extesions, Firefox addons, Userscripts, Bookmarklets and simple JavaScript modules 6 | - Live-reload and React HMR 7 | - [Plasmo](https://www.plasmo.com/) - The Browser Extension Framework 8 | - [esbuild](https://esbuild.github.io/) - Bundler 9 | - React 10 | - TypeScript 11 | - [Prettier](https://github.com/prettier/prettier) - Code Formatter 12 | - [XO](https://github.com/xojs/xo) - JavaScript/TypeScript linter 13 | 14 | ## Showcases 15 | 16 | - [🏷️ UTags - Add usertags to links](https://github.com/utags/utags) - Allow users to add custom tags to links. 17 | - [🔗 Links Helper](https://github.com/utags/links-helper) - Open external links in a new tab, open internal links matching the specified rules in a new tab, convert text to hyperlinks, convert image links to image tags, parse Markdown style links and image tags, parse BBCode style links and image tags 18 | 19 | ## How To Make A New Extension 20 | 21 | 1. Fork [this starter repo](https://github.com/utags/browser-extension-starter), and rename repo to your extension name 22 | 23 | 2. Clone your repo 24 | 25 | 3. Install dependencies 26 | 27 | ```bash 28 | pnpm install 29 | # or 30 | npm install 31 | ``` 32 | 33 | ## Getting Started 34 | 35 | First, run the development server: 36 | 37 | ```bash 38 | pnpm dev 39 | # or 40 | npm run dev 41 | ``` 42 | 43 | Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`. 44 | 45 | You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser. 46 | 47 | For further guidance, [visit our Documentation](https://docs.plasmo.com/) 48 | 49 | ## Making production build 50 | 51 | Run the following: 52 | 53 | ```bash 54 | pnpm build 55 | # or 56 | npm run build 57 | ``` 58 | 59 | This should create a production bundle for your extension, ready to be zipped and published to the stores. 60 | 61 | ## Submit to the webstores 62 | 63 | The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission! 64 | 65 | ## License 66 | 67 | Copyright (c) 2023 [Pipecraft](https://www.pipecraft.net). Licensed under the [MIT License](LICENSE). 68 | 69 | ## >\_ 70 | 71 | [![Pipecraft](https://img.shields.io/badge/site-pipecraft-brightgreen)](https://www.pipecraft.net) 72 | [![UTags](https://img.shields.io/badge/site-UTags-brightgreen)](https://utags.pipecraft.net) 73 | [![DTO](https://img.shields.io/badge/site-DTO-brightgreen)](https://dto.pipecraft.net) 74 | [![BestXTools](https://img.shields.io/badge/site-bestxtools-brightgreen)](https://www.bestxtools.com) 75 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/utags/browser-extension-starter/17e9976e4be5a649ff63db30c32dcb405bdcce3d/assets/icon.png -------------------------------------------------------------------------------- /build/userscript-prod/my-extension.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name My-Extension 3 | // @name:zh-CN 我的扩展 4 | // @namespace https://github.com/utags/browser-extension-starter 5 | // @homepageURL https://github.com/utags/browser-extension-starter#readme 6 | // @supportURL https://github.com/utags/browser-extension-starter/issues 7 | // @version 0.1.0 8 | // @description try to take over the world! 9 | // @description:zh-CN 让世界更美好! 10 | // @icon https://www.tampermonkey.net/favicon.ico 11 | // @author You 12 | // @license MIT 13 | // @match https://*/* 14 | // @match http://*/* 15 | // @grant GM.getValue 16 | // @grant GM.setValue 17 | // @grant GM_addValueChangeListener 18 | // @grant GM_removeValueChangeListener 19 | // @grant GM_addElement 20 | // @grant GM.registerMenuCommand 21 | // ==/UserScript== 22 | // 23 | ;(() => { 24 | "use strict" 25 | var listeners = {} 26 | var getValue = async (key) => { 27 | const value = await GM.getValue(key) 28 | return value && value !== "undefined" ? JSON.parse(value) : void 0 29 | } 30 | var setValue = async (key, value) => { 31 | if (value !== void 0) { 32 | const newValue = JSON.stringify(value) 33 | if (listeners[key]) { 34 | const oldValue = await GM.getValue(key) 35 | await GM.setValue(key, newValue) 36 | if (newValue !== oldValue) { 37 | for (const func of listeners[key]) { 38 | func(key, oldValue, newValue) 39 | } 40 | } 41 | } else { 42 | await GM.setValue(key, newValue) 43 | } 44 | } 45 | } 46 | var _addValueChangeListener = (key, func) => { 47 | listeners[key] = listeners[key] || [] 48 | listeners[key].push(func) 49 | return () => { 50 | if (listeners[key] && listeners[key].length > 0) { 51 | for (let i3 = listeners[key].length - 1; i3 >= 0; i3--) { 52 | if (listeners[key][i3] === func) { 53 | listeners[key].splice(i3, 1) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | var addValueChangeListener = (key, func) => { 60 | if (typeof GM_addValueChangeListener !== "function") { 61 | console.warn("Do not support GM_addValueChangeListener!") 62 | return _addValueChangeListener(key, func) 63 | } 64 | const listenerId = GM_addValueChangeListener(key, func) 65 | return () => { 66 | GM_removeValueChangeListener(listenerId) 67 | } 68 | } 69 | var doc = document 70 | if (typeof String.prototype.replaceAll !== "function") { 71 | String.prototype.replaceAll = String.prototype.replace 72 | } 73 | var $ = (selectors, element) => (element || doc).querySelector(selectors) 74 | var $$ = (selectors, element) => [ 75 | ...(element || doc).querySelectorAll(selectors), 76 | ] 77 | var getRootElement = (type) => 78 | type === 1 79 | ? doc.head || doc.body || doc.documentElement 80 | : type === 2 81 | ? doc.body || doc.documentElement 82 | : doc.documentElement 83 | var createElement = (tagName, attributes) => 84 | setAttributes(doc.createElement(tagName), attributes) 85 | var addElement = (parentNode, tagName, attributes) => { 86 | if (typeof parentNode === "string") { 87 | return addElement(null, parentNode, tagName) 88 | } 89 | if (!tagName) { 90 | return 91 | } 92 | if (!parentNode) { 93 | parentNode = /^(script|link|style|meta)$/.test(tagName) 94 | ? getRootElement(1) 95 | : getRootElement(2) 96 | } 97 | if (typeof tagName === "string") { 98 | const element = createElement(tagName, attributes) 99 | parentNode.append(element) 100 | return element 101 | } 102 | setAttributes(tagName, attributes) 103 | parentNode.append(tagName) 104 | return tagName 105 | } 106 | var addEventListener = (element, type, listener, options) => { 107 | if (!element) { 108 | return 109 | } 110 | if (typeof type === "object") { 111 | for (const type1 in type) { 112 | if (Object.hasOwn(type, type1)) { 113 | element.addEventListener(type1, type[type1]) 114 | } 115 | } 116 | } else if (typeof type === "string" && typeof listener === "function") { 117 | element.addEventListener(type, listener, options) 118 | } 119 | } 120 | var removeEventListener = (element, type, listener, options) => { 121 | if (!element) { 122 | return 123 | } 124 | if (typeof type === "object") { 125 | for (const type1 in type) { 126 | if (Object.hasOwn(type, type1)) { 127 | element.removeEventListener(type1, type[type1]) 128 | } 129 | } 130 | } else if (typeof type === "string" && typeof listener === "function") { 131 | element.removeEventListener(type, listener, options) 132 | } 133 | } 134 | var setAttribute = (element, name, value) => 135 | element ? element.setAttribute(name, value) : void 0 136 | var setAttributes = (element, attributes) => { 137 | if (element && attributes) { 138 | for (const name in attributes) { 139 | if (Object.hasOwn(attributes, name)) { 140 | const value = attributes[name] 141 | if (value === void 0) { 142 | continue 143 | } 144 | if (/^(value|textContent|innerText)$/.test(name)) { 145 | element[name] = value 146 | } else if (/^(innerHTML)$/.test(name)) { 147 | element[name] = createHTML(value) 148 | } else if (name === "style") { 149 | setStyle(element, value, true) 150 | } else if (/on\w+/.test(name)) { 151 | const type = name.slice(2) 152 | addEventListener(element, type, value) 153 | } else { 154 | setAttribute(element, name, value) 155 | } 156 | } 157 | } 158 | } 159 | return element 160 | } 161 | var addClass = (element, className) => { 162 | if (!element || !element.classList) { 163 | return 164 | } 165 | element.classList.add(className) 166 | } 167 | var removeClass = (element, className) => { 168 | if (!element || !element.classList) { 169 | return 170 | } 171 | element.classList.remove(className) 172 | } 173 | var setStyle = (element, values, overwrite) => { 174 | if (!element) { 175 | return 176 | } 177 | const style = element.style 178 | if (typeof values === "string") { 179 | style.cssText = overwrite ? values : style.cssText + ";" + values 180 | return 181 | } 182 | if (overwrite) { 183 | style.cssText = "" 184 | } 185 | for (const key in values) { 186 | if (Object.hasOwn(values, key)) { 187 | style[key] = values[key].replace("!important", "") 188 | } 189 | } 190 | } 191 | if (typeof Object.hasOwn !== "function") { 192 | Object.hasOwn = (instance, prop) => 193 | Object.prototype.hasOwnProperty.call(instance, prop) 194 | } 195 | var parseInt10 = (number, defaultValue) => { 196 | if (typeof number === "number" && !Number.isNaN(number)) { 197 | return number 198 | } 199 | if (typeof defaultValue !== "number") { 200 | defaultValue = Number.NaN 201 | } 202 | if (!number) { 203 | return defaultValue 204 | } 205 | const result = Number.parseInt(number, 10) 206 | return Number.isNaN(result) ? defaultValue : result 207 | } 208 | var rootFuncArray = [] 209 | var headFuncArray = [] 210 | var bodyFuncArray = [] 211 | var headBodyObserver 212 | var startObserveHeadBodyExists = () => { 213 | if (headBodyObserver) { 214 | return 215 | } 216 | headBodyObserver = new MutationObserver(() => { 217 | if (doc.head && doc.body) { 218 | headBodyObserver.disconnect() 219 | } 220 | if (doc.documentElement && rootFuncArray.length > 0) { 221 | for (const func of rootFuncArray) { 222 | func() 223 | } 224 | rootFuncArray.length = 0 225 | } 226 | if (doc.head && headFuncArray.length > 0) { 227 | for (const func of headFuncArray) { 228 | func() 229 | } 230 | headFuncArray.length = 0 231 | } 232 | if (doc.body && bodyFuncArray.length > 0) { 233 | for (const func of bodyFuncArray) { 234 | func() 235 | } 236 | bodyFuncArray.length = 0 237 | } 238 | }) 239 | headBodyObserver.observe(doc, { 240 | childList: true, 241 | subtree: true, 242 | }) 243 | } 244 | var runWhenHeadExists = (func) => { 245 | if (!doc.head) { 246 | headFuncArray.push(func) 247 | startObserveHeadBodyExists() 248 | return 249 | } 250 | func() 251 | } 252 | var runWhenBodyExists = (func) => { 253 | if (!doc.body) { 254 | bodyFuncArray.push(func) 255 | startObserveHeadBodyExists() 256 | return 257 | } 258 | func() 259 | } 260 | var runWhenDomReady = (func) => { 261 | if (doc.readyState === "interactive" || doc.readyState === "complete") { 262 | return func() 263 | } 264 | const handler = () => { 265 | if (doc.readyState === "interactive" || doc.readyState === "complete") { 266 | func() 267 | removeEventListener(doc, "readystatechange", handler) 268 | } 269 | } 270 | addEventListener(doc, "readystatechange", handler) 271 | } 272 | var escapeHTMLPolicy = 273 | typeof trustedTypes !== "undefined" && 274 | typeof trustedTypes.createPolicy === "function" 275 | ? trustedTypes.createPolicy("beuEscapePolicy", { 276 | createHTML: (string) => string, 277 | }) 278 | : void 0 279 | var createHTML = (html) => { 280 | return escapeHTMLPolicy ? escapeHTMLPolicy.createHTML(html) : html 281 | } 282 | var addElement2 = 283 | typeof GM_addElement === "function" 284 | ? (parentNode, tagName, attributes) => { 285 | if (typeof parentNode === "string") { 286 | return addElement2(null, parentNode, tagName) 287 | } 288 | if (!tagName) { 289 | return 290 | } 291 | if (!parentNode) { 292 | parentNode = /^(script|link|style|meta)$/.test(tagName) 293 | ? getRootElement(1) 294 | : getRootElement(2) 295 | } 296 | if (typeof tagName === "string") { 297 | let attributes2 298 | if (attributes) { 299 | const entries1 = [] 300 | const entries2 = [] 301 | for (const entry of Object.entries(attributes)) { 302 | if (/^(on\w+|innerHTML)$/.test(entry[0])) { 303 | entries2.push(entry) 304 | } else { 305 | entries1.push(entry) 306 | } 307 | } 308 | attributes = Object.fromEntries(entries1) 309 | attributes2 = Object.fromEntries(entries2) 310 | } 311 | const element = GM_addElement(null, tagName, attributes) 312 | setAttributes(element, attributes2) 313 | parentNode.append(element) 314 | return element 315 | } 316 | setAttributes(tagName, attributes) 317 | parentNode.append(tagName) 318 | return tagName 319 | } 320 | : addElement 321 | var addStyle = (styleText) => 322 | addElement2(null, "style", { textContent: styleText }) 323 | var registerMenuCommand = (name, callback, accessKey) => { 324 | if (window !== top) { 325 | return 326 | } 327 | if (typeof GM.registerMenuCommand !== "function") { 328 | console.warn("Do not support GM.registerMenuCommand!") 329 | return 330 | } 331 | GM.registerMenuCommand(name, callback, accessKey) 332 | } 333 | var style_default = 334 | '#browser_extension_settings_container{--browser-extension-settings-background-color: #f2f2f7;--browser-extension-settings-text-color: #444444;--browser-extension-settings-link-color: #217dfc;--sb-track-color: #00000000;--sb-thumb-color: #33334480;--sb-size: 2px;position:fixed;top:10px;right:30px;max-height:90%;height:600px;overflow:hidden;display:none;z-index:100000;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings_container .browser_extension_settings_wrapper{display:flex;height:100%;overflow:hidden;background-color:var(--browser-extension-settings-background-color)}#browser_extension_settings_container .browser_extension_settings_wrapper h1{font-size:26px;font-weight:800;border:none;color:var(--browser-extension-settings-text-color);margin:18px 0;padding:0}#browser_extension_settings_container .browser_extension_settings_wrapper h2{font-size:18px;font-weight:600;border:none;color:var(--browser-extension-settings-text-color);margin:14px 0;padding:0}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container{overflow-x:auto;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div{background-color:#fff;font-size:14px;border-top:1px solid #ccc;padding:6px 15px 6px 15px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited{display:flex;justify-content:space-between;align-items:center;cursor:pointer;text-decoration:none;color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited:hover{text-decoration:none;color:var(--browser-extension-settings-text-color)}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div a:visited span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a span,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div a:visited span{margin-right:10px;line-height:24px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div.active,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:hover,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div.active,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:hover{background-color:#e4e4e6}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div.active a,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div.active a{cursor:default}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:first-of-type,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:first-of-type{border-top:none;border-top-right-radius:10px;border-top-left-radius:10px}#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .installed_extension_list div:last-of-type,#browser_extension_settings_container .browser_extension_settings_wrapper .extension_list_container .related_extension_list div:last-of-type{border-bottom-right-radius:10px;border-bottom-left-radius:10px}#browser_extension_settings_container .thin_scrollbar{scrollbar-color:var(--sb-thumb-color) var(--sb-track-color);scrollbar-width:thin}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar{width:var(--sb-size)}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar-track{background:var(--sb-track-color);border-radius:10px}#browser_extension_settings_container .thin_scrollbar::-webkit-scrollbar-thumb{background:var(--sb-thumb-color);border-radius:10px}#browser_extension_settings_main{min-width:250px;overflow-y:auto;overflow-x:hidden;box-sizing:border-box;padding:10px 15px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_main h2{text-align:center;margin:5px 0 0;font-size:18px;font-weight:600;border:none;color:var(--browser-extension-settings-text-color)}#browser_extension_settings_main footer{display:flex;justify-content:center;flex-direction:column;font-size:11px;margin:10px auto 0px;background-color:var(--browser-extension-settings-background-color);color:var(--browser-extension-settings-text-color)}#browser_extension_settings_main footer a{color:var(--browser-extension-settings-link-color) !important;text-decoration:none;padding:0}#browser_extension_settings_main footer p{text-align:center;padding:0;margin:2px;line-height:13px}#browser_extension_settings_main a.navigation_go_previous{color:var(--browser-extension-settings-link-color);cursor:pointer;display:none}#browser_extension_settings_main a.navigation_go_previous::before{content:"< "}#browser_extension_settings_main .option_groups{background-color:#fff;padding:6px 15px 6px 15px;border-radius:10px;display:flex;flex-direction:column;margin:10px 0 0}#browser_extension_settings_main .option_groups .action{font-size:14px;padding:6px 0 6px 0;color:var(--browser-extension-settings-link-color);cursor:pointer}#browser_extension_settings_main .bes_external_link{font-size:14px;padding:6px 0 6px 0}#browser_extension_settings_main .bes_external_link a,#browser_extension_settings_main .bes_external_link a:visited,#browser_extension_settings_main .bes_external_link a:hover{color:var(--browser-extension-settings-link-color);text-decoration:none;cursor:pointer}#browser_extension_settings_main .option_groups textarea{font-size:12px;margin:10px 0 10px 0;height:100px;width:100%;border:1px solid #a9a9a9;border-radius:4px;box-sizing:border-box}#browser_extension_settings_main .switch_option,#browser_extension_settings_main .select_option{display:flex;justify-content:space-between;align-items:center;padding:6px 0 6px 0;font-size:14px}#browser_extension_settings_main .option_groups>*{border-top:1px solid #ccc}#browser_extension_settings_main .option_groups>*:first-child{border-top:none}#browser_extension_settings_main .bes_option>.bes_icon{width:24px;height:24px;margin-right:10px}#browser_extension_settings_main .bes_option>.bes_title{margin-right:10px;flex-grow:1}#browser_extension_settings_main .bes_option>.bes_select{box-sizing:border-box;background-color:#fff;height:24px;padding:0 2px 0 2px;margin:0;border-radius:6px;border:1px solid #ccc}#browser_extension_settings_main .option_groups .bes_tip{position:relative;margin:0;padding:0 15px 0 0;border:none;max-width:none;font-size:14px}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_anchor{cursor:help;text-decoration:underline}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_content{position:absolute;bottom:15px;left:0;background-color:#fff;color:var(--browser-extension-settings-text-color);text-align:left;padding:10px;display:none;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#browser_extension_settings_main .option_groups .bes_tip .bes_tip_anchor:hover+.bes_tip_content,#browser_extension_settings_main .option_groups .bes_tip .bes_tip_content:hover{display:block}#browser_extension_settings_main .option_groups .bes_tip p,#browser_extension_settings_main .option_groups .bes_tip pre{margin:revert;padding:revert}#browser_extension_settings_main .option_groups .bes_tip pre{font-family:Consolas,panic sans,bitstream vera sans mono,Menlo,microsoft yahei,monospace;font-size:13px;letter-spacing:.015em;line-height:120%;white-space:pre;overflow:auto;background-color:#f5f5f5;word-break:normal;overflow-wrap:normal;padding:.5em;border:none}#browser_extension_settings_main .container{--button-width: 51px;--button-height: 24px;--toggle-diameter: 20px;--color-off: #e9e9eb;--color-on: #34c759;width:var(--button-width);height:var(--button-height);position:relative;padding:0;margin:0;flex:none;user-select:none}#browser_extension_settings_main input[type=checkbox]{opacity:0;width:0;height:0;position:absolute}#browser_extension_settings_main .switch{width:100%;height:100%;display:block;background-color:var(--color-off);border-radius:calc(var(--button-height)/2);border:none;cursor:pointer;transition:all .2s ease-out}#browser_extension_settings_main .switch::before{display:none}#browser_extension_settings_main .slider{width:var(--toggle-diameter);height:var(--toggle-diameter);position:absolute;left:2px;top:calc(50% - var(--toggle-diameter)/2);border-radius:50%;background:#fff;box-shadow:0px 3px 8px rgba(0,0,0,.15),0px 3px 1px rgba(0,0,0,.06);transition:all .2s ease-out;cursor:pointer}#browser_extension_settings_main input[type=checkbox]:checked+.switch{background-color:var(--color-on)}#browser_extension_settings_main input[type=checkbox]:checked+.switch .slider{left:calc(var(--button-width) - var(--toggle-diameter) - 2px)}#browser_extension_side_menu{min-height:80px;width:30px;opacity:0;position:fixed;top:80px;right:0;padding-top:20px;z-index:10000}#browser_extension_side_menu:hover{opacity:1}#browser_extension_side_menu button{cursor:pointer;width:24px;height:24px;padding:0;border:none;background-color:rgba(0,0,0,0);background-image:none}#browser_extension_side_menu button svg{width:24px;height:24px}#browser_extension_side_menu button:hover{opacity:70%}#browser_extension_side_menu button:active{opacity:100%}@media(max-width: 500px){#browser_extension_settings_container{right:10px}#browser_extension_settings_container .extension_list_container{display:none}#browser_extension_settings_container .extension_list_container.bes_active{display:block}#browser_extension_settings_container .extension_list_container.bes_active+div{display:none}#browser_extension_settings_main a.navigation_go_previous{display:block}}' 335 | function createSwitch(options = {}) { 336 | const container = createElement("label", { class: "container" }) 337 | const checkbox = createElement( 338 | "input", 339 | options.checked ? { type: "checkbox", checked: "" } : { type: "checkbox" } 340 | ) 341 | addElement2(container, checkbox) 342 | const switchElm = createElement("span", { class: "switch" }) 343 | addElement2(switchElm, "span", { class: "slider" }) 344 | addElement2(container, switchElm) 345 | if (options.onchange) { 346 | addEventListener(checkbox, "change", options.onchange) 347 | } 348 | return container 349 | } 350 | function createSwitchOption(icon, text, options) { 351 | if (typeof text !== "string") { 352 | return createSwitchOption(void 0, icon, text) 353 | } 354 | const div = createElement("div", { class: "switch_option bes_option" }) 355 | if (icon) { 356 | addElement2(div, "img", { src: icon, class: "bes_icon" }) 357 | } 358 | addElement2(div, "span", { textContent: text, class: "bes_title" }) 359 | div.append(createSwitch(options)) 360 | return div 361 | } 362 | var besVersion = 50 363 | var openButton = 364 | '' 365 | var openInNewTabButton = 366 | '' 367 | var settingButton = 368 | '\n\n' 369 | function initI18n(messageMaps, language) { 370 | language = (language || navigator.language).toLowerCase() 371 | const language2 = language.slice(0, 2) 372 | let messagesDefault 373 | let messagesLocal 374 | for (const entry of Object.entries(messageMaps)) { 375 | const langs = new Set( 376 | entry[0] 377 | .toLowerCase() 378 | .split(",") 379 | .map((v) => v.trim()) 380 | ) 381 | const value = entry[1] 382 | if (langs.has(language)) { 383 | messagesLocal = value 384 | } 385 | if (langs.has(language2) && !messagesLocal) { 386 | messagesLocal = value 387 | } 388 | if (langs.has("en")) { 389 | messagesDefault = value 390 | } 391 | if (langs.has("en-us") && !messagesDefault) { 392 | messagesDefault = value 393 | } 394 | } 395 | if (!messagesLocal) { 396 | messagesLocal = {} 397 | } 398 | if (!messagesDefault || messagesDefault === messagesLocal) { 399 | messagesDefault = {} 400 | } 401 | return function (key, ...parameters) { 402 | let text = messagesLocal[key] || messagesDefault[key] || key 403 | if (parameters && parameters.length > 0 && text !== key) { 404 | for (let i3 = 0; i3 < parameters.length; i3++) { 405 | text = text.replaceAll( 406 | new RegExp("\\{".concat(i3 + 1, "\\}"), "g"), 407 | String(parameters[i3]) 408 | ) 409 | } 410 | } 411 | return text 412 | } 413 | } 414 | var messages = { 415 | "settings.displaySettingsButtonInSideMenu": 416 | "Display Settings Button in Side Menu", 417 | "settings.menu.settings": "\u2699\uFE0F Settings", 418 | "settings.extensions.utags.title": 419 | "\u{1F3F7}\uFE0F UTags - Add usertags to links", 420 | "settings.extensions.links-helper.title": "\u{1F517} Links Helper", 421 | "settings.extensions.v2ex.rep.title": 422 | "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C", 423 | "settings.extensions.v2ex.min.title": 424 | "v2ex.min - V2EX Minimalist (\u6781\u7B80\u98CE\u683C)", 425 | "settings.extensions.replace-ugly-avatars.title": "Replace Ugly Avatars", 426 | "settings.extensions.more-by-pipecraft.title": 427 | "Find more useful userscripts", 428 | } 429 | var en_default = messages 430 | var messages2 = { 431 | "settings.displaySettingsButtonInSideMenu": 432 | "\u5728\u4FA7\u8FB9\u680F\u83DC\u5355\u4E2D\u663E\u793A\u8BBE\u7F6E\u6309\u94AE", 433 | "settings.menu.settings": "\u2699\uFE0F \u8BBE\u7F6E", 434 | "settings.extensions.utags.title": 435 | "\u{1F3F7}\uFE0F \u5C0F\u9C7C\u6807\u7B7E (UTags) - \u4E3A\u94FE\u63A5\u6DFB\u52A0\u7528\u6237\u6807\u7B7E", 436 | "settings.extensions.links-helper.title": 437 | "\u{1F517} \u94FE\u63A5\u52A9\u624B", 438 | "settings.extensions.v2ex.rep.title": 439 | "V2EX.REP - \u4E13\u6CE8\u63D0\u5347 V2EX \u4E3B\u9898\u56DE\u590D\u6D4F\u89C8\u4F53\u9A8C", 440 | "settings.extensions.v2ex.min.title": 441 | "v2ex.min - V2EX \u6781\u7B80\u98CE\u683C", 442 | "settings.extensions.replace-ugly-avatars.title": 443 | "\u8D50\u4F60\u4E2A\u5934\u50CF\u5427", 444 | "settings.extensions.more-by-pipecraft.title": 445 | "\u66F4\u591A\u6709\u8DA3\u7684\u811A\u672C", 446 | } 447 | var zh_cn_default = messages2 448 | var i = initI18n({ 449 | "en,en-US": en_default, 450 | "zh,zh-CN": zh_cn_default, 451 | }) 452 | var lang = navigator.language 453 | var locale 454 | if (lang === "zh-TW" || lang === "zh-HK") { 455 | locale = "zh-TW" 456 | } else if (lang.includes("zh")) { 457 | locale = "zh-CN" 458 | } else { 459 | locale = "en" 460 | } 461 | var relatedExtensions = [ 462 | { 463 | id: "utags", 464 | title: i("settings.extensions.utags.title"), 465 | url: "https://greasyfork.org/".concat( 466 | locale, 467 | "/scripts/460718-utags-add-usertags-to-links" 468 | ), 469 | }, 470 | { 471 | id: "links-helper", 472 | title: i("settings.extensions.links-helper.title"), 473 | description: 474 | "\u5728\u65B0\u6807\u7B7E\u9875\u4E2D\u6253\u5F00\u7B2C\u4E09\u65B9\u7F51\u7AD9\u94FE\u63A5\uFF0C\u56FE\u7247\u94FE\u63A5\u8F6C\u56FE\u7247\u6807\u7B7E\u7B49", 475 | url: "https://greasyfork.org/".concat( 476 | locale, 477 | "/scripts/464541-links-helper" 478 | ), 479 | }, 480 | { 481 | id: "v2ex.rep", 482 | title: i("settings.extensions.v2ex.rep.title"), 483 | url: "https://greasyfork.org/".concat( 484 | locale, 485 | "/scripts/466589-v2ex-rep-%E4%B8%93%E6%B3%A8%E6%8F%90%E5%8D%87-v2ex-%E4%B8%BB%E9%A2%98%E5%9B%9E%E5%A4%8D%E6%B5%8F%E8%A7%88%E4%BD%93%E9%AA%8C" 486 | ), 487 | }, 488 | { 489 | id: "v2ex.min", 490 | title: i("settings.extensions.v2ex.min.title"), 491 | url: "https://greasyfork.org/".concat( 492 | locale, 493 | "/scripts/463552-v2ex-min-v2ex-%E6%9E%81%E7%AE%80%E9%A3%8E%E6%A0%BC" 494 | ), 495 | }, 496 | { 497 | id: "replace-ugly-avatars", 498 | title: i("settings.extensions.replace-ugly-avatars.title"), 499 | url: "https://greasyfork.org/".concat( 500 | locale, 501 | "/scripts/472616-replace-ugly-avatars" 502 | ), 503 | }, 504 | { 505 | id: "more-by-pipecraft", 506 | title: i("settings.extensions.more-by-pipecraft.title"), 507 | url: "https://greasyfork.org/".concat(locale, "/users/1030884-pipecraft"), 508 | }, 509 | ] 510 | var getInstalledExtesionList = () => { 511 | return $(".extension_list_container .installed_extension_list") 512 | } 513 | var getRelatedExtesionList = () => { 514 | return $(".extension_list_container .related_extension_list") 515 | } 516 | var isInstalledExtension = (id) => { 517 | const list = getInstalledExtesionList() 518 | if (!list) { 519 | return false 520 | } 521 | const installed = $('[data-extension-id="'.concat(id, '"]'), list) 522 | return Boolean(installed) 523 | } 524 | var addCurrentExtension = (extension) => { 525 | const list = getInstalledExtesionList() 526 | if (!list) { 527 | return 528 | } 529 | if (isInstalledExtension(extension.id)) { 530 | return 531 | } 532 | const element = createInstalledExtension(extension) 533 | list.append(element) 534 | const list2 = getRelatedExtesionList() 535 | if (list2) { 536 | updateRelatedExtensions(list2) 537 | } 538 | } 539 | var activeExtension = (id) => { 540 | const list = getInstalledExtesionList() 541 | if (!list) { 542 | return false 543 | } 544 | for (const element of $$(".active", list)) { 545 | removeClass(element, "active") 546 | } 547 | const installed = $('[data-extension-id="'.concat(id, '"]'), list) 548 | if (installed) { 549 | addClass(installed, "active") 550 | } 551 | } 552 | var activeExtensionList = () => { 553 | const extensionListContainer = $(".extension_list_container") 554 | if (extensionListContainer) { 555 | addClass(extensionListContainer, "bes_active") 556 | } 557 | } 558 | var deactiveExtensionList = () => { 559 | const extensionListContainer = $(".extension_list_container") 560 | if (extensionListContainer) { 561 | removeClass(extensionListContainer, "bes_active") 562 | } 563 | } 564 | var createInstalledExtension = (installedExtension) => { 565 | const div = createElement("div", { 566 | class: "installed_extension", 567 | "data-extension-id": installedExtension.id, 568 | }) 569 | const a = addElement2(div, "a", { 570 | onclick: installedExtension.onclick, 571 | }) 572 | addElement2(a, "span", { 573 | textContent: installedExtension.title, 574 | }) 575 | const svg = addElement2(a, "svg") 576 | svg.outerHTML = createHTML(openButton) 577 | return div 578 | } 579 | var updateRelatedExtensions = (container) => { 580 | const relatedExtensionElements = $$("[data-extension-id]", container) 581 | if (relatedExtensionElements.length > 0) { 582 | for (const relatedExtensionElement of relatedExtensionElements) { 583 | if ( 584 | isInstalledExtension( 585 | relatedExtensionElement.dataset.extensionId || "noid" 586 | ) 587 | ) { 588 | relatedExtensionElement.remove() 589 | } 590 | } 591 | } else { 592 | container.innerHTML = createHTML("") 593 | } 594 | for (const relatedExtension of relatedExtensions) { 595 | if ( 596 | isInstalledExtension(relatedExtension.id) || 597 | $('[data-extension-id="'.concat(relatedExtension.id, '"]'), container) 598 | ) { 599 | continue 600 | } 601 | if ($$("[data-extension-id]", container).length >= 4) { 602 | return 603 | } 604 | const div4 = addElement2(container, "div", { 605 | class: "related_extension", 606 | "data-extension-id": relatedExtension.id, 607 | }) 608 | const a = addElement2(div4, "a", { 609 | href: relatedExtension.url, 610 | target: "_blank", 611 | }) 612 | addElement2(a, "span", { 613 | textContent: relatedExtension.title, 614 | }) 615 | const svg = addElement2(a, "svg") 616 | svg.outerHTML = createHTML(openInNewTabButton) 617 | } 618 | } 619 | function createExtensionList(installedExtensions) { 620 | const div = createElement("div", { 621 | class: "extension_list_container thin_scrollbar", 622 | }) 623 | addElement2(div, "h1", { textContent: "Settings" }) 624 | const div2 = addElement2(div, "div", { 625 | class: "installed_extension_list", 626 | }) 627 | for (const installedExtension of installedExtensions) { 628 | if (isInstalledExtension(installedExtension.id)) { 629 | continue 630 | } 631 | const element = createInstalledExtension(installedExtension) 632 | div2.append(element) 633 | } 634 | addElement2(div, "h2", { textContent: "Other Extensions" }) 635 | const div3 = addElement2(div, "div", { 636 | class: "related_extension_list", 637 | }) 638 | updateRelatedExtensions(div3) 639 | return div 640 | } 641 | var prefix = "browser_extension_settings_" 642 | var randomId = String(Math.round(Math.random() * 1e4)) 643 | var settingsContainerId = prefix + "container_" + randomId 644 | var settingsElementId = prefix + "main_" + randomId 645 | var getSettingsElement = () => $("#" + settingsElementId) 646 | var getSettingsStyle = () => 647 | style_default 648 | .replaceAll(/browser_extension_settings_container/gm, settingsContainerId) 649 | .replaceAll(/browser_extension_settings_main/gm, settingsElementId) 650 | var storageKey = "settings" 651 | var settingsOptions 652 | var settingsTable = {} 653 | var settings = {} 654 | async function getSettings() { 655 | var _a 656 | return (_a = await getValue(storageKey)) != null ? _a : {} 657 | } 658 | async function saveSettingsValue(key, value) { 659 | const settings2 = await getSettings() 660 | settings2[key] = 661 | settingsTable[key] && settingsTable[key].defaultValue === value 662 | ? void 0 663 | : value 664 | await setValue(storageKey, settings2) 665 | } 666 | function getSettingsValue(key) { 667 | var _a 668 | return Object.hasOwn(settings, key) 669 | ? settings[key] 670 | : (_a = settingsTable[key]) == null 671 | ? void 0 672 | : _a.defaultValue 673 | } 674 | var closeModal = () => { 675 | const settingsContainer = getSettingsContainer() 676 | if (settingsContainer) { 677 | settingsContainer.style.display = "none" 678 | } 679 | removeEventListener(document, "click", onDocumentClick, true) 680 | removeEventListener(document, "keydown", onDocumentKeyDown, true) 681 | } 682 | var onDocumentClick = (event) => { 683 | const target = event.target 684 | if ( 685 | target == null ? void 0 : target.closest(".".concat(prefix, "container")) 686 | ) { 687 | return 688 | } 689 | closeModal() 690 | } 691 | var onDocumentKeyDown = (event) => { 692 | if (event.defaultPrevented) { 693 | return 694 | } 695 | if (event.key === "Escape") { 696 | closeModal() 697 | event.preventDefault() 698 | } 699 | } 700 | async function updateOptions() { 701 | if (!getSettingsElement()) { 702 | return 703 | } 704 | for (const key in settingsTable) { 705 | if (Object.hasOwn(settingsTable, key)) { 706 | const item = settingsTable[key] 707 | const type = item.type || "switch" 708 | switch (type) { 709 | case "switch": { 710 | const checkbox = $( 711 | "#" 712 | .concat( 713 | settingsElementId, 714 | ' .option_groups .switch_option[data-key="' 715 | ) 716 | .concat(key, '"] input') 717 | ) 718 | if (checkbox) { 719 | checkbox.checked = getSettingsValue(key) 720 | } 721 | break 722 | } 723 | case "select": { 724 | const options = $$( 725 | "#" 726 | .concat( 727 | settingsElementId, 728 | ' .option_groups .select_option[data-key="' 729 | ) 730 | .concat(key, '"] .bes_select option') 731 | ) 732 | for (const option of options) { 733 | option.selected = option.value === String(getSettingsValue(key)) 734 | } 735 | break 736 | } 737 | case "textarea": { 738 | const textArea = $( 739 | "#" 740 | .concat( 741 | settingsElementId, 742 | ' .option_groups textarea[data-key="' 743 | ) 744 | .concat(key, '"]') 745 | ) 746 | if (textArea) { 747 | textArea.value = getSettingsValue(key) 748 | } 749 | break 750 | } 751 | default: { 752 | break 753 | } 754 | } 755 | } 756 | } 757 | if (typeof settingsOptions.onViewUpdate === "function") { 758 | const settingsMain = createSettingsElement() 759 | settingsOptions.onViewUpdate(settingsMain) 760 | } 761 | } 762 | function getSettingsContainer() { 763 | const container = $(".".concat(prefix, "container")) 764 | if (container) { 765 | const theVersion = parseInt10(container.dataset.besVersion, 0) 766 | if (theVersion < besVersion) { 767 | container.id = settingsContainerId 768 | container.dataset.besVersion = String(besVersion) 769 | } 770 | return container 771 | } 772 | return addElement2(doc.body, "div", { 773 | id: settingsContainerId, 774 | class: "".concat(prefix, "container"), 775 | "data-bes-version": besVersion, 776 | style: "display: none;", 777 | }) 778 | } 779 | function getSettingsWrapper() { 780 | const container = getSettingsContainer() 781 | return ( 782 | $(".".concat(prefix, "wrapper"), container) || 783 | addElement2(container, "div", { 784 | class: "".concat(prefix, "wrapper"), 785 | }) 786 | ) 787 | } 788 | function initExtensionList() { 789 | const wrapper = getSettingsWrapper() 790 | if (!$(".extension_list_container", wrapper)) { 791 | const list = createExtensionList([]) 792 | wrapper.append(list) 793 | } 794 | addCurrentExtension({ 795 | id: settingsOptions.id, 796 | title: settingsOptions.title, 797 | onclick: showSettings, 798 | }) 799 | } 800 | function createSettingsElement() { 801 | let settingsMain = getSettingsElement() 802 | if (!settingsMain) { 803 | const wrapper = getSettingsWrapper() 804 | for (const element of $$(".".concat(prefix, "main"))) { 805 | element.remove() 806 | } 807 | settingsMain = addElement2(wrapper, "div", { 808 | id: settingsElementId, 809 | class: "".concat(prefix, "main thin_scrollbar"), 810 | }) 811 | addElement2(settingsMain, "a", { 812 | textContent: "Settings", 813 | class: "navigation_go_previous", 814 | onclick() { 815 | activeExtensionList() 816 | }, 817 | }) 818 | if (settingsOptions.title) { 819 | addElement2(settingsMain, "h2", { textContent: settingsOptions.title }) 820 | } 821 | const optionGroups = [] 822 | const getOptionGroup = (index) => { 823 | if (index > optionGroups.length) { 824 | for (let i3 = optionGroups.length; i3 < index; i3++) { 825 | optionGroups.push( 826 | addElement2(settingsMain, "div", { 827 | class: "option_groups", 828 | }) 829 | ) 830 | } 831 | } 832 | return optionGroups[index - 1] 833 | } 834 | for (const key in settingsTable) { 835 | if (Object.hasOwn(settingsTable, key)) { 836 | const item = settingsTable[key] 837 | const type = item.type || "switch" 838 | const group = item.group || 1 839 | const optionGroup = getOptionGroup(group) 840 | switch (type) { 841 | case "switch": { 842 | const switchOption = createSwitchOption(item.icon, item.title, { 843 | async onchange(event) { 844 | const checkbox = event.target 845 | if (checkbox) { 846 | await saveSettingsValue(key, checkbox.checked) 847 | } 848 | }, 849 | }) 850 | switchOption.dataset.key = key 851 | addElement2(optionGroup, switchOption) 852 | break 853 | } 854 | case "textarea": { 855 | let timeoutId 856 | const div = addElement2(optionGroup, "div", { 857 | class: "bes_textarea", 858 | }) 859 | addElement2(div, "textarea", { 860 | "data-key": key, 861 | placeholder: item.placeholder || "", 862 | onkeyup(event) { 863 | const textArea = event.target 864 | if (timeoutId) { 865 | clearTimeout(timeoutId) 866 | timeoutId = void 0 867 | } 868 | timeoutId = setTimeout(async () => { 869 | if (textArea) { 870 | await saveSettingsValue(key, textArea.value.trim()) 871 | } 872 | }, 100) 873 | }, 874 | }) 875 | break 876 | } 877 | case "action": { 878 | addElement2(optionGroup, "a", { 879 | class: "action", 880 | textContent: item.title, 881 | onclick: item.onclick, 882 | }) 883 | break 884 | } 885 | case "externalLink": { 886 | const div4 = addElement2(optionGroup, "div", { 887 | class: "bes_external_link", 888 | }) 889 | addElement2(div4, "a", { 890 | textContent: item.title, 891 | href: item.url, 892 | target: "_blank", 893 | }) 894 | break 895 | } 896 | case "select": { 897 | const div = addElement2(optionGroup, "div", { 898 | class: "select_option bes_option", 899 | "data-key": key, 900 | }) 901 | if (item.icon) { 902 | addElement2(div, "img", { src: item.icon, class: "bes_icon" }) 903 | } 904 | addElement2(div, "span", { 905 | textContent: item.title, 906 | class: "bes_title", 907 | }) 908 | const select = addElement2(div, "select", { 909 | class: "bes_select", 910 | async onchange() { 911 | await saveSettingsValue(key, select.value) 912 | }, 913 | }) 914 | for (const option of Object.entries(item.options)) { 915 | addElement2(select, "option", { 916 | textContent: option[0], 917 | value: option[1], 918 | }) 919 | } 920 | break 921 | } 922 | case "tip": { 923 | const tip = addElement2(optionGroup, "div", { 924 | class: "bes_tip", 925 | }) 926 | addElement2(tip, "a", { 927 | class: "bes_tip_anchor", 928 | textContent: item.title, 929 | }) 930 | const tipContent = addElement2(tip, "div", { 931 | class: "bes_tip_content", 932 | innerHTML: createHTML(item.tipContent), 933 | }) 934 | break 935 | } 936 | } 937 | } 938 | } 939 | if (settingsOptions.footer) { 940 | const footer = addElement2(settingsMain, "footer") 941 | footer.innerHTML = createHTML( 942 | typeof settingsOptions.footer === "string" 943 | ? settingsOptions.footer 944 | : '

Made with \u2764\uFE0F by\n \n Pipecraft\n

' 945 | ) 946 | } 947 | } 948 | return settingsMain 949 | } 950 | function addSideMenu() { 951 | if (!getSettingsValue("displaySettingsButtonInSideMenu")) { 952 | return 953 | } 954 | const menu = 955 | $("#browser_extension_side_menu") || 956 | addElement2(doc.body, "div", { 957 | id: "browser_extension_side_menu", 958 | "data-bes-version": besVersion, 959 | }) 960 | const button = $("button[data-bes-version]", menu) 961 | if (button) { 962 | const theVersion = parseInt10(button.dataset.besVersion, 0) 963 | if (theVersion >= besVersion) { 964 | return 965 | } 966 | button.remove() 967 | } 968 | addElement2(menu, "button", { 969 | type: "button", 970 | "data-bes-version": besVersion, 971 | title: i("settings.menu.settings"), 972 | onclick() { 973 | setTimeout(showSettings, 1) 974 | }, 975 | innerHTML: settingButton, 976 | }) 977 | } 978 | function addCommonSettings(settingsTable3) { 979 | let maxGroup = 0 980 | for (const key in settingsTable3) { 981 | if (Object.hasOwn(settingsTable3, key)) { 982 | const item = settingsTable3[key] 983 | const group = item.group || 1 984 | if (group > maxGroup) { 985 | maxGroup = group 986 | } 987 | } 988 | } 989 | settingsTable3.displaySettingsButtonInSideMenu = { 990 | title: i("settings.displaySettingsButtonInSideMenu"), 991 | defaultValue: !( 992 | typeof GM === "object" && typeof GM.registerMenuCommand === "function" 993 | ), 994 | group: maxGroup + 1, 995 | } 996 | } 997 | function handleShowSettingsUrl() { 998 | if (location.hash === "#bes-show-settings") { 999 | setTimeout(showSettings, 100) 1000 | } 1001 | } 1002 | async function showSettings() { 1003 | const settingsContainer = getSettingsContainer() 1004 | const settingsMain = createSettingsElement() 1005 | await updateOptions() 1006 | settingsContainer.style.display = "block" 1007 | addEventListener(document, "click", onDocumentClick, true) 1008 | addEventListener(document, "keydown", onDocumentKeyDown, true) 1009 | activeExtension(settingsOptions.id) 1010 | deactiveExtensionList() 1011 | } 1012 | var initSettings = async (options) => { 1013 | settingsOptions = options 1014 | settingsTable = options.settingsTable || {} 1015 | addCommonSettings(settingsTable) 1016 | addValueChangeListener(storageKey, async () => { 1017 | settings = await getSettings() 1018 | await updateOptions() 1019 | addSideMenu() 1020 | if (typeof options.onValueChange === "function") { 1021 | options.onValueChange() 1022 | } 1023 | }) 1024 | settings = await getSettings() 1025 | runWhenHeadExists(() => { 1026 | addStyle(getSettingsStyle()) 1027 | }) 1028 | runWhenDomReady(() => { 1029 | initExtensionList() 1030 | addSideMenu() 1031 | }) 1032 | registerMenuCommand(i("settings.menu.settings"), showSettings, "o") 1033 | handleShowSettingsUrl() 1034 | } 1035 | var content_default = 1036 | '#myprefix_div{color:#000;box-sizing:border-box;padding:10px 15px;background-color:#fff;border-radius:5px;-webkit-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);-moz-box-shadow:0px 10px 39px 10px rgba(62,66,66,.22);box-shadow:0px 10px 39px 10px rgba(62,66,66,.22) !important}#myprefix_div div{color:green}#myprefix_test_link{--border-color: linear-gradient(-45deg, #ffae00, #7e03aa, #00fffb);--border-width: 0.125em;--curve-size: 0.5em;--blur: 30px;--bg: #080312;--color: #afffff;color:var(--color);position:relative;isolation:isolate;display:inline-grid;place-content:center;padding:.5em 1.5em;font-size:17px;border:0;text-transform:uppercase;box-shadow:10px 10px 20px rgba(0,0,0,.6);clip-path:polygon(0% var(--curve-size), var(--curve-size) 0, 100% 0, 100% calc(100% - var(--curve-size)), calc(100% - var(--curve-size)) 100%, 0 100%);transition:color 250ms}#myprefix_test_link::after,#myprefix_test_link::before{content:"";position:absolute;inset:0}#myprefix_test_link::before{background:var(--border-color);background-size:300% 300%;animation:move-bg7234 5s ease infinite;z-index:-2}@keyframes move-bg7234{0%{background-position:31% 0%}50%{background-position:70% 100%}100%{background-position:31% 0%}}#myprefix_test_link::after{background:var(--bg);z-index:-1;clip-path:polygon(var(--border-width) calc(var(--curve-size) + var(--border-width) * 0.5), calc(var(--curve-size) + var(--border-width) * 0.5) var(--border-width), calc(100% - var(--border-width)) var(--border-width), calc(100% - var(--border-width)) calc(100% - (var(--curve-size) + var(--border-width) * 0.5)), calc(100% - (var(--curve-size) + var(--border-width) * 0.5)) calc(100% - var(--border-width)), var(--border-width) calc(100% - var(--border-width)));transition:clip-path 500ms}#myprefix_test_link:where(:hover,:focus)::after{clip-path:polygon(calc(100% - var(--border-width)) calc(100% - (var(--curve-size) + var(--border-width) * 0.5)), calc(100% - var(--border-width)) var(--border-width), calc(100% - var(--border-width)) var(--border-width), calc(100% - var(--border-width)) calc(100% - (var(--curve-size) + var(--border-width) * 0.5)), calc(100% - (var(--curve-size) + var(--border-width) * 0.5)) calc(100% - var(--border-width)), calc(100% - (var(--curve-size) + var(--border-width) * 0.5)) calc(100% - var(--border-width)));transition:200ms}#myprefix_test_link:where(:hover,:focus){color:#fff}' 1037 | var messages3 = { 1038 | "settings.test1": "Test1", 1039 | "settings.title": "My Extension", 1040 | "settings.information": 1041 | "After changing the settings, reload the page to take effect", 1042 | "settings.report": "Report and Issue...", 1043 | } 1044 | var en_default2 = messages3 1045 | var messages4 = { 1046 | "settings.test1": "\u6D4B\u8BD51", 1047 | "settings.title": "\u6211\u7684\u6269\u5C55", 1048 | "settings.information": 1049 | "\u66F4\u6539\u8BBE\u7F6E\u540E\uFF0C\u91CD\u65B0\u52A0\u8F7D\u9875\u9762\u5373\u53EF\u751F\u6548", 1050 | "settings.report": "\u53CD\u9988\u95EE\u9898", 1051 | } 1052 | var zh_cn_default2 = messages4 1053 | var i2 = initI18n({ 1054 | "en,en-US": en_default2, 1055 | "zh,zh-CN": zh_cn_default2, 1056 | }) 1057 | var settingsTable2 = { 1058 | test1: { 1059 | title: i2("settings.test1"), 1060 | defaultValue: true, 1061 | }, 1062 | test2: { 1063 | title: "Test2", 1064 | defaultValue: false, 1065 | }, 1066 | test3: { 1067 | title: "Test3", 1068 | defaultValue: false, 1069 | group: 2, 1070 | }, 1071 | test4: { 1072 | title: "Test4", 1073 | defaultValue: true, 1074 | group: 2, 1075 | }, 1076 | test5: { 1077 | title: "Test5", 1078 | defaultValue: true, 1079 | group: 3, 1080 | }, 1081 | test6: { 1082 | title: "Test6", 1083 | defaultValue: "", 1084 | placeholder: "Input value", 1085 | type: "textarea", 1086 | group: 3, 1087 | }, 1088 | } 1089 | function showVisitCount(visitCount) { 1090 | const div = 1091 | $("#myprefix_div") || 1092 | addElement2(document.body, "div", { 1093 | id: "myprefix_div", 1094 | style: 1095 | "display: block; position: fixed; top: 50px; right: 50px; z-index: 1000;", 1096 | }) 1097 | const div2 = 1098 | $$("div", div)[0] || 1099 | addElement2(div, "div", { 1100 | style: 1101 | "display: block; background-color: yellow; margin-bottom: 10px; padding: 4px 12px; box-sizing: border-box;", 1102 | }) 1103 | div2.innerHTML = visitCount 1104 | } 1105 | async function main() { 1106 | await initSettings({ 1107 | id: "my-extension", 1108 | title: i2("settings.title"), 1109 | footer: "\n

" 1110 | .concat( 1111 | i2("settings.information"), 1112 | '

\n

\n \n ' 1113 | ) 1114 | .concat( 1115 | i2("settings.report"), 1116 | '\n

\n

Made with \u2764\uFE0F by\n \n Pipecraft\n

' 1117 | ), 1118 | settingsTable: settingsTable2, 1119 | }) 1120 | console.log(getSettingsValue("test1")) 1121 | console.log(getSettingsValue("test2")) 1122 | const visitCount = (await getValue("visitCount")) || "0" 1123 | let visitCountInt = Number.parseInt(visitCount, 10) 1124 | showVisitCount(String(++visitCountInt)) 1125 | await setValue("visitCount", visitCountInt) 1126 | addValueChangeListener("visitCount", async () => { 1127 | const visitCount2 = (await getValue("visitCount")) || "0" 1128 | showVisitCount(visitCount2) 1129 | }) 1130 | addElement2($("#myprefix_div"), "a", { 1131 | id: "myprefix_test_link", 1132 | href: "https://utags.pipecraft.net/", 1133 | target: "_blank", 1134 | textContent: "Get UTags", 1135 | }) 1136 | addElement2(document.head, "style", { 1137 | textContent: content_default, 1138 | }) 1139 | addStyle("#myprefix_div { padding: 6px; };") 1140 | } 1141 | runWhenBodyExists(async () => { 1142 | if (doc.documentElement.dataset.myextensionId === void 0) { 1143 | doc.documentElement.dataset.myextensionId = "" 1144 | await main() 1145 | } 1146 | }) 1147 | })() 1148 | -------------------------------------------------------------------------------- /manual-installation.md: -------------------------------------------------------------------------------- 1 | # Manual installation 2 | 3 | ## Chrome 4 | 5 | 1. Download the [latest version](https://github.com/utags/browser-extension-starter/releases) or one of the [releases](https://github.com/utags/browser-extension-starter/releases) 6 | 2. Extract the archive 7 | 3. Paste `chrome://extensions/` into the address bar of your browser 8 | 4. Activate `Developer mode` 9 | 5. Click on the `Load unpacked` button and select the extracted folder 10 | 11 | ## Edge 12 | 13 | 1. Download the [latest version](https://github.com/utags/browser-extension-starter/releases) or one of the [releases](https://github.com/utags/browser-extension-starter/releases) 14 | 2. Extract the archive 15 | 3. Paste `edge://extensions/` into the address bar of your browser 16 | 4. Activate `Developer mode` 17 | 5. Click on the `Load unpacked` button and select the extracted folder 18 | 19 | ## Firefox 20 | 21 | 1. Download the [latest version](https://github.com/utags/browser-extension-starter/releases) or one of the [releases](https://github.com/utags/browser-extension-starter/releases) 22 | 2. Extract the archive 23 | 3. Paste `about:debugging#/runtime/this-firefox` into the address bar of your browser 24 | 4. Click on the `Load unpacked` button and select `manifest.json` file from the extracted folder 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-extension", 3 | "displayName": "My-Extension", 4 | "displayName:zh-CN": "我的扩展", 5 | "version": "0.1.0", 6 | "description": "try to take over the world!", 7 | "description:zh-CN": "让世界更美好!", 8 | "author": "You", 9 | "namespace": "https://github.com/utags/browser-extension-starter", 10 | "icon": "https://www.tampermonkey.net/favicon.ico", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/utags/browser-extension-starter/issues" 14 | }, 15 | "homepage": "https://github.com/utags/browser-extension-starter#readme", 16 | "scripts": { 17 | "p": "prettier --write .", 18 | "lint": "run-s lint:*", 19 | "lint:code": "prettier --write . && xo --fix", 20 | "lint:type": "tsc --noemit", 21 | "dev": "run-p dev:*", 22 | "dev:chrome": "plasmo dev", 23 | "dev:firefox": "sleep 2 && plasmo dev --target=firefox-mv2", 24 | "dev:userscript": "node scripts/userscript/watch.mjs", 25 | "dev:bookmarklet": "node scripts/bookmarklet/watch.mjs", 26 | "dev:module": "node scripts/module/watch.mjs", 27 | "staging": "run-p staging:*", 28 | "staging:userscript": "node scripts/userscript/build.mjs --staging && pnpm prettier --loglevel warn --write build/userscript-staging/ && http-server build/userscript-staging/ -o", 29 | "build": "run-s build:*", 30 | "build:chrome": "plasmo build", 31 | "build:firefox": "plasmo build --target=firefox-mv2", 32 | "build:userscript": "node scripts/userscript/build.mjs && pnpm prettier --loglevel warn --write build/userscript-prod/", 33 | "build:bookmarklet": "node scripts/bookmarklet/build.mjs", 34 | "build:module": "node scripts/module/build.mjs && pnpm prettier --loglevel warn --write build/module-prod/", 35 | "package": "run-s package:*", 36 | "package:chrome": "plasmo package", 37 | "package:firefox": "plasmo package --target=firefox-mv2" 38 | }, 39 | "dependencies": { 40 | "browser-extension-i18n": "^0.0.6", 41 | "browser-extension-settings": "^0.5.3", 42 | "browser-extension-storage": "^0.1.2", 43 | "browser-extension-utils": "^0.1.18", 44 | "plasmo": "^0.77.5", 45 | "react": "^18.2.0", 46 | "react-dom": "^18.2.0" 47 | }, 48 | "devDependencies": { 49 | "@plasmohq/prettier-plugin-sort-imports": "^3.6.4", 50 | "@types/chrome": "^0.0.243", 51 | "@types/node": "^20.5.0", 52 | "@types/react": "^18.2.20", 53 | "@types/react-dom": "^18.2.7", 54 | "bookmarkleter": "^1.1.0", 55 | "esbuild": "^0.19.2", 56 | "http-server": "^14.1.1", 57 | "npm-run-all": "^4.1.5", 58 | "prettier": "^2.8.8", 59 | "sass": "^1.65.1", 60 | "typescript": "^5.1.6", 61 | "xo": "^0.56.0" 62 | }, 63 | "manifest": { 64 | "host_permissions": [ 65 | "https://*/*" 66 | ], 67 | "permissions": [ 68 | "storage", 69 | "tabs" 70 | ] 71 | }, 72 | "xo": { 73 | "space": 2, 74 | "prettier": true, 75 | "globals": [ 76 | "document" 77 | ], 78 | "rules": { 79 | "import/extensions": 0, 80 | "import/order": 0, 81 | "@typescript-eslint/prefer-nullish-coalescing": 0, 82 | "capitalized-comments": 0 83 | }, 84 | "overrides": [ 85 | { 86 | "files": "src/messages/*.ts", 87 | "rules": { 88 | "@typescript-eslint/naming-convention": 0 89 | } 90 | } 91 | ] 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /scripts/bookmarklet/build.mjs: -------------------------------------------------------------------------------- 1 | import bookmarkleter from "bookmarkleter" 2 | import * as esbuild from "esbuild" 3 | import fs from "node:fs" 4 | 5 | import { getBuildOptions } from "../common.mjs" 6 | 7 | const target = "bookmarklet" 8 | const tag = "prod" 9 | 10 | // TODO: add name and version to output 11 | const config = JSON.parse(fs.readFileSync("package.json", "utf8")) 12 | 13 | const buildOptions = { 14 | ...getBuildOptions(target, "prod"), 15 | minify: true, 16 | sourcemap: false, 17 | outfile: `build/${target}-${tag}/${config.name}.bookmarklet.link`, 18 | } 19 | buildOptions.alias = { 20 | ...buildOptions.alias, 21 | "browser-extension-storage": "browser-extension-storage/local-storage", 22 | } 23 | 24 | await esbuild.build(buildOptions) 25 | 26 | const text = fs.readFileSync(buildOptions.outfile, "utf8") 27 | const options = { 28 | urlencode: true, 29 | iife: false, 30 | mangleVars: true, 31 | transpile: true, 32 | } 33 | const bookmarklet = bookmarkleter(text, options) 34 | fs.writeFileSync(buildOptions.outfile, bookmarklet) 35 | -------------------------------------------------------------------------------- /scripts/bookmarklet/watch.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs" 2 | 3 | import { getBuildOptions, runDevServer } from "../common.mjs" 4 | 5 | const target = "bookmarklet" 6 | const tag = "dev" 7 | 8 | const config = JSON.parse(fs.readFileSync("package.json", "utf8")) 9 | 10 | const buildOptions = getBuildOptions(target, tag) 11 | buildOptions.alias = { 12 | ...buildOptions.alias, 13 | "browser-extension-storage": "browser-extension-storage/local-storage", 14 | } 15 | 16 | const { port } = await runDevServer(buildOptions, target, tag) 17 | 18 | const bookmarklet = `(function () { 19 | "use strict"; 20 | 21 | const script = document.createElement("script"); 22 | script.src = "http://localhost:${port}/content.js"; 23 | script.async = true; 24 | script.defer = true; 25 | document.body.append(script); 26 | })();` 27 | .replaceAll(/^\s*/gm, "") 28 | .replaceAll(/\n/gm, "") 29 | 30 | let linkProd = "" 31 | const fileProd = `build/${target}-prod/${config.name}.bookmarklet.link` 32 | if (fs.existsSync(fileProd)) { 33 | const bookmarkletProd = fs.readFileSync(fileProd, "utf8") 34 | linkProd = `
Production version: Drag me to the bookmark bar` 35 | } 36 | 37 | const html = ` 38 | 39 | Install Extension - target: ${target} 40 | 41 | 42 |

43 | Development version: Drag me to the bookmark bar 44 | ${linkProd} 45 |

46 |

Add this code to the bookmark

47 | 51 | 54 | 55 | 56 | ` 57 | 58 | fs.writeFileSync(`build/${target}-${tag}/index.html`, html) 59 | -------------------------------------------------------------------------------- /scripts/common.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild" 2 | import fs from "node:fs" 3 | import * as sass from "sass" 4 | 5 | const EMOJI_LIST = [ 6 | // 7 | "⚽️", 8 | "🏀", 9 | "🏈", 10 | "⚾️", 11 | "🥎", 12 | "🎾", 13 | "🏐", 14 | "🏉", 15 | "🥏", 16 | "🎱", 17 | ] 18 | 19 | function getRandomInt(max) { 20 | return Math.floor(Math.random() * max) 21 | } 22 | 23 | export const logger = (target, emoji) => { 24 | emoji = emoji || EMOJI_LIST[getRandomInt(EMOJI_LIST.length)] 25 | return (message) => { 26 | console.log(`${emoji} [target: ${target}]`, message) 27 | } 28 | } 29 | 30 | const schemeImportPlugin = ({ compressCss }) => ({ 31 | name: "schemeImport", 32 | setup(build) { 33 | build.onResolve({ filter: /^[\w-]+:/ }, async (args) => { 34 | const result = await build.resolve(args.path.split(":")[1], { 35 | kind: "import-statement", 36 | resolveDir: args.resolveDir, 37 | }) 38 | if (result.errors.length > 0) { 39 | return { errors: result.errors } 40 | } 41 | 42 | return { path: result.path, namespace: "schemeImport-ns" } 43 | }) 44 | build.onLoad( 45 | { filter: /\.(s[ac]ss|css)$/, namespace: "schemeImport-ns" }, 46 | async (args) => ({ 47 | contents: ( 48 | (await sass.compileAsync(args.path, { 49 | style: compressCss ? "compressed" : "expanded", 50 | })) || { 51 | css: "", 52 | } 53 | ).css, 54 | loader: "text", 55 | }) 56 | ) 57 | build.onLoad( 58 | { filter: /.*/, namespace: "schemeImport-ns" }, 59 | async (args) => ({ 60 | contents: await fs.promises.readFile(args.path), 61 | loader: "text", 62 | }) 63 | ) 64 | }, 65 | }) 66 | 67 | export const getBuildOptions = (target, tag, fileName = "content") => { 68 | return { 69 | entryPoints: [`src/${fileName}.ts`], 70 | bundle: true, 71 | plugins: [ 72 | schemeImportPlugin({ compressCss: tag === "prod" || tag === "staging" }), 73 | ], 74 | define: { 75 | "process.env.PLASMO_TARGET": `"${target}"`, 76 | "process.env.PLASMO_TAG": `"${tag}"`, 77 | }, 78 | target: ["chrome58", "firefox57", "safari11", "edge16"], 79 | outfile: `build/${target}-${tag}/${fileName}.js`, 80 | } 81 | } 82 | 83 | const waitUntilFileExists = async (path, timeout = 10_000) => { 84 | return new Promise((resolve, reject) => { 85 | const timeoutId = setTimeout(() => { 86 | reject(new Error("File does not exits. " + path)) 87 | }, timeout) 88 | 89 | const check = () => { 90 | if (fs.existsSync(path)) { 91 | clearTimeout(timeoutId) 92 | resolve() 93 | return 94 | } 95 | 96 | setTimeout(check, 100) 97 | } 98 | 99 | check() 100 | }) 101 | } 102 | 103 | export const runDevServer = async (buildOptions, target, tag) => { 104 | const log = logger(target) 105 | const ctx = await esbuild.context(buildOptions) 106 | 107 | await ctx.watch() 108 | log("watching...") 109 | 110 | await waitUntilFileExists(buildOptions.outfile) 111 | 112 | const { host, port } = await ctx.serve({ 113 | servedir: `build/${target}-${tag}`, 114 | }) 115 | log(`Server is running at http://localhost:${port}/`) 116 | log("Hit CTRL-C to stop the server") 117 | 118 | return { host, port } 119 | } 120 | -------------------------------------------------------------------------------- /scripts/module/build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild" 2 | import fs from "node:fs" 3 | 4 | import { getBuildOptions } from "../common.mjs" 5 | 6 | const target = "module" 7 | const tag = "prod" 8 | 9 | // TODO: add name and version to output 10 | const config = JSON.parse(fs.readFileSync("package.json", "utf8")) 11 | 12 | const buildOptions = { 13 | ...getBuildOptions(target, "prod"), 14 | minify: false, 15 | sourcemap: false, 16 | outfile: `build/${target}-${tag}/${config.name}.js`, 17 | } 18 | buildOptions.alias = { 19 | ...buildOptions.alias, 20 | "browser-extension-storage": "browser-extension-storage/local-storage", 21 | } 22 | 23 | await esbuild.build(buildOptions) 24 | 25 | let text = fs.readFileSync(buildOptions.outfile, "utf8") 26 | // Remove all commenets staret with '// ' 27 | text = text.replaceAll(/^\s*\/\/ [^=@].*$/gm, "") 28 | text = text.replaceAll(/\n+/gm, "\n") 29 | 30 | fs.writeFileSync(buildOptions.outfile, text) 31 | 32 | await esbuild.build({ 33 | ...buildOptions, 34 | minify: true, 35 | sourcemap: true, 36 | outfile: `build/${target}-${tag}/${config.name}.min.js`, 37 | }) 38 | -------------------------------------------------------------------------------- /scripts/module/watch.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs" 2 | 3 | import { getBuildOptions, runDevServer } from "../common.mjs" 4 | 5 | const target = "module" 6 | const tag = "dev" 7 | 8 | const buildOptions = getBuildOptions(target, tag) 9 | buildOptions.alias = { 10 | ...buildOptions.alias, 11 | "browser-extension-storage": "browser-extension-storage/local-storage", 12 | } 13 | 14 | const { port } = await runDevServer(buildOptions, target, tag) 15 | 16 | const code = `` 34 | 35 | const html = ` 36 | 37 | Install Extension - target: ${target} 38 | 39 | 40 |

Add this code to the HTML file

41 | 45 | 48 | ${code} 49 | 50 | 51 | ` 52 | 53 | fs.writeFileSync(`build/${target}-${tag}/index.html`, html) 54 | -------------------------------------------------------------------------------- /scripts/userscript/banner.txt: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name {displayName} 3 | // @name:zh-CN {displayName:zh-CN} 4 | // @namespace {namespace} 5 | // @homepageURL {homepage} 6 | // @supportURL {bugs.url} 7 | // @version {version} 8 | // @description {description} 9 | // @description:zh-CN {description:zh-CN} 10 | // @icon {icon} 11 | // @author {author} 12 | // @license {license} 13 | // @match https://*/* 14 | // @match http://*/* 15 | // ==/UserScript== 16 | // 17 | -------------------------------------------------------------------------------- /scripts/userscript/build.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild" 2 | import fs from "node:fs" 3 | import process from "node:process" 4 | 5 | import { getBuildOptions } from "../common.mjs" 6 | 7 | const target = "userscript" 8 | const tag = process.argv.includes("--staging") ? "staging" : "prod" 9 | 10 | const config = JSON.parse(fs.readFileSync("package.json", "utf8")) 11 | 12 | let banner = fs.readFileSync("scripts/userscript/banner.txt", "utf8") 13 | 14 | if (tag !== "prod") { 15 | banner = banner.replaceAll(/({displayName(:.+)?})/gm, `$1 - ${tag}`) 16 | } 17 | 18 | const buildOptions = { 19 | ...getBuildOptions(target, tag), 20 | banner: { 21 | js: banner, 22 | }, 23 | outfile: `build/${target}-${tag}/${config.name}.user.js`, 24 | } 25 | buildOptions.alias = { 26 | ...buildOptions.alias, 27 | "browser-extension-storage": "browser-extension-storage/userscript", 28 | "browser-extension-utils": "browser-extension-utils/userscript", 29 | } 30 | 31 | await esbuild.build(buildOptions) 32 | 33 | let text = fs.readFileSync(buildOptions.outfile, "utf8") 34 | 35 | if (config.bugs && config.bugs.url) { 36 | text = text.replace("{bugs.url}", config.bugs.url) 37 | } 38 | 39 | const keys = banner 40 | .split("\n") 41 | .map((v) => /{([\w\-.:]+)}/.exec(v)) 42 | .filter(Boolean) 43 | .map((v) => v[1]) 44 | 45 | for (const key of keys) { 46 | text = text.replace("{" + key + "}", config[key]) 47 | } 48 | 49 | // Get all userscript GM_* and GM.* functions 50 | const matched = new Set() 51 | text.replaceAll(/(GM[_.]\w+)/gm, (match) => { 52 | matched.add(match) 53 | }) 54 | const grants = [...matched] 55 | .map((v) => `// @grant${" ".repeat(16)}${v}`) 56 | .join("\n") 57 | text = text.replace("// ==/UserScript==", `${grants}\n// ==/UserScript==`) 58 | 59 | // Replace first one to 'use strict' 60 | text = text.replace("{", '{\n "use strict";') 61 | // Remove all commenets staret with '// ' 62 | text = text.replaceAll(/^\s*\/\/ [^=@].*$/gm, "") 63 | text = text.replaceAll(/\n+/gm, "\n") 64 | 65 | fs.writeFileSync(buildOptions.outfile, text) 66 | -------------------------------------------------------------------------------- /scripts/userscript/watch.mjs: -------------------------------------------------------------------------------- 1 | import fs from "node:fs" 2 | 3 | import { getBuildOptions, runDevServer } from "../common.mjs" 4 | 5 | const target = "userscript" 6 | const tag = "dev" 7 | 8 | const buildOptions = getBuildOptions(target, tag) 9 | buildOptions.alias = { 10 | ...buildOptions.alias, 11 | "browser-extension-storage": "browser-extension-storage/userscript", 12 | "browser-extension-utils": "browser-extension-utils/userscript", 13 | } 14 | 15 | const { port } = await runDevServer(buildOptions, target, tag) 16 | 17 | const text = fs.readFileSync(`build/${target}-${tag}/content.js`, "utf8") 18 | // Get all userscript GM_* and GM.* functions 19 | const matched = new Set() 20 | text.replaceAll(/(GM[_.]\w+)/gm, (match) => { 21 | matched.add(match) 22 | }) 23 | 24 | const grants = [...matched] 25 | .map((v) => `// @grant${" ".repeat(8)}${v}`) 26 | .join("\n") 27 | 28 | matched.add("GM") 29 | 30 | const apiExports = [...matched] 31 | .filter((v) => !v.includes("GM.")) 32 | .map((v) => ` "${v}": typeof ${v} === "undefined" ? undefined : ${v},`) 33 | .join("\n") 34 | 35 | const code = `// ==UserScript== 36 | // @name localhost:${port} 37 | // @namespace http://tampermonkey.net/ 38 | // @version 0.0.1 39 | // @description try to take over the world! 40 | // @author You 41 | // @match https://*/* 42 | // @match http://*/* 43 | ${grants} 44 | // ==/UserScript== 45 | 46 | (function () { 47 | "use strict"; 48 | if (!document.body) { 49 | return; 50 | } 51 | 52 | document.GMFunctions = { 53 | ${apiExports} 54 | } 55 | 56 | const script = document.createElement("script"); 57 | script.src = "http://localhost:${port}/content.js"; 58 | document.body.append(script); 59 | 60 | new EventSource("http://localhost:${port}/esbuild").addEventListener( 61 | "change", 62 | () => { 63 | location.reload(); 64 | } 65 | ); 66 | })(); 67 | // END` 68 | 69 | const html = ` 70 | 71 | Install Extension - target: ${target} 72 | 73 | 74 |

Click this to install

75 |

Or add the code below to Tampermonkey

76 | 79 | 82 | 83 | 84 | ` 85 | 86 | fs.writeFileSync(`build/${target}-${tag}/index.html`, html) 87 | fs.writeFileSync(`build/${target}-${tag}/index.user.js`, code) 88 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | console.log( 3 | "HELLO WORLD FROM BGSCRIPTS", 4 | // eslint-disable-next-line n/prefer-global/process 5 | process.env.PLASMO_TARGET, 6 | // eslint-disable-next-line n/prefer-global/process 7 | process.env.PLASMO_TAG 8 | ) 9 | -------------------------------------------------------------------------------- /src/content.scss: -------------------------------------------------------------------------------- 1 | #myprefix_div { 2 | color: black; 3 | box-sizing: border-box; 4 | padding: 10px 15px; 5 | background-color: #fff; 6 | border-radius: 5px; 7 | -webkit-box-shadow: 0px 10px 39px 10px rgba(62, 66, 66, 0.22); 8 | -moz-box-shadow: 0px 10px 39px 10px rgba(62, 66, 66, 0.22); 9 | box-shadow: 0px 10px 39px 10px rgba(62, 66, 66, 0.22) !important; 10 | 11 | div { 12 | color: green; 13 | } 14 | } 15 | 16 | #myprefix_test_link { 17 | --border-color: linear-gradient(-45deg, #ffae00, #7e03aa, #00fffb); 18 | --border-width: 0.125em; 19 | --curve-size: 0.5em; 20 | --blur: 30px; 21 | --bg: #080312; 22 | --color: #afffff; 23 | color: var(--color); 24 | /* use position: relative; so that BG is only for #myprefix_test_link */ 25 | position: relative; 26 | isolation: isolate; 27 | display: inline-grid; 28 | place-content: center; 29 | padding: 0.5em 1.5em; 30 | font-size: 17px; 31 | border: 0; 32 | text-transform: uppercase; 33 | box-shadow: 10px 10px 20px rgba(0, 0, 0, 0.6); 34 | clip-path: polygon( 35 | /* Top-left */ 0% var(--curve-size), 36 | var(--curve-size) 0, 37 | /* top-right */ 100% 0, 38 | 100% calc(100% - var(--curve-size)), 39 | /* bottom-right 1 */ calc(100% - var(--curve-size)) 100%, 40 | /* bottom-right 2 */ 0 100% 41 | ); 42 | transition: color 250ms; 43 | } 44 | 45 | #myprefix_test_link::after, 46 | #myprefix_test_link::before { 47 | content: ""; 48 | position: absolute; 49 | inset: 0; 50 | } 51 | 52 | #myprefix_test_link::before { 53 | background: var(--border-color); 54 | background-size: 300% 300%; 55 | animation: move-bg7234 5s ease infinite; 56 | z-index: -2; 57 | } 58 | 59 | @keyframes move-bg7234 { 60 | 0% { 61 | background-position: 31% 0%; 62 | } 63 | 64 | 50% { 65 | background-position: 70% 100%; 66 | } 67 | 68 | 100% { 69 | background-position: 31% 0%; 70 | } 71 | } 72 | 73 | #myprefix_test_link::after { 74 | background: var(--bg); 75 | z-index: -1; 76 | clip-path: polygon( 77 | /* Top-left */ var(--border-width) 78 | calc(var(--curve-size) + var(--border-width) * 0.5), 79 | calc(var(--curve-size) + var(--border-width) * 0.5) var(--border-width), 80 | /* top-right */ calc(100% - var(--border-width)) var(--border-width), 81 | calc(100% - var(--border-width)) 82 | calc(100% - calc(var(--curve-size) + var(--border-width) * 0.5)), 83 | /* bottom-right 1 */ 84 | calc(100% - calc(var(--curve-size) + var(--border-width) * 0.5)) 85 | calc(100% - var(--border-width)), 86 | /* bottom-right 2 */ var(--border-width) calc(100% - var(--border-width)) 87 | ); 88 | transition: clip-path 500ms; 89 | } 90 | 91 | #myprefix_test_link:where(:hover, :focus)::after { 92 | clip-path: polygon( 93 | /* Top-left */ calc(100% - var(--border-width)) 94 | calc(100% - calc(var(--curve-size) + var(--border-width) * 0.5)), 95 | calc(100% - var(--border-width)) var(--border-width), 96 | /* top-right */ calc(100% - var(--border-width)) var(--border-width), 97 | calc(100% - var(--border-width)) 98 | calc(100% - calc(var(--curve-size) + var(--border-width) * 0.5)), 99 | /* bottom-right 1 */ 100 | calc(100% - calc(var(--curve-size) + var(--border-width) * 0.5)) 101 | calc(100% - var(--border-width)), 102 | /* bottom-right 2 */ 103 | calc(100% - calc(var(--curve-size) + var(--border-width) * 0.5)) 104 | calc(100% - var(--border-width)) 105 | ); 106 | transition: 200ms; 107 | } 108 | 109 | #myprefix_test_link:where(:hover, :focus) { 110 | color: #fff; 111 | } 112 | -------------------------------------------------------------------------------- /src/content.ts: -------------------------------------------------------------------------------- 1 | import { getSettingsValue, initSettings } from "browser-extension-settings" 2 | import { 3 | addValueChangeListener, 4 | getValue, 5 | setValue, 6 | } from "browser-extension-storage" 7 | import { 8 | $, 9 | $$, 10 | addElement, 11 | addStyle, 12 | doc, 13 | runWhenBodyExists, 14 | } from "browser-extension-utils" 15 | import styleText from "data-text:./content.scss" 16 | 17 | import { i } from "./messages" 18 | 19 | const settingsTable = { 20 | test1: { 21 | title: i("settings.test1"), 22 | defaultValue: true, 23 | }, 24 | test2: { 25 | title: "Test2", 26 | defaultValue: false, 27 | }, 28 | test3: { 29 | title: "Test3", 30 | defaultValue: false, 31 | group: 2, 32 | }, 33 | test4: { 34 | title: "Test4", 35 | defaultValue: true, 36 | group: 2, 37 | }, 38 | test5: { 39 | title: "Test5", 40 | defaultValue: true, 41 | group: 3, 42 | }, 43 | test6: { 44 | title: "Test6", 45 | defaultValue: "", 46 | placeholder: "Input value", 47 | type: "textarea", 48 | group: 3, 49 | }, 50 | } 51 | 52 | function showVisitCount(visitCount: string) { 53 | const div = 54 | $("#myprefix_div") || 55 | addElement(document.body, "div", { 56 | id: "myprefix_div", 57 | style: 58 | "display: block; position: fixed; top: 50px; right: 50px; z-index: 1000;", 59 | }) 60 | 61 | const div2 = 62 | $$("div", div)[0] || 63 | addElement(div, "div", { 64 | style: 65 | "display: block; background-color: yellow; margin-bottom: 10px; padding: 4px 12px; box-sizing: border-box;", 66 | }) 67 | 68 | div2.innerHTML = visitCount 69 | } 70 | 71 | async function main() { 72 | await initSettings({ 73 | id: "my-extension", 74 | title: i("settings.title"), 75 | footer: ` 76 |

${i("settings.information")}

77 |

78 | 79 | ${i("settings.report")} 80 |

81 |

Made with ❤️ by 82 | 83 | Pipecraft 84 |

`, 85 | settingsTable, 86 | }) 87 | 88 | console.log(getSettingsValue("test1")) 89 | console.log(getSettingsValue("test2")) 90 | 91 | const visitCount = ((await getValue("visitCount")) as string) || "0" 92 | let visitCountInt = Number.parseInt(visitCount, 10) 93 | showVisitCount(String(++visitCountInt)) 94 | await setValue("visitCount", visitCountInt) 95 | 96 | addValueChangeListener("visitCount", async () => { 97 | const visitCount = ((await getValue("visitCount")) as string) || "0" 98 | showVisitCount(visitCount) 99 | }) 100 | 101 | addElement($("#myprefix_div")!, "a", { 102 | id: "myprefix_test_link", 103 | href: "https://utags.pipecraft.net/", 104 | target: "_blank", 105 | textContent: "Get UTags", 106 | }) 107 | 108 | addElement(document.head, "style", { 109 | textContent: styleText, 110 | }) 111 | 112 | addStyle("#myprefix_div { padding: 6px; };") 113 | } 114 | 115 | runWhenBodyExists(async () => { 116 | if (doc.documentElement.dataset.myextensionId === undefined) { 117 | doc.documentElement.dataset.myextensionId = "" 118 | await main() 119 | } 120 | }) 121 | -------------------------------------------------------------------------------- /src/messages/en.ts: -------------------------------------------------------------------------------- 1 | const messages = { 2 | "settings.test1": "Test1", 3 | "settings.title": "My Extension", 4 | "settings.information": "After changing the settings, reload the page to take effect", 5 | "settings.report": "Report and Issue...", 6 | } 7 | 8 | export default messages 9 | -------------------------------------------------------------------------------- /src/messages/index.ts: -------------------------------------------------------------------------------- 1 | import { initI18n } from "browser-extension-i18n" 2 | 3 | import messagesEn from "./en" 4 | import messagesZh from "./zh-cn" 5 | 6 | export const i = initI18n({ 7 | "en,en-US": messagesEn, 8 | "zh,zh-CN": messagesZh, 9 | }) 10 | -------------------------------------------------------------------------------- /src/messages/zh-cn.ts: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | "settings.test1": "测试1", 3 | "settings.title": "我的扩展", 4 | "settings.information": "更改设置后,重新加载页面即可生效", 5 | "settings.report": "反馈问题", 6 | } 7 | 8 | export default messages 9 | -------------------------------------------------------------------------------- /src/options.tsx: -------------------------------------------------------------------------------- 1 | function IndexOptions() { 2 | return ( 3 |
10 |

My Extension

11 | 17 |
18 | ) 19 | } 20 | 21 | export default IndexOptions 22 | -------------------------------------------------------------------------------- /src/popup.tsx: -------------------------------------------------------------------------------- 1 | function IndexPopup() { 2 | return ( 3 |
10 |

My Extension

11 | 17 |
18 | ) 19 | } 20 | 21 | export default IndexPopup 22 | -------------------------------------------------------------------------------- /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 | "./src/*" 15 | ] 16 | }, 17 | "strictNullChecks": true, 18 | "baseUrl": "." 19 | } 20 | } 21 | --------------------------------------------------------------------------------