// ==UserScript==
// @name setupCommands
// @license MIT
// @namespace rtonne
// @match *://*/*
// @version 2.1
// @author Rtonne
// @description Library that creates regular, toggle, and radio menu commands for userscript managers
// @grant GM.registerMenuCommand
// @grant GM.unregisterMenuCommand
// @grant GM.getValue
// @grant GM.setValue
// ==/UserScript==
/**
* Can be the only function of this library used externally.
* @param {_Command[]} command_list
*/
async function setupCommands(command_list) {
for (const command of command_list) {
await _runCommandCheckFunctions(command);
}
for (const command of command_list) {
await _registerCommand(command_list, command);
}
}
/**
* @typedef {_ButtonCommand | _ToggleCommand | _RadioCommandGroup} _Command
*/
/**
* @typedef _ButtonCommand
* @type {Object}
* @property {"button"} type A string declaring what type of menu command this is.
* @property {string} text The text displayed.
* @property {() => void} clickFunction A function to be run when clicking the command.
* @property {string} id The id of the command. Required so that if in place replacement is not supported it can be removed.
* @property {string} [tooltip] The tooltip shown while the cursor hovers the command.
* @property {boolean} [auto_close] If the userscript manager popup closes when the command is clicked. Its "false" by default.
* @property {string} [access_key] A key shortcut for the command.
*/
/**
* @typedef _ToggleCommand
* @type {Object}
* @property {"toggle"} type A string declaring what type of menu command this is.
* @property {string} id The id of the toggle and the key for the value.
* @property {string} text The text displayed.
* @property {boolean} [default_value] The default value and toggle state. Its "false" and off by default.
* @property {string} [tooltip] The tooltip shown while the cursor hovers the command.
* @property {boolean} [auto_close] If the userscript manager popup closes when the toggle is clicked. Its "false" by default.
* @property {string} [access_key] A key shortcut for the command.
* @property {() => void} [uncheckedFunction] A function to be run when this command is unchecked. This will run once on startup if command is unchecked.
* @property {() => void} [checkedFunction] A function to be run when this command is checked. This will run once on startup if command is checked
*/
/**
* @typedef _RadioCommandGroup
* @type {Object}
* @property {"radio"} type
* @property {string} id The key for the value.
* @property {*} [default_value] The default value and which radio is checked by default. If not set or value does not correspond to a radio, no radio will be checked.
* @property {_RadioCommand[]} radios
*
* @typedef _RadioCommand
* @type {Object}
* @property {string} text The text displayed.
* @property {*} value The value that is set to the group's id when clicked.
* @property {string} id The id of the command. Required so that if in place replacement is not supported it can be removed.
* @property {string} [tooltip] The tooltip shown while the cursor hovers the command.
* @property {boolean} [auto_close] If the userscript manager popup closes when the command is clicked. Its "false" by default.
* @property {string} [access_key] A key shortcut for the command.
* @property {() => void} [uncheckedFunction] A function to be run when another command in the group is checked. This will run once on startup if command is unchecked.
* @property {() => void} [checkedFunction] A function to be run when this command is checked. This will run once on startup if command is checked
*/
// To check if in place command replacement is supported
// https://violentmonkey.github.io/api/gm/#gm_registermenucommand
const _can_replace_in_place =
"test" === GM.registerMenuCommand("test", () => {}, { id: "test" });
GM.unregisterMenuCommand("test");
/**
* @param {_Command[]} command_list The list of all commands (may be used to replace old commands).
* @param {_Command} command
*/
async function _registerCommand(command_list, command) {
if (command.type === "radio") {
const checked_radio_value = await GM.getValue(
command.id,
command.default_value
);
for (const radio of command.radios) {
if (radio.value === checked_radio_value) {
const text_prefix = "🞊 ";
GM.registerMenuCommand(text_prefix + radio.text, () => {}, {
id: radio.id,
title: radio.tooltip,
accessKey: radio.access_key,
autoClose: radio.auto_close !== undefined && radio.auto_close,
});
} else {
const text_prefix = "🞅 ";
GM.registerMenuCommand(
text_prefix + radio.text,
() => _radioCommand(command_list, command, radio.value),
{
id: radio.id,
title: radio.tooltip,
accessKey: radio.access_key,
autoClose: radio.auto_close !== undefined && radio.auto_close,
}
);
}
}
} else if (command.type === "toggle") {
let text_prefix;
if (await GM.getValue(command.id, command.default_value)) {
text_prefix = "🞕 ";
} else {
text_prefix = "🞎 ";
}
GM.registerMenuCommand(
text_prefix + command.text,
() => _toggleCommand(command_list, command),
{
id: command.id,
title: command.tooltip,
accessKey: command.access_key,
autoClose: command.auto_close !== undefined && command.auto_close,
}
);
} else if (command.type === "button") {
GM.registerMenuCommand(command.text, command.clickFunction, {
id: command.id,
title: command.tooltip,
accessKey: command.access_key,
autoClose: command.auto_close !== undefined && command.auto_close,
});
}
}
/**
* The callback to be added to the GM.registerCommand of RadioCommand.
* @param {_Command[]} command_list The list of all commands (may be used to replace old commands).
* @param {_RadioCommandGroup} command The group of the command being checked.
* @param {string} value The value of the RadioCommand being checked.
*/
async function _radioCommand(command_list, command, value) {
await GM.setValue(command.id, value);
_runCommandCheckFunctions(command);
if (_can_replace_in_place) {
await _registerCommand(command_list, command);
} else {
// If we can't replace commands, we need to remove them all, then re-add them
_unregisterCommands(command_list);
for (const command of command_list) {
await _registerCommand(command_list, command);
}
}
}
/**
* The callback to be added to the GM.registerCommand of ToggleCommand
* @param {_Command[]} command_list The list of all commands (may be used to replace old commands).
* @param {_ToggleCommand} command The command being toggled.
*/
async function _toggleCommand(command_list, command) {
await GM.setValue(
command.id,
!(await GM.getValue(command.id, command.default_value))
);
_runCommandCheckFunctions(command);
if (_can_replace_in_place) {
await _registerCommand(command_list, command);
} else {
// If we can't replace commands, we need to remove them all, then re-add them
_unregisterCommands(command_list);
for (const command of command_list) {
await _registerCommand(command_list, command);
}
}
}
/**
* @param {_Command[]} command_list
*/
function _unregisterCommands(command_list) {
for (const command of command_list) {
if (command.type === "radio") {
for (const radio of command.radios) {
GM.unregisterMenuCommand(radio.id);
}
continue;
}
GM.unregisterMenuCommand(command.id);
}
}
/**
* Runs the required uncheckedFunction() or checkedFunction() of the command.
* @param {_Command} command
*/
async function _runCommandCheckFunctions(command) {
if (command.type === "toggle") {
if (await GM.getValue(command.id, command.default_value)) {
if (command.checkedFunction) {
command.checkedFunction();
}
} else {
if (command.uncheckedFunction) {
command.uncheckedFunction();
}
}
} else if (command.type === "radio") {
const value = await GM.getValue(command.id, command.default_value);
for (const radio of command.radios) {
if (value === radio.value) {
if (radio.checkedFunction) {
radio.checkedFunction();
}
} else {
if (radio.uncheckedFunction) {
radio.uncheckedFunction();
}
}
}
}
}