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
15import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';15import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
16import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';16import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
17import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';17import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
18import { Popup, POPUP_RESULT } from '../../popup.js';
19import { t } from '../../i18n.js';
18export { MODULE_NAME };20export { MODULE_NAME };
1921
20/**22/**
@@ -1852,11 +1854,23 @@ async function handleFileUpload(url, formData) {
1852 }1854 }
1853}1855}
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 */
1862function withoutExtension(fileName) {
1863 return fileName.replace(/\.[^/.]+$/, '');
1864}
1865
1855async function onClickExpressionUpload(event) {1866async function onClickExpressionUpload(event) {
1856 // Prevents the expression from being set1867 // Prevents the expression from being set
1857 event.stopPropagation();1868 event.stopPropagation();
18581869
1859 const expression = $(this).closest('.expression_list_item').data('expression');1870 const expressionListItem = $(this).closest('.expression_list_item');
1871
1872 const clickedFileName = expressionListItem.attr('data-expression-type') !== 'failure' ? expressionListItem.attr('data-filename') : null;
1873 const expression = expressionListItem.data('expression');
1860 const name = $('#image_list').data('name');1874 const name = $('#image_list').data('name');
18611875
1862 const handleExpressionUploadChange = async (e) => {1876 const handleExpressionUploadChange = async (e) => {
@@ -1866,21 +1880,70 @@ async function onClickExpressionUpload(event) {
1866 return;1880 return;
1867 }1881 }
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 data1884
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
1873 // if (!userChoice) {1887 if (extension_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 proceed1890 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
1880 const formData = new FormData();1942 const formData = new FormData();
1881 formData.append('name', name);1943 formData.append('name', name);
1882 formData.append('label', expression);1944 formData.append('label', expression);
1883 formData.append('avatar', file);1945 formData.append('avatar', file);
1946 formData.append('spriteName', spriteName);
18841947
1885 await handleFileUpload('/api/sprites/upload', formData);1948 await handleFileUpload('/api/sprites/upload', formData);
18861949
public/scripts/extensions/expressions/settings.html+1 -0
@@ -87,6 +87,7 @@
87 </div>87 </div>
88 <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>88 <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>
89 <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>89 <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>
90 <h3 id="image_list_header">91 <h3 id="image_list_header">
91 <strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>92 <strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
92 </h3>93 </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) => {
227 const file = request.file;227 const file = request.file;
228 const label = request.body.label;228 const label = request.body.label;
229 const name = request.body.name;229 const name = request.body.name;
230 const spriteName = request.body.spriteName || label;
230231
231 if (!file || !label || !name) {232 if (!file || !label || !name) {
232 return response.sendStatus(400);233 return response.sendStatus(400);
@@ -249,12 +250,12 @@ router.post('/upload', urlencodedParser, async (request, response) => {
249250
250 // Remove existing sprite with the same label251 // Remove existing sprite with the same label
251 for (const file of files) {252 for (const file of files) {
252 if (path.parse(file).name === label) {253 if (path.parse(file).name === spriteName) {
253 fs.rmSync(path.join(spritesPath, file));254 fs.rmSync(path.join(spritesPath, file));
254 }255 }
255 }256 }
256257
257 const filename = label + path.parse(file.originalname).ext;258 const filename = spriteName + path.parse(file.originalname).ext;
258 const spritePath = path.join(file.destination, file.filename);259 const spritePath = path.join(file.destination, file.filename);
259 const pathToFile = path.join(spritesPath, filename);260 const pathToFile = path.join(spritesPath, filename);
260 // Copy uploaded file to sprites folder261 // Copy uploaded file to sprites folder