溯水流光 发表于 2025-5-28 18:39:32

油猴菜单库 水果玉米系列

本帖最后由 溯水流光 于 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

王一之 发表于 2025-5-28 20:00:30

ggnb,感谢哥哥的分享

熊猫666 发表于 2025-5-28 23:05:34

王一之 发表于 2025-5-28 20:00
ggnb,感谢哥哥的分享

111111111111111111111111111

empyrealtear 发表于 3 天前

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:}

youhou22322 发表于 3 天前

谢谢楼主分享,厉害啊
页: [1]
查看完整版本: 油猴菜单库 水果玉米系列