// ==UserScript==
// @name Tampermonkey Config
// @name:zh-CN Tampermonkey 配置
// @license gpl-3.0
// @namespace http://tampermonkey.net/
// @version 0.4.0
// @description Simple Tampermonkey script config library
// @description:zh-CN 简易的 Tampermonkey 脚本配置库
// @author PRO
// @match *
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// ==/UserScript==
// let debug = (...args) => console.debug("[Tampermonkey Config]", ...args); // Debug function
let debug = () => {};
let GM_config_event = `GM_config_${Math.random().toString(36).slice(2)}`;
function _GM_config_get(config_desc, prop) {
let value = GM_getValue(prop, undefined);
if (value !== undefined) {
return value;
} else {
return config_desc[prop].value;
}
}
let _GM_config_builtin_processors = {
same: (v) => v,
not: (v) => !v,
int: (s) => {
let value = parseInt(s);
if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
},
int_range: (s, min_s, max_s) => {
let value = parseInt(s);
if (isNaN(value)) throw `Invalid value: ${s}, expected integer!`;
let min = (min_s === "") ? -Infinity : parseInt(min_s);
let 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) => {
let value = parseFloat(s);
if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
},
float_range: (s, min_s, max_s) => {
let value = parseFloat(s);
if (isNaN(value)) throw `Invalid value: ${s}, expected float!`;
let min = (min_s === "") ? -Infinity : parseFloat(min_s);
let 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;
},
};
let _GM_config_builtin_formatters = {
default: (name, value) => `${name}: ${value}`,
boolean: (name, value) => `${name}: ${value ? "✔" : "✘"}`,
};
let _GM_config_wrapper = {
get: function (target, prop) {
// Return stored value, else default value
let value = _GM_config_get(target, prop);
// Dispatch get event
let event = new CustomEvent(GM_config_event, {
detail: {
type: "get",
prop: prop,
before: value,
after: value
}
});
window.dispatchEvent(event);
return value;
}
, set: function (desc, prop, value) {
// Dispatch set event
let event = new CustomEvent(GM_config_event, {
detail: {
type: "set",
prop: prop,
before: _GM_config_get(desc, prop),
after: value
}
});
// Store value
GM_setValue(prop, value);
window.dispatchEvent(event);
return true;
}
};
let _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
let _GM_config_builtin_inputs = {
current: (prop, orig) => { return orig },
prompt: (prop, orig) => {
let s = prompt(`🤔 New value for ${desc[prop].name}:`, orig);
if (s === null) return orig;
return 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 (let prop in desc) {
if (prop === until) {
flag = true;
}
if (!flag) continue;
let name = desc[prop].name;
let orig = _GM_config_get(desc, prop);
let input = desc[prop].input || "prompt";
let input_func = typeof input === "function" ? input : _GM_config_builtin_inputs[input];
let formatter = desc[prop].formatter || "default";
let formatter_func = typeof formatter === "function" ? formatter : _GM_config_builtin_formatters[formatter];
let id = GM_registerMenuCommand(formatter_func(name, orig), function () {
let value;
try {
value = input_func(prop, orig);
let processor = desc[prop].processor || "same";
if (typeof processor === "function") { // Process user input
value = processor(value);
} else if (typeof processor === "string") {
let parts = processor.split("-");
let 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;
}
});
debug(`+ Registered menu command: prop="${prop}", id=${id}`);
_GM_config_registered.push([id, prop]);
}
};
function GM_config(desc, menu=true) { // Register menu items based on given config description
// Get proxied config
let config = new Proxy(desc, _GM_config_wrapper);
// Register menu items
if (menu) {
_GM_config_register(desc, config);
} else {
// Register menu items after user clicks "Show configuration"
let id = GM_registerMenuCommand("Show configuration", function () {
_GM_config_register(desc, config);
});
debug(`+ Registered menu command: prop="Show configuration", id=${id}`);
_GM_config_registered.push([id, null]);
}
window.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;
};