Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greasyfork.org/scripts/496315/1392531/QuickMenu.js
K nainstalování skriptu Stylus potřebujete nejprve nainstalovat doplňek prohlížeče.
K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Stylus.
K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Stylus.
K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských stylů.
K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských stylů.
(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)
// ==UserScript==
// @name QuickMenu
// @namespace https://github.com/JiyuShao/greasyfork-scripts
// @version 2024-06-11
// @description 油猴菜单库,支持开关菜单,支持状态保持,支持 Iframe
// @author Jiyu Shao <jiyu.shao@gmail.com>
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addValueChangeListener
// ==/UserScript==
// 快捷生成菜单逻辑
const QuickMenu = {
isInited: false,
label: '',
isUpdating: false,
stateConfigMap: {}, // 状态数据
storeConfigMap: {}, // 缓存数据
init: function () {
if (this.isInited) {
return;
}
this.isInited = true;
// 初始化 label
if (!this.label) {
let level = 0;
let currentWindow = window;
while (currentWindow.parent && currentWindow !== currentWindow.parent) {
// 增加计数器,因为我们进入了更深层的嵌套
level++;
// 移动到父级窗口
currentWindow = currentWindow.parent;
// 可选:设置一个最大嵌套层数以避免无限循环
if (level > 10) {
// 这个数字可以根据实际情况调整
console.warn('可能存在无限循环的iframe嵌套,停止计数');
break;
}
}
// 如果level大于0,我们至少在一个嵌套的iframe中
const currentOrderMap = GM_getValue('QM_ORDER_MAP') || {};
const currentOrder = (currentOrderMap[level] || -1) + 1;
GM_setValue('QM_ORDER_MAP', {
...currentOrderMap,
[level]: currentOrder,
});
this.label = `第${level}层第${currentOrder}个`;
}
// 添加更新监听回调,保证菜单展示正确,不需要销毁
GM_addValueChangeListener(
'QM_TRIGGER_UPDATE',
(_key, _oldValue, _newValue, remote) => {
console.log('[QuickMenu] QM_TRIGGER_UPDATE', {
currentLabel: this.label,
remote,
oldValue: _oldValue,
newValue: _newValue,
});
if (remote) {
this._update({
useStore: true, // 表示触发源是远程其他模块,需要从 store 中获取数据
triggerCallback: true, // 需要重新执行回调刷新逻辑
triggerRemote: false, // 不需要再次触发远程更新
});
}
}
);
},
// 存储菜单
setMenuConfigStore: function () {
Object.values(this.stateConfigMap).forEach((e) => {
this.storeConfigMap[e.name] = {
value: e.value,
};
});
GM_setValue('QM_MENU', this.storeConfigMap);
// 触发其他实例进行更新
GM_setValue('QM_TRIGGER_UPDATE', `${this.label}:${Math.random()}`);
},
// 获取菜单配置
getMenuConfigStore: function () {
// 初始化 store 数据
this.storeConfigMap = GM_getValue('QM_MENU') || {};
},
clearStore: function () {
// 清空 store 数据
GM_setValue('QM_MENU', undefined);
this._update({
useStore: true, // 使用 store 数据,只更新当前环境
triggerCallback: true, // 当前环境也要执行回调
triggerRemote: true, // 需要再次触发远程更新
});
},
// 添加菜单配置
add: function (config) {
this.init();
// 兼容数组配置
if (Array.isArray(config)) {
config.forEach((e) => this.add(e));
for (var i in config) {
this.add(config[i]);
}
return;
}
// 检查配置名称
if (!config.name && typeof config === 'object') {
alert('QM_MENU.add Config name is need.');
return;
}
// 添加到状态配置数据中
this.stateConfigMap[config.name] = {
...config,
isInited: false, // 需要执行回调初始化执行的逻辑
};
// 执行更新的逻辑
if (!this.isUpdating) {
this.isUpdating = true;
// 这里放到宏任务队列中执行,批量更新
setTimeout(() => {
this.isUpdating = false;
// 更新数据 & UI
this._update();
}, 0);
}
},
// 更新状态数据
_updateState: function (options) {
const { useStore = false } = options || {};
this.getMenuConfigStore();
Object.values(this.stateConfigMap).forEach((currentConfig) => {
let menuDisplay = currentConfig.name;
// 为 Toggle 定制展示名称
if (currentConfig.type === 'toggle') {
// 使用 store 里的缓存值,有以下两种情况:
// 1. 当前配置还没初始化
// 2. 由于会有多实例的情况,useStore 的话以 store 数据为准
if (!currentConfig.isInited || useStore) {
const currentStoreConfig = this.storeConfigMap[currentConfig.name];
currentConfig.value =
currentStoreConfig && currentStoreConfig.value
? currentStoreConfig.value
: 'off';
}
// 如果没有值的话,默认为 off
currentConfig.value = currentConfig.value ? currentConfig.value : 'off';
menuDisplay = `${menuDisplay}[${
currentConfig.value === 'on' ? 'x' : ' '
}]`;
}
});
console.debug(`[QuickMenu] ${this.label}: 状态已更新`, {
options,
stateConfigMap: this.stateConfigMap,
});
},
// 更新菜单、执行初始化回调、保存 store、触发远程更新
_commitUpdate: function (options) {
const { triggerCallback = false, triggerRemote = true } = options || {};
Object.values(this.stateConfigMap).forEach((currentConfig) => {
// 判断是否可以执行菜单回调
let runCallbackFlag = false;
if (typeof currentConfig.shouldInitRun === 'boolean') {
runCallbackFlag = currentConfig.shouldInitRun;
} else if (typeof currentConfig.shouldInitRun === 'function') {
runCallbackFlag = !!currentConfig.shouldInitRun.call(null);
}
// 判断是否需要注入菜单
let updateMenuFlag = true;
if (typeof currentConfig.shouldAddMenu === 'boolean') {
updateMenuFlag = currentConfig.shouldAddMenu;
} else if (typeof currentConfig.shouldAddMenu === 'function') {
updateMenuFlag = !!currentConfig.shouldAddMenu.call(null);
}
// 生成菜单名称
let menuDisplay = currentConfig.name;
if (currentConfig.type === 'toggle') {
// 如果没有值的话,默认为 off
currentConfig.value = currentConfig.value ? currentConfig.value : 'off';
menuDisplay = `${menuDisplay}[${
currentConfig.value === 'on' ? 'x' : ' '
}]`;
}
// 执行回调有两个时机
// 1. 初始化时
// 2. 接收到远程更新时
if ((!currentConfig.isInited || triggerCallback) && runCallbackFlag) {
currentConfig.isInited = true;
currentConfig.callback &&
currentConfig.callback.call(null, currentConfig.value);
}
// 有时候需要更新菜单,所以这里先卸载
if (currentConfig.id) {
GM_unregisterMenuCommand(currentConfig.id); // 删除菜单
delete currentConfig.id;
}
if (updateMenuFlag) {
currentConfig.id = GM_registerMenuCommand(
menuDisplay,
() => {
console.debug(`[QuickMenu] ${this.label}:点击${menuDisplay}`);
// 切换 value,并更新,实际执行时只有 toggle 的值会更新
currentConfig.value = { on: 'off', off: 'on' }[currentConfig.value];
// 使用最新的 value 执行用户回调
currentConfig.callback &&
currentConfig.callback.call(null, currentConfig.value);
// 放到最后更新,因为用户回调有可能会影响数据,如清空 Store
this._update();
},
{ autoClose: false }
);
}
});
// 远程的不需要更新数据,只需要注册菜单
if (triggerRemote) {
this.setMenuConfigStore();
}
console.debug(`[QuickMenu] ${this.label}: 更新已提交`);
},
// 触发更新
_update: function (options) {
this._updateState(options);
this._commitUpdate(options);
},
};