油猴菜单库 水果玉米系列
本帖最后由 溯水流光 于 2025-5-28 18:41 编辑> 库地址: https://scriptcat.org/zh-CN/script-show-page/3508
> // @require https://scriptcat.org/lib/3508/1.0.2/MenuManager.js
## 本库的功能
油猴菜单库,支持开关菜单,支持批量添加,为您解决批量添加和开关菜单的烦恼
+ 支持批量添加菜单
+ 支持快速实现开关菜单
+ 支持开关状态自动存储并持久化
实际应用的案例, 油管自动跳转: https://bbs.tampermonkey.net.cn/thread-8787-1-1.html
油管播放短视频时, 自动跳转为长视频播放页面
文末通过一个简单的小案例,带你了解 MenuManager 的使用。
## 灵感来源
https://greasyfork.org/zh-CN/scripts/411512-gm-createmenu
5年前的库了, 很感谢作者的开源和分享, 但是其代码质量实在无法恭维, 我重构和整理了 2 个多小时, 最后还是选择直接推倒重来
但本库的 api 设计上是有参考这个库的
## 使用方式
> 本库是对 GM_registerMenuCommand 和 GM_unregisterMenuCommand 的封装
>
> 所以使用前, 请先参考 GM_registerMenuCommand 的使用教程:
>
> https://bbs.tampermonkey.net.cn/thread-271-1-1.html
### 使用前的准备
```
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
```
我们需要为脚本申请这两个权限, 才能成功通过 menuManager 创建菜单项
去除 grant none, 这会导致我们无法申请 GM 函数, 直接去除就好了:
```
// @grant none
```
或着可以修改为:
```
// @grant unsafeWindow
```
### 创建一个开关菜单
```js
menuManager.addAndCreate({
// 开关默认状态, 不写 default 的话, 默认为 true
default : true,
on : {
// 开关 开启状态的提示信息
name : "自动跳转状态: 开启✅ (点我关闭)",
},
off : {
// 开关 关闭状态的提示信息
name : "自动跳转状态: 关闭❎ (点我开启)",
},
accessKey: 'E', // 快捷键
callback(state, isInit){
// state 为当前开关状态, isInit 为是否为初始化
console.log(state, isInit)
}
});
```
大功告成了:
<img src="data/attachment/forum/202505/28/183450jaztzaojlfgwptfi.png" width="400" />
可以自己点击菜单, 观察控制台输出
menuManager 创建开关菜单的内部逻辑:
```
menuManager addAndCreate 时
会通过 on 和 off 的 name, 获取唯一 name, 称之为 storeKey
menuManager 会通过这个 storeKey 去 localStorage 获取存储的 开关状态的 boolean 值
如果为 null, 则使用 default 指定的值为 开关状态的 boolean 值
如果不为 null, 则转换 "true" "false" 为 true false, 并作为 开关状态的 boolean 值
开关状态的 boolean, 我们用 currState 存储
然后会调用 callback(currState, true) 这里便是初始化
然后会通过 GM_registerMenuCommand 注册菜单, 来显示对应的 on 或 off 的 name
当菜单点击的时候, 会反转 currState, 并调用 callback(currState, false)
并通过一系列操作, 更新显示正确的 on 或 off name
```
可以直接阅读源码, 获取更多细节, 源码不算长, 才 261 行
### 创建普通菜单
```js
menuManager.addAndCreate(
{
name : "点击我",
accessKey: 'C', // 快捷键
callback(){
alert("menu is been clicked");
}
}
);
```
<img src="data/attachment/forum/202505/28/183633f9mqneoteqmte689.png" width="400" />
### 批量创建菜单
menuManager.addAndCreate 可以传入数组
```js
menuManager.addAndCreate([
{
// 开关状态, 不写 default 的话, 默认为 true
default : true,
on : {
// 开关 开启状态的提示信息
name : "自动跳转状态: 开启✅ (点我关闭)",
},
off : {
// 开关 关闭状态的提示信息
name : "自动跳转状态: 关闭❎ (点我开启)",
},
accessKey: 'E', // 快捷键
callback(state, isInit){
// state 为当前开关状态, isInit 为是否为初始化
console.log(state, isInit)
}
},
{
name : "点击我",
accessKey: 'C', // 快捷键
callback(){
alert("menu is been clicked");
}
}
]);
```
## API
下面的 api, 都返回了 menuManager, 方便链式调用
```
// 添加单条或多条菜单项
add(entryOrEntries)
```
```
// 关闭 menuManager 的日志
disableLog()
```
```
// add 添加后, 要 create 才能正常使用
create()
```
```
// add 并自动 create
addAndCreate(entryOrEntries)
```
## 综合练习
我们写一个小案例, 来熟悉 menuManager 的使用
案例目标: Google 自动聚焦
> 默认 Google 就是聚焦的, 这里的功能为, 自动展开搜索记录, 本案例只是为了练习
### 引入 vite-monkey-plugin
> https://github.com/lisonge/vite-plugin-monkey
vite-monkey-plugin 支持 HMR: 本地 ctrl + s 保存,浏览器自动更新脚本。
使用 vite-monkey-plugin ,有诸多好处:
1.可以使用现代的 IDE 进行脚本开发,如我们这里使用免费的 WebStrom,WebStrom 在去年的 1024 程序员节,支持开源项目免费使用了。
2.使用了 vite 了,我们可以模块化开发脚本,使用 ES Module 来开发脚本,可以把脚本拆分为多个模块,方便复用和维护。
### 初始化项目
```
pnpm create monkey
```
名字叫: `monkey-google-focus`
选择 Vanilla, 也就是纯 JS 开发脚本
src下, main.js 清空, 其他全部删除
目录结构如下所示:
```
$ tree
.
|-- src
| `-- main.js
|-- package.json
|-- pnpm-lock.yaml
`-- vite.config.js
19 directories, 52 files
```
```
pnpm run dev
```
vite-monkey-plugin 默认创建出来的项目就是匹配 google, 无需配置
main.js 可以简单打印 `console.log("hello world")` 来检查是否生效
### 配置
```js
export default defineConfig({
plugins: [
monkey({
entry: 'src/main.js',
userscript: {
icon: 'https://vitejs.dev/logo.svg',
namespace: 'npm/vite-plugin-monkey',
match: ['https://www.google.com/'],
},
}),
],
});
```
修改为
```js
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
monkey({
entry: 'src/main.js', // 脚本入口
userscript: {
icon: 'https://vitejs.dev/logo.svg',
namespace: 'npm/vite-plugin-monkey',
match: ['https://www.google.com/*'],
version: "1.0.0",
author: "hzx", // 你的名字
license: "MIT", // 开源协议
description: "google search auto focus", // 介绍
},
server: { mountGmApi: true }, // 将 GM 函数注入为全局变量
}),
],
});
```
### 导入库
为了更好的代码提示, 我们选择直接把油猴库保存到本地
我们这里用了:
MenuManager:
```
https://scriptcat.org/zh-CN/script-show-page/3508
```
elmGetter 魔改版 (你也可以用原版, 这完全是习惯问题):
```
https://scriptcat.org/zh-CN/script-show-page/2847
```
把代码保存到本地
```
$ tree -I 'node_modules'
.
|-- src
| |-- lib
| | |-- elmGetter.js
| | `-- menuManager.js
| `-- main.js
|-- package.json
|-- pnpm-lock.yaml
`-- vite.config.js
3 directories, 7 files
```
修改为 ES module 导出:
```
- var menuManager = (() => {
+ export const menuManager = (() => {
```
两个都要修改
### 开始梭哈
elmGetter.get 使用起来, 其实就是异步的 querySelector:
```js
import {elmGetter} from "./lib/elmGetter.js";
async function main() {
const searchEl = await elmGetter.get(".gLFyf")
searchEl.click();
}
main()
```
然后添加开关菜单栏:
```js
import {elmGetter} from "./lib/elmGetter.js";
import {menuManager} from "./lib/menuManager.js";
async function main() {
menuManager.addAndCreate({
// 开关状态, 不写 default 的话, 默认为 true
default : true,
on : {
// 开关 开启状态的提示信息
name : "自动聚焦状态: 开启✅ (点我关闭)",
},
off : {
// 开关 关闭状态的提示信息
name : "自动聚焦状态: 关闭❎ (点我开启)",
},
accessKey: 'E', // 快捷键
// state 为当前开关状态, isInit 为是否为初始化
callback(state, isInit){
if (state) {
setFocus();
}
}
});
}
main()
async function setFocus() {
const searchEl = await elmGetter.get(".gLFyf")
searchEl.click();
}
```
如果 callback 要调用 async, 同时要保证这些 async 有序执行, 可以自己实现一个异步队列:
callback 中, 将 async 函数, addToQueue 就 ok 了
```js
let queue = [];
let isProcessing = false;
async function processQueue() {
if (isProcessing || queue.length === 0) return;
isProcessing = true;
const task = queue.shift();
await task();
isProcessing = false;
await processQueue();
}
function addToQueue(task) {
queue.push(task);
processQueue();
}
export { addToQueue };
```
## 尾声
感谢你的阅读, 如果有任何问题, 欢迎评论区讨论
开源地址:https://github.com/HHsomeHand/monkey-lib-menu-manager
欢迎提 issue, 发 PR ggnb,感谢哥哥的分享 王一之 发表于 2025-5-28 20:00
ggnb,感谢哥哥的分享
111111111111111111111111111 const options = {
menus: {
autoFocus: { toStr: (x) => '自动聚焦状态:' + (x ? '开启✅ (点我关闭)' : '关闭❎ (点我开启)') },
},
loads: function () {
Object.keys(this.menus).forEach(v => {
let val = GM_getValue(v)
this.menus['_menu'] = GM_registerMenuCommand(this.menus.toStr(val), () => {
GM_setValue(v, !val)
Object.keys(this.menus).forEach(v => GM_unregisterMenuCommand(this.menus['_menu']))
this.loads()
})
})
}
}
options.loads()
一直都是这样写菜单的,开关一多就使用一堆的字段{:4_97:} 谢谢楼主分享,厉害啊
页:
[1]