Tampermonkey 配置

简易的 Tampermonkey 脚本配置库

目前为 2024-09-16 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/470224/1448594/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 1.0.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. // @grant GM_addValueChangeListener
  17. // ==/UserScript==
  18.  
  19. class GM_config extends EventTarget {
  20. /**
  21. * The version of the GM_config library
  22. */
  23. static get version() {
  24. return "1.0.0";
  25. }
  26. /**
  27. * Built-in processors for user input
  28. * @type {Object<string, Function>}
  29. */
  30. static #builtin_processors = {
  31. same: (v) => v,
  32. not: (v) => !v,
  33. int: (s) => {
  34. const value = parseInt(s);
  35. if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
  36. return value;
  37. },
  38. int_range: (s, min_s, max_s) => {
  39. const value = parseInt(s);
  40. if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
  41. const min = (min_s === "") ? -Infinity : parseInt(min_s);
  42. const max = (max_s === "") ? +Infinity : parseInt(max_s);
  43. if (min !== NaN && value < min) throw `Invalid value: ${s}, expected integer >= ${min}!`;
  44. if (max !== NaN && value > max) throw `Invalid value: ${s}, expected integer <= ${max}!`;
  45. return value;
  46. },
  47. float: (s) => {
  48. const value = parseFloat(s);
  49. if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
  50. return value;
  51. },
  52. float_range: (s, min_s, max_s) => {
  53. const value = parseFloat(s);
  54. if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
  55. const min = (min_s === "") ? -Infinity : parseFloat(min_s);
  56. const max = (max_s === "") ? +Infinity : parseFloat(max_s);
  57. if (min !== NaN && value < min) throw `Invalid value: ${s}, expected float >= ${min}!`;
  58. if (max !== NaN && value > max) throw `Invalid value: ${s}, expected float <= ${max}!`;
  59. return value;
  60. },
  61. };
  62. /**
  63. * Built-in formatters for user input
  64. * @type {Object<string, Function>}
  65. */
  66. static #builtin_formatters = {
  67. normal: (name, value) => `${name}: ${value}`,
  68. boolean: (name, value) => `${name}: ${value ? "✔" : "✘"}`,
  69. };
  70. /**
  71. * The proxied config object, to be initialized in the constructor
  72. */
  73. proxy = {};
  74. /**
  75. * Whether to show debug information
  76. * @type {boolean}
  77. */
  78. debug = false;
  79. /**
  80. * The config description object, to be initialized in the constructor
  81. */
  82. #desc = {};
  83. /**
  84. * The built-in input functions
  85. * @type {Object<string, Function>}
  86. */
  87. #builtin_inputs = {
  88. current: (prop, orig) => orig,
  89. prompt: (prop, orig) => {
  90. const s = prompt(`🤔 New value for ${this.#desc[prop].name}:`, orig);
  91. return s === null ? orig : s;
  92. },
  93. };
  94. /**
  95. * A mapping for the registered menu items, from property to menu id
  96. */
  97. #registered = {};
  98. /**
  99. * The constructor of the GM_config class
  100. * @param {Object} desc The config description object
  101. * @param {Object} [options] Optional settings
  102. * @param {boolean} [options.immediate=true] Whether to register menu items immediately
  103. * @param {boolean} [options.debug=false] Whether to show debug information
  104. */
  105. constructor(desc, options) { // Register menu items based on given config description
  106. super();
  107. // Calc true default value
  108. const $default = Object.assign({
  109. input: "prompt",
  110. processor: "same",
  111. formatter: "normal"
  112. }, desc["$default"] ?? {});
  113. Object.assign(this.#desc, desc);
  114. delete this.#desc.$default;
  115. // Handle value change events
  116. function onValueChange(prop, before, after, remote) {
  117. const defaultValue = this.#desc[prop].value;
  118. // If `before` or `after` is `undefined`, replace it with default value
  119. if (before === undefined) before = defaultValue;
  120. if (after === undefined) after = defaultValue;
  121. this.#dispatch(true, { prop, before, after, remote });
  122. }
  123. // Complete desc & setup value change listeners
  124. for (const key in this.#desc) {
  125. this.#desc[key] = Object.assign({}, $default, this.#desc[key]);
  126. GM_addValueChangeListener(key, onValueChange.bind(this));
  127. }
  128. // Proxied config
  129. this.proxy = new Proxy(this.#desc, {
  130. get: (desc, prop) => {
  131. return this.get(prop);
  132. },
  133. set: (desc, prop, value) => {
  134. return this.set(prop, value);
  135. }
  136. });
  137. // Register menu items
  138. if (window === window.top) {
  139. if (options?.immediate ?? true) {
  140. this.#register();
  141. } else {
  142. // Register menu items after user clicks "Show configuration"
  143. const id = GM_registerMenuCommand("Show configuration", () => {
  144. this.#register();
  145. }, {
  146. autoClose: false,
  147. title: "Show configuration options for this script"
  148. });
  149. this.#log(`+ Registered menu command: prop="Show configuration", id=${id}`);
  150. this.#registered[null] = id;
  151. }
  152. this.addEventListener("set", (e) => { // Auto update menu items
  153. if (e.detail.before !== e.detail.after) {
  154. this.#log(`🔧 "${e.detail.prop}" changed from ${e.detail.before} to ${e.detail.after}, remote: ${e.detail.remote}`);
  155. const id = this.#registered[e.detail.prop];
  156. if (id !== undefined) {
  157. this.#register_item(e.detail.prop);
  158. } else {
  159. this.#log(`+ Skipped updating menu since it's not registered: prop="${e.detail.prop}"`);
  160. }
  161. }
  162. });
  163. this.addEventListener("get", (e) => {
  164. this.#log(`🔍 "${e.detail.prop}" requested, value is ${e.detail.after}`);
  165. });
  166. }
  167. this.debug = options?.debug ?? this.debug;
  168. }
  169. /**
  170. * Get the value of a property
  171. * @param {string} prop The property name
  172. * @returns {any} The value of the property
  173. */
  174. get(prop) {
  175. // Return stored value, else default value
  176. const value = this.#get(prop);
  177. // Dispatch get event
  178. this.#dispatch(false, {
  179. prop,
  180. before: value,
  181. after: value,
  182. remote: false
  183. });
  184. return value;
  185. }
  186. /**
  187. * Set the value of a property
  188. * @param {string} prop The property name
  189. * @param {any} value The value to be set
  190. * @returns {boolean} Whether the value is set successfully
  191. */
  192. set(prop, value) {
  193. // Store value
  194. const default_value = this.#desc[prop].value;
  195. if (value === default_value && typeof GM_deleteValue === "function") {
  196. GM_deleteValue(prop); // Delete stored value if it's the same as default value
  197. this.#log(`🗑️ "${prop}" deleted`);
  198. } else {
  199. GM_setValue(prop, value);
  200. }
  201. // Dispatch set event (will be handled by value change listeners)
  202. return true;
  203. }
  204. /**
  205. * Get the value of a property (only for internal use; won't trigger events)
  206. * @param {string} prop The property name
  207. * @returns {any} The value of the property
  208. */
  209. #get(prop) {
  210. return GM_getValue(prop, this.#desc[prop].value);
  211. }
  212. /**
  213. * Log a message if debug is enabled
  214. * @param {...any} args The message to log
  215. */
  216. #log(...args) {
  217. if (this.debug) {
  218. console.log("[GM_config]", ...args);
  219. }
  220. }
  221. /**
  222. * Dispatches the `GM_config_event` event
  223. * @param {string} isSet Whether the event is a set event (`true` for set, `false` for get)
  224. * @param {Object} detail The detail object
  225. * @param {string} detail.prop The property name
  226. * @param {any} detail.before The value before the operation
  227. * @param {any} detail.after The value after the operation
  228. * @param {boolean} detail.remote Whether the operation is remote (always `false` for `get`)
  229. * @returns {boolean} Always `true`
  230. */
  231. #dispatch(isSet, detail) {
  232. const event = new CustomEvent(isSet ? "set" : "get", {
  233. detail: detail
  234. });
  235. return this.dispatchEvent(event);
  236. }
  237. /**
  238. * Register menu items
  239. */
  240. #register() {
  241. // Unregister old menu items
  242. for (const prop in this.#registered) {
  243. const id = this.#registered[prop];
  244. GM_unregisterMenuCommand(id);
  245. delete this.#registered[prop];
  246. this.#log(`- Unregistered menu command: prop="${prop}", id=${id}`);
  247. }
  248. for (const prop in this.#desc) {
  249. this.#registered[prop] = this.#register_item(prop);
  250. }
  251. }
  252. /**
  253. * (Re-)register a single menu item, return its menu id
  254. * @param {string} prop The property
  255. */
  256. #register_item(prop) {
  257. const name = this.#desc[prop].name;
  258. const orig = this.#get(prop);
  259. const input = this.#desc[prop].input;
  260. const input_func = typeof input === "function" ? input : this.#builtin_inputs[input];
  261. const formatter = this.#desc[prop].formatter;
  262. const formatter_func = typeof formatter === "function" ? formatter : GM_config.#builtin_formatters[formatter];
  263. const option = {
  264. accessKey: this.#desc[prop].accessKey,
  265. autoClose: this.#desc[prop].autoClose,
  266. title: this.#desc[prop].title,
  267. id: this.#registered[prop],
  268. };
  269. const id = GM_registerMenuCommand(formatter_func(name, orig), () => {
  270. let value;
  271. try {
  272. value = input_func(prop, orig);
  273. const processor = this.#desc[prop].processor;
  274. if (typeof processor === "function") { // Process user input
  275. value = processor(value);
  276. } else if (typeof processor === "string") {
  277. const parts = processor.split("-");
  278. const processor_func = GM_config.#builtin_processors[parts[0]];
  279. if (processor_func !== undefined) // Process user input
  280. value = processor_func(value, ...parts.slice(1));
  281. else // Unknown processor
  282. throw `Unknown processor: ${processor}`;
  283. } else {
  284. throw `Unknown processor format: ${typeof processor}`;
  285. }
  286. } catch (error) {
  287. alert("⚠️ " + error);
  288. return;
  289. }
  290. if (value !== orig) {
  291. this.set(prop, value);
  292. }
  293. }, option);
  294. this.#log(`+ Registered menu command: prop="${prop}", id=${id}, option=`, option);
  295. return id;
  296. }
  297. }

QingJ © 2025

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