Tampermonkey Config

Simple Tampermonkey script config library

目前為 2023-12-30 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/470224/1303666/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.6.0
  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 GM_info?.scriptHandler === "Tampermonkey" // Tampermonkey is detected
  63. && versionCompare(GM_info.version, minVer) >= 0; // Compare version
  64. }
  65. const supportsOption = supports("4.20.0");
  66. debug(`Tampermonkey ${GM_info.version} detected, ${supportsOption ? "supports" : "does not support"} menu command options`);
  67. const registerMenuCommand = supportsOption ? GM_registerMenuCommand : (name, func, option) => GM_registerMenuCommand(name, func, option.accessKey);
  68.  
  69. function _GM_config_get(config_desc, prop) {
  70. return GM_getValue(prop, config_desc[prop].value);
  71. }
  72. const _GM_config_builtin_processors = {
  73. same: (v) => v,
  74. not: (v) => !v,
  75. int: (s) => {
  76. const value = parseInt(s);
  77. if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
  78. return value;
  79. },
  80. int_range: (s, min_s, max_s) => {
  81. const value = parseInt(s);
  82. if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
  83. const min = (min_s === "") ? -Infinity : parseInt(min_s);
  84. const max = (max_s === "") ? +Infinity : parseInt(max_s);
  85. if (min !== NaN && value < min) throw `Invalid value: ${s}, expected integer >= ${min}!`;
  86. if (max !== NaN && value > max) throw `Invalid value: ${s}, expected integer <= ${max}!`;
  87. return value;
  88. },
  89. float: (s) => {
  90. const value = parseFloat(s);
  91. if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
  92. return value;
  93. },
  94. float_range: (s, min_s, max_s) => {
  95. const value = parseFloat(s);
  96. if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
  97. const min = (min_s === "") ? -Infinity : parseFloat(min_s);
  98. const max = (max_s === "") ? +Infinity : parseFloat(max_s);
  99. if (min !== NaN && value < min) throw `Invalid value: ${s}, expected float >= ${min}!`;
  100. if (max !== NaN && value > max) throw `Invalid value: ${s}, expected float <= ${max}!`;
  101. return value;
  102. },
  103. };
  104. const _GM_config_builtin_formatters = {
  105. normal: (name, value) => `${name}: ${value}`,
  106. boolean: (name, value) => `${name}: ${value ? "✔" : "✘"}`,
  107. };
  108. const _GM_config_wrapper = {
  109. get: function (desc, prop) {
  110. // Return stored value, else default value
  111. const value = _GM_config_get(desc, prop);
  112. // Dispatch get event
  113. const event = new CustomEvent(GM_config_event, {
  114. detail: {
  115. type: "get",
  116. prop: prop,
  117. before: value,
  118. after: value
  119. }
  120. });
  121. window.top.dispatchEvent(event);
  122. return value;
  123. }, set: function (desc, prop, value) {
  124. // Dispatch set event
  125. const before = _GM_config_get(desc, prop);
  126. const event = new CustomEvent(GM_config_event, {
  127. detail: {
  128. type: "set",
  129. prop: prop,
  130. before: before,
  131. after: value
  132. }
  133. });
  134. // Store value
  135. const default_value = desc[prop].value;
  136. if (value === default_value && typeof GM_deleteValue === "function") {
  137. GM_deleteValue(prop); // Delete stored value if it's the same as default value
  138. debug(`🗑️ "${prop}" deleted`);
  139. } else {
  140. GM_setValue(prop, value);
  141. }
  142. window.top.dispatchEvent(event);
  143. return true;
  144. }
  145. };
  146. const _GM_config_registered = []; // Items: [id, prop]
  147. // (Re-)register menu items on demand
  148. function _GM_config_register(desc, config, until = undefined) {
  149. // `until` is the first property to be re-registered
  150. // If `until` is undefined, all properties will be re-registered
  151. const _GM_config_builtin_inputs = {
  152. current: (prop, orig) => orig,
  153. prompt: (prop, orig) => {
  154. const s = prompt(`🤔 New value for ${desc[prop].name}:`, orig);
  155. return s === null ? orig : s;
  156. },
  157. };
  158. // Unregister old menu items
  159. let id, prop, pack;
  160. let flag = true;
  161. while (pack = _GM_config_registered.pop()) {
  162. [id, prop] = pack; // prop=null means the menu command is currently a placeholder ("Show configuration")
  163. GM_unregisterMenuCommand(id);
  164. debug(`- Unregistered menu command: prop="${prop}", id=${id}`);
  165. if (prop === until) { // Nobody in their right mind would use `null` as a property name
  166. flag = false;
  167. break;
  168. }
  169. }
  170. for (const prop in desc) {
  171. if (prop === until) {
  172. flag = true;
  173. }
  174. if (!flag) continue;
  175. const name = desc[prop].name;
  176. const orig = _GM_config_get(desc, prop);
  177. const input = desc[prop].input;
  178. const input_func = typeof input === "function" ? input : _GM_config_builtin_inputs[input];
  179. const formatter = desc[prop].formatter;
  180. const formatter_func = typeof formatter === "function" ? formatter : _GM_config_builtin_formatters[formatter];
  181. const option = {
  182. accessKey: desc[prop].accessKey,
  183. autoClose: desc[prop].autoClose,
  184. title: desc[prop].title
  185. };
  186. const id = registerMenuCommand(formatter_func(name, orig), function () {
  187. let value;
  188. try {
  189. value = input_func(prop, orig);
  190. const processor = desc[prop].processor;
  191. if (typeof processor === "function") { // Process user input
  192. value = processor(value);
  193. } else if (typeof processor === "string") {
  194. const parts = processor.split("-");
  195. const processor_func = _GM_config_builtin_processors[parts[0]];
  196. if (processor_func !== undefined) // Process user input
  197. value = processor_func(value, ...parts.slice(1));
  198. else // Unknown processor
  199. throw `Unknown processor: ${processor}`;
  200. } else {
  201. throw `Unknown processor format: ${typeof processor}`;
  202. }
  203. } catch (error) {
  204. alert("⚠️ " + error);
  205. return;
  206. }
  207. if (value !== orig) {
  208. config[prop] = value;
  209. }
  210. }, option);
  211. debug(`+ Registered menu command: prop="${prop}", id=${id}, option=`, option);
  212. _GM_config_registered.push([id, prop]);
  213. }
  214. };
  215.  
  216. function GM_config(desc, menu = true) { // Register menu items based on given config description
  217. // Calc true default value
  218. const $default = Object.assign({
  219. input: "prompt",
  220. processor: "same",
  221. formatter: "normal"
  222. }, desc["$default"] || {});
  223. delete desc.$default;
  224. // Complete desc
  225. for (const key in desc) {
  226. desc[key] = Object.assign(Object.assign({}, $default), desc[key]);
  227. }
  228. // Get proxied config
  229. const config = new Proxy(desc, _GM_config_wrapper);
  230. // Register menu items
  231. if (window === window.top) {
  232. if (menu) {
  233. _GM_config_register(desc, config);
  234. } else {
  235. // Register menu items after user clicks "Show configuration"
  236. const id = registerMenuCommand("Show configuration", function () {
  237. _GM_config_register(desc, config);
  238. }, {
  239. autoClose: false,
  240. title: "Show configuration options for this script"
  241. });
  242. debug(`+ Registered menu command: prop="Show configuration", id=${id}`);
  243. _GM_config_registered.push([id, null]);
  244. }
  245. window.top.addEventListener(GM_config_event, (e) => { // Auto update menu items
  246. if (e.detail.type === "set" && e.detail.before !== e.detail.after) {
  247. debug(`🔧 "${e.detail.prop}" changed from ${e.detail.before} to ${e.detail.after}`);
  248. _GM_config_register(desc, config, e.detail.prop);
  249. } else if (e.detail.type === "get") {
  250. debug(`🔍 "${e.detail.prop}" requested, value is ${e.detail.after}`);
  251. }
  252. });
  253. }
  254. // Return proxied config
  255. return config;
  256. };

QingJ © 2025

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