diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 56a176497..c99255713 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -8,7 +8,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; import { renderTemplate, renderTemplateAsync } from './templates.js'; -import { equalsIgnoreCaseAndAccents, isSubsetOf, isTrueBoolean, setValueByPath } from './utils.js'; +import { equalsIgnoreCaseAndAccents, isFalseBoolean, isSubsetOf, isTrueBoolean, setValueByPath } from './utils.js'; export { getContext, getApiUrl, @@ -1050,33 +1050,50 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') { } /** - * @param {boolean} enable - Whether to enable or disable the extension + * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension * @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise} */ -function getExtensionToggleCallback(enable) { +function getExtensionActionCallback(action) { return async (args, extensionName) => { if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.'); if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.'); if (!extensionName) { - toastr.warning(`Extension name must be provided as an argument to ${enable ? 'enable' : 'disable'} this extension.`); + toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`); return ''; } const reload = isTrueBoolean(args?.reload); - const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName)); if (!internalExtensionName) { toastr.warning(`Extension ${extensionName} does not exist.`); return ''; } - if (enable === !extension_settings.disabledExtensions.includes(internalExtensionName)) { - toastr.info(`Extension ${extensionName} is already ${enable ? 'enabled' : 'disabled'}.`); + + const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName); + + if (action === 'enable' && isEnabled) { + toastr.info(`Extension ${extensionName} is already enabled.`); return internalExtensionName; } - reload && toastr.info(`${enable ? 'Enabling' : 'Disabling'} extension ${extensionName} and reloading...`); - await enableExtension(internalExtensionName, reload); - toastr.success(`Extension ${extensionName} ${enable ? 'enabled' : 'disabled'}.`); + if (action === 'disable' && !isEnabled) { + toastr.info(`Extension ${extensionName} is already disabled.`); + return internalExtensionName; + } + + if (action === 'toggle') { + action = isEnabled ? 'disable' : 'enable'; + } + + reload && toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`); + + if (action === 'enable') { + await enableExtension(internalExtensionName, reload); + } else { + await disableExtension(internalExtensionName, reload); + } + + toastr.success(`Extension ${extensionName} ${action}d.`); return internalExtensionName; }; @@ -1085,7 +1102,7 @@ function getExtensionToggleCallback(enable) { function registerExtensionSlashCommands() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'extension-enable', - callback: getExtensionToggleCallback(true), + callback: getExtensionActionCallback('enable'), namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'reload', @@ -1125,7 +1142,7 @@ function registerExtensionSlashCommands() { })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'extension-disable', - callback: getExtensionToggleCallback(false), + callback: getExtensionActionCallback('enable'), namedArgumentList: [ SlashCommandNamedArgument.fromProps({ name: 'reload', @@ -1163,6 +1180,64 @@ function registerExtensionSlashCommands() { `, })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'extension-toggle', + callback: async (args, extensionName) => { + if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.'); + if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.'); + + const action = isTrueBoolean(args?.state) ? 'enable' : + isFalseBoolean(args?.state) ? 'disable' : + 'toggle'; + + return await getExtensionActionCallback(action)(args, extensionName); + }, + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'reload', + description: 'Whether to reload the page after toggling the extension', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'true', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + SlashCommandNamedArgument.fromProps({ + name: 'state', + description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.', + typeList: [ARGUMENT_TYPE.BOOLEAN], + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'Extension name', + typeList: [ARGUMENT_TYPE.STRING], + isRequired: true, + enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)), + forceEnum: true, + }), + ], + helpString: ` +
+ Toggles the state of a specified extension. +
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed. + The page either needs to be refreshed, or /reload-page has to be called. +
+
+ Example: + +
+ `, + })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'reload-page',