Tampermonkey 配置

简易的 Tampermonkey 脚本配置库

当前为 2023-12-25 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/470224/1301110/Tampermonkey%20Config.js

  1. // ==UserScript==
  2. // @name Tampermonkey Config
  3. // @name:zh-CN Tampermonkey 配置
  4. // @license gpl-3.0
  5. // @namespace http://tampermonkey.net/
  6. // @version 0.5.5
  7. // @description Simple Tampermonkey script config library
  8. // @description:zh-CN 简易的 Tampermonkey 脚本配置库
  9. // @author PRO
  10. // @match *
  11. // @grant GM_setValue
  12. // @grant GM_getValue
  13. // @grant GM_deleteValue
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_unregisterMenuCommand
  16. // ==/UserScript==
  17.  
  18. // const debug = console.debug.bind(console, "[Tampermonkey Config]"); // Debug function
  19. const debug = () => { };
  20. const GM_config_event = "GM_config_event"; // Compatibility with old versions
  21. // Adapted from https://stackoverflow.com/a/6832721
  22. // Returns 1 if a > b, -1 if a < b, 0 if a == b
  23. function versionCompare(v1, v2, options) {
  24. var lexicographical = options && options.lexicographical,
  25. zeroExtend = options && options.zeroExtend,
  26. v1parts = v1.split('.'),
  27. v2parts = v2.split('.');
  28. function isValidPart(x) {
  29. return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
  30. }
  31. if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
  32. return NaN;
  33. }
  34. if (zeroExtend) {
  35. while (v1parts.length < v2parts.length) v1parts.push("0");
  36. while (v2parts.length < v1parts.length) v2parts.push("0");
  37. }
  38. if (!lexicographical) {
  39. v1parts = v1parts.map(Number);
  40. v2parts = v2parts.map(Number);
  41. }
  42. for (var i = 0; i < v1parts.length; ++i) {
  43. if (v2parts.length == i) {
  44. return 1;
  45. }
  46. if (v1parts[i] == v2parts[i]) {
  47. continue;
  48. }
  49. else if (v1parts[i] > v2parts[i]) {
  50. return 1;
  51. }
  52. else {
  53. return -1;
  54. }
  55. }
  56. if (v1parts.length != v2parts.length) {
  57. return -1;
  58. }
  59. return 0;
  60. }
  61. function supports(minVer) { // Minimum version of Tampermonkey required
  62. return typeof GM_info === "object" // Has API GM_info
  63. && GM_info.scriptHandler === "Tampermonkey" // Tampermonkey is detected
  64. && versionCompare(GM_info.version, minVer) >= 0; // Compare version
  65. }
  66. const supportsOption = supports("4.20.0");
  67. debug(`Tampermonkey ${GM_info.version} detected, ${supportsOption ? "supports" : "does not support"} menu command options`);
  68. const registerMenuCommand = supportsOption ? GM_registerMenuCommand : (name, func, option) => GM_registerMenuCommand(name, func, option.accessKey);
  69.  
  70. function _GM_config_get(config_desc, prop) {
  71. return GM_getValue(prop, config_desc[prop].value);
  72. }
  73. const _GM_config_builtin_processors = {
  74. same: (v) => v,
  75. not: (v) => !v,
  76. int: (s) => {
  77. const value = parseInt(s);
  78. if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
  79. return value;
  80. },
  81. int_range: (s, min_s, max_s) => {
  82. const value = parseInt(s);
  83. if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
  84. const min = (min_s === "") ? -Infinity : parseInt(min_s);
  85. const max = (max_s === "") ? +Infinity : parseInt(max_s);
  86. if (min !== NaN && value < min) throw `Invalid value: ${s}, expected integer >= ${min}!`;
  87. if (max !== NaN && value > max) throw `Invalid value: ${s}, expected integer <= ${max}!`;
  88. return value;
  89. },
  90. float: (s) => {
  91. const value = parseFloat(s);
  92. if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
  93. return value;
  94. },
  95. float_range: (s, min_s, max_s) => {
  96. const value = parseFloat(s);
  97. if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
  98. const min = (min_s === "") ? -Infinity : parseFloat(min_s);
  99. const max = (max_s === "") ? +Infinity : parseFloat(max_s);
  100. if (min !== NaN && value < min) throw `Invalid value: ${s}, expected float >= ${min}!`;
  101. if (max !== NaN && value > max) throw `Invalid value: ${s}, expected float <= ${max}!`;
  102. return value;
  103. },
  104. };
  105. const _GM_config_builtin_formatters = {
  106. default: (name, value) => `${name}: ${value}`,
  107. boolean: (name, value) => `${name}: ${value ? "✔" : "✘"}`,
  108. };
  109. const _GM_config_wrapper = {
  110. get: function (target, prop) {
  111. // Return stored value, else default value
  112. const value = _GM_config_get(target, prop);
  113. // Dispatch get event
  114. const event = new CustomEvent(GM_config_event, {
  115. detail: {
  116. type: "get",
  117. prop: prop,
  118. before: value,
  119. after: value
  120. }
  121. });
  122. window.top.dispatchEvent(event);
  123. return value;
  124. }, set: function (desc, prop, value) {
  125. // Dispatch set event
  126. const before = _GM_config_get(desc, prop);
  127. const event = new CustomEvent(GM_config_event, {
  128. detail: {
  129. type: "set",
  130. prop: prop,
  131. before: before,
  132. after: value
  133. }
  134. });
  135. // Store value
  136. const default_value = desc[prop].value;
  137. if (value === default_value && typeof GM_deleteValue === "function") {
  138. GM_deleteValue(prop); // Delete stored value if it's the same as default value
  139. debug(`🗑️ "${prop}" deleted`);
  140. } else {
  141. GM_setValue(prop, value);
  142. }
  143. window.top.dispatchEvent(event);
  144. return true;
  145. }
  146. };
  147. const _GM_config_registered = []; // Items: [id, prop]
  148. // (Re-)register menu items on demand
  149. function _GM_config_register(desc, config, until = undefined) {
  150. // `until` is the first property to be re-registered
  151. // If `until` is undefined, all properties will be re-registered
  152. const _GM_config_builtin_inputs = {
  153. current: (prop, orig) => orig,
  154. prompt: (prop, orig) => {
  155. const s = prompt(`🤔 New value for ${desc[prop].name}:`, orig);
  156. return s === null ? orig : s;
  157. },
  158. };
  159. // Unregister old menu items
  160. let id, prop, pack;
  161. let flag = true;
  162. while (pack = _GM_config_registered.pop()) {
  163. [id, prop] = pack; // prop=null means the menu command is currently a placeholder ("Show configuration")
  164. GM_unregisterMenuCommand(id);
  165. debug(`- Unregistered menu command: prop="${prop}", id=${id}`);
  166. if (prop === until) { // Nobody in their right mind would use `null` as a property name
  167. flag = false;
  168. break;
  169. }
  170. }
  171. for (const prop in desc) {
  172. if (prop === until) {
  173. flag = true;
  174. }
  175. if (!flag) continue;
  176. const name = desc[prop].name;
  177. const orig = _GM_config_get(desc, prop);
  178. const input = desc[prop].input || "prompt";
  179. const input_func = typeof input === "function" ? input : _GM_config_builtin_inputs[input];
  180. const formatter = desc[prop].formatter || "default";
  181. const formatter_func = typeof formatter === "function" ? formatter : _GM_config_builtin_formatters[formatter];
  182. const option = {
  183. accessKey: desc[prop].accessKey,
  184. autoClose: desc[prop].autoClose,
  185. title: desc[prop].title
  186. };
  187. const id = registerMenuCommand(formatter_func(name, orig), function () {
  188. let value;
  189. try {
  190. value = input_func(prop, orig);
  191. let processor = desc[prop].processor || "same";
  192. if (typeof processor === "function") { // Process user input
  193. value = processor(value);
  194. } else if (typeof processor === "string") {
  195. const parts = processor.split("-");
  196. const processor_func = _GM_config_builtin_processors[parts[0]];
  197. if (processor_func !== undefined) // Process user input
  198. value = processor_func(value, ...parts.slice(1));
  199. else // Unknown processor
  200. throw `Unknown processor: ${processor}`;
  201. } else {
  202. throw `Unknown processor format: ${typeof processor}`;
  203. }
  204. } catch (error) {
  205. alert("⚠️ " + error);
  206. return;
  207. }
  208. if (value !== orig) {
  209. config[prop] = value;
  210. }
  211. }, option);
  212. debug(`+ Registered menu command: prop="${prop}", id=${id}, option=`, option);
  213. _GM_config_registered.push([id, prop]);
  214. }
  215. };
  216.  
  217. function GM_config(desc, menu = true) { // Register menu items based on given config description
  218. // Get proxied config
  219. const config = new Proxy(desc, _GM_config_wrapper);
  220. // Register menu items
  221. if (window === window.top) {
  222. if (menu) {
  223. _GM_config_register(desc, config);
  224. } else {
  225. // Register menu items after user clicks "Show configuration"
  226. const id = registerMenuCommand("Show configuration", function () {
  227. _GM_config_register(desc, config);
  228. }, {
  229. autoClose: false,
  230. title: "Show configuration options for this script"
  231. });
  232. debug(`+ Registered menu command: prop="Show configuration", id=${id}`);
  233. _GM_config_registered.push([id, null]);
  234. }
  235. window.top.addEventListener(GM_config_event, (e) => { // Auto update menu items
  236. if (e.detail.type === "set" && e.detail.before !== e.detail.after) {
  237. debug(`🔧 "${e.detail.prop}" changed from ${e.detail.before} to ${e.detail.after}`);
  238. _GM_config_register(desc, config, e.detail.prop);
  239. } else if (e.detail.type === "get") {
  240. debug(`🔍 "${e.detail.prop}" requested, value is ${e.detail.after}`);
  241. }
  242. });
  243. }
  244. // Return proxied config
  245. return config;
  246. };

QingJ © 2025

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