Upload expressions update
| @@ -15,6 +15,8 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm | ||
| 15 | 15 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 16 | 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 17 | 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 18 | +import { Popup, POPUP_RESULT } from '../../popup.js'; | |
| 19 | +import { t } from '../../i18n.js'; | |
| 18 | 20 | export { MODULE_NAME }; |
| 19 | 21 | |
| 20 | 22 | /** |
| @@ -1852,11 +1854,23 @@ async function handleFileUpload(url, formData) { | ||
| 1852 | 1854 | } |
| 1853 | 1855 | } |
| 1854 | 1856 | |
| 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 | + | |
| 1855 | 1866 | async function onClickExpressionUpload(event) { |
| 1856 | 1867 | // Prevents the expression from being set |
| 1857 | 1868 | event.stopPropagation(); |
| 1858 | 1869 | |
| 1859 | 1870 | 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'); | |
| 1860 | 1874 | const name = $('#image_list').data('name'); |
| 1861 | 1875 | |
| 1862 | 1876 | const handleExpressionUploadChange = async (e) => { |
| @@ -1866,21 +1880,70 @@ async function onClickExpressionUpload(event) { | ||
| 1866 | 1880 | return; |
| 1867 | 1881 | } |
| 1868 | 1882 | |
| 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 | + | |
| 1873 | 1887 | // 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 | + } | |
| 1879 | 1941 | |
| 1880 | 1942 | const formData = new FormData(); |
| 1881 | 1943 | formData.append('name', name); |
| 1882 | 1944 | formData.append('label', expression); |
| 1883 | 1945 | formData.append('avatar', file); |
| 1946 | + formData.append('spriteName', spriteName); | |
| 1884 | 1947 | |
| 1885 | 1948 | await handleFileUpload('/api/sprites/upload', formData); |
| 1886 | 1949 | |
| @@ -87,6 +87,7 @@ | ||
| 87 | 87 | </div> |
| 88 | 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 | 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 | 91 | <h3 id="image_list_header"> |
| 91 | 92 | <strong data-i18n="Sprite set:">Sprite set:</strong> <span id="image_list_header_name"></span> |
| 92 | 93 | </h3> |
| @@ -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}} | |
| @@ -227,6 +227,7 @@ router.post('/upload', urlencodedParser, async (request, response) => { | ||
| 227 | 227 | const file = request.file; |
| 228 | 228 | const label = request.body.label; |
| 229 | 229 | const name = request.body.name; |
| 230 | + const spriteName = request.body.spriteName || label; | |
| 230 | 231 | |
| 231 | 232 | if (!file || !label || !name) { |
| 232 | 233 | return response.sendStatus(400); |
| @@ -249,12 +250,12 @@ router.post('/upload', urlencodedParser, async (request, response) => { | ||
| 249 | 250 | |
| 250 | 251 | // Remove existing sprite with the same label |
| 251 | 252 | for (const file of files) { |
| 252 | 253 | if (path.parse(file).name === labelspriteName) { |
| 253 | 254 | fs.rmSync(path.join(spritesPath, file)); |
| 254 | 255 | } |
| 255 | 256 | } |
| 256 | 257 | |
| 257 | 258 | const filename = labelspriteName + path.parse(file.originalname).ext; |
| 258 | 259 | const spritePath = path.join(file.destination, file.filename); |
| 259 | 260 | const pathToFile = path.join(spritesPath, filename); |
| 260 | 261 | // Copy uploaded file to sprites folder |