setupCommands

Library that creates regular, toggle, and radio menu commands for userscript managers

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/498119/1399005/setupCommands.js

  1. // ==UserScript==
  2. // @name setupCommands
  3. // @license MIT
  4. // @namespace rtonne
  5. // @match *://*/*
  6. // @version 2.1
  7. // @author Rtonne
  8. // @description Library that creates regular, toggle, and radio menu commands for userscript managers
  9. // @grant GM.registerMenuCommand
  10. // @grant GM.unregisterMenuCommand
  11. // @grant GM.getValue
  12. // @grant GM.setValue
  13. // ==/UserScript==
  14.  
  15. /**
  16. * Can be the only function of this library used externally.
  17. * @param {_Command[]} command_list
  18. */
  19. async function setupCommands(command_list) {
  20. for (const command of command_list) {
  21. await _runCommandCheckFunctions(command);
  22. }
  23. for (const command of command_list) {
  24. await _registerCommand(command_list, command);
  25. }
  26. }
  27.  
  28. /**
  29. * @typedef {_ButtonCommand | _ToggleCommand | _RadioCommandGroup} _Command
  30. */
  31.  
  32. /**
  33. * @typedef _ButtonCommand
  34. * @type {Object}
  35. * @property {"button"} type A string declaring what type of menu command this is.
  36. * @property {string} text The text displayed.
  37. * @property {() => void} clickFunction A function to be run when clicking the command.
  38. * @property {string} id The id of the command. Required so that if in place replacement is not supported it can be removed.
  39. * @property {string} [tooltip] The tooltip shown while the cursor hovers the command.
  40. * @property {boolean} [auto_close] If the userscript manager popup closes when the command is clicked. Its "false" by default.
  41. * @property {string} [access_key] A key shortcut for the command.
  42. */
  43.  
  44. /**
  45. * @typedef _ToggleCommand
  46. * @type {Object}
  47. * @property {"toggle"} type A string declaring what type of menu command this is.
  48. * @property {string} id The id of the toggle and the key for the value.
  49. * @property {string} text The text displayed.
  50. * @property {boolean} [default_value] The default value and toggle state. Its "false" and off by default.
  51. * @property {string} [tooltip] The tooltip shown while the cursor hovers the command.
  52. * @property {boolean} [auto_close] If the userscript manager popup closes when the toggle is clicked. Its "false" by default.
  53. * @property {string} [access_key] A key shortcut for the command.
  54. * @property {() => void} [uncheckedFunction] A function to be run when this command is unchecked. This will run once on startup if command is unchecked.
  55. * @property {() => void} [checkedFunction] A function to be run when this command is checked. This will run once on startup if command is checked
  56. */
  57.  
  58. /**
  59. * @typedef _RadioCommandGroup
  60. * @type {Object}
  61. * @property {"radio"} type
  62. * @property {string} id The key for the value.
  63. * @property {*} [default_value] The default value and which radio is checked by default. If not set or value does not correspond to a radio, no radio will be checked.
  64. * @property {_RadioCommand[]} radios
  65. *
  66. * @typedef _RadioCommand
  67. * @type {Object}
  68. * @property {string} text The text displayed.
  69. * @property {*} value The value that is set to the group's id when clicked.
  70. * @property {string} id The id of the command. Required so that if in place replacement is not supported it can be removed.
  71. * @property {string} [tooltip] The tooltip shown while the cursor hovers the command.
  72. * @property {boolean} [auto_close] If the userscript manager popup closes when the command is clicked. Its "false" by default.
  73. * @property {string} [access_key] A key shortcut for the command.
  74. * @property {() => void} [uncheckedFunction] A function to be run when another command in the group is checked. This will run once on startup if command is unchecked.
  75. * @property {() => void} [checkedFunction] A function to be run when this command is checked. This will run once on startup if command is checked
  76. */
  77.  
  78. // To check if in place command replacement is supported
  79. // https://violentmonkey.github.io/api/gm/#gm_registermenucommand
  80. const _can_replace_in_place =
  81. "test" === GM.registerMenuCommand("test", () => {}, { id: "test" });
  82. GM.unregisterMenuCommand("test");
  83.  
  84. /**
  85. * @param {_Command[]} command_list The list of all commands (may be used to replace old commands).
  86. * @param {_Command} command
  87. */
  88. async function _registerCommand(command_list, command) {
  89. if (command.type === "radio") {
  90. const checked_radio_value = await GM.getValue(
  91. command.id,
  92. command.default_value
  93. );
  94. for (const radio of command.radios) {
  95. if (radio.value === checked_radio_value) {
  96. const text_prefix = "🞊 ";
  97. GM.registerMenuCommand(text_prefix + radio.text, () => {}, {
  98. id: radio.id,
  99. title: radio.tooltip,
  100. accessKey: radio.access_key,
  101. autoClose: radio.auto_close !== undefined && radio.auto_close,
  102. });
  103. } else {
  104. const text_prefix = "🞅 ";
  105. GM.registerMenuCommand(
  106. text_prefix + radio.text,
  107. () => _radioCommand(command_list, command, radio.value),
  108. {
  109. id: radio.id,
  110. title: radio.tooltip,
  111. accessKey: radio.access_key,
  112. autoClose: radio.auto_close !== undefined && radio.auto_close,
  113. }
  114. );
  115. }
  116. }
  117. } else if (command.type === "toggle") {
  118. let text_prefix;
  119. if (await GM.getValue(command.id, command.default_value)) {
  120. text_prefix = "🞕 ";
  121. } else {
  122. text_prefix = "🞎 ";
  123. }
  124. GM.registerMenuCommand(
  125. text_prefix + command.text,
  126. () => _toggleCommand(command_list, command),
  127. {
  128. id: command.id,
  129. title: command.tooltip,
  130. accessKey: command.access_key,
  131. autoClose: command.auto_close !== undefined && command.auto_close,
  132. }
  133. );
  134. } else if (command.type === "button") {
  135. GM.registerMenuCommand(command.text, command.clickFunction, {
  136. id: command.id,
  137. title: command.tooltip,
  138. accessKey: command.access_key,
  139. autoClose: command.auto_close !== undefined && command.auto_close,
  140. });
  141. }
  142. }
  143.  
  144. /**
  145. * The callback to be added to the GM.registerCommand of RadioCommand.
  146. * @param {_Command[]} command_list The list of all commands (may be used to replace old commands).
  147. * @param {_RadioCommandGroup} command The group of the command being checked.
  148. * @param {string} value The value of the RadioCommand being checked.
  149. */
  150. async function _radioCommand(command_list, command, value) {
  151. await GM.setValue(command.id, value);
  152. _runCommandCheckFunctions(command);
  153. if (_can_replace_in_place) {
  154. await _registerCommand(command_list, command);
  155. } else {
  156. // If we can't replace commands, we need to remove them all, then re-add them
  157. _unregisterCommands(command_list);
  158. for (const command of command_list) {
  159. await _registerCommand(command_list, command);
  160. }
  161. }
  162. }
  163.  
  164. /**
  165. * The callback to be added to the GM.registerCommand of ToggleCommand
  166. * @param {_Command[]} command_list The list of all commands (may be used to replace old commands).
  167. * @param {_ToggleCommand} command The command being toggled.
  168. */
  169. async function _toggleCommand(command_list, command) {
  170. await GM.setValue(
  171. command.id,
  172. !(await GM.getValue(command.id, command.default_value))
  173. );
  174. _runCommandCheckFunctions(command);
  175. if (_can_replace_in_place) {
  176. await _registerCommand(command_list, command);
  177. } else {
  178. // If we can't replace commands, we need to remove them all, then re-add them
  179. _unregisterCommands(command_list);
  180. for (const command of command_list) {
  181. await _registerCommand(command_list, command);
  182. }
  183. }
  184. }
  185.  
  186. /**
  187. * @param {_Command[]} command_list
  188. */
  189. function _unregisterCommands(command_list) {
  190. for (const command of command_list) {
  191. if (command.type === "radio") {
  192. for (const radio of command.radios) {
  193. GM.unregisterMenuCommand(radio.id);
  194. }
  195. continue;
  196. }
  197. GM.unregisterMenuCommand(command.id);
  198. }
  199. }
  200.  
  201. /**
  202. * Runs the required uncheckedFunction() or checkedFunction() of the command.
  203. * @param {_Command} command
  204. */
  205. async function _runCommandCheckFunctions(command) {
  206. if (command.type === "toggle") {
  207. if (await GM.getValue(command.id, command.default_value)) {
  208. if (command.checkedFunction) {
  209. command.checkedFunction();
  210. }
  211. } else {
  212. if (command.uncheckedFunction) {
  213. command.uncheckedFunction();
  214. }
  215. }
  216. } else if (command.type === "radio") {
  217. const value = await GM.getValue(command.id, command.default_value);
  218. for (const radio of command.radios) {
  219. if (value === radio.value) {
  220. if (radio.checkedFunction) {
  221. radio.checkedFunction();
  222. }
  223. } else {
  224. if (radio.uncheckedFunction) {
  225. radio.uncheckedFunction();
  226. }
  227. }
  228. }
  229. }
  230. }