- // ==UserScript==
- // @name Tampermonkey Config
- // @name:zh-CN Tampermonkey 配置
- // @license gpl-3.0
- // @namespace http://tampermonkey.net/
- // @version 0.6.0
- // @description Simple Tampermonkey script config library
- // @description:zh-CN 简易的 Tampermonkey 脚本配置库
- // @author PRO
- // @match *
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // ==/UserScript==
-
- // const debug = console.debug.bind(console, "[Tampermonkey Config]"); // Debug function
- const debug = () => { };
- const GM_config_event = "GM_config_event"; // Compatibility with old versions
- // Adapted from https://stackoverflow.com/a/6832721
- // Returns 1 if a > b, -1 if a < b, 0 if a == b
- function versionCompare(v1, v2, options) {
- var lexicographical = options && options.lexicographical,
- zeroExtend = options && options.zeroExtend,
- v1parts = v1.split('.'),
- v2parts = v2.split('.');
- function isValidPart(x) {
- return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x);
- }
- if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) {
- return NaN;
- }
- if (zeroExtend) {
- while (v1parts.length < v2parts.length) v1parts.push("0");
- while (v2parts.length < v1parts.length) v2parts.push("0");
- }
- if (!lexicographical) {
- v1parts = v1parts.map(Number);
- v2parts = v2parts.map(Number);
- }
- for (var i = 0; i < v1parts.length; ++i) {
- if (v2parts.length == i) {
- return 1;
- }
- if (v1parts[i] == v2parts[i]) {
- continue;
- }
- else if (v1parts[i] > v2parts[i]) {
- return 1;
- }
- else {
- return -1;
- }
- }
- if (v1parts.length != v2parts.length) {
- return -1;
- }
- return 0;
- }
- function supports(minVer) { // Minimum version of Tampermonkey required
- return GM_info?.scriptHandler === "Tampermonkey" // Tampermonkey is detected
- && versionCompare(GM_info.version, minVer) >= 0; // Compare version
- }
- const supportsOption = supports("4.20.0");
- debug(`Tampermonkey ${GM_info.version} detected, ${supportsOption ? "supports" : "does not support"} menu command options`);
- const registerMenuCommand = supportsOption ? GM_registerMenuCommand : (name, func, option) => GM_registerMenuCommand(name, func, option.accessKey);
-
- function _GM_config_get(config_desc, prop) {
- return GM_getValue(prop, config_desc[prop].value);
- }
- const _GM_config_builtin_processors = {
- same: (v) => v,
- not: (v) => !v,
- int: (s) => {
- const value = parseInt(s);
- if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
- return value;
- },
- int_range: (s, min_s, max_s) => {
- const value = parseInt(s);
- if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
- const min = (min_s === "") ? -Infinity : parseInt(min_s);
- const max = (max_s === "") ? +Infinity : parseInt(max_s);
- if (min !== NaN && value < min) throw `Invalid value: ${s}, expected integer >= ${min}!`;
- if (max !== NaN && value > max) throw `Invalid value: ${s}, expected integer <= ${max}!`;
- return value;
- },
- float: (s) => {
- const value = parseFloat(s);
- if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
- return value;
- },
- float_range: (s, min_s, max_s) => {
- const value = parseFloat(s);
- if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
- const min = (min_s === "") ? -Infinity : parseFloat(min_s);
- const max = (max_s === "") ? +Infinity : parseFloat(max_s);
- if (min !== NaN && value < min) throw `Invalid value: ${s}, expected float >= ${min}!`;
- if (max !== NaN && value > max) throw `Invalid value: ${s}, expected float <= ${max}!`;
- return value;
- },
- };
- const _GM_config_builtin_formatters = {
- normal: (name, value) => `${name}: ${value}`,
- boolean: (name, value) => `${name}: ${value ? "✔" : "✘"}`,
- };
- const _GM_config_wrapper = {
- get: function (desc, prop) {
- // Return stored value, else default value
- const value = _GM_config_get(desc, prop);
- // Dispatch get event
- const event = new CustomEvent(GM_config_event, {
- detail: {
- type: "get",
- prop: prop,
- before: value,
- after: value
- }
- });
- window.top.dispatchEvent(event);
- return value;
- }, set: function (desc, prop, value) {
- // Dispatch set event
- const before = _GM_config_get(desc, prop);
- const event = new CustomEvent(GM_config_event, {
- detail: {
- type: "set",
- prop: prop,
- before: before,
- after: value
- }
- });
- // Store value
- const default_value = desc[prop].value;
- if (value === default_value && typeof GM_deleteValue === "function") {
- GM_deleteValue(prop); // Delete stored value if it's the same as default value
- debug(`🗑️ "${prop}" deleted`);
- } else {
- GM_setValue(prop, value);
- }
- window.top.dispatchEvent(event);
- return true;
- }
- };
- const _GM_config_registered = []; // Items: [id, prop]
- // (Re-)register menu items on demand
- function _GM_config_register(desc, config, until = undefined) {
- // `until` is the first property to be re-registered
- // If `until` is undefined, all properties will be re-registered
- const _GM_config_builtin_inputs = {
- current: (prop, orig) => orig,
- prompt: (prop, orig) => {
- const s = prompt(`🤔 New value for ${desc[prop].name}:`, orig);
- return s === null ? orig : s;
- },
- };
- // Unregister old menu items
- let id, prop, pack;
- let flag = true;
- while (pack = _GM_config_registered.pop()) {
- [id, prop] = pack; // prop=null means the menu command is currently a placeholder ("Show configuration")
- GM_unregisterMenuCommand(id);
- debug(`- Unregistered menu command: prop="${prop}", id=${id}`);
- if (prop === until) { // Nobody in their right mind would use `null` as a property name
- flag = false;
- break;
- }
- }
- for (const prop in desc) {
- if (prop === until) {
- flag = true;
- }
- if (!flag) continue;
- const name = desc[prop].name;
- const orig = _GM_config_get(desc, prop);
- const input = desc[prop].input;
- const input_func = typeof input === "function" ? input : _GM_config_builtin_inputs[input];
- const formatter = desc[prop].formatter;
- const formatter_func = typeof formatter === "function" ? formatter : _GM_config_builtin_formatters[formatter];
- const option = {
- accessKey: desc[prop].accessKey,
- autoClose: desc[prop].autoClose,
- title: desc[prop].title
- };
- const id = registerMenuCommand(formatter_func(name, orig), function () {
- let value;
- try {
- value = input_func(prop, orig);
- const processor = desc[prop].processor;
- if (typeof processor === "function") { // Process user input
- value = processor(value);
- } else if (typeof processor === "string") {
- const parts = processor.split("-");
- const processor_func = _GM_config_builtin_processors[parts[0]];
- if (processor_func !== undefined) // Process user input
- value = processor_func(value, ...parts.slice(1));
- else // Unknown processor
- throw `Unknown processor: ${processor}`;
- } else {
- throw `Unknown processor format: ${typeof processor}`;
- }
- } catch (error) {
- alert("⚠️ " + error);
- return;
- }
- if (value !== orig) {
- config[prop] = value;
- }
- }, option);
- debug(`+ Registered menu command: prop="${prop}", id=${id}, option=`, option);
- _GM_config_registered.push([id, prop]);
- }
- };
-
- function GM_config(desc, menu = true) { // Register menu items based on given config description
- // Calc true default value
- const $default = Object.assign({
- input: "prompt",
- processor: "same",
- formatter: "normal"
- }, desc["$default"] || {});
- delete desc.$default;
- // Complete desc
- for (const key in desc) {
- desc[key] = Object.assign(Object.assign({}, $default), desc[key]);
- }
- // Get proxied config
- const config = new Proxy(desc, _GM_config_wrapper);
- // Register menu items
- if (window === window.top) {
- if (menu) {
- _GM_config_register(desc, config);
- } else {
- // Register menu items after user clicks "Show configuration"
- const id = registerMenuCommand("Show configuration", function () {
- _GM_config_register(desc, config);
- }, {
- autoClose: false,
- title: "Show configuration options for this script"
- });
- debug(`+ Registered menu command: prop="Show configuration", id=${id}`);
- _GM_config_registered.push([id, null]);
- }
- window.top.addEventListener(GM_config_event, (e) => { // Auto update menu items
- if (e.detail.type === "set" && e.detail.before !== e.detail.after) {
- debug(`🔧 "${e.detail.prop}" changed from ${e.detail.before} to ${e.detail.after}`);
- _GM_config_register(desc, config, e.detail.prop);
- } else if (e.detail.type === "get") {
- debug(`🔍 "${e.detail.prop}" requested, value is ${e.detail.after}`);
- }
- });
- }
- // Return proxied config
- return config;
- };