Upload expressions update

3282c9426c72d22c0ed82e224eac5f491f391ca2

Wolfsblvt <wolfsblvt@gmail.com>

4 files changed, +90 -13Ignore whitespace
public/scripts/extensions/expressions/index.js+74 -11
@@ -15,6 +15,8 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
1515import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1616import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
1717import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
18+import { Popup, POPUP_RESULT } from '../../popup.js';
19+import { t } from '../../i18n.js';
1820export { MODULE_NAME };
1921
2022/**
@@ -1852,11 +1854,23 @@ async function handleFileUpload(url, formData) {
18521854 }
18531855}
18541856
1857+/**
1858+ * Removes the file extension from a file name
1859+ * @param {string} fileName The file name to remove the extension from
1860+ * @returns {string} The file name without the extension
1861+ */
1862+function withoutExtension(fileName) {
1863+ return fileName.replace(/\.[^/.]+$/, '');
1864+}
1865+
18551866async function onClickExpressionUpload(event) {
18561867 // Prevents the expression from being set
18571868 event.stopPropagation();
18581869
18591870 const expressionexpressionListItem = $(this).closest('.expression_list_item').data('expression');
1871+
1872+ const clickedFileName = expressionListItem.attr('data-expression-type') !== 'failure' ? expressionListItem.attr('data-filename') : null;
1873+ const expression = expressionListItem.data('expression');
18601874 const name = $('#image_list').data('name');
18611875
18621876 const handleExpressionUploadChange = async (e) => {
@@ -1866,21 +1880,70 @@ async function onClickExpressionUpload(event) {
18661880 return;
18671881 }
18681882
1869- // // If extension_settings.expressions.allowMultiple is false and there's already a main image, ask user:
1883+ const existingFiles = spriteCache[name]?.find(x => x.label === expression)?.files || [];
1870- // let hasMainImage = true; // Check from your item data
1884+
1871- // if (!extension_settings.expressions.allowMultiple && hasMainImage) {
1885+ let spriteName = expression;
1872- // let userChoice = await callPopup('<h3>Replace existing main image?</h3><p>Press Ok to replace, Cancel to abort.</p>', 'confirm');
1886+
18731887 // if (!userChoiceextension_settings.expressions.allowMultiple) {
1874- // return;
1888+ const matchesExisting = existingFiles.some(x => x.fileName === file.name);
1875- // }
1889+ const fileNameWithoutExtension = withoutExtension(file.name);
1876- // // If user chooses replace, remove the old file, then proceed
1890+ const filenameValidationRegex = new RegExp(`^${expression}(?:[-\\.].*?)?$`);
1877- // // ...existing code to remove old file...
1891+ const validFileName = filenameValidationRegex.test(fileNameWithoutExtension);
1878- // }
1892+
1893+ // If there is no expression yet and it's a valid expression, we just take it
1894+ if (!clickedFileName && validFileName) {
1895+ spriteName = fileNameWithoutExtension;
1896+ }
1897+ // If the filename matches the one that was clicked, we just take it and replace it
1898+ else if (clickedFileName === file.name) {
1899+ spriteName = fileNameWithoutExtension;
1900+ }
1901+ // If it's a valid filename and there's no existing file with the same name, we just take it
1902+ else if (!matchesExisting && validFileName) {
1903+ spriteName = fileNameWithoutExtension;
1904+ }
1905+ else {
1906+ /** @type {import('../../popup.js').CustomPopupButton[]} */
1907+ const customButtons = [];
1908+ if (clickedFileName) {
1909+ customButtons.push({
1910+ text: t`Replace Existing`,
1911+ result: POPUP_RESULT.NEGATIVE,
1912+ action: () => {
1913+ console.debug('Replacing existing sprite');
1914+ spriteName = withoutExtension(clickedFileName);
1915+ },
1916+ });
1917+ }
1918+
1919+ const message = await renderExtensionTemplateAsync(MODULE_NAME, 'templates/upload-expression', { expression, clickedFileName });
1920+
1921+ spriteName = null;
1922+ const result = await Popup.show.input(t`Upload Expression Sprite`, message,
1923+ `${expression}-${existingFiles.length}`, { customButtons: customButtons });
1924+
1925+ if (result) {
1926+ if (!filenameValidationRegex.test(result)) {
1927+ toastr.warning(t`The name you entered does not follow the naming schema for the selected expression '${expression}'.`, t`Invalid Expression Sprite Name`);
1928+ return;
1929+ }
1930+ spriteName = result;
1931+ }
1932+ }
1933+ } else {
1934+ spriteName = withoutExtension(clickedFileName);
1935+ }
1936+
1937+ if (!spriteName) {
1938+ toastr.warning(t`Cancelled uploading sprite.`, t`Upload Cancelled`);
1939+ return;
1940+ }
18791941
18801942 const formData = new FormData();
18811943 formData.append('name', name);
18821944 formData.append('label', expression);
18831945 formData.append('avatar', file);
1946+ formData.append('spriteName', spriteName);
18841947
18851948 await handleFileUpload('/api/sprites/upload', formData);
18861949
public/scripts/extensions/expressions/settings.html+1 -0
@@ -87,6 +87,7 @@
8787 </div>
8888 <p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
8989 <span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>
90+ <span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a dash. Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt>, <tt>美しい-17.png</tt>
9091 <h3 id="image_list_header">
9192 <strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
9293 </h3>
public/scripts/extensions/expressions/templates/upload-expression.html+12 -0
@@ -0,0 +1,12 @@
1+<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div>
2+<div class="m-b-1" data-i18n="upload_expression_naming_1">
3+ Sprite names must follow the naming schema for the selected expression: {{expression}}
4+</div>
5+<div data-i18n="upload_expression_naming_2">
6+ For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'.
7+</div>
8+<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt>, <tt>美しい-17.png</tt>
9+{{#if clickedFileName}}
10+<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div>
11+<tt>{{clickedFileName}}</tt>
12+{{/if}}
src/endpoints/sprites.js+3 -2
@@ -227,6 +227,7 @@ router.post('/upload', urlencodedParser, async (request, response) => {
227227 const file = request.file;
228228 const label = request.body.label;
229229 const name = request.body.name;
230+ const spriteName = request.body.spriteName || label;
230231
231232 if (!file || !label || !name) {
232233 return response.sendStatus(400);
@@ -249,12 +250,12 @@ router.post('/upload', urlencodedParser, async (request, response) => {
249250
250251 // Remove existing sprite with the same label
251252 for (const file of files) {
252253 if (path.parse(file).name === labelspriteName) {
253254 fs.rmSync(path.join(spritesPath, file));
254255 }
255256 }
256257
257258 const filename = labelspriteName + path.parse(file.originalname).ext;
258259 const spritePath = path.join(file.destination, file.filename);
259260 const pathToFile = path.join(spritesPath, filename);
260261 // Copy uploaded file to sprites folder