QuickMenu

油猴菜单库,支持开关菜单,支持状态保持,支持 Iframe

目前为 2024-05-28 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/496315/1384546/QuickMenu.js

  1. // ==UserScript==
  2. // @name QuickMenu
  3. // @namespace https://github.com/JiyuShao/greasyfork-scripts
  4. // @version 2024-05-23
  5. // @description 油猴菜单库,支持开关菜单,支持状态保持,支持 Iframe
  6. // @author Jiyu Shao <jiyu.shao@gmail.com>
  7. // @license MIT
  8. // @grant unsafeWindow
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_unregisterMenuCommand
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_addValueChangeListener
  14. // ==/UserScript==
  15.  
  16. // 快捷生成菜单逻辑
  17. const QuickMenu = {
  18. isInited: false,
  19. label: '',
  20. isUpdating: false,
  21. stateConfigMap: {}, // 状态数据
  22. storeConfigMap: {}, // 缓存数据
  23. init: function () {
  24. if (this.isInited) {
  25. return;
  26. }
  27. this.isInited = true;
  28.  
  29. // 初始化 label
  30. if (!this.label) {
  31. let level = 0;
  32. let currentWindow = window;
  33. while (currentWindow.parent && currentWindow !== currentWindow.parent) {
  34. // 增加计数器,因为我们进入了更深层的嵌套
  35. level++;
  36. // 移动到父级窗口
  37. currentWindow = currentWindow.parent;
  38. // 可选:设置一个最大嵌套层数以避免无限循环
  39. if (level > 10) {
  40. // 这个数字可以根据实际情况调整
  41. console.warn('可能存在无限循环的iframe嵌套,停止计数');
  42. break;
  43. }
  44. }
  45. // 如果level大于0,我们至少在一个嵌套的iframe中
  46. const currentOrderMap = GM_getValue('MD_ORDER_MAP') || {};
  47. const currentOrder = (currentOrderMap[level] || -1) + 1;
  48. GM_setValue('MD_ORDER_MAP', {
  49. ...currentOrderMap,
  50. [level]: currentOrder,
  51. });
  52. this.label = `第${level}层第${currentOrder}个`;
  53. }
  54.  
  55. // 添加更新监听回调,保证菜单展示正确,不需要销毁
  56. GM_addValueChangeListener(
  57. 'MD_TRIGGER_UPDATE',
  58. (_key, _oldValue, _newValue, remote) => {
  59. console.log('[Monkey Debuger] MD_TRIGGER_UPDATE', {
  60. currentLabel: this.label,
  61. remote,
  62. oldValue: _oldValue,
  63. newValue: _newValue,
  64. });
  65. if (remote) {
  66. this._update({
  67. useStore: true, // 表示触发源是远程其他模块,需要从 store 中获取数据
  68. triggerCallback: true, // 需要重新执行回调刷新逻辑
  69. triggerRemote: false, // 不需要再次触发远程更新
  70. });
  71. }
  72. }
  73. );
  74. },
  75. // 存储菜单
  76. setMenuConfigStore: function () {
  77. Object.values(this.stateConfigMap).forEach((e) => {
  78. this.storeConfigMap[e.name] = {
  79. value: e.value,
  80. };
  81. });
  82. GM_setValue('MD_MENU', this.storeConfigMap);
  83. // 触发其他实例进行更新
  84. GM_setValue('MD_TRIGGER_UPDATE', `${this.label}:${Math.random()}`);
  85. },
  86. // 获取菜单配置
  87. getMenuConfigStore: function () {
  88. // 初始化 store 数据
  89. this.storeConfigMap = GM_getValue('MD_MENU') || {};
  90. },
  91. clearStore: function () {
  92. // 清空 store 数据
  93. GM_setValue('MD_MENU', undefined);
  94. this._update({
  95. useStore: true, // 使用 store 数据,只更新当前环境
  96. triggerCallback: true, // 当前环境也要执行回调
  97. triggerRemote: true, // 需要再次触发远程更新
  98. });
  99. },
  100. // 添加菜单配置
  101. add: function (config) {
  102. this.init();
  103. // 兼容数组配置
  104. if (Array.isArray(config)) {
  105. config.forEach((e) => this.add(e));
  106. for (var i in config) {
  107. this.add(config[i]);
  108. }
  109. return;
  110. }
  111. // 检查配置名称
  112. if (!config.name && typeof config === 'object') {
  113. alert('MD_MENU.add Config name is need.');
  114. return;
  115. }
  116. // 添加到状态配置数据中
  117. this.stateConfigMap[config.name] = {
  118. ...config,
  119. isInited: false, // 需要执行回调初始化执行的逻辑
  120. };
  121.  
  122. // 执行更新的逻辑
  123. if (!this.isUpdating) {
  124. this.isUpdating = true;
  125. // 这里放到宏任务队列中执行,批量更新
  126. setTimeout(() => {
  127. this.isUpdating = false;
  128. // 更新数据 & UI
  129. this._update();
  130. }, 0);
  131. }
  132. },
  133. // 更新状态数据
  134. _updateState: function (options) {
  135. const { useStore = false } = options || {};
  136. this.getMenuConfigStore();
  137. Object.values(this.stateConfigMap).forEach((currentConfig) => {
  138. let menuDisplay = currentConfig.name;
  139. // 为 Toggle 定制展示名称
  140. if (currentConfig.type === 'toggle') {
  141. // 使用 store 里的缓存值,有以下两种情况:
  142. // 1. 当前配置还没初始化
  143. // 2. 由于会有多实例的情况,useStore 的话以 store 数据为准
  144. if (!currentConfig.isInited || useStore) {
  145. const currentStoreConfig = this.storeConfigMap[currentConfig.name];
  146. currentConfig.value =
  147. currentStoreConfig && currentStoreConfig.value
  148. ? currentStoreConfig.value
  149. : 'off';
  150. }
  151.  
  152. // 如果没有值的话,默认为 off
  153. currentConfig.value = currentConfig.value ? currentConfig.value : 'off';
  154. menuDisplay = `${menuDisplay}[${
  155. currentConfig.value === 'on' ? 'x' : ' '
  156. }]`;
  157. }
  158. });
  159.  
  160. console.debug(`[Monkey Debuger] ${this.label}: 状态已更新`, {
  161. options,
  162. stateConfigMap: this.stateConfigMap,
  163. });
  164. },
  165. // 更新菜单、执行初始化回调、保存 store、触发远程更新
  166. _commitUpdate: function (options) {
  167. const { triggerCallback = false, triggerRemote = true } = options || {};
  168. Object.values(this.stateConfigMap).forEach((currentConfig) => {
  169. // 判断是否可以执行菜单回调
  170. let runCallbackFlag = false;
  171. if (typeof currentConfig.shouldInitRun === 'boolean') {
  172. runCallbackFlag = currentConfig.shouldInitRun;
  173. } else if (typeof currentConfig.shouldInitRun === 'function') {
  174. runCallbackFlag = !!currentConfig.shouldInitRun.call(null);
  175. }
  176. // 判断是否需要注入菜单
  177. let updateMenuFlag = true;
  178. if (typeof currentConfig.shouldAddMenu === 'boolean') {
  179. updateMenuFlag = currentConfig.shouldAddMenu;
  180. } else if (typeof currentConfig.shouldAddMenu === 'function') {
  181. updateMenuFlag = !!currentConfig.shouldAddMenu.call(null);
  182. }
  183. // 生成菜单名称
  184. let menuDisplay = currentConfig.name;
  185. if (currentConfig.type === 'toggle') {
  186. // 如果没有值的话,默认为 off
  187. currentConfig.value = currentConfig.value ? currentConfig.value : 'off';
  188. menuDisplay = `${menuDisplay}[${
  189. currentConfig.value === 'on' ? 'x' : ' '
  190. }]`;
  191. }
  192. // 执行回调有两个时机
  193. // 1. 初始化时
  194. // 2. 接收到远程更新时
  195. if ((!currentConfig.isInited || triggerCallback) && runCallbackFlag) {
  196. currentConfig.isInited = true;
  197. currentConfig.callback &&
  198. currentConfig.callback.call(null, currentConfig.value);
  199. }
  200. // 有时候需要更新菜单,所以这里先卸载
  201. if (currentConfig.id) {
  202. GM_unregisterMenuCommand(currentConfig.id); // 删除菜单
  203. delete currentConfig.id;
  204. }
  205. if (updateMenuFlag) {
  206. currentConfig.id = GM_registerMenuCommand(
  207. menuDisplay,
  208. () => {
  209. console.debug(`[Monkey Debuger] ${this.label}:点击${menuDisplay}`);
  210. // 切换 value,并更新,实际执行时只有 toggle 的值会更新
  211. currentConfig.value = { on: 'off', off: 'on' }[currentConfig.value];
  212. // 使用最新的 value 执行用户回调
  213. currentConfig.callback &&
  214. currentConfig.callback.call(null, currentConfig.value);
  215. // 放到最后更新,因为用户回调有可能会影响数据,如清空 Store
  216. this._update();
  217. },
  218. { autoClose: false }
  219. );
  220. }
  221. });
  222. // 远程的不需要更新数据,只需要注册(不可用)菜单
  223. if (triggerRemote) {
  224. this.setMenuConfigStore();
  225. }
  226. console.debug(`[Monkey Debuger] ${this.label}: 更新已提交`);
  227. },
  228. // 触发更新
  229. _update: function (options) {
  230. this._updateState(options);
  231. this._commitUpdate(options);
  232. },
  233. };

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址