diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index a076f15a2..9e91bd888 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -15,6 +15,8 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; +import { Popup, POPUP_RESULT } from '../../popup.js'; +import { t } from '../../i18n.js'; export { MODULE_NAME }; /** @@ -1852,11 +1854,23 @@ async function handleFileUpload(url, formData) { } } +/** + * Removes the file extension from a file name + * @param {string} fileName The file name to remove the extension from + * @returns {string} The file name without the extension + */ +function withoutExtension(fileName) { + return fileName.replace(/\.[^/.]+$/, ''); +} + async function onClickExpressionUpload(event) { // Prevents the expression from being set event.stopPropagation(); - const expression = $(this).closest('.expression_list_item').data('expression'); + const expressionListItem = $(this).closest('.expression_list_item'); + + const clickedFileName = expressionListItem.attr('data-expression-type') !== 'failure' ? expressionListItem.attr('data-filename') : null; + const expression = expressionListItem.data('expression'); const name = $('#image_list').data('name'); const handleExpressionUploadChange = async (e) => { @@ -1866,21 +1880,70 @@ async function onClickExpressionUpload(event) { return; } - // // If extension_settings.expressions.allowMultiple is false and there's already a main image, ask user: - // let hasMainImage = true; // Check from your item data - // if (!extension_settings.expressions.allowMultiple && hasMainImage) { - // let userChoice = await callPopup('

Replace existing main image?

Press Ok to replace, Cancel to abort.

', 'confirm'); - // if (!userChoice) { - // return; - // } - // // If user chooses replace, remove the old file, then proceed - // // ...existing code to remove old file... - // } + const existingFiles = spriteCache[name]?.find(x => x.label === expression)?.files || []; + + let spriteName = expression; + + if (extension_settings.expressions.allowMultiple) { + const matchesExisting = existingFiles.some(x => x.fileName === file.name); + const fileNameWithoutExtension = withoutExtension(file.name); + const filenameValidationRegex = new RegExp(`^${expression}(?:[-\\.].*?)?$`); + const validFileName = filenameValidationRegex.test(fileNameWithoutExtension); + + // If there is no expression yet and it's a valid expression, we just take it + if (!clickedFileName && validFileName) { + spriteName = fileNameWithoutExtension; + } + // If the filename matches the one that was clicked, we just take it and replace it + else if (clickedFileName === file.name) { + spriteName = fileNameWithoutExtension; + } + // If it's a valid filename and there's no existing file with the same name, we just take it + else if (!matchesExisting && validFileName) { + spriteName = fileNameWithoutExtension; + } + else { + /** @type {import('../../popup.js').CustomPopupButton[]} */ + const customButtons = []; + if (clickedFileName) { + customButtons.push({ + text: t`Replace Existing`, + result: POPUP_RESULT.NEGATIVE, + action: () => { + console.debug('Replacing existing sprite'); + spriteName = withoutExtension(clickedFileName); + }, + }); + } + + const message = await renderExtensionTemplateAsync(MODULE_NAME, 'templates/upload-expression', { expression, clickedFileName }); + + spriteName = null; + const result = await Popup.show.input(t`Upload Expression Sprite`, message, + `${expression}-${existingFiles.length}`, { customButtons: customButtons }); + + if (result) { + if (!filenameValidationRegex.test(result)) { + toastr.warning(t`The name you entered does not follow the naming schema for the selected expression '${expression}'.`, t`Invalid Expression Sprite Name`); + return; + } + spriteName = result; + } + } + } else { + spriteName = withoutExtension(clickedFileName); + } + + if (!spriteName) { + toastr.warning(t`Cancelled uploading sprite.`, t`Upload Cancelled`); + return; + } const formData = new FormData(); formData.append('name', name); formData.append('label', expression); formData.append('avatar', file); + formData.append('spriteName', spriteName); await handleFileUpload('/api/sprites/upload', formData); diff --git a/public/scripts/extensions/expressions/settings.html b/public/scripts/extensions/expressions/settings.html index 66c55b3a4..fd5f9c3a6 100644 --- a/public/scripts/extensions/expressions/settings.html +++ b/public/scripts/extensions/expressions/settings.html @@ -87,6 +87,7 @@

Hint: Create new folder in the /characters/ folder of your user data directory and name it as the name of the character. Put images with expressions there. File names should follow the pattern: [expression_label].[image_format]

+ In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a dash. Examples: joy.png, joy-1.png, joy.expressive.png, 美しい-17.png

Sprite set: 

diff --git a/public/scripts/extensions/expressions/templates/upload-expression.html b/public/scripts/extensions/expressions/templates/upload-expression.html new file mode 100644 index 000000000..a7901ae7f --- /dev/null +++ b/public/scripts/extensions/expressions/templates/upload-expression.html @@ -0,0 +1,12 @@ +
Please enter a name for the sprite (without extension).
+
+ Sprite names must follow the naming schema for the selected expression: {{expression}} +
+
+ For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'. +
+Examples: {{expression}}.png, {{expression}}-1.png, {{expression}}.expressive.png, 美しい-17.png +{{#if clickedFileName}} +
Click 'Replace' to replace the existing expression:
+{{clickedFileName}} +{{/if}} diff --git a/src/endpoints/sprites.js b/src/endpoints/sprites.js index a87c554bd..4c5c5af85 100644 --- a/src/endpoints/sprites.js +++ b/src/endpoints/sprites.js @@ -227,6 +227,7 @@ router.post('/upload', urlencodedParser, async (request, response) => { const file = request.file; const label = request.body.label; const name = request.body.name; + const spriteName = request.body.spriteName || label; if (!file || !label || !name) { return response.sendStatus(400); @@ -249,12 +250,12 @@ router.post('/upload', urlencodedParser, async (request, response) => { // Remove existing sprite with the same label for (const file of files) { - if (path.parse(file).name === label) { + if (path.parse(file).name === spriteName) { fs.rmSync(path.join(spritesPath, file)); } } - const filename = label + path.parse(file.originalname).ext; + const filename = spriteName + path.parse(file.originalname).ext; const spritePath = path.join(file.destination, file.filename); const pathToFile = path.join(spritesPath, filename); // Copy uploaded file to sprites folder