Rework expression slash commands - Common naming schema for slash commands, all starting with the name of the expression - moved the original names to aliases - Make char name optional for /expression-last if not in group chat - Removed legacy 'format' argument handling from /expression-classify - Fixed /expression-upload to the new backend call, added optional 'spriteName' argument
| @@ -1,11 +1,11 @@ | |||
| 1 | import { Fuse } from '../../../lib.js'; | 1 | import { Fuse } from '../../../lib.js'; |
| 2 | 2 | ||
| 3 | import { eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js'; | 3 | import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js'; |
| 4 | import { dragElement, isMobile } from '../../RossAscends-mods.js'; | 4 | import { dragElement, isMobile } from '../../RossAscends-mods.js'; |
| 5 | import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; | 5 | import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 6 | import { loadMovingUIState, power_user } from '../../power-user.js'; | 6 | import { loadMovingUIState, power_user } from '../../power-user.js'; |
| 7 | import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js'; | 7 | import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js'; |
| 8 | import { hideMutedSprites } from '../../group-chats.js'; | 8 | import { hideMutedSprites, selected_group } from '../../group-chats.js'; |
| 9 | import { isJsonSchemaSupported } from '../../textgen-settings.js'; | 9 | import { isJsonSchemaSupported } from '../../textgen-settings.js'; |
| 10 | import { debounce_timeout } from '../../constants.js'; | 10 | import { debounce_timeout } from '../../constants.js'; |
| 11 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 11 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -92,6 +92,8 @@ let lastTalkingStateMessage = null; // last message as seen by `updateTalkingSt | |||
| 92 | let spriteCache = {}; | 92 | let spriteCache = {}; |
| 93 | let inApiCall = false; | 93 | let inApiCall = false; |
| 94 | let lastServerResponseTime = 0; | 94 | let lastServerResponseTime = 0; |
| 95 | |||
| 96 | /** @type {{[characterName: string]: string}} */ | ||
| 95 | export let lastExpression = {}; | 97 | export let lastExpression = {}; |
| 96 | 98 | ||
| 97 | function isTalkingHeadEnabled() { | 99 | function isTalkingHeadEnabled() { |
| @@ -1033,17 +1035,21 @@ function spriteFolderNameFromCharacter(char) { | |||
| 1033 | * @param {object} args | 1035 | * @param {object} args |
| 1034 | * @param {string} args.name Character name or avatar key, passed through findChar | 1036 | * @param {string} args.name Character name or avatar key, passed through findChar |
| 1035 | * @param {string} args.label Expression label | 1037 | * @param {string} args.label Expression label |
| 1036 | * @param {string} args.folder Sprite folder path, processed using backslash rules | 1038 | * @param {string} [args.folder=null] Optional sprite folder path, processed using backslash rules |
| 1039 | * @param {string?} [args.spriteName=null] Optional sprite name | ||
| 1037 | * @param {string} imageUrl Image URI to fetch and upload | 1040 | * @param {string} imageUrl Image URI to fetch and upload |
| 1038 | * @returns {Promise<void>} | 1041 | * @returns {Promise<string>} the sprite name |
| 1039 | */ | 1042 | */ |
| 1040 | async function uploadSpriteCommand({ name, label, folder }, imageUrl) { | 1043 | async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) { |
| 1041 | if (!imageUrl) throw new Error('Image URL is required'); | 1044 | if (!imageUrl) throw new Error('Image URL is required'); |
| 1042 | if (!label || typeof label !== 'string') throw new Error('Expression label is required'); | 1045 | if (!label || typeof label !== 'string') throw new Error('Expression label is required'); |
| 1043 | 1046 | ||
| 1044 | label = label.replace(/[^a-z]/gi, '').toLowerCase().trim(); | 1047 | label = label.replace(/[^a-z]/gi, '').toLowerCase().trim(); |
| 1045 | if (!label) throw new Error('Expression label must contain at least one letter'); | 1048 | if (!label) throw new Error('Expression label must contain at least one letter'); |
| 1046 | 1049 | ||
| 1050 | spriteName = spriteName || label; | ||
| 1051 | if (!validateExpressionSpriteName(label, spriteName)) throw new Error('Invalid sprite name. Must follow the naming pattern for expression sprites.'); | ||
| 1052 | |||
| 1047 | name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; | 1053 | name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; |
| 1048 | const char = findChar({ name }); | 1054 | const char = findChar({ name }); |
| 1049 | 1055 | ||
| @@ -1062,7 +1068,8 @@ async function uploadSpriteCommand({ name, label, folder }, imageUrl) { | |||
| 1062 | const formData = new FormData(); | 1068 | const formData = new FormData(); |
| 1063 | formData.append('name', folder); // this is the folder or character name | 1069 | formData.append('name', folder); // this is the folder or character name |
| 1064 | formData.append('label', label); // this is the expression label | 1070 | formData.append('label', label); // this is the expression label |
| 1065 | formData.append('avatar', file); // this is the image file | 1071 | formData.append('avatar', file); // this is the image file |
| 1072 | formData.append('spriteName', spriteName); // this is a redundant comment | ||
| 1066 | 1073 | ||
| 1067 | await handleFileUpload('/api/sprites/upload', formData); | 1074 | await handleFileUpload('/api/sprites/upload', formData); |
| 1068 | console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`); | 1075 | console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`); |
| @@ -1070,6 +1077,8 @@ async function uploadSpriteCommand({ name, label, folder }, imageUrl) { | |||
| 1070 | console.error(`[${MODULE_NAME}] Error uploading file:`, error); | 1077 | console.error(`[${MODULE_NAME}] Error uploading file:`, error); |
| 1071 | throw error; | 1078 | throw error; |
| 1072 | } | 1079 | } |
| 1080 | |||
| 1081 | return spriteName; | ||
| 1073 | } | 1082 | } |
| 1074 | 1083 | ||
| 1075 | /** | 1084 | /** |
| @@ -1876,6 +1885,12 @@ function withoutExtension(fileName) { | |||
| 1876 | return fileName.replace(/\.[^/.]+$/, ''); | 1885 | return fileName.replace(/\.[^/.]+$/, ''); |
| 1877 | } | 1886 | } |
| 1878 | 1887 | ||
| 1888 | function validateExpressionSpriteName(expression, spriteName) { | ||
| 1889 | const filenameValidationRegex = new RegExp(`^${expression}(?:[-\\.].*?)?$`); | ||
| 1890 | const validFileName = filenameValidationRegex.test(spriteName); | ||
| 1891 | return validFileName; | ||
| 1892 | } | ||
| 1893 | |||
| 1879 | async function onClickExpressionUpload(event) { | 1894 | async function onClickExpressionUpload(event) { |
| 1880 | // Prevents the expression from being set | 1895 | // Prevents the expression from being set |
| 1881 | event.stopPropagation(); | 1896 | event.stopPropagation(); |
| @@ -1900,8 +1915,7 @@ async function onClickExpressionUpload(event) { | |||
| 1900 | if (extension_settings.expressions.allowMultiple) { | 1915 | if (extension_settings.expressions.allowMultiple) { |
| 1901 | const matchesExisting = existingFiles.some(x => x.fileName === file.name); | 1916 | const matchesExisting = existingFiles.some(x => x.fileName === file.name); |
| 1902 | const fileNameWithoutExtension = withoutExtension(file.name); | 1917 | const fileNameWithoutExtension = withoutExtension(file.name); |
| 1903 | const filenameValidationRegex = new RegExp(`^${expression}(?:[-\\.].*?)?$`); | 1918 | const validFileName = validateExpressionSpriteName(expression, fileNameWithoutExtension); |
| 1904 | const validFileName = filenameValidationRegex.test(fileNameWithoutExtension); | ||
| 1905 | 1919 | ||
| 1906 | // If there is no expression yet and it's a valid expression, we just take it | 1920 | // If there is no expression yet and it's a valid expression, we just take it |
| 1907 | if (!clickedFileName && validFileName) { | 1921 | if (!clickedFileName && validFileName) { |
| @@ -1932,15 +1946,15 @@ async function onClickExpressionUpload(event) { | |||
| 1932 | const message = await renderExtensionTemplateAsync(MODULE_NAME, 'templates/upload-expression', { expression, clickedFileName }); | 1946 | const message = await renderExtensionTemplateAsync(MODULE_NAME, 'templates/upload-expression', { expression, clickedFileName }); |
| 1933 | 1947 | ||
| 1934 | spriteName = null; | 1948 | spriteName = null; |
| 1935 | const result = await Popup.show.input(t`Upload Expression Sprite`, message, | 1949 | const input = await Popup.show.input(t`Upload Expression Sprite`, message, |
| 1936 | `${expression}-${existingFiles.length}`, { customButtons: customButtons }); | 1950 | `${expression}-${existingFiles.length}`, { customButtons: customButtons }); |
| 1937 | 1951 | ||
| 1938 | if (result) { | 1952 | if (input) { |
| 1939 | if (!filenameValidationRegex.test(result)) { | 1953 | if (!validateExpressionSpriteName(expression, input)) { |
| 1940 | toastr.warning(t`The name you entered does not follow the naming schema for the selected expression '${expression}'.`, t`Invalid Expression Sprite Name`); | 1954 | toastr.warning(t`The name you entered does not follow the naming schema for the selected expression '${expression}'.`, t`Invalid Expression Sprite Name`); |
| 1941 | return; | 1955 | return; |
| 1942 | } | 1956 | } |
| 1943 | spriteName = result; | 1957 | spriteName = input; |
| 1944 | } | 1958 | } |
| 1945 | } | 1959 | } |
| 1946 | } else { | 1960 | } else { |
| @@ -2350,23 +2364,23 @@ function migrateSettings() { | |||
| 2350 | }; | 2364 | }; |
| 2351 | 2365 | ||
| 2352 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2366 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2353 | name: 'sprite', | 2367 | name: 'expression-set', |
| 2354 | aliases: ['emote'], | 2368 | aliases: ['sprite', 'emote'], |
| 2355 | callback: setSpriteSlashCommand, | 2369 | callback: setSpriteSlashCommand, |
| 2356 | unnamedArgumentList: [ | 2370 | unnamedArgumentList: [ |
| 2357 | SlashCommandArgument.fromProps({ | 2371 | SlashCommandArgument.fromProps({ |
| 2358 | description: 'spriteId', | 2372 | description: 'expression label to set', |
| 2359 | typeList: [ARGUMENT_TYPE.STRING], | 2373 | typeList: [ARGUMENT_TYPE.STRING], |
| 2360 | isRequired: true, | 2374 | isRequired: true, |
| 2361 | enumProvider: localEnumProviders.expressions, | 2375 | enumProvider: localEnumProviders.expressions, |
| 2362 | }), | 2376 | }), |
| 2363 | ], | 2377 | ], |
| 2364 | helpString: 'Force sets the sprite for the current character.', | 2378 | helpString: 'Force sets the expression for the current character.', |
| 2365 | returns: 'the currently set sprite label after setting it.', | 2379 | returns: 'The currently set expression label after setting it.', |
| 2366 | })); | 2380 | })); |
| 2367 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2381 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2368 | name: 'spriteoverride', | 2382 | name: 'expression-folder-override', |
| 2369 | aliases: ['costume'], | 2383 | aliases: ['spriteoverride', 'costume'], |
| 2370 | callback: setSpriteSetCommand, | 2384 | callback: setSpriteSetCommand, |
| 2371 | unnamedArgumentList: [ | 2385 | unnamedArgumentList: [ |
| 2372 | new SlashCommandArgument( | 2386 | new SlashCommandArgument( |
| @@ -2376,55 +2390,52 @@ function migrateSettings() { | |||
| 2376 | helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', | 2390 | helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', |
| 2377 | })); | 2391 | })); |
| 2378 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2392 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2379 | name: 'lastsprite', | 2393 | name: 'expression-last', |
| 2380 | callback: (_, name) => { | 2394 | aliases: ['lastsprite'], |
| 2395 | /** @type {(args: object, name: string) => Promise<string>} */ | ||
| 2396 | callback: async (_, name) => { | ||
| 2381 | if (typeof name !== 'string') throw new Error('name must be a string'); | 2397 | if (typeof name !== 'string') throw new Error('name must be a string'); |
| 2398 | if (!name) { | ||
| 2399 | if (selected_group) { | ||
| 2400 | toastr.error(t`In group chats, you must specify a character name.`, t`No character name specified`); | ||
| 2401 | return ''; | ||
| 2402 | } | ||
| 2403 | name = characters[this_chid]?.avatar; | ||
| 2404 | } | ||
| 2405 | |||
| 2382 | const char = findChar({ name: name }); | 2406 | const char = findChar({ name: name }); |
| 2407 | if (!char) toastr.warning(t`Couldn't find character ${name}.`, t`Character not found`); | ||
| 2408 | |||
| 2383 | const sprite = lastExpression[char?.name ?? name] ?? ''; | 2409 | const sprite = lastExpression[char?.name ?? name] ?? ''; |
| 2384 | return sprite; | 2410 | return sprite; |
| 2385 | }, | 2411 | }, |
| 2386 | returns: 'the last set sprite / expression for the named character.', | 2412 | returns: 'the last set expression for the named character.', |
| 2387 | unnamedArgumentList: [ | 2413 | unnamedArgumentList: [ |
| 2388 | SlashCommandArgument.fromProps({ | 2414 | SlashCommandArgument.fromProps({ |
| 2389 | description: 'Character name - or unique character identifier (avatar key)', | 2415 | description: 'Character name - or unique character identifier (avatar key). If not provided, the current character for this chat will be used (does not work in group chats)', |
| 2390 | typeList: [ARGUMENT_TYPE.STRING], | 2416 | typeList: [ARGUMENT_TYPE.STRING], |
| 2391 | isRequired: true, | ||
| 2392 | enumProvider: commonEnumProviders.characters('character'), | 2417 | enumProvider: commonEnumProviders.characters('character'), |
| 2393 | forceEnum: true, | 2418 | forceEnum: true, |
| 2394 | }), | 2419 | }), |
| 2395 | ], | 2420 | ], |
| 2396 | helpString: 'Returns the last set sprite / expression for the named character.', | 2421 | helpString: 'Returns the last set expression for the named character.', |
| 2397 | })); | 2422 | })); |
| 2398 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2423 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2399 | name: 'th', | 2424 | name: 'expression-talkinghead', |
| 2400 | callback: toggleTalkingHeadCommand, | 2425 | callback: toggleTalkingHeadCommand, |
| 2401 | aliases: ['talkinghead'], | 2426 | aliases: ['th', 'talkinghead'], |
| 2402 | helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', | 2427 | helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', |
| 2403 | returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.', | 2428 | returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.', |
| 2404 | })); | 2429 | })); |
| 2405 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2430 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2406 | name: 'classify-expressions', | 2431 | name: 'expression-classify', |
| 2407 | aliases: ['expressions'], | 2432 | aliases: ['classify-expressions', 'expressions'], |
| 2433 | /** @type {(args: {return: string}) => Promise<string>} */ | ||
| 2408 | callback: async (args) => { | 2434 | callback: async (args) => { |
| 2409 | /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ | 2435 | let returnType = |
| 2410 | // @ts-ignore | 2436 | /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */ |
| 2411 | let returnType = args.return; | 2437 | (args.return); |
| 2412 | |||
| 2413 | // Old legacy return type handling | ||
| 2414 | if (args.format) { | ||
| 2415 | toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning'); | ||
| 2416 | const type = String(args?.format).toLowerCase().trim(); | ||
| 2417 | switch (type) { | ||
| 2418 | case 'json': | ||
| 2419 | returnType = 'object'; | ||
| 2420 | break; | ||
| 2421 | default: | ||
| 2422 | returnType = 'pipe'; | ||
| 2423 | break; | ||
| 2424 | } | ||
| 2425 | } | ||
| 2426 | 2438 | ||
| 2427 | // Now the actual new return type handling | ||
| 2428 | const list = await getExpressionsList(); | 2439 | const list = await getExpressionsList(); |
| 2429 | 2440 | ||
| 2430 | return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') }); | 2441 | return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') }); |
| @@ -2438,22 +2449,13 @@ function migrateSettings() { | |||
| 2438 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), | 2449 | enumList: slashCommandReturnHelper.enumList({ allowObject: true }), |
| 2439 | forceEnum: true, | 2450 | forceEnum: true, |
| 2440 | }), | 2451 | }), |
| 2441 | // TODO remove some day | ||
| 2442 | SlashCommandNamedArgument.fromProps({ | ||
| 2443 | name: 'format', | ||
| 2444 | description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.', | ||
| 2445 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 2446 | enumList: [ | ||
| 2447 | new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '), | ||
| 2448 | new SlashCommandEnumValue('json', null, enumTypes.enum, '[]'), | ||
| 2449 | ], | ||
| 2450 | }), | ||
| 2451 | ], | 2452 | ], |
| 2452 | returns: 'The comma-separated list of available expressions, including custom expressions.', | 2453 | returns: 'The comma-separated list of available expressions, including custom expressions.', |
| 2453 | helpString: 'Returns a list of available expressions, including custom expressions.', | 2454 | helpString: 'Returns a list of available expressions, including custom expressions.', |
| 2454 | })); | 2455 | })); |
| 2455 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2456 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2456 | name: 'classify', | 2457 | name: 'expression-classify', |
| 2458 | aliases: ['classify'], | ||
| 2457 | callback: classifyCallback, | 2459 | callback: classifyCallback, |
| 2458 | namedArgumentList: [ | 2460 | namedArgumentList: [ |
| 2459 | SlashCommandNamedArgument.fromProps({ | 2461 | SlashCommandNamedArgument.fromProps({ |
| @@ -2492,11 +2494,13 @@ function migrateSettings() { | |||
| 2492 | `, | 2494 | `, |
| 2493 | })); | 2495 | })); |
| 2494 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2496 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2495 | name: 'uploadsprite', | 2497 | name: 'expression-upload', |
| 2498 | aliases: ['uploadsprite'], | ||
| 2499 | /** @type {(args: {name: string, label: string, folder: string?, spriteName: string?}, url: string) => Promise<string>} */ | ||
| 2496 | callback: async (args, url) => { | 2500 | callback: async (args, url) => { |
| 2497 | await uploadSpriteCommand(args, url); | 2501 | return await uploadSpriteCommand(args, url); |
| 2498 | return ''; | ||
| 2499 | }, | 2502 | }, |
| 2503 | returns: 'the resulting sprite name', | ||
| 2500 | unnamedArgumentList: [ | 2504 | unnamedArgumentList: [ |
| 2501 | SlashCommandArgument.fromProps({ | 2505 | SlashCommandArgument.fromProps({ |
| 2502 | description: 'URL of the image to upload', | 2506 | description: 'URL of the image to upload', |
| @@ -2510,7 +2514,6 @@ function migrateSettings() { | |||
| 2510 | description: 'Character name or avatar key (default is current character)', | 2514 | description: 'Character name or avatar key (default is current character)', |
| 2511 | typeList: [ARGUMENT_TYPE.STRING], | 2515 | typeList: [ARGUMENT_TYPE.STRING], |
| 2512 | isRequired: false, | 2516 | isRequired: false, |
| 2513 | acceptsMultiple: false, | ||
| 2514 | }), | 2517 | }), |
| 2515 | SlashCommandNamedArgument.fromProps({ | 2518 | SlashCommandNamedArgument.fromProps({ |
| 2516 | name: 'label', | 2519 | name: 'label', |
| @@ -2518,16 +2521,32 @@ function migrateSettings() { | |||
| 2518 | typeList: [ARGUMENT_TYPE.STRING], | 2521 | typeList: [ARGUMENT_TYPE.STRING], |
| 2519 | enumProvider: localEnumProviders.expressions, | 2522 | enumProvider: localEnumProviders.expressions, |
| 2520 | isRequired: true, | 2523 | isRequired: true, |
| 2521 | acceptsMultiple: false, | ||
| 2522 | }), | 2524 | }), |
| 2523 | SlashCommandNamedArgument.fromProps({ | 2525 | SlashCommandNamedArgument.fromProps({ |
| 2524 | name: 'folder', | 2526 | name: 'folder', |
| 2525 | description: 'Override folder to upload into', | 2527 | description: 'Override folder to upload into', |
| 2526 | typeList: [ARGUMENT_TYPE.STRING], | 2528 | typeList: [ARGUMENT_TYPE.STRING], |
| 2527 | isRequired: false, | 2529 | isRequired: false, |
| 2528 | acceptsMultiple: false, | 2530 | }), |
| 2531 | SlashCommandNamedArgument.fromProps({ | ||
| 2532 | name: 'spriteName', | ||
| 2533 | description: 'Override sprite name to allow multiple sprites per expressions. Has to follow the naming pattern. If unspecified, the label will be used as sprite name.', | ||
| 2534 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 2535 | isRequired: false, | ||
| 2529 | }), | 2536 | }), |
| 2530 | ], | 2537 | ], |
| 2531 | helpString: '<div>Upload a sprite from a URL.</div><div>Example:</div><pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre>', | 2538 | helpString: ` |
| 2539 | <div> | ||
| 2540 | Upload a sprite from a URL. | ||
| 2541 | </div> | ||
| 2542 | <div> | ||
| 2543 | <strong>Example:</strong> | ||
| 2544 | <ul> | ||
| 2545 | <li> | ||
| 2546 | <pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre> | ||
| 2547 | </li> | ||
| 2548 | </ul> | ||
| 2549 | </div> | ||
| 2550 | `, | ||
| 2532 | })); | 2551 | })); |
| 2533 | })(); | 2552 | })(); |