├── .babelrc ├── .browserslistrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── jsconfig.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── src ├── components │ ├── button │ │ ├── index.scss │ │ └── index.tsx │ ├── checkbox │ │ ├── index.scss │ │ └── index.tsx │ ├── index.scss │ ├── index.ts │ └── input │ │ ├── index.scss │ │ └── index.tsx ├── composables │ ├── use-expose.ts │ └── use-gm-value.ts ├── directives │ ├── index.ts │ └── v-ripple │ │ ├── index.scss │ │ ├── index.ts │ │ └── utils.ts ├── global.d.ts ├── helpers │ └── toast.tsx ├── scripts-header │ ├── bilibili.js │ ├── dark-mode.js │ ├── element-ui.js │ ├── github.js │ ├── google-redirect.js │ ├── lanhu.js │ ├── mdn-web-docs.js │ ├── pixiv.js │ ├── redirect.js │ ├── tieba.js │ ├── view-ui.js │ └── widescreen.js ├── scripts │ ├── bilibili │ │ ├── index.ts │ │ └── speed.ts │ ├── dark-mode │ │ └── index.ts │ ├── element-ui │ │ ├── catalogue.scss │ │ ├── catalogue.tsx │ │ └── index.ts │ ├── github │ │ └── index.ts │ ├── google-redirect │ │ └── index.ts │ ├── lanhu │ │ ├── index.ts │ │ ├── password.ts │ │ ├── record.scss │ │ ├── record.tsx │ │ └── types.ts │ ├── mdn-web-docs │ │ ├── index.ts │ │ ├── style.ts │ │ └── utils.ts │ ├── pixiv │ │ ├── index.ts │ │ ├── pixels.ts │ │ └── previewer.ts │ ├── redirect │ │ ├── index.ts │ │ ├── sites │ │ │ ├── index.ts │ │ │ ├── mp-weixin-qq-com.ts │ │ │ ├── t-cn.ts │ │ │ ├── weixin110-qq-com.ts │ │ │ ├── www-360doc-com.ts │ │ │ └── www-pixiv-net.ts │ │ └── types.ts │ ├── tieba │ │ ├── api.ts │ │ ├── index.ts │ │ ├── sign.ts │ │ ├── store.ts │ │ ├── types.ts │ │ ├── ui │ │ │ ├── ForumList.tsx │ │ │ ├── index.scss │ │ │ └── index.tsx │ │ └── utils │ │ │ ├── index.ts │ │ │ ├── request.ts │ │ │ └── signature.ts │ ├── view-ui │ │ ├── hide.lazy.scss │ │ ├── index.ts │ │ ├── ui.scss │ │ └── ui.tsx │ └── widescreen │ │ ├── control.scss │ │ ├── control.tsx │ │ ├── index.ts │ │ ├── sites │ │ ├── bbs-mihoyo-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── bcy-net │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── blog-csdn-net │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── crates-io │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── d-weibo-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── jianshu-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── juejin-cn │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── mixins.scss │ │ ├── movie-douban-com │ │ │ ├── index.ts │ │ │ ├── review.ts │ │ │ ├── subject.lazy.scss │ │ │ └── subject.ts │ │ ├── mp-weixin-qq-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── segmentfault-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── space-bilibili-com │ │ │ └── index.ts │ │ ├── t-bilibili-com │ │ │ ├── detail.lazy.scss │ │ │ ├── detail.ts │ │ │ ├── index.lazy.scss │ │ │ ├── index.ts │ │ │ ├── mocha-official-gifts.lazy.scss │ │ │ └── mocha-official-gifts.ts │ │ ├── tieba-baidu-com │ │ │ ├── f.lazy.scss │ │ │ ├── f.ts │ │ │ ├── index.ts │ │ │ ├── p.lazy.scss │ │ │ └── p.ts │ │ ├── weibo-com │ │ │ ├── article.lazy.scss │ │ │ ├── article.ts │ │ │ ├── home.string.scss │ │ │ ├── index.ts │ │ │ └── play-detail.string.scss │ │ ├── www-baidu-com │ │ │ ├── index.string.scss │ │ │ └── index.ts │ │ ├── www-bilibili-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── www-douban-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── www-google-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── www-sogou-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── www-toutiao-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ ├── zhihu-com │ │ │ ├── home.lazy.scss │ │ │ ├── home.ts │ │ │ ├── index.ts │ │ │ ├── question.lazy.scss │ │ │ ├── question.ts │ │ │ ├── topic.lazy.scss │ │ │ └── topic.ts │ │ └── zhuanlan-zhihu-com │ │ │ ├── index.lazy.scss │ │ │ └── index.ts │ │ └── types.ts ├── store │ └── index.ts └── utils │ ├── base.ts │ ├── compatibility.ts │ ├── dom.ts │ ├── log.ts │ ├── mount-component.ts │ ├── querystring.ts │ ├── queue.ts │ ├── ready-state.ts │ ├── selector.ts │ ├── visibility-state.ts │ └── vue-root.ts ├── stylelint.config.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "assumptions": { 3 | // Babel 7.13.0 起取代了之前 plugins 中的 loose 选项 4 | "privateFieldsAsProperties": true 5 | }, 6 | // 逆序执行 7 | "presets": [ 8 | [ 9 | "@babel/preset-env", { 10 | // "debug": true, 11 | "modules": false, 12 | "useBuiltIns": "usage", 13 | "corejs": 3, 14 | "bugfixes": true, 15 | "exclude": [ 16 | "es.array.includes", 17 | "es.array.reduce", 18 | "es.regexp.constructor", 19 | "es.regexp.exec", 20 | "es.string.replace" 21 | ] 22 | } 23 | ], 24 | "@babel/preset-typescript" 25 | ], 26 | // 顺序执行 27 | "plugins": [ 28 | "@vue/babel-plugin-jsx", 29 | ["@babel/plugin-proposal-class-properties"], 30 | ["@babel/plugin-proposal-private-methods"] 31 | ] 32 | } -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | Firefox >= 75 2 | Edge >= 80 3 | Chrome >= 80 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'standard', 9 | 'plugin:vue/vue3-recommended', 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 12, 14 | sourceType: 'module', 15 | }, 16 | overrides: [ 17 | // 为了兼容原有js文件 18 | { 19 | files: ['*.ts', '*.tsx'], 20 | extends: [ 21 | 'plugin:@typescript-eslint/recommended', 22 | ], 23 | plugins: [ 24 | '@typescript-eslint', // 同 @typescript-eslint/eslint-plugin 25 | ], 26 | parserOptions: { 27 | project: './tsconfig.json', 28 | // ecmaFeatures: { 29 | // jsx: true, 30 | // }, 31 | }, 32 | rules: { 33 | '@typescript-eslint/no-explicit-any': 'off', 34 | '@typescript-eslint/no-non-null-assertion': 'off', 35 | '@typescript-eslint/ban-ts-comment': 'off', 36 | 'func-call-spacing': 'off', // 泛型多参数时空格有点问题 37 | }, 38 | }, 39 | ], 40 | rules: { 41 | 'comma-dangle': [ 42 | 'error', 43 | 'always-multiline', 44 | ], 45 | 'space-before-function-paren': [ 46 | 'error', 47 | { 48 | anonymous: 'never', 49 | named: 'never', 50 | asyncArrow: 'always', 51 | }, 52 | ], 53 | 'vue/one-component-per-file': 'off', 54 | 'vue/require-default-prop': 'off', 55 | }, 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | .idea 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tampermonkey-scripts 2 | 3 | Firefox version Firefox version Chrome version 4 | 5 | 仅在最新版上通过,如果使用其它浏览器**必须要保证 Chromium 版本 100 +** 6 | 7 | ## 使用 8 | 9 | 1. 先在浏览器商店安装 Tampermonkey 扩展(链接:[Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/tampermonkey/), [Edge](https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd), [Chrome](https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo)) 10 | 2. 然后在下面脚本中选择需要的进行安装 11 | 12 | ### 脚本 13 | 14 | 详细说明点击查看 15 | 16 | - [宽屏](https://greasyfork.org/zh-CN/scripts/411260-网页宽屏) 17 | - [tieba 签到](https://greasyfork.org/zh-CN/scripts/410874-百度贴吧签到) 18 | - [重定向](https://greasyfork.org/zh-CN/scripts/416338-redirect-外链跳转) 19 | - [Pixiv](https://greasyfork.org/zh-CN/scripts/419761-pixiv-工具箱) 20 | - [Github](https://greasyfork.org/zh-CN/scripts/423178-github-工具箱) 21 | - [蓝湖](https://greasyfork.org/zh-CN/scripts/411030-蓝湖-lanhu) 22 | - [MDN Web Dosc 文档辅助](https://greasyfork.org/zh-CN/scripts/420958-mdn-文档辅助) 23 | - [Element UI 文档辅助](https://greasyfork.org/zh-CN/scripts/418173-element-ui文档辅助) 24 | - [View UI 文档辅助](https://greasyfork.org/zh-CN/scripts/417770-view-ui文档辅助) 25 | 26 | 你可以在[这个分支](https://github.com/sakura-flutter/tampermonkey-scripts/tree/gh-pages)查看所有脚本 27 | 28 | ## 运行 29 | 30 | 建议在 [Node](https://nodejs.org/en/) >= 16 版本上进行 31 | 32 | ```bash 33 | npm install 34 | 35 | npm run serve 36 | // 或者(输出到dist目录) 37 | // npm run dev 38 | 39 | // 输出到dist目录 40 | npm run build 41 | ``` 42 | 43 | ## Thanks 44 | 45 | [![JetBrains](https://avatars0.githubusercontent.com/u/878437?s=120&v=4)](https://www.jetbrains.com/?from=tampermonkey-scripts) 46 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tampermonkey-scripts", 3 | "version": "1.0.0", 4 | "description": "TampermonkeyScripts", 5 | "scripts": { 6 | "serve": "webpack serve --mode=development", 7 | "dev": "webpack --mode=development --watch --progress", 8 | "build": "webpack --mode=production --progress", 9 | "predeploy": "npm run build", 10 | "deploy": "gh-pages -d ./dist", 11 | "lint": "eslint ./ --ext .js,.ts,.tsx", 12 | "stylelint": "stylelint **/*.scss", 13 | "prepare": "husky install" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/sakura-flutter/tampermonkey-scripts.git" 18 | }, 19 | "keywords": [ 20 | "tampermonkey" 21 | ], 22 | "author": "sakura-flutter", 23 | "license": "GPL-3.0", 24 | "bugs": { 25 | "url": "https://github.com/sakura-flutter/tampermonkey-scripts/issues" 26 | }, 27 | "homepage": "https://github.com/sakura-flutter/tampermonkey-scripts#readme", 28 | "dependencies": { 29 | "core-js": "^3.24.1" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.18.9", 33 | "@babel/plugin-proposal-class-properties": "^7.18.6", 34 | "@babel/plugin-proposal-private-methods": "^7.18.6", 35 | "@babel/preset-env": "^7.18.9", 36 | "@babel/preset-typescript": "^7.18.6", 37 | "@types/crypto-js": "^4.1.1", 38 | "@types/jquery": "^3.5.14", 39 | "@types/tampermonkey": "^4.0.5", 40 | "@typescript-eslint/eslint-plugin": "^5.31.0", 41 | "@typescript-eslint/parser": "^5.31.0", 42 | "@vue/babel-plugin-jsx": "^1.1.1", 43 | "autoprefixer": "^10.4.8", 44 | "babel-loader": "^8.2.5", 45 | "clean-webpack-plugin": "^4.0.0", 46 | "copy-webpack-plugin": "^11.0.0", 47 | "crypto-js": "^4.1.1", 48 | "css-loader": "^6.7.1", 49 | "eslint": "^8.20.0", 50 | "eslint-config-standard": "^17.0.0", 51 | "eslint-plugin-import": "^2.26.0", 52 | "eslint-plugin-node": "^11.1.0", 53 | "eslint-plugin-promise": "^6.0.0", 54 | "eslint-plugin-vue": "^9.3.0", 55 | "eslint-webpack-plugin": "^3.2.0", 56 | "gh-pages": "^4.0.0", 57 | "husky": "^8.0.1", 58 | "lint-staged": "^13.0.3", 59 | "postcss-loader": "^7.0.1", 60 | "sass": "^1.54.0", 61 | "sass-loader": "^13.0.2", 62 | "semver": "^7.3.7", 63 | "style-loader": "^3.3.1", 64 | "stylelint": "^14.9.1", 65 | "stylelint-config-sass-guidelines": "^9.0.1", 66 | "stylelint-config-standard": "^26.0.0", 67 | "stylelint-webpack-plugin": "^3.3.0", 68 | "terser-webpack-plugin": "^5.3.3", 69 | "typescript": "^4.7.4", 70 | "viewerjs": "^1.10.5", 71 | "vue": "^3.2.37", 72 | "webpack": "^5.74.0", 73 | "webpack-cli": "^4.10.0", 74 | "webpack-dev-server": "^4.9.3" 75 | }, 76 | "lint-staged": { 77 | "*.{js,ts,tsx}": [ 78 | "npm run lint" 79 | ], 80 | "*.{scss}": [ 81 | "npm run stylelint" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /src/components/button/index.scss: -------------------------------------------------------------------------------- 1 | .skr-button { 2 | border: 1px solid; 3 | border-radius: 2px; 4 | box-shadow: var(--skr-button-box-shadow); 5 | cursor: pointer; 6 | line-height: 1.5715; 7 | transition: var(--skr-button-transition); 8 | 9 | &:hover { 10 | filter: brightness(1.15); 11 | } 12 | 13 | &:focus:not(:focus-visible) { 14 | outline: 0; 15 | } 16 | 17 | &--primary { 18 | background-color: var(--skr-primary-color); 19 | border-color: var(--skr-primary-color); 20 | color: var(--skr-text-inverse-color); 21 | } 22 | 23 | &--default { 24 | background-color: var(--skr-white-color); 25 | border-color: var(--skr-border-color); 26 | color: var(--skr-text-primary-color); 27 | 28 | &:hover { 29 | border-color: currentcolor; 30 | color: var(--skr-primary-color); 31 | filter: brightness(1); 32 | } 33 | } 34 | 35 | &--round { 36 | border-radius: 50%; 37 | } 38 | 39 | &--shadow { 40 | box-shadow: var(--skr-box-shadow-normal); 41 | } 42 | 43 | &--mini { 44 | font-size: 12px; 45 | padding: 2px 7px; 46 | } 47 | 48 | &--small { 49 | font-size: 12px; 50 | padding: 4px 8px; 51 | } 52 | 53 | &--normal { 54 | font-size: 14px; 55 | padding: 4px 15px; 56 | } 57 | 58 | &--large { 59 | font-size: 15px; 60 | padding: 10px 20px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed, type PropType } from 'vue' 2 | import { vRipple, type RippleOptions } from '@/directives' 3 | import './index.scss' 4 | 5 | const prefixCls = 'skr-button' 6 | // button type 非 default 时覆盖一层白色 7 | const rippleColor = 'rgb(255 255 255 / 15%)' 8 | 9 | const Button = defineComponent({ 10 | name: 'SkrButton', 11 | directives: { 12 | ripple: vRipple, 13 | }, 14 | props: { 15 | type: { 16 | type: String as PropType<'primary' | 'info' | 'warning' | 'danger' | 'default'>, 17 | validator: (value: string) => ['primary', 'info', 'warning', 'danger', 'default'].includes(value), 18 | default: 'default', 19 | }, 20 | plain: { 21 | type: Boolean, 22 | default: false, 23 | }, 24 | round: { 25 | type: Boolean, 26 | default: false, 27 | }, 28 | shadow: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | size: { 33 | type: String as PropType<'mini' | 'small' | 'normal' | 'large'>, 34 | validator: (value: string) => ['mini', 'small', 'normal', 'large'].includes(value), 35 | default: 'normal', 36 | }, 37 | // 涟漪效果 object 时参数会透传给 ripple 38 | ripple: { 39 | type: [Boolean, Object] as PropType, 40 | default: true, 41 | }, 42 | }, 43 | setup(props, { slots }) { 44 | const rippleOptions = computed(() => { 45 | return Object.assign( 46 | {}, { 47 | color: props.type === 'default' ? undefined : rippleColor, 48 | }, 49 | typeof props.ripple === 'boolean' ? { disabled: !props.ripple } : props.ripple, 50 | ) 51 | }) 52 | 53 | return () => ( 54 | 68 | ) 69 | }, 70 | }) 71 | 72 | export default Button 73 | -------------------------------------------------------------------------------- /src/components/checkbox/index.scss: -------------------------------------------------------------------------------- 1 | .skr-checkbox { 2 | cursor: pointer; 3 | height: 20px; 4 | margin-left: 8px; 5 | text-shadow: 0 1px 3px #fff; 6 | 7 | input { 8 | margin-right: 4px; 9 | vertical-align: text-top; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref } from 'vue' 2 | import './index.scss' 3 | 4 | const prefixCls = 'skr-checkbox' 5 | 6 | const Checkbox = defineComponent({ 7 | name: 'SkrCheckbox', 8 | props: { 9 | checked: { 10 | type: Boolean, 11 | required: true, 12 | }, 13 | title: String, 14 | disabled: Boolean, 15 | }, 16 | emits: [ 17 | 'update:checked', 18 | ], 19 | setup(props, { slots, emit }) { 20 | const inputRef = ref() 21 | const handleChange = (event: Event) => { 22 | emit('update:checked', (event.target as HTMLInputElement).checked) 23 | // 受控 24 | inputRef.value!.checked = !!props.checked 25 | } 26 | 27 | return () => ( 28 | 38 | ) 39 | }, 40 | }) 41 | 42 | export default Checkbox 43 | -------------------------------------------------------------------------------- /src/components/index.scss: -------------------------------------------------------------------------------- 1 | /* var */ 2 | @mixin var($selector: ':root') { 3 | #{$selector} { 4 | --skr-primary-color: #2878ff; 5 | --skr-primary-lighten-color: rgb(24 144 255 / 20%); 6 | --skr-white-color: #fff; 7 | 8 | /* transition */ 9 | --skr-transition-duration-fast: 0.1s; 10 | --skr-transition-duration-normal: 0.3s; 11 | 12 | /* shadow */ 13 | --skr-box-shadow-lighten: 0 1px 6px rgb(0 0 0 / 15%); 14 | --skr-box-shadow-normal: 0 1px 6px rgb(0 0 0 / 20%); 15 | 16 | /* border */ 17 | --skr-border-color: #d9d9d9; 18 | 19 | /* text */ 20 | --skr-text-primary-color: #303133; 21 | --skr-text-regular-color: #666; 22 | --skr-text-secondary-color: #909399; 23 | --skr-text-inverse-color: var(--skr-white-color); 24 | 25 | /* button */ 26 | --skr-button-transition: all var(--skr-transition-duration-normal); 27 | --skr-button-box-shadow: 0 2px 0 rgb(0 0 0 / 4.5%); 28 | 29 | /* ripple */ 30 | --skr-ripple-color: rgb(138 218 255 / 20%); 31 | } 32 | } 33 | 34 | /* reset */ 35 | @mixin reset { 36 | [class*='skr-'] { 37 | box-sizing: border-box; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Input } from './input' 2 | export { default as Button } from './button' 3 | export { default as Checkbox } from './checkbox' 4 | -------------------------------------------------------------------------------- /src/components/input/index.scss: -------------------------------------------------------------------------------- 1 | .skr-input { 2 | border: 1px solid #d9d9d9; 3 | margin-top: 5px; 4 | transition: all 0.3s; 5 | width: 100%; 6 | padding: { 7 | left: 8px; 8 | right: 8px; 9 | } 10 | 11 | &:hover { 12 | border-color: var(--skr-primary-color); 13 | } 14 | 15 | &:focus { 16 | @extend:hover; 17 | 18 | box-shadow: 0 0 0 2px var(--skr-primary-lighten-color); 19 | } 20 | 21 | &--small { 22 | padding: { 23 | bottom: 2px; 24 | top: 2px; 25 | } 26 | } 27 | &--small#{& + "--scale"} { 28 | &:focus { 29 | font-size: 14px; 30 | padding-bottom: 6px; 31 | padding-top: 6px; 32 | } 33 | } 34 | 35 | &--normal { 36 | padding: { 37 | bottom: 6px; 38 | top: 6px; 39 | } 40 | } 41 | 42 | &--large { 43 | padding: { 44 | bottom: 10px; 45 | top: 10px; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/input/index.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, type PropType } from 'vue' 2 | import './index.scss' 3 | 4 | const prefixCls = 'skr-input' 5 | 6 | const Input = defineComponent({ 7 | name: 'SkrInput', 8 | props: { 9 | modelValue: { 10 | type: [String, Number], 11 | default: '', 12 | }, 13 | size: { 14 | type: String as PropType<'small' | 'normal' | 'large'>, 15 | validator: (value: string) => ['small', 'normal', 'large'].includes(value), 16 | default: 'normal', 17 | }, 18 | scale: { 19 | type: Boolean, 20 | default: false, 21 | }, 22 | }, 23 | emits: [ 24 | 'update:modelValue', 25 | ], 26 | setup(props, { emit }) { 27 | const handleInput = (event: Event) => { 28 | // vue 自带的 29 | if (!(event.target as any).composing) { 30 | emit('update:modelValue', (event.target as HTMLInputElement).value) 31 | } 32 | } 33 | 34 | return () => ( 35 | 45 | ) 46 | }, 47 | }) 48 | 49 | export default Input 50 | -------------------------------------------------------------------------------- /src/composables/use-expose.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 引用:https://github.com/youzan/vant/blob/dev/src/composables/use-expose.ts 3 | */ 4 | 5 | import { getCurrentInstance } from 'vue' 6 | 7 | /** 8 | * expose public api 9 | * @deprecated vue3.2 已经支持 10 | */ 11 | export function useExpose(apis: Record) { 12 | const instance = getCurrentInstance() 13 | if (instance) { 14 | Object.assign(instance.proxy!, apis) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/composables/use-gm-value.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted, ref, watch } from 'vue' 2 | 3 | /** 4 | * 同 GM_getValue、GM_setValue 5 | */ 6 | export function useGMvalue(name: string, defaultValue: T, _options?: boolean | { 7 | /** 用于页面间同步 */ 8 | listening?: boolean 9 | /** vue watch.deep */ 10 | deep?: boolean 11 | }) { 12 | const { listening, deep } = Object.assign({ 13 | listening: typeof _options === 'boolean' ? _options : true, 14 | deep: false, 15 | }, _options) 16 | 17 | const value = ref(GM_getValue(name, defaultValue)) 18 | watch(value, () => { GM_setValue(name, value.value) }, { deep }) 19 | 20 | if (listening) { 21 | onUnmounted(() => { 22 | GM_removeValueChangeListener(id) 23 | }) 24 | const id = GM_addValueChangeListener(name, (name, oldVal, newVal) => { 25 | value.value = newVal 26 | }) 27 | } 28 | 29 | return value 30 | } 31 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | export * from './v-ripple' 2 | export { default as vRipple } from './v-ripple' 3 | -------------------------------------------------------------------------------- /src/directives/v-ripple/index.scss: -------------------------------------------------------------------------------- 1 | $prefix-cls: '.skr-ripple'; 2 | 3 | #{$prefix-cls}-container { 4 | border-radius: inherit !important; 5 | bottom: 0; 6 | contain: strict; 7 | left: 0; 8 | margin: 0 !important; 9 | overflow: hidden; 10 | padding: 0 !important; 11 | pointer-events: none !important; 12 | position: absolute; 13 | right: 0; 14 | top: 0; 15 | } 16 | 17 | #{$prefix-cls} { 18 | animation: skr-ripple forwards cubic-bezier(0.23, 1, 0.32, 1); 19 | background: var(--skr-ripple-color); 20 | border-radius: 100%; 21 | contain: layout; 22 | margin: 0 !important; 23 | padding: 0 !important; 24 | pointer-events: none; 25 | position: absolute; 26 | transform: scale(0); 27 | transition: opacity 2s cubic-bezier(0.23, 1, 0.32, 1); // 时长不能短于animation的时长 28 | } 29 | 30 | @keyframes skr-ripple { 31 | to { 32 | // 保证对角线完全能覆盖到 33 | transform: scale(3); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/directives/v-ripple/index.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectDirective } from 'vue' 2 | import { calcDiagInRect } from './utils' 3 | import './index.scss' 4 | 5 | const containerClassname = 'skr-ripple-container' 6 | const rippleClassname = 'skr-ripple' 7 | const weakmap = new WeakMap() 8 | 9 | export interface ObjectRippleOptions { 10 | disabled?: boolean 11 | color?: string 12 | } 13 | 14 | export type RippleOptions = Required['disabled'] | ObjectRippleOptions 15 | 16 | /** 17 | * 创建容器元素 18 | */ 19 | function createRippleContainer() { 20 | const div = document.createElement('div') 21 | div.classList.add(containerClassname) 22 | return div 23 | } 24 | /** 25 | * 创建涟漪元素 26 | */ 27 | function createRippleEl() { 28 | const span = document.createElement('div') 29 | span.classList.add(rippleClassname) 30 | return span 31 | } 32 | function normalizeOptions(options: RippleOptions) { 33 | if (typeof options === 'boolean') { 34 | return { 35 | disabled: !options, 36 | } 37 | } 38 | return options 39 | } 40 | 41 | /** 42 | * 添加涟漪效果 43 | */ 44 | const addRippleEffect = function(_options: RippleOptions = {}) { 45 | let options = normalizeOptions(_options) 46 | // 涟漪个数 47 | let count = 0 48 | 49 | function listener(event: MouseEvent) { 50 | if (options.disabled) return 51 | const currentTarget = event.currentTarget as HTMLElement 52 | 53 | // 优化: 处理过后不再调用getComputedStyle 54 | if (weakmap.get(currentTarget).position === false) { 55 | weakmap.get(currentTarget).position = true 56 | // 注意:会改变当前元素定位方式 57 | if (getComputedStyle(currentTarget).position === 'static') { 58 | currentTarget.style.position = 'relative' 59 | } 60 | } 61 | 62 | const rect = currentTarget.getBoundingClientRect() 63 | const rippleEl = createRippleEl() 64 | // 取元素长的一边作为涟漪的周长 65 | const side = Math.max(rect.width, rect.height) 66 | const radius = side / 2 67 | // 鼠标在元素中的坐标 68 | const left = event.pageX - rect.left - window.scrollX 69 | const top = event.pageY - rect.top - window.scrollY 70 | 71 | // 选项加入到元素中 72 | options.color && (rippleEl.style.background = options.color) 73 | rippleEl.style.width = side + 'px' 74 | rippleEl.style.height = side + 'px' 75 | // 元素定位再各减自身的宽高一半 76 | rippleEl.style.top = top - radius + 'px' 77 | rippleEl.style.left = left - radius + 'px' 78 | // 动画在元素中间扩散时基础时长1.5s,当点击范围处于元素边缘时,动画扩散比在元素中间位置要长,所以要加快动画进行 79 | const base = 1.5 80 | const diagonal = calcDiagInRect(rect.width, rect.height)(left, top) 81 | rippleEl.style.animationDuration = base - base * diagonal / side + 's' 82 | 83 | let container = currentTarget.querySelector(`.${containerClassname}`) 84 | if (!container) { 85 | container = createRippleContainer() 86 | currentTarget.appendChild(container) 87 | } 88 | container.appendChild(rippleEl) 89 | count++ 90 | 91 | const unlisten = (() => { 92 | const leaveEvents = ['mouseup', 'mouseleave'] 93 | const listener = () => { 94 | // 为了尽量能看清动画效果,延时一下再进行透明 95 | setTimeout(() => { 96 | rippleEl.style.opacity = '0' 97 | }, 100) 98 | } 99 | leaveEvents.forEach(eventname => currentTarget.addEventListener(eventname, listener)) 100 | 101 | return () => { 102 | leaveEvents.forEach(eventname => currentTarget.removeEventListener(eventname, listener)) 103 | } 104 | })() 105 | 106 | // 移除涟漪元素 107 | rippleEl.addEventListener('transitionend', transEvent => { 108 | if (transEvent.propertyName === 'opacity') { 109 | unlisten() 110 | rippleEl.remove() 111 | // 没有涟漪元素时移除容器 112 | if (--count <= 0) { 113 | container?.remove() 114 | } 115 | } 116 | }) 117 | } 118 | 119 | // 更新配置项 120 | function update(newOpts: RippleOptions) { 121 | options = Object.assign({}, options, normalizeOptions(newOpts)) 122 | } 123 | 124 | return { 125 | listener, 126 | update, 127 | } 128 | } 129 | 130 | const vRipple: ObjectDirective = { 131 | mounted(el, binding) { 132 | const { listener, update } = addRippleEffect(binding.value) 133 | weakmap.set(el, { 134 | listener, 135 | update, // 更新配置项函数 136 | position: false, // 是否已经改变了 el 的定位方式 137 | }) 138 | el.addEventListener('mousedown', listener, false) 139 | }, 140 | updated(el, binding) { 141 | const val = weakmap.get(el) 142 | val.update(binding.value) 143 | }, 144 | } 145 | 146 | export default vRipple 147 | -------------------------------------------------------------------------------- /src/directives/v-ripple/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 计算一个点离矩形中心点的距离 3 | * @param width 矩形宽 4 | * @param height 矩形高 5 | * @return (left top 在矩形内点的坐标) => {} => () => {} 距离 6 | */ 7 | export function calcDiagInRect(width: number, height: number) { 8 | const halfWidth = width / 2 9 | const halfHeight = height / 2 10 | 11 | return function(left: number, top: number) { 12 | const a = left <= halfWidth 13 | ? halfWidth - left 14 | : left - halfWidth 15 | const b = top <= halfHeight 16 | ? halfHeight - top 17 | : top - halfHeight 18 | const c = Math.sqrt((a * a) + (b * b)) 19 | return c 20 | } 21 | } 22 | 23 | /** 24 | * 计算当前值离总值中心的位置 越靠近中心值为1,远离中心值为0 25 | * @param value 当前值 26 | * @param extent 总值 27 | * @return 取值 0-1 28 | * @example value:50 extent:100 则计算 50 在 0-100 中的位置返回 1 29 | * value:0 或 100 extent:100 返回 0 30 | */ 31 | export function closeness(value: number, extent: number) { 32 | if (!value || !extent) return 0 33 | 34 | const half = extent / 2 35 | return value <= half 36 | ? value / half 37 | : 1 - value / extent 38 | } 39 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | import { type ToastApi } from '@/helpers/toast' 2 | 3 | type CSSLazyClasses = { 4 | readonly use(): void 5 | readonly unuse(): void 6 | } 7 | 8 | declare global { 9 | interface Window { 10 | Toast: ToastApi 11 | } 12 | 13 | const Toast: ToastApi 14 | 15 | module '*.lazy.scss' { 16 | const classes: CSSLazyClasses 17 | export default classes 18 | } 19 | module '*.scss' { 20 | const css: string 21 | export default css 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/helpers/toast.tsx: -------------------------------------------------------------------------------- 1 | /* Toast */ 2 | 3 | import { createVNode, defineComponent, render, isVNode, onMounted, reactive, Transition } from 'vue' 4 | import type { VNode, PropType } from 'vue' 5 | 6 | const toastTypes = ['info', 'success', 'warning', 'error'] as const 7 | 8 | export type ObjectToastOptions = { 9 | content: string | VNode 10 | type?: typeof toastTypes[number] 11 | closable?: boolean 12 | /** 默认 3s,0 时不会自动关闭 */ 13 | duration?: number 14 | } 15 | 16 | export type ToastOptions = ObjectToastOptions['content'] | ObjectToastOptions 17 | 18 | function normalizeOptions(options: ToastOptions, duration: ObjectToastOptions['duration']) { 19 | if (typeof options === 'string' || isVNode(options)) { 20 | options = { content: options } 21 | } 22 | options.duration = duration ?? options.duration 23 | return options 24 | } 25 | 26 | const Toast = function(_opts: ToastOptions, duration?: ObjectToastOptions['duration']) { 27 | const options = normalizeOptions(_opts, duration) 28 | 29 | const container = document.createElement('div') 30 | const ToastConstructor = defineComponent({ 31 | props: { 32 | content: { 33 | type: [String, Object] as PropType, 34 | default: '', 35 | }, 36 | type: { 37 | type: String as PropType, 38 | validator: (value: any) => toastTypes.includes(value), 39 | default: 'info', 40 | }, 41 | closable: { 42 | type: Boolean as PropType, 43 | default: null, 44 | }, 45 | duration: { 46 | type: Number as PropType['duration']>, 47 | default: 3000, 48 | }, 49 | }, 50 | setup(props, context) { 51 | const { expose } = context 52 | const state = reactive({ 53 | closable: (props.duration === 0 && props.closable == null) ? true : props.closable, // 0 时 closable 默认打开 54 | visible: false, 55 | }) 56 | 57 | onMounted(() => { 58 | state.visible = true 59 | if (props.duration > 0) { 60 | setTimeout(close, props.duration) 61 | } 62 | }) 63 | 64 | const close = () => { 65 | state.visible = false 66 | } 67 | 68 | const onAfterLeave = () => { 69 | // 销毁 70 | render(null, container) 71 | container.remove() 72 | } 73 | 74 | expose({ 75 | close, 76 | }) 77 | 78 | return () => ( 79 | 80 | {state.visible && ( 81 |
82 |
83 |
{props.content}
84 | {state.closable && } 85 |
86 |
87 | )} 88 |
89 | ) 90 | }, 91 | }) 92 | 93 | // toast 94 | const vm = createVNode(ToastConstructor, options) 95 | render(vm, container) 96 | insertElementInContainer(container) 97 | 98 | return { 99 | close: vm.component?.exposed?.close, 100 | } 101 | } 102 | 103 | toastTypes.forEach(type => { 104 | (Toast as any)[type] = function(_opts: ToastOptions, duration?: ObjectToastOptions['duration']) { 105 | const options = { 106 | ...normalizeOptions(_opts, duration), 107 | type, 108 | } 109 | return Toast(options, duration) 110 | } 111 | }) 112 | 113 | export type ToastApi = { 114 | info: typeof Toast 115 | success: typeof Toast 116 | warning: typeof Toast 117 | error: typeof Toast 118 | } & typeof Toast 119 | 120 | window.Toast = Toast as ToastApi 121 | 122 | function safeAppendElement(cb: () => void) { 123 | document.body ? cb() : window.addEventListener('DOMContentLoaded', cb) 124 | } 125 | 126 | function insertElementInContainer(elememnt: Element) { 127 | function getContainer() { 128 | const classname = 'inject-toast-container' 129 | let containerEl = document.querySelector('.' + classname) 130 | if (containerEl == null) { 131 | containerEl = document.createElement('div') 132 | containerEl.classList.add(classname) 133 | document.body.appendChild(containerEl) 134 | } 135 | return containerEl 136 | } 137 | safeAppendElement(() => { 138 | getContainer().appendChild(elememnt) 139 | }) 140 | } 141 | 142 | (function addStyle() { 143 | const styleEl = document.createElement('style') 144 | styleEl.appendChild(document.createTextNode(` 145 | .inject-toast-container { 146 | position: fixed; 147 | z-index: 99999; 148 | top: 80px; 149 | right: 0; 150 | left: 0; 151 | pointer-events: none; 152 | text-align: center; 153 | } 154 | .inject-toast { 155 | contain: content; 156 | max-height: 100vh; 157 | transition: all .3s ease-in-out; 158 | } 159 | |> { 160 | pointer-events: auto; 161 | display: inline-flex; 162 | justify-content: center; 163 | margin-bottom: 10px; 164 | padding: 8px 16px; 165 | max-width: 90vw; 166 | font-size: 14px; 167 | line-height: 1.5em; 168 | border: 1px solid; 169 | box-shadow: 0 2px 3px rgba(0,0,0,.1); 170 | } 171 | |>--info { 172 | color: #2e8bf0; 173 | background: #f0faff; 174 | border-color: #d4eeff; 175 | } 176 | |>--success { 177 | color: #19bf6c; 178 | background: #edfff3; 179 | border-color: #bbf2cf; 180 | } 181 | |>--warning { 182 | color: #f90; 183 | background: #fff9e6; 184 | border-color: #ffe7a3; 185 | } 186 | |>--error { 187 | color: #ed3f13; 188 | background: #ffefe6; 189 | border-color: #ffcfb8; 190 | } 191 | |>-text { 192 | flex: auto; 193 | } 194 | |>-close { 195 | flex: none; 196 | width: 20px; 197 | margin: 0 -8px 0 10px; 198 | padding: 0; 199 | font-size: 16px; 200 | color: #ababab; 201 | border: none; 202 | background: transparent; 203 | cursor: pointer; 204 | } 205 | 206 | /* 动画 */ 207 | .inject-toast-slide-fade-enter-active, 208 | .inject-toast-slide-fade-leave-active { 209 | transition: all .3s; 210 | } 211 | .inject-toast-slide-fade-enter-from { 212 | transform: translateY(-50%); 213 | opacity: 0; 214 | } 215 | .inject-toast-slide-fade-leave-to { 216 | transform: translateY(50%); 217 | max-height: 0; 218 | padding: 0; 219 | opacity: 0; 220 | } 221 | `.replace(/\|>/g, '.inject-toast-content'))) 222 | 223 | // fix: tampermonkey 偶尔会获取不到 head 224 | safeAppendElement(() => { 225 | document.head.appendChild(styleEl) 226 | }) 227 | })() 228 | -------------------------------------------------------------------------------- /src/scripts-header/bilibili.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name bilibili 工具箱 4 | // @version 1.5.0 5 | // @description 长按 S 键倍速播放 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @noframes 13 | // @match https://www.bilibili.com/video/* 14 | // @match https://www.bilibili.com/bangumi/play/* 15 | // ==/UserScript== 16 | ` 17 | -------------------------------------------------------------------------------- /src/scripts-header/dark-mode.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name Dark Mode 暗黑模式 4 | // @version 0.0.1 5 | // @description 将网页变更为暗黑显示,不适合有背景图的网站 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license MIT 9 | // @compatible chrome >= Latest 10 | // @compatible firefox >= Latest 11 | // @run-at document-start 12 | // @match *://*/* 13 | // @grant GM_addStyle 14 | // ==/UserScript== 15 | ` 16 | -------------------------------------------------------------------------------- /src/scripts-header/element-ui.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name Element UI文档辅助 4 | // @version 1.0.4 5 | // @description 在Element UI文档中增加示例目录导航,同时支持v2与v3(element-plus)版本,类似于Ant右侧悬浮的导航 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @match https://element-plus.gitee.io/* 13 | // @match https://element-plus.org/* 14 | // @match https://element.eleme.cn/* 15 | // @match https://element.eleme.io/* 16 | // @require https://unpkg.com/vue@3/dist/vue.runtime.global${isProd ? '.prod.min' : ''}.js 17 | // ==/UserScript== 18 | ` 19 | -------------------------------------------------------------------------------- /src/scripts-header/github.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name GitHub 工具箱 4 | // @name:en GitHub ToolBox 5 | // @version 1.0.0 6 | // @description 添加用VS Code阅读代码按钮(github1s) 7 | // @description:en Read code with VS Code(github1s) 8 | // @author sakura-flutter 9 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 10 | // @license MIT 11 | // @compatible chrome Latest 12 | // @compatible firefox Latest 13 | // @compatible edge Latest 14 | // @noframes 15 | // @grant window.onurlchange 16 | // @match https://github.com/* 17 | // ==/UserScript== 18 | ` 19 | -------------------------------------------------------------------------------- /src/scripts-header/google-redirect.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name 谷歌重定向 4 | // @version 1.0.0 5 | // @description hk -> jp 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @run-at document-start 13 | // @noframes 14 | // @match https://www.google.com.hk/search* 15 | // ==/UserScript== 16 | ` 17 | -------------------------------------------------------------------------------- /src/scripts-header/lanhu.js: -------------------------------------------------------------------------------- 1 | module.exports = (isProd, depsVersion) => 2 | `// ==UserScript== 3 | // @name 蓝湖 工具箱 4 | // @version 1.12.1 5 | // @description 自动填充填写过的产品密码(不是蓝湖账户);快捷查看打开过的项目 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @noframes 13 | // @match https://lanhuapp.com/web/ 14 | // @grant GM_registerMenuCommand 15 | // @grant GM_setValue 16 | // @grant GM_getValue 17 | // @grant GM_addValueChangeListener 18 | // @grant GM_addStyle 19 | // @grant GM_setClipboard 20 | // @require https://unpkg.com/vue@${depsVersion.vue}/dist/vue.runtime.global${isProd ? '.prod' : ''}.js 21 | // @require https://greasyfork.org/scripts/411093-toast/code/Toast.js?version=1081231 22 | // ==/UserScript== 23 | ` 24 | -------------------------------------------------------------------------------- /src/scripts-header/mdn-web-docs.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name MDN 文档辅助 4 | // @version 2.3.0 5 | // @description 在提供中文语言的页面自动切换为中文 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @noframes 13 | // @grant window.onurlchange 14 | // @match https://developer.mozilla.org/* 15 | // ==/UserScript== 16 | ` 17 | -------------------------------------------------------------------------------- /src/scripts-header/pixiv.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name Pixiv 工具箱 4 | // @version 1.4.1 5 | // @description 增强P站查看原图功能;显示原图尺寸 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @noframes 13 | // @match https://www.pixiv.net 14 | // @match https://www.pixiv.net/* 15 | // @grant window.onurlchange 16 | // @grant GM_getResourceText 17 | // @grant GM_addStyle 18 | // @resource viewerCSS https://unpkg.com/viewerjs@1/dist/viewer${isProd ? '.min' : ''}.css 19 | // @require https://unpkg.com/viewerjs@1/dist/viewer${isProd ? '.min' : ''}.js 20 | // ==/UserScript== 21 | ` 22 | -------------------------------------------------------------------------------- /src/scripts-header/redirect.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name redirect 外链跳转 4 | // @version 1.61.0 5 | // @description 自动跳转(重定向)到目标链接,免去点击步骤。适配了简书、知乎、微博、QQ邮箱、QQPC、QQNT、印象笔记、贴吧、CSDN、YouTube、微信、企业微信、微信开放社区、开发者知识库、豆瓣、个人图书馆、Pixiv、搜狗、Google、站长之家、OSCHINA、掘金、腾讯文档、pc6下载站、爱发电、Gitee、天眼查、爱企查、企查查、优设网、51CTO、力扣、花瓣网、飞书、Epic、Steam、语雀、牛客网、哔哩哔哩、少数派、5ch、金山文档、石墨文档、urlshare、酷安、网盘分享、腾讯云开发者社区、腾讯兔小巢、云栖社区、NodeSeek、亿企查、异次元软件、HelloGitHub、知更鸟、巴哈姆特 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @run-at document-start 13 | // @grant unsafeWindow 14 | // @match *://www.jianshu.com/go-wild* 15 | // @match *://link.zhihu.com/* 16 | // @match *://t.cn/* 17 | // @match *://weibo.cn/sinaurl* 18 | // @match *://mail.qq.com/cgi-bin/* 19 | // @match *://wx.mail.qq.com/xmspamcheck/xmsafejump* 20 | // @match *://c.pc.qq.com/middlem.html* 21 | // @match *://c.pc.qq.com/middlect.html* 22 | // @match *://c.pc.qq.com/pc.html* 23 | // @match *://c.pc.qq.com/ios.html* 24 | // @match *://c.pc.qq.com/android.html* 25 | // @match *://app.yinxiang.com/OutboundRedirect.action* 26 | // @match *://jump.bdimg.com/safecheck/* 27 | // @match *://jump2.bdimg.com/safecheck/* 28 | // @match *://link.csdn.net/* 29 | // @match *://www.youtube.com/redirect* 30 | // @match *://mp.weixin.qq.com/s/* 31 | // @match *://weixin110.qq.com/cgi-bin/mmspamsupport-bin/newredirectconfirmcgi* 32 | // @match *://open.work.weixin.qq.com/wwopen/uriconfirm* 33 | // @match *://developers.weixin.qq.com/community/middlepage/href* 34 | // @match *://www.itdaan.com/link/* 35 | // @match *://www.douban.com/link2/* 36 | // @match *://www.360doc.com/content/* 37 | // @match *://www.pixiv.net/jump.php* 38 | // @match *://m.sogou.com/*/tc* 39 | // @match *://m.sogou.com*/tc* 40 | // @match *://www.chinaz.com/go.shtml* 41 | // @match *://www.oschina.net/action/GoToLink* 42 | // @match *://link.juejin.cn/* 43 | // @match *://docs.qq.com/scenario/link.html* 44 | // @match *://www.pc6.com/goread.html* 45 | // @match *://afdian.net/link* 46 | // @match *://afdian.com/link* 47 | // @match *://ifdian.net/link* 48 | // @match *://gitee.com/link* 49 | // @match *://www.tianyancha.com/security* 50 | // @match *://aiqicha.baidu.com/safetip* 51 | // @match *://www.qcc.com/web/transfer-link* 52 | // @match *://link.uisdc.com/* 53 | // @match *://blog.51cto.com/transfer* 54 | // @match *://leetcode.cn/link* 55 | // @match *://huaban.com/go* 56 | // @match *://security.feishu.cn/link/safety* 57 | // @match *://redirect.epicgames.com/* 58 | // @match *://steamcommunity.com/linkfilter/* 59 | // @match *://*.yuque.com/r/goto* 60 | // @match *://hd.nowcoder.com/link.html* 61 | // @match *://game.bilibili.com/linkfilter/* 62 | // @match *://sspai.com/link* 63 | // @match *://niu.sspai.com/link* 64 | // @match *://jump.5ch.net/* 65 | // @match *://www.kdocs.cn/office/link* 66 | // @match *://shimo.im/outlink/black* 67 | // @match *://google.urlshare.cn/umirror_url_check* 68 | // @match *://www.coolapk.com/link* 69 | // @match *://wpfx.org/go* 70 | // @match *://cloud.tencent.com/developer/tools/blog-entry* 71 | // @match *://support.qq.com/products/*/link-jump* 72 | // @match *://txc.qq.com/products/*/link-jump* 73 | // @match *://yq.aliyun.com/go/articleRenderRedirect* 74 | // @match *://www.nodeseek.com/jump* 75 | // @match *://www.yiqicha.com/thirdPage* 76 | // @match *://www.iplaysoft.com/link* 77 | // @match *://hellogithub.com/periodical/statistics/click* 78 | // @match *://zmingcx.com/go.html* 79 | // @match *://ref.gamer.com.tw/redir.php* 80 | // @include ${/^https?:\/\/www\.google\..{2,7}url/} 81 | // ==/UserScript== 82 | ` 83 | -------------------------------------------------------------------------------- /src/scripts-header/tieba.js: -------------------------------------------------------------------------------- 1 | module.exports = (isProd, depsVersion) => 2 | `// ==UserScript== 3 | // @name 百度贴吧签到 4 | // @version 3.4.4 5 | // @description 网页版签到或模拟客户端签到,模拟客户端可获得与客户端相同经验并且签到速度更快~ 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @run-at document-end 13 | // @match https://tieba.baidu.com/index.html 14 | // @match https://tieba.baidu.com/ 15 | // @connect tieba.baidu.com 16 | // @grant unsafeWindow 17 | // @grant GM_xmlhttpRequest 18 | // @grant GM_setValue 19 | // @grant GM_getValue 20 | // @grant GM_deleteValue 21 | // @grant GM_addValueChangeListener 22 | // @grant GM_removeValueChangeListener 23 | // @grant GM_addStyle 24 | // @require https://unpkg.com/crypto-js@${depsVersion['crypto-js']}/core.js 25 | // @require https://unpkg.com/crypto-js@${depsVersion['crypto-js']}/md5.js 26 | // @require https://unpkg.com/vue@${depsVersion.vue}/dist/vue.runtime.global${isProd ? '.prod' : ''}.js 27 | // @require https://greasyfork.org/scripts/411093-toast/code/Toast.js?version=1081231 28 | // ==/UserScript== 29 | ` 30 | -------------------------------------------------------------------------------- /src/scripts-header/view-ui.js: -------------------------------------------------------------------------------- 1 | module.exports = isProd => 2 | `// ==UserScript== 3 | // @name View UI v4 文档辅助 4 | // @version 1.0.5 5 | // @description (原iView)隐藏文档中菜单项:Pro、物料 6 | // @author sakura-flutter 7 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 8 | // @license GPL-3.0 9 | // @compatible chrome Latest 10 | // @compatible firefox Latest 11 | // @compatible edge Latest 12 | // @match *://v4.iviewui.com/* 13 | // @grant GM_setValue 14 | // @grant GM_getValue 15 | // @grant GM_addValueChangeListener 16 | // @grant GM_removeValueChangeListener 17 | // @require https://unpkg.com/vue@3/dist/vue.runtime.global${isProd ? '.prod.min' : ''}.js 18 | // ==/UserScript== 19 | ` 20 | -------------------------------------------------------------------------------- /src/scripts-header/widescreen.js: -------------------------------------------------------------------------------- 1 | /* @include 使用的是正则,模板里面用变量,避免变成字符串 */ 2 | 3 | module.exports = (isProd, depsVersion) => 4 | `// ==UserScript== 5 | // @name 网页宽屏 6 | // @version 2.15.16 7 | // @description 适配了半次元、微信公众号、知乎、掘金、简书、贴吧、百度搜索、搜狗搜索、segmentfault、哔哩哔哩、微博、豆瓣、今日头条、Google、CSDN、crates.io、米游社原神 8 | // @author sakura-flutter 9 | // @namespace https://github.com/sakura-flutter/tampermonkey-scripts 10 | // @license GPL-3.0 11 | // @compatible chrome Latest 12 | // @compatible firefox Latest 13 | // @compatible edge Latest 14 | // @run-at document-start 15 | // @noframes 16 | // @match https://bcy.net/item/detail/* 17 | // @match https://mp.weixin.qq.com/s* 18 | // @match https://zhuanlan.zhihu.com/p/* 19 | // @match https://www.zhihu.com/question/* 20 | // @match https://www.zhihu.com/ 21 | // @match https://www.zhihu.com/follow 22 | // @match https://www.zhihu.com/hot* 23 | // @match https://www.zhihu.com/topic* 24 | // @match https://juejin.cn/post/* 25 | // @match https://www.jianshu.com/p/* 26 | // @match https://www.baidu.com/s* 27 | // @match https://www.baidu.com/?* 28 | // @match https://www.baidu.com/ 29 | // @match https://www.sogou.com/web* 30 | // @match https://tieba.baidu.com/p/* 31 | // @match https://tieba.baidu.com/f?* 32 | // @match https://segmentfault.com/a/* 33 | // @match https://segmentfault.com/q/* 34 | // @match https://www.bilibili.com/read/cv* 35 | // @match https://t.bilibili.com/* 36 | // @match https://space.bilibili.com/* 37 | // @match https://www.weibo.com/* 38 | // @match https://weibo.com/* 39 | // @match https://d.weibo.com/* 40 | // @match https://www.douban.com/gallery/* 41 | // @match https://www.douban.com/note/* 42 | // @match https://movie.douban.com/subject/* 43 | // @match https://movie.douban.com/review/* 44 | // @match https://www.toutiao.com/* 45 | // @match https://crates.io/crates/* 46 | // @match https://bbs.mihoyo.com/* 47 | // @include ${/^https:\/\/www\.google\..{2,7}search/} 48 | // @include ${/^https:\/\/blog\.csdn\.net\/(\w|-)+\/article\/details\//} 49 | // @grant unsafeWindow 50 | // @grant GM_registerMenuCommand 51 | // @grant GM_addStyle 52 | // @grant GM_setValue 53 | // @grant GM_getValue 54 | // @grant GM_deleteValue 55 | // @grant GM_addValueChangeListener 56 | // @grant GM_removeValueChangeListener 57 | // @require https://unpkg.com/vue@${depsVersion.vue}/dist/vue.runtime.global${isProd ? '.prod' : ''}.js 58 | // @require https://greasyfork.org/scripts/411093-toast/code/Toast.js?version=1081231 59 | // ==/UserScript== 60 | ` 61 | -------------------------------------------------------------------------------- /src/scripts/bilibili/index.ts: -------------------------------------------------------------------------------- 1 | import speed from './speed' 2 | 3 | speed() 4 | -------------------------------------------------------------------------------- /src/scripts/bilibili/speed.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | 3 | export default function speed() { 4 | longPress('KeyS', () => { 5 | const video = ($('#bilibili-player video') || $('#bilibili-player bwp-video')) as HTMLVideoElement 6 | const oldPlaybackRate = video.playbackRate 7 | video.playbackRate = 6 8 | 9 | window.addEventListener('keyup', () => { 10 | video.playbackRate = oldPlaybackRate 11 | }, { once: true }) 12 | }) 13 | } 14 | 15 | /** 16 | * 长按键盘 17 | * @param code keyCode 18 | * @param callback 19 | * @param duration 长按时间 20 | */ 21 | function longPress(code: string, callback: () => void, duration = 350) { 22 | let timeoutID: NodeJS.Timeout | undefined 23 | 24 | window.addEventListener('keypress', event => { 25 | if (event.code === code && timeoutID) return 26 | 27 | if (event.code !== code) { 28 | if (timeoutID) { 29 | clearTimeout(timeoutID) 30 | timeoutID = undefined 31 | } 32 | return 33 | } 34 | 35 | timeoutID = setTimeout(() => { 36 | callback() 37 | }, duration) 38 | 39 | window.addEventListener('keyup', () => { 40 | clearTimeout(timeoutID) 41 | timeoutID = undefined 42 | }, { once: true }) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/scripts/dark-mode/index.ts: -------------------------------------------------------------------------------- 1 | GM_addStyle(` 2 | html { 3 | filter: invert(1) hue-rotate(180deg); 4 | background: #fff !important; 5 | } 6 | 7 | html img, 8 | html video { 9 | filter: invert(1) hue-rotate(180deg); 10 | } 11 | `) 12 | -------------------------------------------------------------------------------- /src/scripts/element-ui/catalogue.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #1890ff; 2 | $element-page-width: 1140px; 3 | 4 | #catalogue-js { 5 | @media (min-width: 1500px) { 6 | left: calc(50% + #{$element-page-width} / 2 + 40px); 7 | right: auto; 8 | } 9 | 10 | contain: content; 11 | position: fixed; 12 | right: 20px; 13 | top: 100px; 14 | z-index: 1000; 15 | 16 | /* 列表样式复制ant,仅略微调整 */ 17 | ul { 18 | border-left: 1px solid #f0f0f0; 19 | font-size: 12px; 20 | list-style: none; 21 | margin: 0; 22 | padding-left: 0; 23 | } 24 | 25 | li { 26 | border-left: 1px solid transparent; 27 | color: rgb(0 0 0 / 85%); 28 | cursor: pointer; 29 | line-height: 1.5; 30 | list-style: none; 31 | margin-left: -1px; 32 | overflow: hidden; 33 | padding: 2px 0 2px 16px; 34 | text-overflow: ellipsis; 35 | transition: all 0.3s ease; 36 | white-space: nowrap; 37 | width: 110px; 38 | 39 | &:hover { 40 | border-left-color: $primary-color; 41 | color: $primary-color; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/scripts/element-ui/catalogue.tsx: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { $, $$ } from '@/utils/selector' 3 | import { mountComponent } from '@/utils/mount-component' 4 | import { warn } from '@/utils/log' 5 | import './catalogue.scss' 6 | 7 | export default class Catalogue { 8 | #scope = '' 9 | #cat = ref<{ id: HTMLElement['id'], text: string }[]>([]) 10 | 11 | constructor({ scope }: { 12 | scope: string 13 | }) { 14 | this.#scope = scope 15 | this.#createUI() 16 | } 17 | 18 | update() { 19 | const els = this.#getElements() 20 | const cat = els.map(el => { 21 | const catItem = { 22 | id: el.id, 23 | text: '', 24 | } 25 | // 仅显示文本节点内容 26 | el.childNodes.forEach(node => { 27 | if (node.nodeName === '#text') { 28 | catItem.text += node.nodeValue 29 | } 30 | }) 31 | catItem.text = catItem.text.trim() 32 | return catItem 33 | }) 34 | warn(els, cat) 35 | this.#cat.value = cat 36 | } 37 | 38 | #getElements() { 39 | return [...$$(this.#scope)] 40 | } 41 | 42 | #createUI() { 43 | // eslint-disable-next-line @typescript-eslint/no-this-alias 44 | const self = this 45 | 46 | mountComponent({ 47 | setup() { 48 | function intoView(item: { id: HTMLElement['id'] }) { 49 | $('#' + item.id)?.scrollIntoView({ block: 'center' }) 50 | } 51 | 52 | return () => ( 53 |
54 |
    55 | {self.#cat.value.map(item => ( 56 |
  • intoView(item)} 60 | > 61 | {item.text} 62 |
  • 63 | ))} 64 |
65 |
66 | ) 67 | }, 68 | }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/scripts/element-ui/index.ts: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'vue' 2 | import { sleep } from '@/utils/base' 3 | import { $ } from '@/utils/selector' 4 | import { checker } from '@/utils/compatibility' 5 | import { getVueRoot, type VueHTMLElement } from '@/utils/vue-root' 6 | import { warn } from '@/utils/log' 7 | import Catalogue from './catalogue' 8 | 9 | async function main() { 10 | if (!checker()) return 11 | 12 | let instance: unknown 13 | // 非国内链接打开较慢,防止未完成加载 14 | while (instance == null) { 15 | ({ instance } = getVueRoot($('#app')!)) 16 | await sleep(500) 17 | } 18 | warn(instance) 19 | // element-plus 已支持 20 | if (($('#app') as VueHTMLElement).__vue_app__) return 21 | 22 | const catalogue = new Catalogue({ 23 | // 注意:选择器要同时兼容element与element plus文档 24 | scope: '.page-container .page-component__content section.element-doc > h3', 25 | }) 26 | 27 | let unwatch: (() => void) | undefined 28 | (instance as any).$watch('$route', function() { 29 | nextTick(() => { 30 | const target = $('.page-component__content') 31 | if (target && unwatch == null) { 32 | unwatch = watchDocs(target) 33 | } else if (!target) { 34 | unwatch?.() 35 | unwatch = undefined 36 | } 37 | }) 38 | }, { immediate: true }) 39 | 40 | function watchDocs(target: Node) { 41 | catalogue.update() 42 | const observer = new MutationObserver(() => catalogue.update()) 43 | observer.observe(target, { 44 | subtree: true, 45 | childList: true, 46 | }) 47 | 48 | return () => { 49 | observer.disconnect() 50 | catalogue.update() 51 | } 52 | } 53 | } 54 | 55 | main() 56 | -------------------------------------------------------------------------------- /src/scripts/github/index.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | 3 | function insert1sButton() { 4 | const actions = $('.pagehead-actions') 5 | if (actions == null || $('#github1s-button')) return 6 | 7 | const btnHTML = '
  • GitHub1s
  • ' 8 | actions.insertAdjacentHTML('afterbegin', btnHTML) 9 | ;($('#github1s-button') as HTMLAnchorElement).onmouseenter = function(this: HTMLAnchorElement) { 10 | const github1sURL = new URL(location.href) 11 | github1sURL.host = 'github1s.com' 12 | this.href = github1sURL.href 13 | } as any 14 | } 15 | 16 | insert1sButton() 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | window.addEventListener('urlchange', info => { 19 | setTimeout(insert1sButton, 200) 20 | }) 21 | -------------------------------------------------------------------------------- /src/scripts/google-redirect/index.ts: -------------------------------------------------------------------------------- 1 | const url = new URL(location.href) 2 | url.hostname = 'www.google.co.jp' 3 | location.replace(url) 4 | -------------------------------------------------------------------------------- /src/scripts/lanhu/index.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | import { sleep } from '@/utils/base' 3 | import { checker } from '@/utils/compatibility' 4 | import { createRecorder } from './record' 5 | import { autofill } from './password' 6 | import type { VueHTMLElement } from '@/utils/vue-root' 7 | 8 | async function main() { 9 | if (!checker()) return 10 | 11 | let app: Record | undefined 12 | // 不确保一次可以获取到 13 | while (!app) { 14 | app = ($('.whole') as VueHTMLElement)?.__vue__ 15 | await sleep(500) 16 | } 17 | 18 | const recorder = createRecorder() 19 | 20 | app.$watch('$route', function() { 21 | autofill() 22 | // 蓝湖title是动态获取的,可能有延时,延时处理 23 | setTimeout(recorder.record, 500) 24 | }, { immediate: true }) 25 | } 26 | 27 | main() 28 | -------------------------------------------------------------------------------- /src/scripts/lanhu/password.ts: -------------------------------------------------------------------------------- 1 | import * as qs from '@/utils/querystring' 2 | import { $ } from '@/utils/selector' 3 | import type { PasswordType } from './types' 4 | 5 | const marks = new WeakSet() 6 | let observer: MutationObserver | null = null 7 | 8 | /* 填充密码 */ 9 | function autofill() { 10 | // 停止上次观察 11 | if (observer) { 12 | observer.disconnect() 13 | observer = null 14 | } 15 | if (!location.hash.startsWith('#/item/project/door')) return 16 | const { pid, pwd } = qs.parse() 17 | // 有些链接自带密码 如果保存过密码但链接自带新密码会有问题 18 | if (!pid || pwd) return 19 | 20 | // 确认登录按钮 21 | let confirmEl: HTMLButtonElement | null = null 22 | // 密码框 23 | let passwordEl: HTMLInputElement | null = null 24 | 25 | function savePassword() { 26 | const savedPassword = GM_getValue('passwords', {}) 27 | const password = passwordEl!.value 28 | GM_setValue('passwords', { 29 | ...savedPassword, 30 | [pid]: password, 31 | }) 32 | } 33 | 34 | observer = new MutationObserver((mutationsList, observer) => { 35 | let filled = false 36 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 37 | for (const _ of mutationsList) { 38 | const [hasConfirmEl, hasPasswordEl] = [ 39 | $('#project-door .mu-raised-button-wrapper') as HTMLButtonElement, 40 | $('#project-door .pass input') as HTMLInputElement, 41 | ] 42 | if (!hasConfirmEl || !hasPasswordEl) continue 43 | 44 | observer.disconnect() 45 | confirmEl = hasConfirmEl 46 | passwordEl = hasPasswordEl 47 | 48 | const pidPassword = GM_getValue('passwords', {})[pid] 49 | // 确保本次内只进行一次操作 50 | if (filled === false && pidPassword) { 51 | filled = true 52 | passwordEl.value = pidPassword 53 | Toast('密码已填写') 54 | confirmEl.click() 55 | } 56 | 57 | // 标记已添加事件的元素 58 | if (marks.has(confirmEl)) break 59 | marks.add(confirmEl) 60 | 61 | // 点击后保存密码 62 | confirmEl.addEventListener('mousedown', savePassword) 63 | // 回车键保存密码 64 | passwordEl.addEventListener('keydown', event => { 65 | if (event.keyCode !== 13) return 66 | savePassword() 67 | }) 68 | } 69 | }) 70 | observer.observe(document.body, { childList: true, subtree: true }) 71 | } 72 | 73 | export { 74 | autofill, 75 | } 76 | -------------------------------------------------------------------------------- /src/scripts/lanhu/record.scss: -------------------------------------------------------------------------------- 1 | @import '~@/components/index'; 2 | 3 | @include var; 4 | 5 | #inject-recorder-ui { 6 | bottom: 8vh; 7 | contain: layout; 8 | opacity: 0.5; 9 | padding: 30px 30px 10px; 10 | position: fixed; 11 | right: 30px; 12 | transition: opacity 0.1s; 13 | width: 240px; 14 | z-index: 1000; 15 | 16 | &:hover { 17 | opacity: 1; 18 | } 19 | 20 | ul { 21 | background: rgb(251 251 251); 22 | box-shadow: var(--skr-box-shadow-lighten); 23 | max-height: 250px; 24 | overflow-x: hidden; 25 | padding: 5px; 26 | transition: width 0.1s; 27 | width: fit-content; 28 | 29 | &::-webkit-scrollbar { 30 | background: #f2f2f2; 31 | height: 4px; 32 | padding-right: 2px; 33 | width: 4px; 34 | } 35 | 36 | &::-webkit-scrollbar-thumb { 37 | background: #b4bbc5; 38 | border: 0; 39 | border-radius: 3px; 40 | } 41 | } 42 | 43 | li { 44 | align-items: center; 45 | box-sizing: content-box; 46 | display: flex; 47 | padding: 0 0 0 5px; 48 | position: relative; 49 | transition: 50 | all var(--skr-transition-duration-normal), 51 | width 0.15s ease-out, 52 | background var(--skr-transition-duration-fast) ease-out; 53 | 54 | &:hover { 55 | background: rgb(220 237 251 / 64%); 56 | } 57 | 58 | &.has-pwd::before { 59 | background: rgb(7 193 96 / 52%); 60 | content: ''; 61 | height: 50%; 62 | left: 1px; 63 | position: absolute; 64 | width: 2px; 65 | } 66 | 67 | a { 68 | flex: none; 69 | line-height: 30px; 70 | overflow: hidden; 71 | padding-right: 4px; 72 | text-overflow: ellipsis; 73 | white-space: nowrap; 74 | width: 132px; 75 | } 76 | 77 | .actions { 78 | white-space: nowrap; 79 | } 80 | 81 | button { 82 | border: none; 83 | height: 20px; 84 | line-height: 20px; 85 | padding: 0; 86 | width: 20px; 87 | 88 | &:not(:hover) { 89 | color: var(--skr-text-secondary-color); 90 | } 91 | 92 | &:nth-of-type(n + 2) { 93 | margin-left: 4px; 94 | } 95 | } 96 | } 97 | 98 | .control { 99 | align-items: center; 100 | display: flex; 101 | justify-content: center; 102 | padding-top: 8px; 103 | 104 | input { 105 | margin-left: 6px; 106 | } 107 | } 108 | 109 | .view-btn { 110 | &:not(:focus-visible) { 111 | outline: none; 112 | } 113 | } 114 | 115 | svg { 116 | fill: currentcolor; 117 | } 118 | 119 | /* 动画1 */ 120 | .inject-slide-fade-enter-active, 121 | .inject-slide-fade-leave-active { 122 | transition: all 0.1s; 123 | } 124 | 125 | .inject-slide-fade-enter-from, 126 | .inject-slide-fade-leave-to { 127 | opacity: 0; 128 | transform: translateY(5px); 129 | } 130 | 131 | /* 动画2 group */ 132 | .inject-slide-hor-fade-move { 133 | transition: all 0.8s; 134 | } 135 | 136 | .inject-slide-hor-fade-active { 137 | position: absolute; 138 | } 139 | 140 | .inject-slide-hor-fade-enter-from, 141 | .inject-slide-hor-fade-leave-to { 142 | opacity: 0; 143 | transform: translateX(30px); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/scripts/lanhu/record.tsx: -------------------------------------------------------------------------------- 1 | import { onMounted, nextTick, ref, reactive, computed, watch, Transition, TransitionGroup } from 'vue' 2 | import * as qs from '@/utils/querystring' 3 | import { mountComponent } from '@/utils/mount-component' 4 | import { useGMvalue } from '@/composables/use-gm-value' 5 | import store from '@/store' 6 | import Button from '@/components/button' 7 | import type { RecordType, PasswordType } from './types' 8 | import './record.scss' 9 | 10 | /* 记录看过的产品 */ 11 | function createRecorder() { 12 | GM_registerMenuCommand('显示/隐藏 最近项目', function() { 13 | const next = !(store.recorder_visible ?? true) 14 | !next && Toast('已隐藏', 1000) 15 | store.recorder_visible = next 16 | }) 17 | 18 | createUI() 19 | 20 | function record() { 21 | const { pid } = qs.parse() 22 | if (!pid) return 23 | 24 | const records = GM_getValue('records', []) 25 | let old: RecordType | undefined 26 | records.find((item, index) => { 27 | if (item.pid === pid) { 28 | old = item 29 | records.splice(index, 1) 30 | return true 31 | } 32 | return false 33 | }) 34 | // 优化标题显示:当前是无意义标题且有旧标题时优先使用旧标题 35 | const title = (['蓝湖', '...'].includes(document.title) && old?.title) ? old.title : document.title 36 | records.push({ 37 | ...old, 38 | pid, 39 | title, 40 | href: location.href, 41 | }) 42 | GM_setValue('records', records) 43 | } 44 | 45 | return { 46 | record, 47 | } 48 | } 49 | 50 | function createUI() { 51 | mountComponent({ 52 | setup() { 53 | const state = reactive({ 54 | recordsVisible: false, 55 | moreActionsVisible: false, 56 | // 初始宽度 57 | width: 160, 58 | records: useGMvalue('records', [], { deep: true }), 59 | unhidden: useGMvalue('unhidden', false), 60 | passwords: useGMvalue('passwords', {}), 61 | }) 62 | const recorderVisible = useGMvalue('recorder_visible', true) 63 | const lisRef = ref([]) 64 | const reversed = computed(() => [...state.records].reverse()) 65 | 66 | onMounted(() => { 67 | watch( 68 | [ 69 | () => state.recordsVisible, 70 | () => state.moreActionsVisible, 71 | () => state.records, 72 | () => state.unhidden, 73 | recorderVisible, 74 | ], 75 | () => { 76 | nextTick(() => { 77 | const [first] = lisRef.value 78 | if (first) { 79 | const width = [...first.children].reduce((totalWidth, el) => totalWidth + el.getBoundingClientRect().width, 0) 80 | state.width = 5 + width // 左边距 81 | } 82 | }) 83 | }, { immediate: true, flush: 'post' }) 84 | }) 85 | 86 | function deleteItem(item: RecordType) { 87 | const index = state.records.findIndex(record => record.pid === item.pid) 88 | index > -1 && state.records.splice(index, 1) 89 | } 90 | 91 | function copy(action: 'all' | 'pwd', item: RecordType) { 92 | let copyString = '' 93 | const password = state.passwords[item.pid] 94 | if (action === 'all') { 95 | copyString += `${item.title}` 96 | password && (copyString += ` (密码:${password})`) 97 | copyString += `\n${item.href}` 98 | } else if (action === 'pwd') { 99 | if (password) { 100 | copyString += password 101 | } else { 102 | Toast.warning('没有密码!') 103 | } 104 | } 105 | 106 | if (!copyString) return 107 | GM_setClipboard(copyString, 'text') 108 | Toast.success('复制成功') 109 | } 110 | 111 | function editCustomTitle(item: RecordType) { 112 | // 取消时不操作 113 | let result = window.prompt('输入自定义标题,不填则会使用原标题', item.customTitle || item.title || undefined) 114 | result &&= result.trim() 115 | if (result === '') { 116 | delete item.customTitle 117 | } else if (result) { 118 | item.customTitle = result 119 | } 120 | } 121 | 122 | function setRecordsVisible(visible: boolean) { 123 | state.recordsVisible = visible 124 | } 125 | function setMoreActionsVisible(visible: boolean) { 126 | state.moreActionsVisible = visible 127 | } 128 | 129 | return () => ( 130 |
    { setRecordsVisible(true) }} 134 | onMouseleave={() => { 135 | setRecordsVisible(false) 136 | setMoreActionsVisible(false) 137 | }} 138 | > 139 | 140 |
    141 | 145 | {reversed.value.map((item, index) => ( 146 |
  • { el && (lisRef.value[index] = (el as HTMLElement)) }} 151 | > 152 | 157 | {item.customTitle || item.title} 158 | 159 |
    { setMoreActionsVisible(true) }}> 160 | 161 | 173 | 181 |
    182 |
  • 183 | ))} 184 |
    185 |
    186 |
    187 |
    188 | 189 | 194 |
    195 |
    196 | ) 197 | }, 198 | }) 199 | } 200 | 201 | const IconCopy = 202 | 203 | const IconEdit = 204 | 205 | export { 206 | createRecorder, 207 | } 208 | -------------------------------------------------------------------------------- /src/scripts/lanhu/types.ts: -------------------------------------------------------------------------------- 1 | export interface RecordType { 2 | /** 主键 */ 3 | pid: string 4 | title: string 5 | customTitle?: string 6 | href: Location['href'] 7 | } 8 | 9 | export type PasswordType = Record 10 | -------------------------------------------------------------------------------- /src/scripts/mdn-web-docs/index.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | import { warn } from '@/utils/log' 3 | import { getSupports, matchLang, isChinese, isEnglish, getLangMenus } from './utils' 4 | import './style' 5 | 6 | let docsLang = matchLang(location.pathname) 7 | let supports: string[] = [] 8 | 9 | async function main() { 10 | supports = await getSupports() 11 | warn(docsLang) 12 | warn(supports) 13 | if (!supports.length) return 14 | 15 | window.addEventListener('urlchange', () => { 16 | docsLang = matchLang(location.pathname) 17 | }) 18 | window.addEventListener('click', function listener(event) { 19 | if (!event.isTrusted) return 20 | 21 | const isInLangMenu = $('.languages-switcher-menu .language-menu')?.contains(event.target as HTMLElement) 22 | if (isInLangMenu) { 23 | // 标记自行切换语言 24 | sessionStorage.setItem('hand-control-language', 'true') 25 | window.removeEventListener('click', listener, true) 26 | } 27 | }, true) 28 | 29 | setLocale() 30 | addLangButton() 31 | } 32 | 33 | function setLocale() { 34 | if (isChinese(docsLang)) return 35 | // 是否自行切换过语言 36 | if (sessionStorage.getItem('hand-control-language') === 'true') return 37 | 38 | for (const item of supports) { 39 | isChinese(matchLang(item)) && selectLang(item) 40 | } 41 | } 42 | 43 | function selectLang(value: string) { 44 | getLangMenus(buttons => { 45 | for (const button of buttons) { 46 | if (button.getAttribute('name') === value) { 47 | button.click() 48 | return false 49 | } 50 | } 51 | }) 52 | } 53 | 54 | function addLangButton() { 55 | const values: [string?, string?] = [] // [0]中 [1]英 排序 56 | for (const item of supports) { 57 | const lang = matchLang(item) 58 | if (isChinese(lang)) { 59 | values[0] = item 60 | } else if (isEnglish(lang)) { 61 | values[1] = item 62 | } 63 | } 64 | if (isChinese(docsLang)) values[0] = docsLang 65 | if (isEnglish(docsLang)) values[1] = docsLang 66 | warn(values) 67 | if (values.filter(Boolean).length < 2) return 68 | 69 | // bug: 会出现一种进来时有翻译,换了另一篇后没翻译,这时按钮仍然显示的问题 70 | const button = document.createElement('button') 71 | button.innerText = '中-英' 72 | button.classList.add('button') 73 | button.classList.add('action') 74 | button.style.cssText = [ 75 | 'position: fixed', 76 | 'right: 0', 77 | 'bottom: 15vh', 78 | 'line-height: 2em', 79 | 'padding: 2px 10px', 80 | 'font-size: 12px', 81 | 'letter-spacing: 2px', 82 | 'border: 1px solid var(--border-secondary)', 83 | 'background-color: var(--button-bg)', 84 | 'box-shadow: var(--shadow-01)', 85 | ].join(';') 86 | button.onclick = function() { 87 | sessionStorage.setItem('hand-control-language', 'true') 88 | selectLang((isChinese(docsLang) ? values[1]! : values[0]!)) 89 | } 90 | 91 | document.body.append(button) 92 | } 93 | 94 | main() 95 | -------------------------------------------------------------------------------- /src/scripts/mdn-web-docs/style.ts: -------------------------------------------------------------------------------- 1 | const stylesheet = ` 2 | /* 让搜索框一直展开 */ 3 | @media screen and (min-width: 1220px) { 4 | .header-search .search-input-field { 5 | width: inherit !important; 6 | } 7 | } 8 | ` 9 | 10 | const style = document.createElement('style') 11 | style.appendChild(document.createTextNode(stylesheet)) 12 | document.head.appendChild(style) 13 | -------------------------------------------------------------------------------- /src/scripts/mdn-web-docs/utils.ts: -------------------------------------------------------------------------------- 1 | import { $, $$ } from '@/utils/selector' 2 | 3 | export function matchLang(str: string) { 4 | // 匹配 pathname 或字符串 5 | // /en-US/docs/Web/API/ 或 en-US 6 | return str.match(/^\/?([\w-]+)/)?.[1] 7 | } 8 | 9 | export function isChinese(lang?: string) { 10 | return /zh-CN/i.test(lang!) 11 | } 12 | 13 | export function isEnglish(lang?: string) { 14 | return /en-US/i.test(lang!) 15 | } 16 | 17 | /** 18 | * 需要点击菜单才能获取支持的语言 19 | * 切换语言后菜单会自动关闭 20 | * callback 返回一个布尔确认操作完后是否自动关闭 21 | */ 22 | export async function getLangMenus(callback?: (buttons: HTMLButtonElement[]) => boolean | undefined) { 23 | const toggle = $('button.languages-switcher-menu') as HTMLElement 24 | // 存在没有翻译的情况 25 | if (toggle == null) return [] 26 | 27 | toggle.click() 28 | // fix: 新版又被 mdn 改掉了,不知道为什么要放在 microtask 才能获取到 buttons 29 | // 由于改为 microtask,调用这个函数都要更改 30 | // https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/dispatchEvent 31 | // 只有浏览器自己触发的事件才是放在一个 task(不是 microtask) 里执行的 32 | // 而人工合成(synthetic)的事件派发(dispatch)是同步执行的,包括执行 click() 和 dispatchEvent() 33 | await Promise.resolve() 34 | // 不要返回 NodeList,和空时返回同样的类型 35 | const buttons = [...$$('.language-menu button[name]')] as HTMLButtonElement[] 36 | const off = callback?.(buttons) ?? true 37 | off && toggle.click() 38 | return buttons 39 | } 40 | 41 | export async function getSupports() { 42 | const langs = (await getLangMenus()).map(button => button.getAttribute('name')!) 43 | return langs 44 | } 45 | -------------------------------------------------------------------------------- /src/scripts/pixiv/index.ts: -------------------------------------------------------------------------------- 1 | import Previewer from './previewer' 2 | import attachPixels from './pixels' 3 | 4 | // eslint-disable-next-line no-new 5 | new Previewer('figure [role="presentation"] a img', { 6 | includePathname: /^\/artworks\/(\w)+/, 7 | }) 8 | 9 | attachPixels('figure [role="presentation"] a img', { 10 | includePathname: /^\/artworks\/(\w)+/, 11 | }) 12 | -------------------------------------------------------------------------------- /src/scripts/pixiv/pixels.ts: -------------------------------------------------------------------------------- 1 | import { $$ } from '@/utils/selector' 2 | import { onVisible } from '@/utils/visibility-state' 3 | 4 | interface Options { 5 | includePathname: RegExp 6 | } 7 | 8 | export default function attachPixels(el: string, options: Options) { 9 | const ws = new WeakSet() 10 | 11 | onVisible(() => { 12 | if (!options.includePathname.test(location.pathname)) return 13 | 14 | $$(el).forEach(img => { 15 | if (ws.has(img)) return 16 | 17 | // 获取原尺寸 18 | let [width, height]: [number | string | null, number | string | null] = [ 19 | img.getAttribute('width'), 20 | img.getAttribute('height'), 21 | ] 22 | if (width === null || height === null) return 23 | 24 | [width, height] = [+width, +height] 25 | img.parentElement!.style.position = 'relative' 26 | const elem = createPixelsElement(img.parentElement!) 27 | elem.innerText = `${width} × ${height} (${calcRectCoincide(width, height).percent})` 28 | ws.add(img) 29 | }) 30 | }) 31 | } 32 | 33 | function createPixelsElement(parentElement: HTMLElement): HTMLElement { 34 | const classname = 'artwork-pixels' 35 | 36 | for (const child of parentElement.children) { 37 | if (child.classList.contains(classname)) return child as HTMLElement 38 | } 39 | 40 | // 没有则插入一个 41 | const elem = document.createElement('span') 42 | elem.classList.add(classname) 43 | elem.style.cssText = [ 44 | 'position: absolute', 45 | 'z-index: 1', 46 | 'top: 32px', 47 | 'right: 8px', 48 | 'padding: 0 4px', 49 | 'border-radius: 8px', 50 | 'font-size: 12px', 51 | 'line-height: initial', 52 | 'color: #fff', 53 | 'background: rgb(0 0 0 / 0.32)', 54 | ].join(';') 55 | parentElement.prepend(elem) 56 | return elem 57 | } 58 | 59 | // 计算图片与屏幕吻合度 60 | function calcRectCoincide(width: number, height: number) { 61 | const { width: sw, height: sh } = window.screen 62 | const rectRate = width / height 63 | const screenRate = sw / sh 64 | let rate 65 | 66 | if (rectRate >= screenRate) { 67 | rate = screenRate / rectRate 68 | } else { 69 | rate = rectRate / screenRate 70 | } 71 | 72 | // 图片小于屏幕尺寸,降低值 73 | if (width < sw && height < sh) { 74 | rate *= (width / sw) * (height / sh) 75 | } 76 | 77 | // 符合屏幕比例且超过屏幕尺寸的图片,提高值 78 | // 接近比例也算符合 79 | if (rate >= 0.99) { 80 | if (width > sw) { 81 | rate *= width / sw 82 | } else if (height > sh) { 83 | rate *= height / sh 84 | } 85 | } 86 | 87 | return { 88 | rate, 89 | percent: (rate * 100).toFixed(0) + '%', 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/scripts/pixiv/previewer.ts: -------------------------------------------------------------------------------- 1 | import Viewer from 'viewerjs' 2 | import { $$ } from '@/utils/selector' 3 | import { warn } from '@/utils/log' 4 | 5 | GM_addStyle(GM_getResourceText('viewerCSS')) 6 | GM_addStyle([ 7 | '.viewer-backdrop { background-color: rgb(0 0 0 / 0.8) }', // 背景暗一点 8 | '.viewer-container .viewer-title { text-shadow: 1px 1px 1px #000 }', // 添加标题阴影 在图片是白底时显示得清楚点 9 | '.viewer-container .viewer-navbar ul, .viewer-container .viewer-navbar li { width: 66px; height: 110px }', // 加大导航栏 10 | ].join('')) 11 | 12 | interface PreviewerOptions { 13 | includePathname: RegExp 14 | } 15 | 16 | export default class Previewer { 17 | #el 18 | #viewer?: Viewer 19 | #options: PreviewerOptions 20 | 21 | constructor(el: string, options: PreviewerOptions) { 22 | this.#process = this.#process.bind(this) 23 | this.#el = el 24 | this.#options = options 25 | this.#init() 26 | } 27 | 28 | #init() { 29 | window.addEventListener('click', this.#process, true) 30 | window.addEventListener('urlchange', info => { 31 | warn('urlchange', info) 32 | this.#viewer?.hide() 33 | }) 34 | } 35 | 36 | #process = function(this: Previewer, event: Event) { 37 | /** 38 | * 这么多的判断多数是没有意义的 39 | * 只是为了日后可能失效,尽量避免影响原点击事件 40 | */ 41 | if (!this.#options.includePathname.test(location.pathname)) return 42 | const artworks = this.#getArtworks() 43 | if (artworks.length === 0) return 44 | let index = -1 45 | // 比较5层深度应该足够了 46 | event.composedPath().slice(0, 5).find(target => { 47 | index = artworks.findIndex(artwork => artwork === target) 48 | return index > -1 49 | }) 50 | warn(event, index) 51 | if (index === -1) return 52 | const originalArtworks = this.#createOriginalImgEls(artworks) 53 | if (originalArtworks.length === 0) return 54 | 55 | event.preventDefault() 56 | event.stopPropagation() 57 | event.stopImmediatePropagation() 58 | 59 | this.#viewer = this.#preview(originalArtworks, { 60 | initialViewIndex: index, 61 | }) 62 | } 63 | 64 | /** 65 | * 获取要预览图片的节点 66 | */ 67 | #getArtworks() { 68 | return [...$$(this.#el)] as HTMLImageElement[] 69 | } 70 | 71 | /** 72 | * 将getArtworks的图片转成原图 73 | * @param {nodes} 74 | * @return {nodes} 75 | */ 76 | #createOriginalImgEls(imgEls: HTMLImageElement[]) { 77 | return imgEls.reduce((acc, img) => { 78 | const parentNode = img.parentNode as HTMLAnchorElement 79 | // 原图在其父级a标签href上 80 | if (parentNode.tagName === 'A') { 81 | const image = new Image() 82 | image.src = parentNode.href 83 | image.alt = img.alt 84 | acc.push(image) 85 | } 86 | 87 | return acc 88 | }, []) 89 | } 90 | 91 | /** 92 | * 预览 93 | * @param {nodes} 94 | * @param {object} viewerOpts 95 | * @return {viewer} 96 | */ 97 | #preview(imgEls: HTMLImageElement[], viewerOpts: Viewer.Options) { 98 | // eslint-disable-next-line @typescript-eslint/no-this-alias 99 | const self = this 100 | const container = document.createElement('div') 101 | container.append(...imgEls) 102 | viewerOpts = Object.assign({ 103 | navbar: imgEls.length > 1, 104 | loop: false, 105 | zoomRatio: 0.5, 106 | minZoomRatio: 0.1, 107 | maxZoomRatio: 1.5, 108 | viewed(this: any) { 109 | this.viewer.tooltip() 110 | }, 111 | // 销毁 112 | hide() { 113 | self.#viewer = undefined 114 | }, 115 | hidden(this: any) { 116 | this.viewer.destroy() 117 | }, 118 | }, viewerOpts) 119 | 120 | const viewer = new Viewer(container, viewerOpts) 121 | viewer.show() 122 | warn('viewer:', container, viewer) 123 | return viewer 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/scripts/redirect/index.ts: -------------------------------------------------------------------------------- 1 | import * as readyState from '@/utils/ready-state' 2 | import { parse } from '@/utils/querystring' 3 | import { $ } from '@/utils/selector' 4 | import { warn, table as logTable } from '@/utils/log' 5 | import sites from './sites' 6 | import type { Site } from './types' 7 | 8 | function hidePage() { 9 | readyState.interactive(() => { 10 | document.body.style.cssText = 'display:none !important;' 11 | }) 12 | } 13 | 14 | class App { 15 | #sites 16 | constructor(sites: Site[]) { 17 | this.#sites = sites 18 | } 19 | 20 | boot() { 21 | const briefURL = location.host + location.pathname 22 | 23 | this.#sites.forEach(async site => { 24 | const { name, test, use } = site 25 | if (!this.#includes(test, briefURL)) return 26 | 27 | const { readyState: state } = site 28 | if (state) await readyState[state]() 29 | 30 | const redirection = await this.#parse(use) 31 | logTable({ name, briefURL, redirection }) 32 | if (!redirection) return 33 | location.replace(redirection) 34 | // 为什么要这样做? 35 | // 只是为了避免被问“哎!怎么好像没有跳转啊?!”的烦恼(实际上跳转了只是外链打开慢)(x_x) 36 | hidePage() 37 | }) 38 | } 39 | 40 | #includes(test: Site['test'], url: string) { 41 | return ([] as Site['test'][]).concat(test).some(item => { 42 | if (typeof item === 'string') return item === url 43 | if (item instanceof RegExp) return item.test(url) 44 | return false 45 | }) 46 | } 47 | 48 | async #parse(use: Site['use']) { 49 | const { query, link, selector, attr } = await use() 50 | let redirection: Location['href'] | undefined 51 | 52 | if (query) { 53 | redirection = parse()[query] 54 | } else if (link) { 55 | redirection = link 56 | } else if (selector) { 57 | redirection = ($(selector) as any)?.[attr ?? 'innerText'] 58 | } 59 | 60 | redirection &&= this.#ensure(redirection.trim()) 61 | return redirection 62 | } 63 | 64 | #ensure(url: string): Location['href'] { 65 | try { 66 | // eslint-disable-next-line no-new 67 | new URL(url) 68 | } catch (error) { 69 | warn(error) 70 | // 修复某些链接没有 protocol 导致跳转不正确 71 | // https://greasyfork.org/zh-CN/scripts/416338-redirect-外链跳转/discussions/69178 72 | const protocol = 'http:' 73 | url = protocol + '//' + url 74 | } 75 | return url 76 | } 77 | } 78 | 79 | new App(sites).boot() 80 | -------------------------------------------------------------------------------- /src/scripts/redirect/sites/mp-weixin-qq-com.ts: -------------------------------------------------------------------------------- 1 | import { warn } from '@/utils/log' 2 | import type { Site } from '../types' 3 | 4 | export const weixin: Site['use'] = () => { 5 | window.addEventListener('click', event => { 6 | const target = event.target as HTMLElement 7 | warn(target) 8 | if (target.nodeName !== 'A') return 9 | if (target.id !== 'js_view_source') return 10 | 11 | const link = (unsafeWindow as any).msg_source_url as string 12 | 13 | if (link) { 14 | event.stopPropagation() 15 | event.preventDefault() 16 | event.stopImmediatePropagation() 17 | window.open(link) 18 | } 19 | }, true) 20 | 21 | return {} 22 | } 23 | -------------------------------------------------------------------------------- /src/scripts/redirect/sites/t-cn.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | 3 | export const weibo = async () => { 4 | let link: string | null = ($('.open-url a[href]') as HTMLAnchorElement)?.href 5 | link ||= await fetch(location.href).then(response => response.headers.get('location')) 6 | 7 | return { 8 | link, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/scripts/redirect/sites/weixin110-qq-com.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { parse } from '@/utils/querystring' 3 | import type { Site } from '../types' 4 | 5 | const { atob } = window 6 | 7 | export const weixin: Site['use'] = () => { 8 | const { main_type, midpagecode } = parse() 9 | 10 | /** 11 | * main_type 貌似是旧的规则 12 | */ 13 | switch (main_type) { 14 | case '2': { 15 | const url = new URL(location.href) 16 | // 转为 1 可还原链接 17 | url.searchParams.set('main_type', '1') 18 | location.replace(url.href) 19 | return {} 20 | } 21 | case '1': 22 | break 23 | } 24 | 25 | /** 26 | * midpagecode 似乎是新的规则 27 | */ 28 | const MAGIC_KEY = atob(atob('Tmpjek56ZGhNbUZrWWpRMFpURTNZekZpTUdGa1lqSTBZalZqWmpKaVpERXlZek0wWkRsaU5UWmxNRFpqWTJRMlpHUTBZekk1TVdJME1qTmlOV0prTjJabU5tUmhZbVJqTlRVM1l6azVNbVkxWkRZd1pEZzVNbUkyT0Rjd1pqYzBOakV3TldNM05HRmhNalJqTXpBMk0yUTNOR1ExT1dJMFlXVTFOVFF6WldJM1lqSmtObVUwT1dOak1qYzNNMkZsTVRjM01UWTNNemcwTmpRM04ySmpOalppTTJNelltUTNPVE5sWkRJNFpEZGhaVE5rTnpZeE0yUm1ZVGRpWW1ReQ==')) 29 | if ( 30 | midpagecode && 31 | midpagecode !== MAGIC_KEY && 32 | !(window as any).cgiData?.url 33 | ) { 34 | const url = new URL(location.href) 35 | // 会还原链接 36 | url.searchParams.set('midpagecode', MAGIC_KEY) 37 | location.replace(url.href) 38 | return {} 39 | } 40 | 41 | return { 42 | // 如果解析得到,会出现在页面这里 43 | selector: '.weui-msg__text-area .ui-ellpisis-content p', 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/scripts/redirect/sites/www-360doc-com.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | import { warn } from '@/utils/log' 3 | 4 | export const doc360 = () => { 5 | ($('#artContent') as HTMLElement).addEventListener('click', event => { 6 | const { target } = event as any 7 | const href: string = target.href 8 | warn(target) 9 | if (target.nodeName !== 'A') return 10 | if (!href) return 11 | // 是否本站 12 | if (new RegExp(location.host).test(new URL(href).host)) return 13 | 14 | event.stopPropagation() 15 | window.open(href) 16 | }, true) 17 | 18 | return {} 19 | } 20 | -------------------------------------------------------------------------------- /src/scripts/redirect/sites/www-pixiv-net.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@/utils/querystring' 2 | 3 | export const pixiv = () => { 4 | let link 5 | // 链接居然是直接拼在url上的 6 | // https://www.pixiv.net/jump.php?https%3A%2F%2Fwww.huawei.com%2Fcn%2Fcorporate-information 7 | for (const [key, value] of Object.entries(parse())) { 8 | try { link ||= new URL(key).href } catch {} 9 | try { link ||= new URL(value).href } catch {} 10 | } 11 | 12 | return { 13 | link, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/redirect/types.ts: -------------------------------------------------------------------------------- 1 | import { ReadyState } from '@/utils/ready-state' 2 | 3 | export interface ParseRule { 4 | /** 获取 location.search 中某个 key */ 5 | query?: string 6 | /** 7 | * 选择页面上元素 8 | * 默认取元素的 innerText 值作为结果 9 | */ 10 | selector?: string 11 | /** 配合 selector,获取元素的 attr 值作为结果 */ 12 | attr?: string 13 | /** 跳转链接 */ 14 | link?: string 15 | } 16 | 17 | export interface Site { 18 | name: string 19 | test: 20 | (string | RegExp) 21 | | (string | RegExp)[] 22 | readyState?: ReadyState 23 | use: () => ParseRule | Promise 24 | } 25 | -------------------------------------------------------------------------------- /src/scripts/tieba/api.ts: -------------------------------------------------------------------------------- 1 | import * as qs from '@/utils/querystring' 2 | import { 3 | GMRequest, 4 | request, 5 | ResponseError, 6 | getPageData, 7 | FAKE_VERSION, 8 | signRequestParams, 9 | encodeRequestParams, 10 | } from './utils' 11 | import store from './store' 12 | import type { 13 | WebApiLikeForumResponse, 14 | WebApiSignResponse, 15 | AppApiLikeForumResponse, 16 | AppApiSignResponse, 17 | AppApiBatchSignResponse, 18 | LikeForumData, 19 | PageData, 20 | } from './types' 21 | 22 | /** 23 | * 24 | * web 接口 25 | * 26 | */ 27 | 28 | /** 29 | * web 获取关注列表 30 | */ 31 | export function getNewmoindex() { 32 | return request.post('/mo/q/newmoindex') 33 | } 34 | 35 | /** 36 | * web 签到 37 | */ 38 | export function doSignWeb(params: { 39 | /** 吧名 */ 40 | kw: LikeForumData['forum_name'] 41 | }) { 42 | const { tbs } = getPageData() 43 | 44 | return request.post( 45 | '/sign/add', 46 | encodeRequestParams({ ie: 'utf-8', tbs, ...params }), 47 | { 48 | headers: { 49 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 50 | }, 51 | }, 52 | ) 53 | } 54 | 55 | /** 56 | * 57 | * app 接口 58 | * 59 | */ 60 | 61 | const appCommonHeader = Object.freeze({ 62 | 'User-agent': `bdtb for Android ${FAKE_VERSION}`, 63 | Accept: '', 64 | 'Content-Type': 'application/x-www-form-urlencoded', 65 | 'Accept-Encoding': 'gzip', 66 | Cookie: 'ka=open', 67 | }) 68 | 69 | /** 70 | * app 获取关注列表 71 | */ 72 | export function getForumLike(params: { 73 | BDUSS: string 74 | tbs: PageData['tbs'] 75 | }) { 76 | return GMRequest.post( 77 | 'http://c.tieba.baidu.com/c/f/forum/like', 78 | qs.stringify(signRequestParams(params)), 79 | { 80 | headers: appCommonHeader, 81 | }, 82 | ) 83 | } 84 | 85 | /** 86 | * app 签到 87 | */ 88 | export function doSignApp(params: { 89 | BDUSS: string 90 | tbs: PageData['tbs'] 91 | /** 吧 id */ 92 | fid: LikeForumData['forum_id'] | string 93 | /** 吧名 */ 94 | kw: LikeForumData['forum_name'] 95 | }) { 96 | return GMRequest.post( 97 | 'http://c.tieba.baidu.com/c/c/forum/sign', 98 | qs.stringify(encodeRequestParams(signRequestParams(params))), 99 | { 100 | headers: appCommonHeader, 101 | }) 102 | } 103 | 104 | /** 105 | * app 批量签到 106 | */ 107 | export function batchSignApp(params: { 108 | BDUSS: string 109 | tbs: PageData['tbs'] 110 | /** 吧 id */ 111 | forum_ids: (LikeForumData['forum_id'] | string)[] 112 | }) { 113 | return GMRequest.post( 114 | 'http://c.tieba.baidu.com/c/c/forum/msign', 115 | qs.stringify(signRequestParams(params)), 116 | { 117 | headers: appCommonHeader, 118 | }) 119 | .then(response => { 120 | if (response.error.errno !== '0') { 121 | throw new ResponseError(response.error.usermsg, response) 122 | } 123 | return response 124 | }) 125 | } 126 | 127 | /** 128 | * 129 | * 合成接口 130 | * 131 | */ 132 | 133 | /** 134 | * 界面上无法获得失效的贴吧,这里调用接口获取所有关注的贴吧 135 | */ 136 | export async function mergeLikeForum() { 137 | const { BDUSS } = store 138 | if (!BDUSS) throw new Error('BDUSS 不能为空') 139 | const { tbs } = getPageData() 140 | const req2 = { 141 | BDUSS, 142 | tbs, 143 | } 144 | const [like1, like2Map] = await Promise.all([ 145 | getNewmoindex() 146 | .then(data => data.data.like_forum), 147 | getForumLike(req2) 148 | .then(data => data.forum_list) 149 | .then(forumList => forumList.reduce( 150 | (acc, val) => (((acc[val.id] = val), acc)), 151 | {} as Record), 152 | ), 153 | ]) 154 | 155 | // 融合数据 156 | like1.forEach(forum => { 157 | const forumId = forum.forum_id 158 | const like2Forum = like2Map[forumId] 159 | if (!like2Forum) return 160 | Object.assign(forum, { 161 | levelup_score: like2Forum.levelup_score, 162 | level_name: like2Forum.level_name, 163 | slogan: like2Forum.slogan, 164 | }) 165 | }) 166 | // 经验降序 167 | like1.sort((a, b) => +b.user_exp - +a.user_exp) 168 | return like1 as LikeForumData[] 169 | } 170 | -------------------------------------------------------------------------------- /src/scripts/tieba/index.ts: -------------------------------------------------------------------------------- 1 | import { checker } from '@/utils/compatibility' 2 | import store from './store' 3 | import { getElementsInPage } from './utils' 4 | import { createUI } from './ui' 5 | 6 | /** 7 | * todo:暂时不支持超过 200 个吧 8 | * 一次只能获取 200 个, 9 | * 而且通过接口没有办法区分吧是否被封,签到时不好处理 10 | */ 11 | 12 | function main() { 13 | if (!checker()) return 14 | 15 | // 未登录时删除已有的 BDUSS 16 | if (!getElementsInPage().moreForum.length) { 17 | delete store.BDUSS 18 | delete store.is_complete 19 | return 20 | } 21 | 22 | createUI() 23 | } 24 | 25 | main() 26 | -------------------------------------------------------------------------------- /src/scripts/tieba/sign.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import Queue from '@/utils/queue' 3 | import { sleep } from '@/utils/base' 4 | import { warn, error as logError } from '@/utils/log' 5 | import { doSignWeb, doSignApp, batchSignApp } from './api' 6 | import { getPageData } from './utils' 7 | import type { AppApiSignResponse } from './types' 8 | 9 | interface Task { 10 | readonly fid?: string 11 | readonly kw?: string 12 | /** BDUSS */ 13 | readonly BDUSS?: string 14 | /** 签到失败次数 */ 15 | fail: number 16 | /** 签到逻辑 */ 17 | execute(): Promise<{ 18 | fid?: string, 19 | kw?: string, 20 | data?: Partial & { 21 | is_sign: 1 22 | }, 23 | }> 24 | } 25 | 26 | /** 27 | * 网页签到 28 | * 29 | * 经验没客户端那么多,但不需要获得 BDUSS 只需登录即可 30 | */ 31 | class WebTask implements Task { 32 | readonly kw 33 | fail = 0 34 | constructor(options: { 35 | kw: string 36 | }) { 37 | this.kw = options.kw 38 | } 39 | 40 | async execute() { 41 | const { kw } = this 42 | try { 43 | await doSignWeb({ kw }) 44 | return { kw } 45 | } catch (e: any) { 46 | // 签过 47 | if (e.response?.no === 1101) { 48 | return { kw } 49 | } 50 | this.fail++ 51 | throw e 52 | } finally { 53 | // 网页签到不能太短,否则很容易出现验证码(ಥ﹏ಥ) 验证码:2150040 54 | const ms = ~~(Math.random() * 500 + 600) 55 | await sleep(ms) 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * 模拟客户端签到 62 | * 63 | * 获得经验与客户端签到相同,需要获得 BDUSS 64 | */ 65 | class AppTask implements Task { 66 | readonly fid 67 | readonly kw 68 | readonly BDUSS 69 | fail = 0 70 | constructor(options: { 71 | fid: string 72 | kw: string 73 | BDUSS: string 74 | }) { 75 | this.fid = options.fid 76 | this.kw = options.kw 77 | this.BDUSS = options.BDUSS 78 | } 79 | 80 | async execute() { 81 | const { fid, kw, BDUSS } = this 82 | const { tbs } = getPageData() 83 | if (!fid) throw new Error('获取吧 id 为空') 84 | try { 85 | const response = await doSignApp({ 86 | BDUSS, 87 | tbs, 88 | fid, 89 | kw, 90 | }) 91 | const { user_info } = response 92 | return { 93 | fid, 94 | kw, 95 | data: { 96 | ...user_info, 97 | // 标记为已签到 98 | is_sign: 1, 99 | } as Awaited>['data'], 100 | } 101 | } catch (e: any) { 102 | // 签过 103 | if (e.response?.error_code === '160002') { 104 | return { 105 | fid, 106 | kw, 107 | data: { 108 | is_sign: 1, 109 | } as Awaited>['data'], 110 | } 111 | } 112 | this.fail++ 113 | throw e 114 | } finally { 115 | // 客户端签到可以将延时缩短,随机延时一下 50ms 以上 116 | const ms = ~~(Math.random() * 20) + 50 117 | await sleep(ms) 118 | } 119 | } 120 | } 121 | 122 | async function batch(options: { 123 | BDUSS: string, 124 | forum_ids: string[] 125 | }) { 126 | const { BDUSS, forum_ids } = options 127 | const { tbs } = getPageData() 128 | const { info } = await batchSignApp({ 129 | BDUSS, 130 | tbs, 131 | forum_ids: forum_ids.slice(0, 200), // 接口限制最多 200 个 132 | }) 133 | 134 | type NewInfoItem = Awaited>['data'] & { 135 | forum_id: string, 136 | forum_name: string, 137 | } 138 | const newInfo: NewInfoItem[] = info.map(item => ({ 139 | forum_id: item.forum_id, 140 | forum_name: item.forum_name, 141 | sign_bonus_point: item.cur_score, 142 | is_sign: 1, 143 | })) 144 | return newInfo 145 | } 146 | 147 | export type SignMode = 'web' | 'app' | 'fast' 148 | 149 | export class Adapter { 150 | options: { 151 | unsigns: { fid: string, kw: string }[], 152 | BDUSS?: string, 153 | onSuccess: (result: Awaited>) => void, 154 | } 155 | 156 | constructor(options: Adapter['options']) { 157 | this.options = { ...options } 158 | this.options.unsigns = [...this.options.unsigns] 159 | } 160 | 161 | /** 162 | * 签到 163 | * @param mode 签到方式 164 | * @returns 签到失败列表 165 | */ 166 | async sign(mode: SignMode) { 167 | let Task: typeof WebTask | typeof AppTask 168 | let limit: number 169 | 170 | switch (mode) { 171 | case 'web': 172 | Task = WebTask 173 | // 网页签到要 1 个个来,太快会被禁止一段时间 174 | limit = 1 175 | break 176 | 177 | case 'app': 178 | case 'fast': 179 | if (!this.options.BDUSS) { 180 | throw new Error('签到方式为 app 时 BDUSS 不能为空') 181 | } 182 | Task = AppTask 183 | // 限制 3 个任务,大于 3 个签到失败的概率好像大点了 184 | limit = 3 185 | break 186 | 187 | default: 188 | // 类型检查 189 | return ((e: never) => { throw new Error(e) })(mode) 190 | } 191 | 192 | const { unsigns } = this.options 193 | 194 | if (mode === 'fast') { 195 | try { 196 | const data = await batch({ 197 | BDUSS: this.options.BDUSS!, 198 | forum_ids: unsigns.map(unsign => unsign.fid), 199 | }) 200 | for (let index = unsigns.length - 1; index >= 0; index--) { 201 | const unsign = unsigns[index] 202 | const found = data.find(item => item.forum_id === unsign.fid) 203 | if (found) { 204 | this.options.onSuccess({ 205 | fid: found.forum_id, 206 | kw: found.forum_name, 207 | data: found, 208 | }) 209 | unsigns.splice(index, 1) 210 | } 211 | } 212 | } catch (error) { 213 | logError.force('批量签到失败', error) 214 | } 215 | } 216 | warn('待签', unsigns) 217 | 218 | // eslint-disable-next-line @typescript-eslint/no-this-alias 219 | const self = this 220 | const failList: typeof unsigns = [] 221 | const queue = new Queue({ limit }) 222 | 223 | queue.enqueue(unsigns.map(unsign => { 224 | const task = new Task({ 225 | fid: unsign.fid, 226 | kw: unsign.kw, 227 | BDUSS: this.options.BDUSS!, 228 | }) 229 | 230 | return async function callback() { 231 | try { 232 | const result = await task.execute() 233 | self.options.onSuccess(result) 234 | } catch (error: any) { 235 | logError.force('签到失败', error, error.response, error.info) 236 | // 失败重签 1 次 237 | if (task.fail <= 1) { 238 | queue.enqueue(callback) 239 | } else { 240 | failList.push(unsign) 241 | } 242 | } 243 | } 244 | })) 245 | await queue.run() 246 | 247 | return failList 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/scripts/tieba/store.ts: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | // 用来解决类型问题 4 | export default store as { 5 | BDUSS?: string 6 | size?: 'small' | 'normal' | 'large' 7 | /** 列表是否倒序展示 */ 8 | is_reverse?: boolean 9 | /** 模拟 app */ 10 | is_simulate?: boolean 11 | /** 自动签到 */ 12 | is_complete?: boolean 13 | } 14 | -------------------------------------------------------------------------------- /src/scripts/tieba/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface WebApiLikeForumResponse { 3 | error: string 4 | no: number 5 | data: { 6 | /** 吧列表 */ 7 | like_forum: { 8 | /** 吧名 */ 9 | forum_name: string 10 | /** 用户等级 */ 11 | user_level: string 12 | /** 经验值 */ 13 | user_exp: string 14 | /** 吧 id */ 15 | forum_id: number 16 | /** 是否关注 */ 17 | is_like: boolean 18 | /** 0未签到 1已签到 */ 19 | is_sign: 0 | 1 20 | /** 不知道什么来的 */ 21 | favo_type: number 22 | }[] 23 | /** 调用接口需要传这个 tbs */ 24 | tbs: string 25 | /** 也许是用户 id */ 26 | uid: number 27 | /** 不知道什么来的 */ 28 | itb_tbs: string 29 | /** 不知道什么来的 */ 30 | ubs_sample_ids: string 31 | /** 不知道什么来的 */ 32 | ubs_abtest_config: { 33 | sid: string 34 | }[] 35 | } 36 | } 37 | 38 | export interface WebApiSignResponse { 39 | errno: number 40 | errmsg: string 41 | sign_version: number 42 | is_block: number 43 | finfo: { 44 | forum_info: { 45 | forum_id: number 46 | forum_name: string 47 | } 48 | current_rank_info: { 49 | sign_count: number 50 | } 51 | } 52 | uinfo: { 53 | user_id: number 54 | is_sign_in: number 55 | user_sign_rank: number 56 | sign_time: number 57 | cont_sign_num: number 58 | total_sign_num: number 59 | cout_total_sing_num: number 60 | hun_sign_num: number 61 | total_resign_num: number 62 | is_org_name: number 63 | } 64 | /** 错误码 成功 0,签过 1101,签太快 1102,其它失败 */ 65 | no: number 66 | /** 错误信息 错误的时候才会有 */ 67 | error?: string 68 | } 69 | 70 | export interface AppApiLikeForumResponse { 71 | ctime: number 72 | error_code: string 73 | logid: number 74 | server_time: string 75 | time: number 76 | forum_list: { 77 | /** 吧 id */ 78 | id: string 79 | /** 吧名 */ 80 | name: string 81 | /** 等级 id */ 82 | level_id: string 83 | /** 等级名称 */ 84 | level_name: string 85 | /** 经验值 */ 86 | cur_score: string 87 | /** 当前等级升满所需经验值 */ 88 | levelup_score: string 89 | /** 吧头像,web 端为空 */ 90 | avatar: '' 91 | /** 不知道什么来的 */ 92 | favo_type: string 93 | /** 不知道什么来的,web 端为空 */ 94 | slogan: '' 95 | }[] 96 | } 97 | 98 | export interface AppApiSignResponse { 99 | user_info: { 100 | user_id: string; 101 | is_sign_in: string; 102 | user_sign_rank: string; 103 | sign_time: string; 104 | cont_sign_num: string; 105 | total_sign_num: string; 106 | cout_total_sing_num: string; 107 | hun_sign_num: string; 108 | total_resign_num: string; 109 | is_org_name: string; 110 | sign_bonus_point: string; 111 | miss_sign_num: string; 112 | level_name: string; 113 | levelup_score: string; 114 | last_level_score: string; 115 | last_score_left: string; 116 | last_level_name: string; 117 | last_level: string; 118 | /** 吧所有等级信息 */ 119 | all_level_info: { 120 | /** 等级 */ 121 | id: string 122 | /** 等级名称 */ 123 | name: string 124 | /** 该等级所需经验值 */ 125 | score: string 126 | }[]; 127 | }; 128 | contri_info: any[]; 129 | server_time: string; 130 | time: number; 131 | ctime: number; 132 | logid: number; 133 | /** 错误码 成功 0,签过 160002,签太快 340011,其它失败 */ 134 | error_code: string; 135 | /** 错误信息 错误的时候才会有 */ 136 | error_msg?: string 137 | } 138 | 139 | export interface AppApiBatchSignResponse { 140 | /** 批量签到的结果,只返回签到成功 */ 141 | info: { 142 | forum_id: string 143 | forum_name: string 144 | /** 0未签到 1已签到 */ 145 | signed: '0' | '1' 146 | is_on: string 147 | is_filter: string 148 | /** 连续签到天数 */ 149 | sign_day_count: string 150 | /** 获得经验 */ 151 | cur_score: string 152 | error: { 153 | err_no: string 154 | usermsg: string 155 | errmsg: string 156 | } 157 | }[] 158 | show_dialog: string 159 | sign_notice: string 160 | is_timeout: string 161 | timeout_notice: string 162 | error: { 163 | errno: string 164 | errmsg: string 165 | usermsg: string 166 | } 167 | server_time: string 168 | time: number 169 | ctime: number 170 | logid: number 171 | error_code: string 172 | } 173 | 174 | export type LikeForumData = 175 | WebApiLikeForumResponse['data']['like_forum'][number] 176 | & Pick 177 | & { 178 | /** 签到所获得的经验值 */ 179 | sign_bonus_point?: string 180 | } 181 | 182 | /** window.PageData */ 183 | export interface PageData { 184 | tbs: string 185 | } 186 | -------------------------------------------------------------------------------- /src/scripts/tieba/ui/ForumList.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed } from 'vue' 2 | import { useGMvalue } from '@/composables/use-gm-value' 3 | import Input from '@/components/input' 4 | import Button from '@/components/button' 5 | import type { PropType } from 'vue' 6 | import type { LikeForumData } from '../types' 7 | 8 | const ForumList = defineComponent({ 9 | props: { 10 | dataSource: { 11 | type: Array as PropType, 12 | required: true, 13 | }, 14 | size: { 15 | type: String, 16 | required: true, 17 | }, 18 | }, 19 | emits: ['clickSize'], 20 | setup(props, { emit }) { 21 | const keyword = useGMvalue('keyword', '') 22 | const isReverse = useGMvalue('is_reverse', false) 23 | 24 | const diaplayForums = computed(() => { 25 | let newList = [...props.dataSource] 26 | isReverse.value && newList.reverse() 27 | if (keyword.value) { 28 | // 忽略大小写 29 | newList = newList.filter(forum => new RegExp(keyword.value, 'i').test(forum.forum_name)) 30 | } 31 | return newList 32 | }) 33 | 34 | const counter = computed(() => ({ 35 | total: props.dataSource.length, 36 | // eslint-disable-next-line camelcase 37 | signed: props.dataSource.filter(({ is_sign }) => is_sign).length, 38 | })) 39 | 40 | function changeReverse() { 41 | isReverse.value = !isReverse.value 42 | } 43 | 44 | function expTitle(item: LikeForumData) { 45 | const MAX_EXP_DAILY = 8 46 | const needed = +item.levelup_score - +item.user_exp 47 | return `距离升级还需要${needed}经验,若每天+${MAX_EXP_DAILY},还需要${Math.ceil(needed / MAX_EXP_DAILY)}天` 48 | } 49 | 50 | return () => (<> 51 | { 52 | props.dataSource.length > 0 &&
    53 |
    54 | 60 | 63 |
    64 |
      65 | { 66 | diaplayForums.value.map(item => ( 67 |
    • 68 | 73 | {item.forum_name} 74 | 75 | {item.is_sign ? ' √' : ''} 76 | 77 | {item.user_level}级 78 | 79 | {item.sign_bonus_point ? ('+' + item.sign_bonus_point) : ''} 80 | 81 | {item.user_exp}/{item.levelup_score} 82 | 83 |
    • 84 | )) 85 | } 86 |
    87 | {/* 太多时显示搜索 */} 88 | { 89 | props.dataSource.length > 25 && 95 | } 96 |
    97 | } 98 | ) 99 | }, 100 | }) 101 | 102 | export default ForumList 103 | -------------------------------------------------------------------------------- /src/scripts/tieba/ui/index.scss: -------------------------------------------------------------------------------- 1 | @import '~@/components/index'; 2 | 3 | @include var; 4 | 5 | #inject-sign { 6 | // 列表宽度 7 | --container-width: 19vw; 8 | // 列表距离右边距离 9 | --container-right: 10px; 10 | 11 | @include reset; 12 | 13 | box-sizing: border-box; 14 | color: var(--skr-text-regular-color); 15 | 16 | &.normal, 17 | &.large { 18 | --container-width: 21vw; 19 | } 20 | 21 | *::-webkit-scrollbar { 22 | background: #f2f2f2; 23 | height: 8px; 24 | width: 8px; 25 | } 26 | 27 | *::-webkit-scrollbar-thumb { 28 | background: #c1c1c1; 29 | border: 0; 30 | } 31 | 32 | a { 33 | color: var(--skr-primary-color); 34 | } 35 | 36 | button { 37 | background-image: none; 38 | } 39 | 40 | // 主按钮 41 | .control { 42 | align-items: center; 43 | bottom: 12px; 44 | contain: content; 45 | display: flex; 46 | position: fixed; 47 | right: max(calc(var(--container-right) + var(--container-width) / 2), 150px); 48 | transform: translateX(50%); 49 | transition: bottom 0.3s, right 0.15s; 50 | user-select: none; 51 | z-index: 500; 52 | 53 | .settings { 54 | display: inline-flex; 55 | flex: 1; 56 | flex-wrap: wrap; 57 | margin-left: 10px; 58 | max-width: 156px; 59 | } 60 | } 61 | 62 | // 列表区域 63 | .forums-container { 64 | background: #fafafa; 65 | bottom: 60px; 66 | box-shadow: 0 2px 4px rgb(0 0 0 / 20%); 67 | contain: content; 68 | display: flex; 69 | flex-direction: column; 70 | max-height: calc(100vh - 124px); 71 | min-width: 280px; 72 | padding: 5px; 73 | position: fixed; 74 | right: var(--container-right); 75 | transition: transform 0.3s, bottom 0.3s, width 0.15s, box-shadow 0.3s; 76 | width: var(--container-width); 77 | z-index: 2; 78 | 79 | &:hover { 80 | box-shadow: 0 2px 4px 3px rgb(0 0 0 / 10%); 81 | } 82 | } 83 | 84 | &.forums-hide { 85 | .forums-container { 86 | bottom: 0; 87 | transform: translateY(calc(100% - 35px)); 88 | } 89 | 90 | .control { 91 | bottom: 40px; 92 | } 93 | } 94 | 95 | &.cover { 96 | .forums-container { 97 | z-index: 9999; 98 | } 99 | } 100 | 101 | header { 102 | display: flex; 103 | margin-bottom: 4px; 104 | } 105 | 106 | .reverse-btn { 107 | flex: 1; 108 | text-align: center; 109 | } 110 | 111 | .resize-btn { 112 | flex: none; 113 | margin-left: 4px; 114 | } 115 | 116 | li { 117 | border-bottom: 1px solid rgb(221 221 221 / 40%); 118 | cursor: default; 119 | display: flex; 120 | transition: height 0.15s; 121 | 122 | &:hover { 123 | background-color: #f0f8ff; 124 | } 125 | 126 | > * { 127 | line-height: 2.325em; 128 | } 129 | 130 | a { 131 | flex: 1; 132 | overflow: hidden; 133 | padding-left: 0.2em; 134 | text-overflow: ellipsis; 135 | white-space: nowrap; 136 | } 137 | 138 | .signed { 139 | width: 0.9em; 140 | } 141 | 142 | .level { 143 | width: 2.4em; 144 | } 145 | 146 | .gain { 147 | width: 1.8em; 148 | } 149 | 150 | .exp { 151 | flex: none; 152 | width: 6.7em; 153 | } 154 | } 155 | 156 | ul { 157 | overflow-x: hidden; 158 | 159 | &.small { 160 | li { 161 | height: 24px; 162 | } 163 | } 164 | 165 | &.normal { 166 | li { 167 | font-size: 13px; 168 | height: 28px; 169 | } 170 | } 171 | 172 | &.large { 173 | li { 174 | font-size: 14px; 175 | height: 32px; 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/scripts/tieba/ui/index.tsx: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | import { useGMvalue } from '@/composables/use-gm-value' 3 | import store from '../store' 4 | import { mountComponent } from '@/utils/mount-component' 5 | import { error as logError } from '@/utils/log' 6 | import { mergeLikeForum } from '../api' 7 | import { Adapter, type SignMode } from '../sign' 8 | import { getElementsInPage } from '../utils' 9 | import Checkbox from '@/components/checkbox' 10 | import Button from '@/components/button' 11 | import ForumList from './ForumList' 12 | import type { LikeForumData } from '../types' 13 | import './index.scss' 14 | 15 | const sizeTick = (function * () { 16 | const sizes = ['small', 'normal', 'large'] as const 17 | let currSize = store.size ?? 'small' 18 | let index = sizes.findIndex(v => v === currSize) 19 | while (true) { 20 | (index >= sizes.length) && (index = 0) 21 | currSize = sizes[index++] 22 | store.size = currSize 23 | yield currSize 24 | } 25 | })() 26 | 27 | export function createUI() { 28 | mountComponent({ 29 | setup() { 30 | const state = reactive({ 31 | loading: false, 32 | size: sizeTick.next().value, 33 | likeForums: [] as LikeForumData[], 34 | }) 35 | const isSimulate = useGMvalue('is_simulate', false) 36 | const isForumsHide = useGMvalue('is_forums_hide', false) 37 | const isComplete = useGMvalue('is_complete', false) 38 | const isCover = useGMvalue('is_cover', false) 39 | const toastTime = useGMvalue('toast_time', undefined) 40 | let setSign: (key: string) => void 41 | 42 | function run(toastVisible = true) { 43 | if (state.loading) { 44 | Toast('签到中') 45 | return 46 | } 47 | 48 | const { unsigns, signs, setSign: _setSign } = getElementsInPage() 49 | setSign = _setSign 50 | 51 | if (unsigns.length === 0) { 52 | const now = new Date() 53 | // 避免每次都提示 54 | if (toastVisible || toastTime.value === undefined || new Date(toastTime.value).getDate() < now.getDate()) { 55 | Toast.success('所有吧已签到') 56 | } 57 | toastTime.value = +now 58 | return 59 | } 60 | 61 | let mode: SignMode 62 | if (isSimulate.value) { 63 | if (!store.BDUSS) { 64 | Toast.error('请先输入 BDUSS 或 BDUSS_BFESS') 65 | return 66 | } 67 | // 签了 20 个以上视为用过批量签到 68 | if (signs.length >= 20) { 69 | mode = 'app' 70 | } else { 71 | mode = 'fast' 72 | } 73 | } else { 74 | mode = 'web' 75 | } 76 | 77 | state.loading = true 78 | const toast = Toast('开始签到,请等待', 0) 79 | new Adapter({ 80 | unsigns, 81 | BDUSS: store.BDUSS, 82 | onSuccess({ fid, kw, data }) { 83 | const key = fid || kw 84 | if (key) setSign(key) 85 | if (fid && data) updateLikeForum(fid, data) 86 | }, 87 | }) 88 | .sign(mode) 89 | .then(async () => { 90 | if (store.BDUSS) await fetchForums() 91 | // 以页面为准,因为有时签到失败但实际上是成功的 92 | const failList = getElementsInPage().unsigns 93 | const length = failList.length 94 | if (length > 0) { 95 | Toast.warning(`签到成功,失败${length}个:${failList.map(v => v.kw).join('、')}`, 0) 96 | } else { 97 | Toast.success('签到成功') 98 | } 99 | }) 100 | .finally(() => { 101 | toast.close() 102 | state.loading = false 103 | }) 104 | } 105 | 106 | function updateLikeForum(fid: LikeForumData['forum_id'] | string, forum: Partial) { 107 | const found = state.likeForums.find(item => +fid === +item.forum_id) 108 | if (!found) return 109 | if (forum.sign_bonus_point) { 110 | found.user_exp = String(Number(found.user_exp) + Number(forum.sign_bonus_point)) 111 | } 112 | Object.assign(found, forum) 113 | } 114 | 115 | // 未签到的靠前 116 | function sort() { 117 | state.likeForums.sort((a, b) => { 118 | if (!a.is_sign && b.is_sign) return -1 119 | return 0 120 | }) 121 | } 122 | 123 | function fetchForums() { 124 | return mergeLikeForum().then(forums => { 125 | state.likeForums = forums 126 | sort() 127 | forums.forEach(forum => { 128 | // 签到可能失败,以这里为准 129 | if (forum.is_sign === 1) { 130 | setSign?.(forum.forum_name) 131 | } 132 | }) 133 | }).catch(error => { 134 | // 爆炸了也没什么需要处理的,这里就不抛了 135 | logError.force(error) 136 | Toast.error('获取贴吧列表失败。。请刷新重试~', 0) 137 | }) 138 | } 139 | 140 | function onSimulateChange(checked: boolean) { 141 | if (checked === false) { 142 | isSimulate.value = checked 143 | return 144 | } 145 | 146 | const { BDUSS } = store 147 | const result = window.prompt('请输入 F12 -> 应用(Application) -> Cookies 中的【BDUSS 或 BDUSS_BFESS】', BDUSS || undefined) 148 | if (result) { 149 | store.BDUSS = result 150 | isSimulate.value = true 151 | location.reload() 152 | } else { 153 | isSimulate.value = false 154 | } 155 | } 156 | 157 | (async () => { 158 | // 获取列表后再自动签到 159 | if (store.BDUSS) { 160 | await fetchForums() 161 | } 162 | if (isComplete.value) { 163 | run(false) 164 | } 165 | })() 166 | 167 | return () => ( 168 |
    176 |
    177 | 185 |
    186 | 191 | 模拟APP 192 | 193 | 194 | 自动签到 195 | 196 | { 197 | state.likeForums.length > 0 && ( 198 | <> 199 | 200 | 隐藏列表 201 | 202 | 203 | 防止遮挡 204 | 205 | 206 | ) 207 | } 208 |
    209 |
    210 | 211 | { 215 | state.size = sizeTick.next().value 216 | }} 217 | /> 218 |
    219 | ) 220 | }, 221 | }) 222 | } 223 | -------------------------------------------------------------------------------- /src/scripts/tieba/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { $$ } from '@/utils/selector' 2 | import { parse } from '@/utils/querystring' 3 | import type { PageData } from '../types' 4 | 5 | export * from './request' 6 | export * from './signature' 7 | 8 | const jQuery: JQueryStatic = (unsafeWindow as any).jQuery 9 | 10 | /** 11 | * 获取页面上的元素 12 | */ 13 | export function getElementsInPage() { 14 | const $moreforumEl = jQuery('#moreforum') 15 | // 必须先触发才能获取剩下的吧 16 | $moreforumEl.trigger('mouseenter') 17 | // 侧边的吧 18 | const likeUnsignEls = $$('#likeforumwraper .unsign') as NodeListOf 19 | const likeSignEls = $$('#likeforumwraper .sign') as NodeListOf 20 | // 查看更多的吧 21 | const alwayUnsignEls = $$('#alwayforum-wraper .unsign') as NodeListOf 22 | const alwaySignEls = $$('#alwayforum-wraper .sign') as NodeListOf 23 | // 关闭面板 24 | $moreforumEl.trigger('click') 25 | 26 | const unsigns = [...likeUnsignEls, ...alwayUnsignEls].map(element => { 27 | const fid = element.dataset.fid! 28 | const { kw } = parse(element.href) 29 | return { fid, kw, element } 30 | }) 31 | 32 | const unsignsMap = unsigns.reduce((map, unsign) => { 33 | // id 与 吧名 作为 key 34 | return map 35 | .set(unsign.fid, unsign.element) 36 | .set(unsign.kw, unsign.element) 37 | }, new Map()) 38 | 39 | return { 40 | /** 查看更多按钮 */ 41 | moreForum: $moreforumEl, 42 | /** 未签到的元素 */ 43 | unsigns, 44 | /** 签到的元素 */ 45 | signs: [...likeSignEls, ...alwaySignEls], 46 | setSign(key: string) { 47 | // 替换成已签到样式 48 | unsignsMap.get(key)?.classList.replace('unsign', 'sign') 49 | }, 50 | } 51 | } 52 | 53 | /** 54 | * 获取 PageData 55 | */ 56 | export function getPageData() { 57 | return (unsafeWindow as any).PageData as PageData 58 | } 59 | 60 | /** 61 | * 编码请求对象的值 62 | * 63 | * kw 存在“+”时会有问题 64 | * fix: https://github.com/sakura-flutter/tampermonkey-scripts/issues/635 65 | */ 66 | export function encodeRequestParams(obj: Record) { 67 | const newObj = { ...obj } 68 | newObj.kw &&= encodeURIComponent(newObj.kw) 69 | return newObj 70 | } 71 | -------------------------------------------------------------------------------- /src/scripts/tieba/utils/request.ts: -------------------------------------------------------------------------------- 1 | import * as qs from '@/utils/querystring' 2 | import { error as logError } from '@/utils/log' 3 | 4 | export class ResponseError extends Error { 5 | readonly name = 'ResponseError' 6 | response 7 | info 8 | constructor(msg = '未知错误', response?: Record, info?: any) { 9 | super(msg) 10 | this.response = response 11 | this.info = info 12 | } 13 | } 14 | 15 | /** 16 | * 跨域请求,依赖 GM_xmlhttpRequest 17 | * 18 | * 15s 超时,0 点高峰期失败概率大,BD 是 1 分钟超时,实际上不必等这么久 19 | */ 20 | export function GMRequest(url: string, options: Omit) { 21 | return new Promise((resolve, reject) => { 22 | GM_xmlhttpRequest({ 23 | timeout: 1000 * 15, 24 | ...options, 25 | url, 26 | onload(res) { 27 | let error 28 | let response 29 | try { 30 | response = JSON.parse(res.response) 31 | } catch (e) { 32 | response = res.response 33 | } 34 | 35 | if (response == null) { 36 | error = new ResponseError('无响应', response, { ...options, ...res }) 37 | } else if (response?.error_code !== '0') { 38 | error = new ResponseError(response.error_msg, response, { ...options, ...res }) 39 | } 40 | 41 | error ? reject(error) : resolve(response) 42 | }, 43 | onerror(error) { 44 | logError.force(error) 45 | reject(error) 46 | }, 47 | }) 48 | }) 49 | } 50 | 51 | GMRequest.post = function(url: string, data: Tampermonkey.Request['data'], options: Omit) { 52 | return GMRequest(url, { 53 | ...options, 54 | data, 55 | method: 'POST', 56 | }) 57 | } 58 | 59 | /** 60 | * 普通请求 61 | */ 62 | export function request(url: RequestInfo | URL, options?: RequestInit): Promise { 63 | return fetch(url, options) 64 | .then(response => response.json()) 65 | .then(resJson => { 66 | if (resJson.no !== 0) { 67 | throw new ResponseError(resJson.error, resJson, { url, ...options }) 68 | } 69 | return resJson 70 | }) 71 | } 72 | 73 | request.post = function(url: RequestInfo | URL, data?: any, options: RequestInit = {}) { 74 | const headers = new Headers(options.headers) 75 | let body = data 76 | if (data) { 77 | if (headers.get('Content-Type')?.includes('application/x-www-form-urlencoded') && Object.prototype.toString.call(data) === '[object Object]') { 78 | body = qs.stringify(data) 79 | } 80 | if (headers.get('Content-Type')?.includes('application/json') && Object.prototype.toString.call(data) === '[object Object]') { 81 | body = JSON.stringify(data) 82 | } 83 | } 84 | 85 | return request(url, { 86 | ...options, 87 | method: 'POST', 88 | headers, 89 | body, 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /src/scripts/tieba/utils/signature.ts: -------------------------------------------------------------------------------- 1 | import MD5 from 'crypto-js/md5' 2 | 3 | export const FAKE_VERSION = '11.8.8.0' 4 | 5 | type Obj = Record 6 | 7 | export function makeFakeParams(obj?: Obj) { 8 | // 不要动这些字段 9 | return Object.assign({ 10 | _client_type: 4, // prohibit 11 | _client_version: FAKE_VERSION, 12 | _phone_imei: '0'.repeat(15), 13 | model: 'HUAWEI P40', // HUAWEI加油 ヾ(◍°∇°◍)ノ゙ 14 | net_type: 1, 15 | stErrorNums: 1, 16 | stMethod: 1, 17 | stMode: 1, 18 | stSize: 320, 19 | stTime: 117, 20 | stTimesNum: 1, 21 | timestamp: Date.now(), 22 | }, obj) 23 | } 24 | 25 | export function sign(payload: Obj) { 26 | const sortKeys = Object.keys(payload).sort() 27 | let str = sortKeys.reduce((acc, key) => (acc += `${key}=${payload[key]}`), '') 28 | str += 'tiebaclient!!!' 29 | return MD5(str).toString() 30 | } 31 | 32 | export function signRequestParams(params: Obj, isFake = true) { 33 | if (isFake) { 34 | params = makeFakeParams(params) 35 | } 36 | const signed = { 37 | ...params, 38 | sign: sign(params), 39 | } 40 | return signed 41 | } 42 | -------------------------------------------------------------------------------- /src/scripts/view-ui/hide.lazy.scss: -------------------------------------------------------------------------------- 1 | .app-left .ivu-menu .ivu-menu-item { 2 | &[data-visible='hidden'] { 3 | display: none; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/scripts/view-ui/index.ts: -------------------------------------------------------------------------------- 1 | import { warn } from '@/utils/log' 2 | import { $$ } from '@/utils/selector' 3 | import './ui' 4 | 5 | function main() { 6 | // 物料 7 | const storeBadge = '.navigate-item-badge-store' 8 | // pro 9 | const proBadge = '.navigate-item-badge-pro' 10 | const prefixSelector = '.app-left .ivu-menu ' 11 | const selector = Array.from([storeBadge, proBadge], item => prefixSelector + item).join() 12 | const badgeEls = $$(selector) as NodeListOf 13 | 14 | warn(selector, badgeEls) 15 | 16 | badgeEls.forEach(el => { 17 | let { parentElement } = el 18 | while (parentElement) { 19 | const { tagName } = parentElement 20 | if (tagName === 'A' && parentElement.classList.contains('ivu-menu-item')) { 21 | // 添加标记 22 | parentElement.dataset.visible = 'hidden' 23 | break 24 | } 25 | if (tagName === 'BODY') break 26 | parentElement = parentElement.parentElement 27 | } 28 | }) 29 | } 30 | 31 | setTimeout(main, 500) 32 | -------------------------------------------------------------------------------- /src/scripts/view-ui/ui.scss: -------------------------------------------------------------------------------- 1 | @import '~@/components/index'; 2 | 3 | @include var; 4 | 5 | #hide-menu-control-js { 6 | bottom: 40px; 7 | contain: content; 8 | left: 0; 9 | padding: 10px 0; 10 | position: fixed; 11 | z-index: 50; 12 | 13 | p { 14 | writing-mode: vertical-lr; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/scripts/view-ui/ui.tsx: -------------------------------------------------------------------------------- 1 | import { watchEffect } from 'vue' 2 | import { mountComponent } from '@/utils/mount-component' 3 | import { useGMvalue } from '@/composables/use-gm-value' 4 | import Button from '@/components/button' 5 | import styles from './hide.lazy.scss' 6 | import './ui.scss' 7 | 8 | mountComponent({ 9 | setup() { 10 | const hidden = useGMvalue('menu_hidden', false) 11 | watchEffect(() => { 12 | hidden.value ? styles.use() : styles.unuse() 13 | }) 14 | 15 | function toggle() { 16 | hidden.value = !hidden.value 17 | } 18 | 19 | return () => ( 20 | 28 | ) 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /src/scripts/widescreen/control.scss: -------------------------------------------------------------------------------- 1 | @import '~@/components/index'; 2 | 3 | @include var; 4 | 5 | .inject-widescreen-js { 6 | align-items: center; 7 | contain: layout; 8 | display: flex; 9 | flex-direction: column; 10 | opacity: 0.5; 11 | position: fixed; 12 | right: 7vw; 13 | top: 150px; 14 | transition: opacity var(--skr-transition-duration-normal); 15 | z-index: 99; 16 | 17 | label { 18 | align-items: center; 19 | bottom: 0; 20 | cursor: pointer; 21 | display: flex; 22 | font-size: 14px; 23 | margin: 0; 24 | padding: 0; 25 | position: absolute; 26 | transform: translateY(-10px); 27 | transition: transform var(--skr-transition-duration-normal); 28 | z-index: -1; 29 | } 30 | 31 | &:hover { 32 | opacity: 1; 33 | 34 | label { 35 | transform: translateY(100%); 36 | } 37 | } 38 | 39 | button { 40 | // 防止某些网站button有默认background-image 41 | background-image: none !important; 42 | } 43 | 44 | input { 45 | margin: 0 2px 0 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/scripts/widescreen/control.tsx: -------------------------------------------------------------------------------- 1 | import { reactive, watchEffect } from 'vue' 2 | import { mountComponent } from '@/utils/mount-component' 3 | import { useGMvalue } from '@/composables/use-gm-value' 4 | import globalStore from '@/store' 5 | import Button from '@/components/button' 6 | import './control.scss' 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | const noop = () => {} 10 | 11 | interface ControlOptions { 12 | store: Record 13 | /** 要执行的函数 */ 14 | execute?(): void 15 | /** 是否可见(后续用show hide控制) */ 16 | visible?: boolean 17 | /** 是否显示通知 */ 18 | silent?: boolean 19 | } 20 | 21 | /** 宽屏开关 */ 22 | export default function createControl(options: ControlOptions) { 23 | const { store, execute = noop, visible = true, silent = false } = options 24 | 25 | const { instance } = mountComponent({ 26 | setup(_, { expose }) { 27 | const state = reactive({ 28 | // 总开关 29 | uiVisible: useGMvalue('ui_visible', true), 30 | visible, 31 | loose: store.loose || false, 32 | }) 33 | 34 | function notify() { 35 | (globalStore.notify_enabled ?? false) && Toast('已宽屏处理') 36 | } 37 | function toggle() { 38 | store.enabled = !store.enabled 39 | location.reload() 40 | } 41 | 42 | expose({ 43 | notify, 44 | show: () => { state.visible = true }, 45 | hide: () => { state.visible = false }, 46 | }) 47 | 48 | if (store.enabled) { 49 | watchEffect(() => { 50 | store.loose = state.loose 51 | document.documentElement.classList[state.loose ? 'add' : 'remove']('inject-widescreen-loose-js') 52 | }) 53 | execute() 54 | !silent && notify() 55 | } 56 | 57 | return () => ( 58 | <> 59 | {state.uiVisible && state.visible &&
    60 | 68 | {store.enabled && } 75 |
    } 76 | 77 | ) 78 | }, 79 | }) 80 | 81 | return instance as unknown as { 82 | notify(): void 83 | show(): void 84 | hide(): void 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/scripts/widescreen/index.ts: -------------------------------------------------------------------------------- 1 | import { checker } from '@/utils/compatibility' 2 | import * as readyState from '@/utils/ready-state' 3 | import { warn } from '@/utils/log' 4 | import globalStore, { createStore as _createStore } from '@/store' 5 | import sites from './sites' 6 | import createControl from './control' 7 | import type { Site } from './types' 8 | 9 | // 主函数 10 | function main() { 11 | if (!checker()) return 12 | 13 | GM_registerMenuCommand('宽屏通知', function() { 14 | const nextStatus = !(globalStore.notify_enabled ?? false) 15 | Toast.success(nextStatus ? '已开启通知' : '已关闭通知') 16 | globalStore.notify_enabled = nextStatus 17 | }) 18 | GM_registerMenuCommand('控制按钮', function() { 19 | const nextStatus = !(globalStore.ui_visible ?? true) 20 | Toast.success(nextStatus ? '已显示按钮' : '已隐藏按钮') 21 | globalStore.ui_visible = nextStatus 22 | }) 23 | 24 | new App(sites).boot() 25 | } 26 | 27 | class App { 28 | #sites 29 | constructor(sites: Site[]) { 30 | this.#sites = sites 31 | } 32 | 33 | boot() { 34 | const briefURL = location.host + location.pathname 35 | 36 | this.#sites.forEach(async site => { 37 | const { name, namespace, test, use } = site 38 | if (!this.#includes(test, briefURL)) return 39 | 40 | const { readyState: state } = site 41 | if (state) await readyState[state]() 42 | // fix: 罕见情况下会获取不到 head,原因未知 43 | // 偶尔会在知乎中出现 44 | if (document.head == null) await readyState.interactive() 45 | 46 | const config = use({ 47 | createControl, 48 | store: createStore(namespace), 49 | }) 50 | warn(name) 51 | config.handler() 52 | }) 53 | } 54 | 55 | #includes(test: Site['test'], url: string) { 56 | return ([] as Site['test'][]).concat(test).some(item => { 57 | if (item instanceof RegExp) return item.test(url) 58 | if (typeof item === 'boolean') return item 59 | return false 60 | }) 61 | } 62 | } 63 | 64 | // 存储 65 | function createStore(namespace: string) { 66 | const store = new Proxy(_createStore(namespace), { 67 | get(target, property: string, receiver) { 68 | let value = Reflect.get(target, property, receiver) 69 | if (property === 'enabled') { 70 | // 默认开启 71 | value ??= true 72 | } 73 | return value 74 | }, 75 | }) 76 | return store 77 | } 78 | 79 | main() 80 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/bbs-mihoyo-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $gap: 20px; // 每个区域之间的间距 4 | 5 | @include substrate(1320px, 82vw, 1330px) { 6 | .root-page-container { 7 | /* 米游社根据 类名 作为页面间区分 */ 8 | 9 | /* 文章页 */ 10 | > .mhy-article-page { 11 | display: flex; 12 | width: var(--inject-page-width); 13 | 14 | /* 主体 */ 15 | .mhy-layout__main { 16 | flex: 1; 17 | padding-right: $gap; 18 | } 19 | } 20 | 21 | /* 左侧悬浮操作 */ 22 | .mhy-article-actions { 23 | margin-left: calc(var(--inject-page-width) / 2 * -1); 24 | transform: translate(calc(-100% - 10px)); // 间距 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/bbs-mihoyo-com/index.ts: -------------------------------------------------------------------------------- 1 | import { $$ } from '@/utils/selector' 2 | import { onVisible } from '@/utils/visibility-state' 3 | import styles from './index.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const mihoyoBBS:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function replaceImgURL() { 9 | onVisible(() => { 10 | // 文章中的图片原图显示 11 | ($$('.mhy-article-page__content .ql-image-box img:not([replaced=true])') as NodeListOf).forEach(img => { 12 | const original = img.getAttribute('large') 13 | if (!original) return 14 | 15 | img.src = original 16 | img.setAttribute('replaced', 'true') // 标记 17 | }) 18 | }) 19 | } 20 | 21 | createControl({ 22 | store, 23 | execute() { 24 | replaceImgURL() 25 | styles.use() 26 | }, 27 | }) 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/bcy-net/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1580px, 75vw, 1440px) { 4 | .container .row { 5 | width: var(--inject-page-width); 6 | } 7 | 8 | .container .row .col-big { 9 | flex: 0.97; 10 | } 11 | 12 | /* 文章头部信息 */ 13 | .detail-main header { 14 | width: auto !important; 15 | } 16 | 17 | /* 相册 */ 18 | .container .row .col-big .album { 19 | width: 100%; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/bcy-net/index.ts: -------------------------------------------------------------------------------- 1 | import { $$ } from '@/utils/selector' 2 | import * as readyState from '@/utils/ready-state' 3 | import styles from './index.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const banciyuan:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function execute() { 9 | readyState.interactive(() => { 10 | // eslint-disable-next-line no-constant-condition 11 | if ('It should not be enabled') return 12 | const { multi } = (unsafeWindow as any).__ssr_data.detail.post_data 13 | const imgEls = $$('.container .album .img-wrap-inner img') as NodeListOf 14 | if (multi.length !== imgEls.length) return 15 | imgEls.forEach((img, index) => { 16 | img.src = multi[index].original_path 17 | }) 18 | }) 19 | 20 | styles.use() 21 | } 22 | 23 | createControl({ store, execute }) 24 | }, 25 | }) 26 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/blog-csdn-net/index.lazy.scss: -------------------------------------------------------------------------------- 1 | /* 处理滚动后页面抖动 */ 2 | html body { 3 | height: auto; 4 | } 5 | 6 | #csdn-toolbar { 7 | position: sticky !important; 8 | top: 0; 9 | z-index: 1; 10 | } 11 | 12 | /* 烦人的登录弹窗 [○・`Д´・ ○] , 必要时battle cookies中的unlogin_scroll_step */ 13 | #passportbox, 14 | .login-mark { 15 | display: none !important; 16 | } 17 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/blog-csdn-net/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const csdn:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ 7 | store, 8 | execute() { 9 | // 关闭登录弹窗 10 | document.cookie = `unlogin_scroll_step=${Date.now()};domain=.csdn.net;path=/` 11 | 12 | styles.use() 13 | }, 14 | }) 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/crates-io/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | /* crates.io package */ 4 | @include substrate(1300px, 82vw, 1400px) { 5 | // 内容区整体宽度 6 | body > main > div:first-of-type { 7 | width: var(--inject-page-width); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/crates-io/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const crates:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/d-weibo-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1300px, 77.5vw, 1330px) { 4 | .WB_frame { 5 | display: flex; 6 | width: var(--inject-page-width) !important; 7 | } 8 | 9 | /* 内容 */ 10 | .WB_frame #plc_main { 11 | display: flex !important; 12 | flex: 1; 13 | } 14 | 15 | .WB_frame_c { 16 | flex: 1; 17 | } 18 | 19 | /* 微博类型 (更多-旅游 中出现) */ 20 | .tab_box { 21 | display: flex; 22 | } 23 | 24 | .tab_box::after { 25 | content: none; 26 | } 27 | 28 | .tab_box .fr_box { 29 | flex: 1; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/d-weibo-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const weiboDynamic:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/index.ts: -------------------------------------------------------------------------------- 1 | import { banciyuan } from './bcy-net' 2 | import { weixin } from './mp-weixin-qq-com' 3 | import { zhihuZhuanlan } from './zhuanlan-zhihu-com' 4 | import { zhihuQuestion, zhihuHome, zhihuTopic } from './zhihu-com' 5 | import { juejin } from './juejin-cn' 6 | import { crates } from './crates-io' 7 | import { jianshu } from './jianshu-com' 8 | import { baidu } from './www-baidu-com' 9 | import { tieba, tiebaForum } from './tieba-baidu-com' 10 | import { sougou } from './www-sogou-com' 11 | import { segmentfault } from './segmentfault-com' 12 | import { bilibili } from './www-bilibili-com' 13 | import { bilibiliDynamic, bilibiliDynamicDetail } from './t-bilibili-com' 14 | import { bilibiliSpace } from './space-bilibili-com' 15 | import { douban } from './www-douban-com' 16 | import { doubanSubject, doubanReview } from './movie-douban-com' 17 | import { toutiao } from './www-toutiao-com' 18 | import { weibo, weiboArticle } from './weibo-com' 19 | import { weiboDynamic } from './d-weibo-com' 20 | import { google } from './www-google-com' 21 | import { csdn } from './blog-csdn-net' 22 | import { mihoyoBBS } from './bbs-mihoyo-com' 23 | import type { Site } from '../types' 24 | 25 | const sites: Site[] = [ 26 | { 27 | name: '半次元', 28 | namespace: 'banciyuan', 29 | test: /^bcy\.net\/item\/detail\//, 30 | use: banciyuan, 31 | }, 32 | { 33 | name: '微信', 34 | namespace: 'weixin', 35 | test: /^mp\.weixin\.qq\.com\/s/, 36 | use: weixin, 37 | }, 38 | { 39 | name: '知乎专栏', 40 | namespace: 'zhihu', 41 | test: /^zhuanlan\.zhihu\.com\/p\//, 42 | use: zhihuZhuanlan, 43 | }, 44 | { 45 | name: '知乎问答', 46 | namespace: 'zhihu', 47 | test: /^www\.zhihu\.com\/question\//, 48 | use: zhihuQuestion, 49 | }, 50 | { 51 | name: '知乎', 52 | namespace: 'zhihu', 53 | test: /^www\.zhihu\.com\/(follow|hot)?$/, 54 | use: zhihuHome, 55 | }, 56 | { 57 | name: '知乎话题', 58 | namespace: 'zhihu', 59 | test: /^www\.zhihu\.com\/topic\//, 60 | use: zhihuTopic, 61 | }, 62 | { 63 | name: '掘金', 64 | namespace: 'juejin', 65 | test: /^juejin\.cn\/post\//, 66 | use: juejin, 67 | }, 68 | { 69 | name: 'Crates.io', 70 | namespace: 'crates', 71 | test: /^crates\.io\/crates\//, 72 | use: crates, 73 | }, 74 | { 75 | name: '简书', 76 | namespace: 'jianshu', 77 | test: /^www\.jianshu\.com\/p\//, 78 | use: jianshu, 79 | }, 80 | { 81 | name: '百度', 82 | namespace: 'baidu', 83 | test: /^www\.baidu\.com\/s?$/, 84 | use: baidu, 85 | }, 86 | { 87 | name: '贴吧', 88 | namespace: 'tieba', 89 | test: /^tieba\.baidu\.com\/p\//, 90 | use: tieba, 91 | }, 92 | { 93 | name: '贴吧吧页', 94 | namespace: 'tieba', 95 | test: /^tieba\.baidu\.com\/f$/, 96 | use: tiebaForum, 97 | }, 98 | { 99 | name: '搜狗', 100 | namespace: 'sougou', 101 | test: /^www\.sogou\.com\/web$/, 102 | use: sougou, 103 | }, 104 | { 105 | name: 'segmentfault', 106 | namespace: 'segmentfault', 107 | test: /^segmentfault\.com\/(a|q)\//, 108 | use: segmentfault, 109 | }, 110 | { 111 | name: 'bilibili', 112 | namespace: 'bilibili', 113 | test: /^www\.bilibili\.com\/read\/cv/, 114 | use: bilibili, 115 | }, 116 | { 117 | name: 'bilibili 动态', 118 | namespace: 'bilibili', 119 | test: /^t\.bilibili\.com\/$/, 120 | use: bilibiliDynamic, 121 | }, 122 | { 123 | name: 'bilibili 动态详情', 124 | namespace: 'bilibili', 125 | test: /^t\.bilibili\.com\/\d+$/, 126 | use: bilibiliDynamicDetail, 127 | }, 128 | { 129 | name: 'bilibili 空间', 130 | namespace: 'bilibili', 131 | test: /^space\.bilibili\.com\/212535360$/, 132 | use: bilibiliSpace, 133 | }, 134 | { 135 | name: '豆瓣', 136 | namespace: 'douban', 137 | test: [ 138 | /^www\.douban\.com\/gallery\/$/, 139 | /^www\.douban\.com\/gallery\/topic\/.+?/, 140 | /^www\.douban\.com\/note\/.+?/, 141 | ], 142 | use: douban, 143 | }, 144 | { 145 | name: '豆瓣电影 详情', 146 | namespace: 'doubanmovie', 147 | test: /^movie\.douban\.com\/subject\//, // 与剧评相关 movie.douban.com/subject/${id}/${xxx} 148 | use: doubanSubject, 149 | }, 150 | { 151 | name: '豆瓣电影 剧评详情', 152 | namespace: 'doubanmovie', 153 | test: /^movie\.douban\.com\/review\//, 154 | use: doubanReview, 155 | }, 156 | { 157 | name: '头条', 158 | namespace: 'toutiao', 159 | test: /^www\.toutiao\.com\/(article|w)\/\d+\/?$/, // article/6884536349483860492、话题 w/1732500407565326 160 | use: toutiao, 161 | }, 162 | { 163 | name: '微博', 164 | namespace: 'weibo', 165 | test: /^(www\.)?weibo.com\//, 166 | use: weibo, 167 | }, 168 | { 169 | name: '微博文章', 170 | namespace: 'weibo', 171 | test: /^(www\.)?weibo.com\/ttarticle\/p\/show$/, 172 | use: weiboArticle, 173 | }, 174 | { 175 | name: '微博动态', 176 | namespace: 'weibo', 177 | test: /^d\.weibo\.com\//, 178 | use: weiboDynamic, 179 | }, 180 | { 181 | name: '谷歌', 182 | namespace: 'google', 183 | test: /^www\.google\..{2,7}search$/, // 应该足够覆盖各个域名 184 | use: google, 185 | }, 186 | { 187 | name: 'CSDN', 188 | namespace: 'csdn', 189 | test: /^blog\.csdn\.net\/(\w|-)+\/article\/details\//, 190 | use: csdn, 191 | }, 192 | { 193 | name: '米游社', 194 | namespace: 'mihoyoBBS', 195 | // ys|bh2|bh3|wd|dby 对应:原神 崩坏2 崩坏3 未定 大别野 196 | // 只用到原神,暂不对其它作处理 197 | test: /^bbs.mihoyo.com\/(ys)\/article\//, 198 | use: mihoyoBBS, 199 | }, 200 | ] 201 | 202 | export default sites 203 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/jianshu-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | /* 简书文章 */ 4 | @include substrate(1250px, 85vw, 1280px) { 5 | #__next { 6 | /* 左侧悬浮按钮 */ 7 | > div:last-child { 8 | left: calc(50% - (var(--inject-page-width) / 2) - 80px); 9 | } 10 | 11 | [role='main'] { 12 | width: var(--inject-page-width); 13 | 14 | /* 内容 */ 15 | > div:first-child { 16 | flex: 1; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/jianshu-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const jianshu:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/juejin-cn/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | /* 掘金文章 */ 4 | @include substrate(1400px, 82vw, 1300px) { 5 | #juejin { 6 | .main-container { 7 | max-width: var(--inject-page-width) !important; 8 | 9 | .main-area { 10 | width: calc(100% - 25rem - 20px); 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/juejin-cn/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const juejin:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin substrate( 2 | $start, 3 | $page-width, 4 | $page-max-width, 5 | $property-name: --inject-page-width 6 | ) { 7 | @media screen and (min-width: $start) { 8 | :root { 9 | #{$property-name}: min(#{$page-width}, #{$page-max-width}); 10 | } 11 | 12 | .inject-widescreen-loose-js { 13 | #{$property-name}: $page-width; 14 | } 15 | 16 | @content; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/movie-douban-com/index.ts: -------------------------------------------------------------------------------- 1 | export { doubanSubject } from './subject' 2 | export { doubanReview } from './review' 3 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/movie-douban-com/review.ts: -------------------------------------------------------------------------------- 1 | // 貌似样式一样的,直接用subject的吧 2 | import styles from './subject.lazy.scss' 3 | import type { Site } from '../../types' 4 | 5 | export const doubanReview:Site['use'] = ({ store, createControl }) => ({ 6 | handler() { 7 | createControl({ store, execute: styles.use }) 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/movie-douban-com/subject.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 675px; 4 | 5 | @include substrate(1300px, 82vw, 1318px) { 6 | #wrapper { 7 | width: var(--inject-page-width) !important; 8 | } 9 | 10 | /* 内容 */ 11 | #content { 12 | .article { 13 | width: calc(100% - 360px); 14 | 15 | /* 电影信息 */ 16 | .subject { 17 | width: calc(100% - 175px); 18 | 19 | #info { 20 | max-width: none; 21 | width: calc(100% - 160px); 22 | } 23 | } 24 | 25 | /* 剧照 */ 26 | #related-pic { 27 | > ul { 28 | // 应该是定死的,宽了不好看 29 | width: $default-width; 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/movie-douban-com/subject.ts: -------------------------------------------------------------------------------- 1 | import styles from './subject.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const doubanSubject:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/mp-weixin-qq-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(750px, 90vw, 1150px) { 4 | /* 文章宽屏 */ 5 | .rich_media_area_primary_inner { 6 | margin-left: auto; 7 | margin-right: auto; 8 | max-width: var(--inject-page-width) !important; 9 | } 10 | 11 | /* 二维码位置 */ 12 | #js_pc_qr_code .qr_code_pc { 13 | opacity: 0.2; 14 | position: fixed; 15 | right: 3vw; 16 | top: 25vh; 17 | } 18 | 19 | #js_pc_qr_code .qr_code_pc:hover { 20 | opacity: 1; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/mp-weixin-qq-com/index.ts: -------------------------------------------------------------------------------- 1 | import { $$ } from '@/utils/selector' 2 | import { interactive } from '@/utils/ready-state' 3 | import styles from './index.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const weixin:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function execute() { 9 | interactive(() => { 10 | // 原图处理 11 | $$('img').forEach(img => { 12 | const dataSrc = img.dataset.src 13 | if (!dataSrc) return 14 | 15 | const url = new URL(dataSrc) 16 | url.pathname = url.pathname.replace('/640', '/') 17 | img.dataset.src = url.href 18 | }) 19 | }) 20 | 21 | styles.use() 22 | } 23 | 24 | createControl({ store, execute }) 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/segmentfault-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | /* 专栏/问答 */ 4 | @include substrate(1390px, 82vw, 1350px) { 5 | .container, 6 | .container-lg, 7 | .container-md, 8 | .container-sm, 9 | .container-xl { 10 | max-width: var(--inject-page-width); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/segmentfault-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const segmentfault:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/space-bilibili-com/index.ts: -------------------------------------------------------------------------------- 1 | import '../t-bilibili-com/mocha-official-gifts' 2 | import type { Site } from '../../types' 3 | 4 | export const bilibiliSpace:Site['use'] = () => ({ 5 | // eslint-disable-next-line @typescript-eslint/no-empty-function 6 | handler() {}, 7 | }) 8 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/t-bilibili-com/detail.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(900px, 75vw, 1039px) { 4 | #app { 5 | /* 容器 */ 6 | .content { 7 | width: var(--inject-page-width) !important; 8 | } 9 | 10 | /* up主内容 */ 11 | .bili-dyn-content { 12 | width: auto !important; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/t-bilibili-com/detail.ts: -------------------------------------------------------------------------------- 1 | import './mocha-official-gifts' 2 | import styles from './detail.lazy.scss' 3 | import type { Site } from '../../types' 4 | 5 | export const bilibiliDynamicDetail:Site['use'] = ({ store, createControl }) => ({ 6 | handler() { 7 | createControl({ store, execute: styles.use }) 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/t-bilibili-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1380px, 85vw, 1454px) { 4 | #app .bili-dyn-home--member { 5 | width: var(--inject-page-width) !important; 6 | 7 | /* 内容 */ 8 | > main { 9 | flex: 1; 10 | 11 | /* up 列表 */ 12 | .bili-dyn-up-list { 13 | width: auto; 14 | } 15 | } 16 | 17 | .bili-dyn-content, 18 | .bili-dyn-content__orig__major { 19 | width: auto !important; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/t-bilibili-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const bilibiliDynamic:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | 10 | export { bilibiliDynamicDetail } from './detail' 11 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/t-bilibili-com/mocha-official-gifts.lazy.scss: -------------------------------------------------------------------------------- 1 | .mocha-strawberry { 2 | bottom: 50px; 3 | position: fixed; 4 | right: 70px; 5 | z-index: 1; 6 | } 7 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/tieba-baidu-com/f.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1390px, 80vw, 1250px) { 4 | /* 头部信息 */ 5 | .head_main .head_middle, 6 | .head_main .head_content { 7 | width: var(--inject-page-width) !important; 8 | } 9 | 10 | /* 内容区域 */ 11 | .content, 12 | .foot { 13 | width: var(--inject-page-width); 14 | } 15 | 16 | /* 这里的border实际上是这里的背景图 */ 17 | .forum_content { 18 | background: #fff; 19 | } 20 | 21 | #content_wrap { 22 | border-right: 1px solid #eee; 23 | width: calc(100% - 248px); 24 | } 25 | 26 | /* 每条帖子 */ 27 | .threadlist_detail { 28 | display: flex; 29 | } 30 | 31 | .threadlist_detail .pull_left { 32 | flex: auto; 33 | } 34 | 35 | .threadlist_detail .pull_left .threadlist_abs { 36 | width: 97%; 37 | } 38 | 39 | /* 发帖区域 */ 40 | .frs_content_footer_pagelet { 41 | width: auto !important; 42 | } 43 | 44 | .tb_rich_poster_container { 45 | margin-left: 0 !important; 46 | } 47 | 48 | /* 右侧悬浮按钮 */ 49 | .tbui_aside_float_bar { 50 | left: calc(50% + (var(--inject-page-width) / 2) + 12px) !important; 51 | margin-left: 0 !important; 52 | right: auto; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/tieba-baidu-com/f.ts: -------------------------------------------------------------------------------- 1 | import styles from './f.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const tiebaForum:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/tieba-baidu-com/index.ts: -------------------------------------------------------------------------------- 1 | import { tieba } from './p' 2 | import { tiebaForum } from './f' 3 | 4 | export { 5 | tieba, 6 | tiebaForum, 7 | } 8 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/tieba-baidu-com/p.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1390px, 80vw, 1250px) { 4 | #container { 5 | width: var(--inject-page-width); 6 | } 7 | 8 | #container > .content { 9 | width: 100%; 10 | } 11 | 12 | .nav_wrap, 13 | .p_thread, 14 | .pb_content, 15 | .core_title_wrap_bright, 16 | .core_reply_wrapper, 17 | .l_post_bright .core_reply_wrapper, 18 | .pb_footer { 19 | width: 100%; 20 | } 21 | 22 | .core_title_absolute_bright { 23 | width: calc(var(--inject-page-width) - 240px); 24 | } 25 | 26 | /* 内容区域 */ 27 | .pb_content { 28 | background-size: 100%; 29 | display: flex; 30 | } 31 | 32 | .pb_content::after { 33 | content: none; 34 | } 35 | 36 | /* 点击展开,查看完整图片 */ 37 | .pb_content .replace_div { 38 | width: fit-content !important; 39 | } 40 | 41 | .pb_content .replace_div .replace_tip { 42 | width: 100% !important; 43 | } 44 | 45 | /* 楼区域 */ 46 | .left_section { 47 | border-right: 2px solid #e4e6eb; 48 | flex: 1; 49 | } 50 | 51 | /* 楼层 广告会覆盖宽度 使用important */ 52 | .l_post_bright { 53 | display: flex; 54 | width: 100% !important; 55 | } 56 | 57 | .l_post_bright .d_post_content_main { 58 | flex: 1; 59 | width: 0; 60 | } 61 | 62 | /* 修正楼层回复中小按钮位置 */ 63 | .l_post_bright .d_post_content_main .core_reply_wrapper .user-hide-post-down, 64 | .l_post_bright .d_post_content_main .core_reply_wrapper .user-hide-post-up, 65 | .l_post_bright .d_post_content_main .core_reply_wrapper .user-hide-post-action { 66 | right: 180px !important; 67 | } 68 | 69 | /* 右侧悬浮按钮 */ 70 | .tbui_aside_float_bar { 71 | left: calc(50% + (var(--inject-page-width) / 2) + 12px); 72 | margin-left: 0; 73 | right: auto; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/tieba-baidu-com/p.ts: -------------------------------------------------------------------------------- 1 | // import { $, $$ } from '@/utils/selector' 2 | // import * as readyState from '@/utils/ready-state' 3 | import styles from './p.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const tieba:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | // const postlistSelector = '#j_p_postlist' 9 | 10 | function execute() { 11 | /** 12 | * 新版本更新后没什么好的办法,先不做处理 13 | */ 14 | /* const replaceOriSrc = (function() { 15 | const process = new WeakSet() 16 | 17 | return function() { 18 | const BDEImgEls = $$(`${postlistSelector} .BDE_Image`) as NodeListOf 19 | BDEImgEls.forEach(img => { 20 | if (process.has(img)) return 21 | process.add(img) 22 | // 忽略疑似上古时代的图片 23 | if (img.src.includes('imgsa.baidu.com/forum')) return 24 | // 贴吧自身根据 25 | // /^http:\/\/[^\/\?]*?\.baidu\.com[:8082]*\/(\w+)\/([^\/\?]+)\/([^\/\?]+)\/(\w+?)\.(?:webp|jpg|jpeg)/ 判断是否相册, 26 | // 后续 chrome 更改必须为 https 访问时可能需要更改这里的逻辑 27 | // eslint-disable-next-line no-useless-escape 28 | if (/^http(s?):\/\/[^\/\?]*?\.baidu\.com[:8082]*\/(\w+)\/([^\/\?]+)\/([^\/\?]+)\/(\w+?)\.(?:webp|jpg|jpeg)/.test(img.src)) { 29 | const protocol = img.src.match(/^(https?:\/\/)/)![0] 30 | img.src = `${protocol}tiebapic.baidu.com/forum/pic/item/${img.src.split('/').slice(-1)[0]}` 31 | // 不能直接用 css:贴吧根据宽高判断,用 css 宽高 auto 时若图片未加载宽高获取到 0 导致无法查看大图 32 | img.style.cssText += 'max-width: 100%; width: auto !important; height: auto; max-height: 130vh;' 33 | } 34 | }) 35 | } 36 | })() 37 | 38 | readyState.interactive(() => { 39 | // 替换原图 40 | replaceOriSrc() 41 | const observer = new MutationObserver(mutationsList => { 42 | mutationsList.forEach(mutation => { 43 | const { target } = mutation 44 | if ((target as HTMLElement).id !== postlistSelector.slice(1)) return 45 | replaceOriSrc() 46 | }) 47 | }) 48 | observer.observe($('.left_section') as HTMLElement, { childList: true, subtree: true }) 49 | }) */ 50 | 51 | styles.use() 52 | } 53 | 54 | createControl({ store, execute }) 55 | }, 56 | }) 57 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/weibo-com/article.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1150px, 90vw, 1380px) { 4 | #articleRoot { 5 | .WB_frame { 6 | width: var(--inject-page-width); 7 | } 8 | 9 | #plc_main { 10 | max-width: 100%; 11 | width: auto; 12 | } 13 | 14 | /* 内容 */ 15 | .WB_frame_a, 16 | .WB_artical { 17 | max-width: 100%; 18 | width: auto; 19 | } 20 | 21 | /* 顶部图片 */ 22 | .main_toppic { 23 | margin: { 24 | left: auto; 25 | right: auto; 26 | } 27 | } 28 | 29 | /* 文章 */ 30 | .WB_editor_iframe_new { 31 | width: auto; 32 | } 33 | } 34 | 35 | /* 右下角浮动按钮 */ 36 | .B_artical [node-type='sidebar'] { 37 | > .W_gotop { 38 | left: calc(50% + var(--inject-page-width) / 2); 39 | margin-left: 0; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/weibo-com/article.ts: -------------------------------------------------------------------------------- 1 | import styles from './article.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const weiboArticle:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/weibo-com/home.string.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1340px, 90vw, 1380px) { 4 | [class*='Frame_content'] { 5 | --main-width: var(--inject-page-width); 6 | 7 | width: var(--inject-page-width); 8 | 9 | /* 中间主内容 */ 10 | > div:nth-of-type(2) { 11 | flex: 1; 12 | } 13 | } 14 | 15 | /* 内容 */ 16 | [class*='Frame_main'], 17 | [class*='Main_full'] { 18 | flex-grow: 1; 19 | } 20 | 21 | /* 列表中固定图片宽度,避免太大 */ 22 | .woo-box-wrap[class*='picture_inlineNum3'] { 23 | max-width: 409px; 24 | } 25 | 26 | /* 列表4张图 */ 27 | .u-col-4.woo-box-wrap { 28 | max-width: 546px; 29 | } 30 | 31 | /* 列表中视频 */ 32 | [class*='content_row'] [class*='card-video_videoBox'] { 33 | max-width: 540px; 34 | } 35 | 36 | /* 列表中文章 */ 37 | [class*='content_row'] [class*='card-article_pic'] { 38 | max-width: 540px; 39 | } 40 | 41 | /* 博主主页头图 */ 42 | [class*='ProfileHeader_pic'] { 43 | overflow: hidden; 44 | } 45 | 46 | /* 返回顶部按钮 */ 47 | [class*='Index_backTop'] { 48 | left: calc(50% + var(--inject-page-width) / 2 + var(--frame-mod-gap-space)); 49 | margin-left: 0; 50 | transform: translateX(0); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/weibo-com/index.ts: -------------------------------------------------------------------------------- 1 | import { once } from '@/utils/base' 2 | import { $ } from '@/utils/selector' 3 | import { warn } from '@/utils/log' 4 | import type { VueHTMLElement } from '@/utils/vue-root' 5 | import type { Site } from '../../types' 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const homeStyles = require('./home.string.scss').default.toString() as string 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | const playDetailStyles = require('./play-detail.string.scss').default.toString() as string 10 | 11 | export { weiboArticle } from './article' 12 | 13 | // hack type 14 | const unsafeWindowAlias = unsafeWindow as Window & { 15 | $CONFIG: any 16 | } 17 | 18 | export const weibo:Site['use'] = ({ store, createControl }) => ({ 19 | handler() { 20 | const uiControl = createControl({ store, visible: false, silent: true }) 21 | execute() 22 | 23 | function execute() { 24 | let proxyConfig: undefined | { 25 | [key: string]: any 26 | } 27 | document.addEventListener('readystatechange', () => { 28 | // 是否启用新版微博 29 | if ($('#app') && ($('#app') as VueHTMLElement).__vue__) { 30 | WbNewVersion() 31 | return 32 | } 33 | if (!unsafeWindowAlias.$CONFIG) return 34 | if (proxyConfig && proxyConfig === unsafeWindowAlias.$CONFIG) return 35 | 36 | proxyConfig = new Proxy(unsafeWindowAlias.$CONFIG, { 37 | set(target, property, value, receiver) { 38 | const oldVal = target[property] 39 | const succeeded = Reflect.set(target, property, value, receiver) 40 | if (property === 'location' && value !== oldVal) { 41 | warn('script:reinsert styleSheet') 42 | addStyle() 43 | } 44 | return succeeded 45 | }, 46 | }) 47 | unsafeWindowAlias.$CONFIG = proxyConfig 48 | 49 | addStyle() 50 | }) 51 | } 52 | 53 | /* 新版========start */ 54 | const WbNewVersion = once(() => { 55 | const uiControl = createControl({ store, visible: false, silent: true }) 56 | const app = ($('#app') as VueHTMLElement).__vue__! 57 | let styleSheet: HTMLStyleElement | undefined 58 | warn('新版本', app) 59 | const pageStyleMap = new Map([ 60 | [ 61 | [ 62 | 'home', // 首页 63 | 'mygroups', // 首页左侧分组 64 | 'profile', // 博主主页 65 | 'nameProfile', // 博主主页(名称) 66 | 'customProfile', // 自定义主页 67 | 'bidDetail', // 微博详情 68 | 'atWeibo', // 消息 at我的 69 | 'cmtInbox', // 消息 评论 70 | 'likeInbox', // 消息 赞 71 | 'follow', // 我的关注、我的粉丝 72 | 'myFollowTab', // 我的关注tab栏 73 | 'fav', // 我的收藏 74 | 'like', // 我的赞 75 | 'weibo', // 热门微博 76 | 'list', // 热门榜单 77 | 'topic', // 话题榜 78 | 'search', // 热搜榜 79 | 'searchResult', // 搜索结果 80 | ], 81 | () => GM_addStyle(homeStyles), 82 | ], 83 | [ 84 | [ 85 | 'Playdetail', // 视频详情 86 | ], 87 | () => GM_addStyle(playDetailStyles), 88 | ], 89 | ]) 90 | 91 | const notify = once(() => { 92 | uiControl.notify() 93 | }) 94 | app.$watch('$route', (to: Record) => { 95 | styleSheet?.remove() 96 | warn('route changed', to) 97 | uiControl.hide() 98 | for (const [routenames, addStyle] of pageStyleMap.entries()) { 99 | if (routenames.includes(to.name)) { 100 | uiControl.show() 101 | if (store.enabled) { 102 | styleSheet = addStyle() 103 | notify() 104 | } 105 | break 106 | } 107 | } 108 | }, { immediate: true }) 109 | }) 110 | /* 新版========end */ 111 | 112 | /* 旧版(保留,不再更新) */ 113 | const addStyle = (function() { 114 | let styleSheet: HTMLStyleElement | undefined 115 | 116 | return function() { 117 | const { $CONFIG } = unsafeWindowAlias 118 | const classnamePrefix = 'inject-ws-' 119 | const getClassname = (classname: string) => `${classnamePrefix}${classname}` 120 | 121 | styleSheet?.remove() 122 | ;[...document.body.classList.values()].forEach(item => { 123 | if (item.startsWith(classnamePrefix)) { 124 | document.body.classList.remove(item) 125 | } 126 | }) 127 | 128 | const pages = { 129 | // 首页(含特别关注)、我的收藏、我的赞、好友圈 130 | mainpage: { 131 | test: /^v6.*_content_home$/.test($CONFIG.location) || /v6_(fav|likes_outbox|content_friends)/.test($CONFIG.location), 132 | use: doMainPage, 133 | }, 134 | // 用户资料页、相册、管理中心、粉丝、服务、财经专家、热门话题 135 | profilepage: { 136 | test: /^page_.*_(home|photos|manage|myfollow|service|expert|topic)$/.test($CONFIG.location), 137 | use: doProfilePage, 138 | }, 139 | // 微博详情 140 | singleweibo: { 141 | test: /^page_.*_single_weibo$/.test($CONFIG.location), 142 | use: doSingleWBPage, 143 | }, 144 | } 145 | const target = Object.entries(pages).find(([, { test }]) => test) 146 | warn(target, $CONFIG.location) 147 | if (!target) return 148 | uiControl.show() 149 | if (!store.enabled) return 150 | 151 | styleSheet = target[1].use(getClassname(target[0])) 152 | document.body.classList.add(getClassname(target[0])) 153 | uiControl.notify() 154 | } 155 | })() 156 | 157 | function doMainPage(classname: string) { 158 | return GM_addStyle(` 159 | :root { 160 | --inject-page-width: min(75vw, 1330px); 161 | } 162 | @media screen and (min-width: 1300px) { 163 | |> .WB_frame { 164 | display: flex; 165 | width: var(--inject-page-width) !important; 166 | } 167 | /* 内容 */ 168 | |> #plc_main { 169 | display: flex !important; 170 | flex: 1; 171 | width: auto !important; 172 | } 173 | |> .WB_main_c { 174 | flex: 1; 175 | } 176 | /* 微博类型 */ 177 | |> .tab_box { 178 | display: flex; 179 | } 180 | |> .tab_box::after { 181 | content: none; 182 | } 183 | |> .tab_box .fr_box { 184 | flex: 1; 185 | } 186 | /* 返回顶部按钮 */ 187 | |> .W_gotop { 188 | left: calc(50% + (var(--inject-page-width) / 2)); 189 | margin-left: 0 !important; 190 | } 191 | } 192 | `.replace(/\|>/g, `.${classname}`)) 193 | } 194 | 195 | function doProfilePage(classname: string) { 196 | return GM_addStyle(` 197 | :root { 198 | --inject-page-width: min(75vw, 1330px); 199 | } 200 | @media screen and (min-width: 1300px) { 201 | |> .WB_frame { 202 | width: var(--inject-page-width) !important; 203 | } 204 | |> .WB_frame_a, .WB_frame_a_fix { 205 | width: 100%; 206 | } 207 | /* 内容 */ 208 | |> #plc_main { 209 | width: 100% !important; 210 | display: flex; 211 | } 212 | /* 这里修复特殊博主页右边距 */ 213 | |> #plc_main > div:last-child { 214 | margin-right: 0; 215 | } 216 | /* 特殊博主页评论 */ 217 | |> .WB_frame_c .input_simple_wrap .inputfunc_simple_wrap { 218 | width: calc(100% - 80px); 219 | } 220 | |> .WB_frame_c { 221 | flex: 1; 222 | } 223 | /* 右侧悬浮时间线 */ 224 | |> .WB_timeline { 225 | left: calc(50% + (var(--inject-page-width) / 2) + 10px); 226 | margin-left: 0; 227 | } 228 | /* 返回顶部按钮 */ 229 | |> .W_gotop { 230 | left: calc(50% + (var(--inject-page-width) / 2)); 231 | margin-left: 0 !important; 232 | } 233 | /* 个人资料 管理中心 */ 234 | |> .WB_frame_a_fix { 235 | display: flex; 236 | justify-content: center; 237 | } 238 | |> .WB_frame_a_fix > .PCD_admin_content { 239 | float: none; 240 | margin-left: 18px; 241 | } 242 | |> .WB_frame_a_fix > .PCD_admin_content .PCD_admin_content { 243 | float: none; 244 | } 245 | } 246 | `.replace(/\|>/g, `.${classname}`)) 247 | } 248 | 249 | function doSingleWBPage(classname: string) { 250 | return GM_addStyle(` 251 | :root { 252 | --inject-page-width: min(75vw, 1330px); 253 | } 254 | @media screen and (min-width: 1300px) { 255 | |> .WB_frame { 256 | width: var(--inject-page-width) !important; 257 | } 258 | /* 内容 */ 259 | |> #plc_main { 260 | display: flex !important; 261 | width: auto !important; 262 | } 263 | |> #plc_main .WB_frame_c { 264 | flex: 1; 265 | } 266 | /* 返回顶部按钮 */ 267 | |> .W_gotop { 268 | left: calc(50% + (var(--inject-page-width) / 2) - 19px); 269 | margin-left: 0 !important; 270 | } 271 | } 272 | `.replace(/\|>/g, `.${classname}`)) 273 | } 274 | }, 275 | }) 276 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/weibo-com/play-detail.string.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1450px, 91vw, 91vw) { 4 | [class*='Frame_content2'] { 5 | max-width: none; 6 | width: var(--inject-page-width); 7 | } 8 | 9 | /* 左列 */ 10 | [class*='Frame_main2'] { 11 | flex-grow: 1; 12 | padding-right: 20px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-baidu-com/index.string.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 560px; 4 | 5 | @include substrate(1460px, 75vw, 1300px) { 6 | /* 顶部搜索 */ 7 | 8 | /* 修复搜索主页换肤后头部异常 */ 9 | #head:not(.s-skin-hasbg) { 10 | backdrop-filter: blur(10px); 11 | background-color: #ffffffd1; 12 | } 13 | 14 | .head_wrapper .s_form { 15 | // 1921px时本身会居中 16 | @media (max-width: 1920px) { 17 | margin-left: auto; 18 | margin-right: auto; 19 | width: fit-content; 20 | } 21 | } 22 | 23 | /* 搜索tab */ 24 | .s_tab { 25 | margin-left: auto; 26 | margin-right: auto; 27 | padding-left: 0 !important; 28 | width: fit-content; 29 | } 30 | 31 | /* 搜索内容 */ 32 | #container { 33 | margin-left: auto !important; 34 | margin-right: auto !important; 35 | width: var(--inject-page-width) !important; 36 | } 37 | 38 | /* 左侧搜索结果 */ 39 | #content_left { 40 | width: calc(var(--inject-page-width) - 450px) !important; 41 | 42 | /* [tpl*=img_address]忽略图片区域,防止宽屏后排版混乱(搜索:樱花) */ 43 | > div:not([tpl*='img_address']) { 44 | width: 100% !important; 45 | } 46 | 47 | /* 视频宽度限制(搜索:路人女主的养成方法) */ 48 | .op-bk-polysemy-video__wrap { 49 | width: $default-width !important; 50 | } 51 | 52 | /* 游戏配置搜索结果卡片中图片的高度处理(搜索:赛博朋克2077配置要求) */ 53 | .wenda-abstract-img-wrap-new { 54 | height: auto; 55 | } 56 | 57 | /* 圆角卡片式,在热榜新闻中偶尔出现 */ 58 | .c-group-wrapper { 59 | .result-op, 60 | .c-group { 61 | width: 95% !important; 62 | } 63 | } 64 | 65 | /* 普通列表 */ 66 | .new-pmd { 67 | .c-span9 { 68 | width: 75%; 69 | 70 | @media (min-width: 1680px) { 71 | width: 81%; 72 | } 73 | } 74 | 75 | /* 百科宽度(搜索:感冒) */ 76 | .c-span12 { 77 | width: 100%; 78 | } 79 | } 80 | } 81 | 82 | /* 分页 */ 83 | .page-inner { 84 | margin-left: auto; 85 | margin-right: auto; 86 | padding-left: 0 !important; 87 | width: var(--inject-page-width); 88 | } 89 | 90 | /* 页脚 */ 91 | .foot-inner { 92 | margin-left: auto; 93 | margin-right: auto; 94 | width: var(--inject-page-width); 95 | } 96 | 97 | #foot .foot-inner #help { 98 | padding-left: 0 !important; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-baidu-com/index.ts: -------------------------------------------------------------------------------- 1 | import * as readyState from '@/utils/ready-state' 2 | import type { Site } from '../../types' 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const styles = require('./index.string.scss').default.toString() as string 5 | 6 | export const baidu:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function execute() { 9 | const styleSheet = GM_addStyle(styles) 10 | 11 | readyState.interactive(() => { 12 | const template = document.createElement('template') 13 | template.appendChild(styleSheet) 14 | // 搜索时百度会清除head这里将样式插入一次到body 15 | document.body.insertAdjacentElement('afterbegin', template) 16 | }) 17 | } 18 | 19 | createControl({ store, execute }) 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-bilibili-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1120px, 83vw, 1160px) { 4 | #app { 5 | .article-detail { 6 | width: var(--inject-page-width); 7 | } 8 | 9 | /* 文章 */ 10 | #article-content { 11 | /* 图片宽度 */ 12 | .img-box img[data-type='preview'] { 13 | height: auto !important; 14 | max-width: 100%; 15 | width: auto !important; 16 | } 17 | } 18 | 19 | /* 右侧悬浮按钮 */ 20 | .right-side-bar { 21 | margin-left: calc(var(--inject-page-width) + 25px); 22 | transition-property: bottom; // 避免更改 margin 触发了过渡 23 | } 24 | 25 | /* 文章下方图片 哎?广告 */ 26 | .activty-image .card-image { 27 | margin: auto; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-bilibili-com/index.ts: -------------------------------------------------------------------------------- 1 | import { $$ } from '@/utils/selector' 2 | import * as readyState from '@/utils/ready-state' 3 | import styles from './index.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const bilibili:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function execute() { 9 | /* 替换为原图 */ 10 | // 稍微延时,待哔哩哔哩处理图片 11 | readyState.DOMContentLoaded(() => { 12 | ($$('#article-content .img-box img[data-type="preview"][data-src]') as NodeListOf).forEach(img => { 13 | const { src } = img.dataset 14 | const original = src!.replace(/@[0-9a-z]+_[0-9a-z]+_/i, '@') 15 | img.dataset.src = original 16 | }) 17 | }) 18 | 19 | styles.use() 20 | } 21 | 22 | createControl({ store, execute }) 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-douban-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 675px; 4 | 5 | @include substrate(1300px, 82vw, 1318px) { 6 | #wrapper { 7 | width: var(--inject-page-width) !important; 8 | } 9 | 10 | /* 内容 */ 11 | #content { 12 | .grid-16-8 { 13 | $right-width: 360px; 14 | 15 | .article { 16 | width: calc(100% - #{$right-width}) !important; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-douban-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const douban:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-google-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable max-line-length */ 2 | @import '../mixins'; 3 | 4 | $default-width: 652px; 5 | 6 | @include substrate(1600px, 73vw, 1530px) { 7 | body { 8 | $google-columns-repeat: 21; 9 | 10 | /* 搜索结果 */ 11 | #rcnt { 12 | grid-template-columns: 210px repeat($google-columns-repeat, calc(#{100% - $google-columns-repeat} / #{$google-columns-repeat})) minmax(0, 1fr); 13 | width: var(--inject-page-width); 14 | } 15 | 16 | /* 列表 */ 17 | #w7tRq { 18 | column-gap: 1%; 19 | grid-template-columns: 0 repeat($google-columns-repeat, calc(#{100% - $google-columns-repeat} / #{$google-columns-repeat})); 20 | 21 | // /* 特殊类型还原宽度(搜索:蛇麻花) */ 22 | // /* stylelint-disable-next-line selector-type-no-unknown */ 23 | // g-section-with-header { 24 | // width: $default-width; 25 | // } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-google-com/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@/utils/querystring' 2 | import styles from './index.lazy.scss' 3 | import type { Site } from '../../types' 4 | 5 | export const google:Site['use'] = ({ store, createControl }) => ({ 6 | handler() { 7 | if (parse().tbm) return // 选择了tab搜索时终止 8 | 9 | createControl({ store, execute: styles.use }) 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-sogou-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 550px; 4 | 5 | @include substrate(1200px, 80vw, 1340px) { 6 | %hor-center { 7 | margin-left: auto; 8 | margin-right: auto; 9 | padding-left: 0; 10 | width: var(--inject-page-width) !important; 11 | } 12 | 13 | /* 头部注意滚动处理 */ 14 | .header .header-box { 15 | @extend %hor-center; 16 | 17 | padding: 0 5px 45px; 18 | position: relative; 19 | 20 | .logo { 21 | top: -8px; 22 | } 23 | } 24 | 25 | .header, 26 | .header.headsearch .header-box { 27 | padding-bottom: 0; 28 | } 29 | 30 | .headsearch { 31 | backdrop-filter: blur(10px); 32 | background-color: #ffffffd1; 33 | } 34 | 35 | /* 搜索结果 */ 36 | #wrapper { 37 | @extend %hor-center; 38 | 39 | display: flex; 40 | } 41 | 42 | #main { 43 | flex: 1; 44 | max-width: none; 45 | padding-right: 74px; 46 | width: 0; 47 | 48 | .results { 49 | width: auto; 50 | 51 | > .vrwrap, 52 | > .rb { 53 | width: auto !important; 54 | } 55 | } 56 | } 57 | 58 | /* 特殊搜索结果恢复原本宽度 */ 59 | .special-wrap, 60 | .vrPicBox { 61 | box-sizing: border-box; 62 | width: $default-width; 63 | } 64 | 65 | /* 底部 */ 66 | .hintBox, 67 | #pagebar_container, 68 | #s_footer > div { 69 | @extend %hor-center; 70 | } 71 | 72 | #s_footer { 73 | padding-left: 0; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-sogou-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const sougou:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-toutiao-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 676px; 4 | 5 | @include substrate(1350px, 88vw, 1470px) { 6 | // 文章 7 | .article-detail-container, 8 | // 话题 9 | .wtt-detail-container { 10 | width: var(--inject-page-width) !important; 11 | 12 | /* 内容 */ 13 | // 不要使用flex,头条的布局都是浮动的 14 | > .main { 15 | width: calc(var(--inject-page-width) - 298px - 60px - 48px * 2) !important; // 右侧 + 右侧间距保留 + 父元素内间距 16 | 17 | /* 评论 */ 18 | .ttp-comment-block { 19 | width: auto; 20 | } 21 | } 22 | 23 | /* 底部信息流 */ 24 | .detail-end-feed { 25 | margin-left: auto; 26 | margin-right: auto; 27 | max-width: $default-width; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/www-toutiao-com/index.ts: -------------------------------------------------------------------------------- 1 | import styles from './index.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const toutiao:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/home.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1100px, 91vw, 1360px) { 4 | .Topstory-container { 5 | width: var(--inject-page-width); 6 | } 7 | 8 | /* 内容 */ 9 | .Topstory-mainColumn { 10 | flex: 1; 11 | } 12 | 13 | /* 右侧 */ 14 | .GlobalSideBar { 15 | flex: initial; 16 | width: 296px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/home.ts: -------------------------------------------------------------------------------- 1 | import styles from './home.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const zhihuHome:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/index.ts: -------------------------------------------------------------------------------- 1 | import { zhihuQuestion } from './question' 2 | import { zhihuHome } from './home' 3 | import { zhihuTopic } from './topic' 4 | 5 | export { 6 | zhihuQuestion, 7 | zhihuHome, 8 | zhihuTopic, 9 | } 10 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/question.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 694px; 4 | 5 | @include substrate(1350px, 75vw, 1300px) { 6 | .QuestionHeader-content, 7 | .QuestionHeader-footer { 8 | margin-left: auto; 9 | margin-right: auto; 10 | padding-left: 0 !important; 11 | width: var(--inject-page-width) !important; 12 | } 13 | 14 | .QuestionHeader-footer-inner { 15 | width: auto; 16 | } 17 | 18 | .QuestionHeader-footer-main { 19 | padding-left: 0; 20 | } 21 | 22 | .QuestionHeader-main { 23 | flex: 1; 24 | width: 0; 25 | } 26 | 27 | .Question-main { 28 | width: var(--inject-page-width) !important; 29 | 30 | .AnswerItem-authorInfo { 31 | max-width: none; 32 | } 33 | 34 | /* 查看全部回答后 结构不太一样 */ 35 | 36 | /* 简短回答 */ 37 | > .ListShortcut { 38 | flex: 1; 39 | width: 0; 40 | 41 | > .Question-mainColumn[data-zop-questionanswerlist] { 42 | padding-right: 10px; 43 | width: auto; 44 | } 45 | } 46 | 47 | /* 全部回答 */ 48 | > .Question-mainColumn { 49 | flex: 1; 50 | padding-right: 10px; 51 | } 52 | } 53 | 54 | /* 内容图片 */ 55 | .ztext .content_image, 56 | .ztext .origin_image { 57 | max-width: $default-width; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/question.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | import * as readyState from '@/utils/ready-state' 3 | import styles from './question.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const zhihuQuestion:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function execute() { 9 | readyState.DOMContentLoaded(() => { 10 | const process = new WeakSet() 11 | const observer = new MutationObserver(mutationsList => { 12 | mutationsList.forEach(mutation => { 13 | const { target, oldValue } = mutation as unknown as { 14 | target: HTMLImageElement, 15 | oldValue: string 16 | } 17 | if ( 18 | process.has(target) || 19 | target.tagName !== 'IMG' || 20 | !oldValue.startsWith('data:image/') || 21 | // 不对非文章图片处理 22 | !$('.ListShortcut')!.contains(target) || 23 | // 与知乎同样的选择器判断 24 | !(target.classList.contains('lazy') && !target.classList.contains('data-thumbnail')) 25 | ) return 26 | process.add(target) 27 | // 替换原图 28 | target.dataset.original && (target.src = target.dataset.original) 29 | }) 30 | }) 31 | // 查看全部回答时知乎会替换Question-mainColumn标签,只能往更父级监听 32 | observer.observe($('.QuestionPage')!, { subtree: true, attributeFilter: ['src'], attributeOldValue: true }) 33 | }) 34 | 35 | styles.use() 36 | } 37 | 38 | createControl({ store, execute }) 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/topic.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | @include substrate(1100px, 91vw, 1295px) { 4 | .ContentLayout { 5 | width: var(--inject-page-width); 6 | } 7 | 8 | /* 内容 */ 9 | .ContentLayout-mainColumn { 10 | flex: 1; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhihu-com/topic.ts: -------------------------------------------------------------------------------- 1 | import styles from './topic.lazy.scss' 2 | import type { Site } from '../../types' 3 | 4 | export const zhihuTopic:Site['use'] = ({ store, createControl }) => ({ 5 | handler() { 6 | createControl({ store, execute: styles.use }) 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhuanlan-zhihu-com/index.lazy.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins'; 2 | 3 | $default-width: 690px; 4 | 5 | @include substrate(1000px, 75vw, 1120px) { 6 | .Post-NormalMain .Post-Header, 7 | .Post-NormalMain > div, 8 | .Post-NormalSub > div { 9 | width: var(--inject-page-width); 10 | } 11 | 12 | .Post-NormalMain { 13 | .Post-Header { 14 | /* 文章头部作者 */ 15 | .AuthorInfo { 16 | max-width: none; 17 | width: 0; 18 | } 19 | } 20 | } 21 | 22 | /* 内容图片 */ 23 | .ztext .content_image, 24 | .ztext .origin_image { 25 | max-width: $default-width; 26 | } 27 | 28 | /* 左侧悬浮按钮 */ 29 | .Post-SideActions { 30 | left: calc(50% - (var(--inject-page-width) / 2) - 120px); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/scripts/widescreen/sites/zhuanlan-zhihu-com/index.ts: -------------------------------------------------------------------------------- 1 | import { $ } from '@/utils/selector' 2 | import * as readyState from '@/utils/ready-state' 3 | import styles from './index.lazy.scss' 4 | import type { Site } from '../../types' 5 | 6 | export const zhihuZhuanlan:Site['use'] = ({ store, createControl }) => ({ 7 | handler() { 8 | function execute() { 9 | readyState.DOMContentLoaded(() => { 10 | const process = new WeakSet() 11 | const observer = new MutationObserver(mutationsList => { 12 | mutationsList.forEach(mutation => { 13 | const { target, oldValue } = mutation as unknown as { 14 | target: HTMLImageElement, 15 | oldValue: string 16 | } 17 | if ( 18 | process.has(target) || 19 | target.tagName !== 'IMG' || 20 | !oldValue.startsWith('data:image/') || 21 | // 与知乎同样的选择器判断 22 | !(target.classList.contains('lazy') && !target.classList.contains('data-thumbnail')) 23 | ) return 24 | process.add(target) 25 | // 替换原图 26 | target.dataset.original && (target.src = target.dataset.original) 27 | }) 28 | }) 29 | observer.observe($('.Post-RichTextContainer')!, { subtree: true, attributeFilter: ['src'], attributeOldValue: true }) 30 | }) 31 | 32 | styles.use() 33 | } 34 | 35 | createControl({ store, execute }) 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /src/scripts/widescreen/types.ts: -------------------------------------------------------------------------------- 1 | import { ReadyState } from '@/utils/ready-state' 2 | import createControl from './control' 3 | 4 | export interface UseReturn { 5 | handler: () => void 6 | } 7 | 8 | export interface Site { 9 | name: string 10 | namespace: string 11 | test: 12 | (string | RegExp) 13 | | (string | RegExp)[] 14 | readyState?: ReadyState 15 | use: (payload: { store: Record, createControl: typeof createControl }) => ({ 16 | handler(): void 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * store 3 | * @param modulename 会加入 [[modulename]]- 前缀 4 | * @param local 是否本地存储 5 | */ 6 | function createStore>(modulename = '', local = true): T { 7 | const getRealProp = (property: string) => 8 | modulename 9 | ? `[[${modulename}]]-${property}` 10 | : property 11 | 12 | const store = new Proxy({}, { 13 | get(target, property: string, receiver) { 14 | const realProp = getRealProp(property) 15 | const value = local 16 | ? GM_getValue(realProp) 17 | : Reflect.get(target, realProp, receiver) 18 | return value 19 | }, 20 | set(target, property: string, value, receiver) { 21 | const realProp = getRealProp(property) 22 | local 23 | ? GM_setValue(realProp, value) 24 | : Reflect.set(target, realProp, value, receiver) 25 | return true 26 | }, 27 | deleteProperty(target, property: string) { 28 | const realProp = getRealProp(property) 29 | local 30 | ? GM_deleteValue(realProp) 31 | : Reflect.deleteProperty(target, realProp) 32 | return true 33 | }, 34 | }) 35 | 36 | return store 37 | } 38 | 39 | export default createStore() 40 | export { 41 | createStore, 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/base.ts: -------------------------------------------------------------------------------- 1 | export function throttle any>(fn: T, delay: number): T { 2 | let timeoutId: NodeJS.Timeout 3 | let begin = Date.now() 4 | 5 | return function(this: any, ...args: any[]) { 6 | // eslint-disable-next-line @typescript-eslint/no-this-alias 7 | const self = this 8 | const cur = Date.now() 9 | 10 | clearTimeout(timeoutId) 11 | 12 | if (cur - begin >= delay) { 13 | fn.apply(self, args) 14 | begin = cur 15 | } else { 16 | timeoutId = setTimeout(function() { 17 | fn.apply(self, args) 18 | }, delay) 19 | } 20 | } as any 21 | } 22 | 23 | export function once any>(fn: T): T { 24 | let called = false 25 | return function(this: any, ...args: any[]) { 26 | if (!called) { 27 | called = true 28 | fn.apply(this, args) 29 | } 30 | } as any 31 | } 32 | 33 | /** 34 | * 延时 35 | * @param ms 毫秒数 36 | */ 37 | export const sleep = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)) 38 | 39 | export function isFunction(value: any): value is (...args: any[]) => any { 40 | return typeof value === 'function' 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/compatibility.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | /** @param 版本号 */ 3 | firefox?: number 4 | edge?: number 5 | chrome?: number 6 | safari?: number 7 | /** @param 是否显示通知 */ 8 | notify?: boolean 9 | } 10 | 11 | /** 12 | * 兼容性检查,只是用来拦截低版本用户 13 | * @return 是否通过 14 | */ 15 | export function checker({ 16 | firefox = 75, 17 | edge = 80, 18 | chrome = 80, 19 | safari = 14, 20 | notify = true, 21 | }: Options = {}): boolean { 22 | const { userAgent } = window.navigator 23 | const firefoxVersion = userAgent.match(/Firefox\/(\d+)/)?.[1] 24 | const edgeVersion = userAgent.match(/Edg\/(\d+)/)?.[1] 25 | const chromeVersion = userAgent.match(/Chrome\/(\d+)/)?.[1] 26 | const safariVersion = userAgent.match(/Version\/(\d+).*Safari/)?.[1] // 不保证兼容 27 | 28 | let pass = false 29 | if ( 30 | (firefoxVersion && Number(firefoxVersion) >= firefox) || 31 | (edgeVersion && Number(edgeVersion) >= edge) || 32 | (chromeVersion && Number(chromeVersion) >= chrome) || 33 | (safariVersion && Number(safariVersion) >= safari) 34 | ) { 35 | pass = true 36 | } 37 | 38 | if (!pass) { 39 | const { Toast } = window 40 | notify && Toast && Toast.error(`哎呀!遇到错误:不支持的浏览器版本(需要Chrome${chrome}或Firefox${firefox}以上~),请更新浏览器版本 o(╥﹏╥)o`, 0) 41 | } 42 | return pass 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export function parseToDOM(str: string | null): NodeListOf { 2 | const div = document.createElement('div') 3 | if (typeof str === 'string') { 4 | div.innerHTML = str 5 | } 6 | 7 | return div.childNodes 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | const isDebug = process.env.NODE_ENV !== 'production' 2 | 3 | function warn(...args: any[]) { 4 | isDebug && warn.force(...args) 5 | } 6 | warn.force = function(...args: any[]) { 7 | console.warn( 8 | '%c warn ', 9 | 'background: #ffa500; padding: 1px; color: #fff;', 10 | ...args, 11 | ) 12 | } 13 | 14 | function error(...args: any[]) { 15 | isDebug && error.force(...args) 16 | } 17 | error.force = function(...args: any[]) { 18 | console.error( 19 | '%c error ', 20 | 'background: red; padding: 1px; color: #fff;', 21 | ...args, 22 | ) 23 | } 24 | 25 | function table(...args: any[]): void { 26 | isDebug && console.table(...args) 27 | } 28 | 29 | export { 30 | warn, 31 | error, 32 | table, 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/mount-component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | 引用:https://github.com/youzan/vant/blob/dev/src/utils/mount-component.ts 3 | */ 4 | 5 | import { createApp, type Component } from 'vue' 6 | 7 | function append(el: Element) { 8 | document.body 9 | ? document.body.appendChild(el) 10 | : window.addEventListener('DOMContentLoaded', () => append(el)) 11 | } 12 | 13 | export function mountComponent(RootComponent: Component) { 14 | const app = createApp(RootComponent) 15 | const root = document.createElement('div') 16 | 17 | append(root) 18 | 19 | return { 20 | instance: app.mount(root), 21 | unmount() { 22 | app.unmount() 23 | document.body.removeChild(root) 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/querystring.ts: -------------------------------------------------------------------------------- 1 | interface ParseReturn { 2 | [key: string]: string 3 | } 4 | 5 | /** 6 | * 解析 query 7 | * @param href 或 带有参数格式的 string;有 search 则不再 hash 8 | */ 9 | export function parse(href: string | null = location.href): ParseReturn { 10 | if (!href) return {} 11 | 12 | let search 13 | try { 14 | // 链接 15 | const url = new URL(href) 16 | ;({ search } = url) 17 | // 主要处理对hash的search 18 | if (!search && url.hash.includes('?')) { 19 | search = url.hash.split('?')[1] 20 | } 21 | } catch { 22 | // 非链接,如:a=1&b=2、?a=1、/foo?a=1、/foo#bar?a=1 23 | if (href.includes('?')) { 24 | search = href.split('?')[1] 25 | } else { 26 | search = href 27 | } 28 | } 29 | 30 | return Object.fromEntries(new URLSearchParams(search)) 31 | } 32 | 33 | export function stringify(obj: ParseReturn | { 34 | [key: string]: number 35 | }): string { 36 | return Object.entries(obj) 37 | // 过滤 undefined,保留 null 且转成 '' 38 | .filter(([, value]) => value !== undefined) 39 | .map(([key, value]) => `${key}=${value ?? ''}`) 40 | .join('&') 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/queue.ts: -------------------------------------------------------------------------------- 1 | export default class Queue { 2 | private tasks: (() => Promise)[] = [] 3 | /** 同时进行任务数 默认 3 个 */ 4 | private limit: number 5 | /** 当前执行数 */ 6 | private count = 0 7 | 8 | constructor({ limit = 3 }: { 9 | limit?: Queue['limit'], 10 | } = {}) { 11 | this.limit = limit 12 | } 13 | 14 | /** 任务数 */ 15 | get size() { 16 | return this.tasks.length 17 | } 18 | 19 | enqueue(tasks: Queue['tasks'][number] | Queue['tasks']): this { 20 | if (Array.isArray(tasks)) { 21 | this.tasks.push(...tasks) 22 | } else { 23 | this.tasks.push(tasks) 24 | } 25 | return this 26 | } 27 | 28 | run(): Promise { 29 | return new Promise(resolve => { 30 | if (this.size === 0) { 31 | resolve() 32 | return 33 | } 34 | 35 | const { tasks } = this 36 | const _run = function(this: Queue) { 37 | const idle = Math.min(this.size, this.limit - this.count) 38 | for (let i = 0; i < idle; i++) { 39 | this.count++ 40 | const task = tasks.shift()! 41 | task().finally(() => { 42 | this.count-- 43 | if (this.size > 0) { 44 | _run() 45 | // fix: 队列为空且当前执行的任务也为空才是结束状态 46 | } else if (this.size === 0 && this.count === 0) { 47 | resolve() 48 | } 49 | }) 50 | } 51 | }.bind(this) 52 | 53 | _run() 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/ready-state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * readyState 因为脚本加载时机不一定监听到所有变化 3 | * 所以 pool 中的状态区分先后顺序 4 | * 靠后定义的会自动将靠前定义的但没有监听到的执行一次,但实际上不再是原来的状态 5 | */ 6 | 7 | import { warn } from '@/utils/log' 8 | 9 | export type ReadyState = DocumentReadyState | 'DOMContentLoaded' | 'load' 10 | type ListenerFunc = () => void 11 | 12 | const pool = new Map([ 13 | ['loading', []], 14 | ['interactive', []], 15 | ['DOMContentLoaded', []], // 扩展状态 16 | ['complete', []], 17 | ['load', []], // 扩展状态,不一定可以监听到 18 | ]) 19 | 20 | let currentState: ReadyState = document.readyState 21 | const execute = (readyState: ReadyState = currentState) => { 22 | currentState = readyState 23 | 24 | for (const [state, functions] of pool) { 25 | while (functions.length) { 26 | functions.shift()!() 27 | } 28 | if (readyState === state) break 29 | } 30 | } 31 | 32 | warn('document.readyState', currentState) 33 | 34 | if (document.readyState !== 'complete') { 35 | document.addEventListener('readystatechange', () => execute(document.readyState)) 36 | window.addEventListener('DOMContentLoaded', () => execute('DOMContentLoaded')) 37 | } 38 | window.addEventListener('load', () => execute('load')) 39 | 40 | const wrapper = (readyState: ReadyState, fn?: ListenerFunc): Promise => new Promise(resolve => { 41 | pool.get(readyState)!.push(function() { 42 | resolve(fn?.()) 43 | }) 44 | 45 | // 立即检查一下 46 | execute() 47 | }) 48 | 49 | export const loading = (fn?: ListenerFunc) => wrapper('loading', fn) 50 | export const interactive = (fn?: ListenerFunc) => wrapper('interactive', fn) 51 | export const DOMContentLoaded = (fn?: ListenerFunc) => wrapper('DOMContentLoaded', fn) 52 | export const complete = (fn?: ListenerFunc) => wrapper('complete', fn) 53 | export const load = (fn?: ListenerFunc) => wrapper('load', fn) 54 | -------------------------------------------------------------------------------- /src/utils/selector.ts: -------------------------------------------------------------------------------- 1 | export const $ = document.querySelector.bind(document) 2 | export const $$ = document.querySelectorAll.bind(document) 3 | -------------------------------------------------------------------------------- /src/utils/visibility-state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 页面 visible 时执行 setInterval 3 | * 参数同 setInterval,返回终止函数 4 | */ 5 | export function onVisible(callback: (...args: TArgs) => void, delay = 500, ...rest: TArgs) { 6 | let intervalId: NodeJS.Timer 7 | function listener() { 8 | clearInterval(intervalId) 9 | if (document.visibilityState === 'hidden') return 10 | // eslint-disable-next-line n/no-callback-literal 11 | callback(...rest) 12 | intervalId = setInterval(callback, delay, ...rest) 13 | } 14 | 15 | listener() 16 | document.addEventListener('visibilitychange', listener) 17 | 18 | return function abort() { 19 | clearInterval(intervalId) 20 | document.removeEventListener('visibilitychange', listener) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/vue-root.ts: -------------------------------------------------------------------------------- 1 | interface VueRoot { 2 | instance?: Record 3 | app?: Record 4 | } 5 | 6 | export interface VueHTMLElement extends HTMLElement { 7 | __vue__?: Record 8 | // eslint-disable-next-line camelcase 9 | __vue_app__?: Record 10 | _vnode?: { 11 | component: { 12 | proxy: Record 13 | } 14 | } 15 | } 16 | 17 | function getVueRoot(rootContainer: HTMLElement): VueRoot { 18 | if (isVue2(rootContainer)) return getVue2Instance(rootContainer) 19 | if (isVue3(rootContainer)) return getVue3Instance(rootContainer) 20 | return {} 21 | } 22 | 23 | function isVue2(rootContainer: HTMLElement) { 24 | return '__vue__' in rootContainer 25 | } 26 | 27 | function isVue3(rootContainer: HTMLElement) { 28 | // https://github.com/vuejs/vue-next/blob/a66e53a24f445b688eef6812ecb872dc53cf2702/packages/runtime-core/src/apiCreateApp.ts#L252 29 | return '__vue_app__' in rootContainer 30 | } 31 | 32 | function getVue2Instance(rootContainer: VueHTMLElement): VueRoot { 33 | return { 34 | instance: rootContainer.__vue__, 35 | } 36 | } 37 | 38 | function getVue3Instance(rootContainer: VueHTMLElement): VueRoot { 39 | return { 40 | app: rootContainer.__vue_app__, 41 | // dev mode下组件el有__vueParentComponent __vnode属性 42 | // https://github.com/vuejs/vue-next/blob/3867bb4c14131ef94098a62bffba97a5b7d1fe66/packages/runtime-core/src/renderer.ts#L767 43 | // https://github.com/vuejs/vue-next/blob/3867bb4c14131ef94098a62bffba97a5b7d1fe66/packages/runtime-core/src/renderer.ts#L763 44 | 45 | // _vnode.component.proxy获取实例,应该就是app.mount返回的 46 | // https://github.com/vuejs/vue-next/blob/a66e53a24f445b688eef6812ecb872dc53cf2702/packages/runtime-core/src/apiCreateApp.ts#L258 47 | // https://github.com/vuejs/vue-next/blob/3867bb4c14131ef94098a62bffba97a5b7d1fe66/packages/runtime-core/src/renderer.ts#L2198 48 | instance: rootContainer._vnode && rootContainer._vnode.component.proxy, 49 | } 50 | } 51 | 52 | export { 53 | getVueRoot, 54 | } 55 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-sass-guidelines', 5 | ], 6 | rules: { 7 | 'selector-class-pattern': null, // 名称限制 8 | 'max-nesting-depth': null, // 嵌套的深度 9 | 'selector-max-id': null, // ID 选择器数量 10 | 'selector-id-pattern': null, // ID 选择器名称 11 | 'selector-max-compound-selectors': null, // 选择器中复合选择器的数量 12 | 'declaration-property-value-disallowed-list': null, // 在声明中指定不允许的属性和值的列表 13 | 'selector-no-qualifying-type': null, // 禁止按类型限定选择器 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | "paths": { 48 | "@/*": ["src/*"] 49 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const semver = require('semver') 4 | const webpack = require('webpack') 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 6 | const ESLintPlugin = require('eslint-webpack-plugin') 7 | const StylelintPlugin = require('stylelint-webpack-plugin') 8 | const TerserPlugin = require('terser-webpack-plugin') 9 | // const CopyPlugin = require('copy-webpack-plugin') 10 | const packageInfo = require('./package.json') 11 | 12 | const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) 13 | 14 | /** 获取所有安装的依赖版本 */ 15 | function getPkgDepsVersion() { 16 | const deps = { 17 | ...packageInfo.devDependencies, 18 | ...packageInfo.dependencies, 19 | } 20 | for (const pkgName in deps) { 21 | if (hasOwn(deps, pkgName)) { 22 | const semverVersion = deps[pkgName] 23 | deps[pkgName] = semver.coerce(semverVersion).version 24 | } 25 | } 26 | return deps 27 | } 28 | 29 | const depsVersion = getPkgDepsVersion() 30 | 31 | function getScriptHeader(filename, argvMode) { 32 | const filepath = path.join(__dirname, './src/scripts-header', `${filename}.js`) 33 | const isProd = argvMode === 'production' 34 | return fs.existsSync(filepath) ? require(filepath)(isProd, depsVersion) : '' 35 | } 36 | 37 | module.exports = (env, argv) => ({ 38 | devtool: false, 39 | entry: { 40 | lanhu: './src/scripts/lanhu', 41 | tieba: './src/scripts/tieba', 42 | widescreen: './src/scripts/widescreen', 43 | redirect: './src/scripts/redirect', 44 | pixiv: './src/scripts/pixiv', 45 | github: './src/scripts/github', 46 | bilibili: './src/scripts/bilibili', 47 | 'view-ui': './src/scripts/view-ui', 48 | 'element-ui': './src/scripts/element-ui', 49 | 'mdn-web-docs': './src/scripts/mdn-web-docs', 50 | 'google-redirect': './src/scripts/google-redirect', 51 | toast: './src/helpers/toast', 52 | }, 53 | output: { 54 | path: path.join(__dirname, 'dist'), 55 | publicPath: '/', 56 | }, 57 | externals: { 58 | vue: 'Vue', 59 | viewerjs: 'Viewer', 60 | 'crypto-js/md5': 'CryptoJS.MD5', 61 | }, 62 | resolve: { 63 | extensions: ['.js', '.ts', '.tsx', '.json'], 64 | alias: { 65 | '@': path.join(__dirname, './src'), 66 | }, 67 | }, 68 | module: { 69 | rules: [ 70 | { 71 | test: /\.(js|ts|tsx)$/, 72 | exclude: /node_modules/, 73 | use: ['babel-loader'], 74 | }, 75 | { 76 | test: /\.s[ac]ss$/i, 77 | exclude: [ 78 | /\.lazy\.s[ac]ss$/i, 79 | /\.string\.s[ac]ss$/i, 80 | ], 81 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'], 82 | }, 83 | { 84 | test: /\.lazy\.s[ac]ss$/i, 85 | use: [ 86 | { loader: 'style-loader', options: { injectType: 'lazyStyleTag' } }, 87 | 'css-loader', 'postcss-loader', 'sass-loader', 88 | ], 89 | }, 90 | { 91 | test: /\.string\.s[ac]ss$/i, 92 | use: ['css-loader', 'postcss-loader', 'sass-loader'], 93 | }, 94 | ], 95 | }, 96 | plugins: [ 97 | new CleanWebpackPlugin(), // 默认依赖 output path 98 | new ESLintPlugin({ 99 | extensions: ['js', 'ts', 'tsx'], 100 | fix: true, 101 | }), 102 | new StylelintPlugin({ 103 | fix: true, 104 | }), 105 | new webpack.BannerPlugin({ 106 | banner: file => getScriptHeader(file.chunk.name, argv.mode), 107 | raw: true, 108 | entryOnly: true, 109 | }), 110 | // new CopyPlugin({ 111 | // patterns: [ 112 | // { from: path.join(__dirname, './src/helpers/toast.js') }, 113 | // ], 114 | // }), 115 | ], 116 | // 遵守Greasy Fork代码规定,不做最小化处理 117 | // https://greasyfork.org/zh-CN/help/code-rules 118 | optimization: { 119 | minimize: false, 120 | minimizer: [new TerserPlugin({ 121 | extractComments: false, 122 | terserOptions: { 123 | output: { 124 | comments: true, 125 | }, 126 | }, 127 | })], 128 | }, 129 | devServer: { 130 | port: 8886, 131 | static: [ 132 | { 133 | directory: path.resolve(__dirname, 'dist'), 134 | }, 135 | ], 136 | }, 137 | }) 138 | --------------------------------------------------------------------------------