Upload expressions update
| @@ -15,6 +15,8 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm | |||
| 15 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 15 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; | 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; | 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 18 | import { Popup, POPUP_RESULT } from '../../popup.js'; | ||
| 19 | import { t } from '../../i18n.js'; | ||
| 18 | export { MODULE_NAME }; | 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 | async function onClickExpressionUpload(event) { | 1866 | async function onClickExpressionUpload(event) { |
| 1856 | // Prevents the expression from being set | 1867 | // Prevents the expression from being set |
| 1857 | event.stopPropagation(); | 1868 | event.stopPropagation(); |
| 1858 | 1869 | ||
| 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'); |
| 1861 | 1875 | ||
| 1862 | const handleExpressionUploadChange = async (e) => { | 1876 | const handleExpressionUploadChange = async (e) => { |
| @@ -1866,21 +1880,70 @@ async function onClickExpressionUpload(event) { | |||
| 1866 | return; | 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 | // 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 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 | 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); | ||
| 1884 | 1947 | ||
| 1885 | await handleFileUpload('/api/sprites/upload', formData); | 1948 | await handleFileUpload('/api/sprites/upload', formData); |
| 1886 | 1949 | ||
| @@ -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> <span id="image_list_header_name"></span> | 92 | <strong data-i18n="Sprite set:">Sprite set:</strong> <span id="image_list_header_name"></span> |
| 92 | </h3> | 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 | 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; | ||
| 230 | 231 | ||
| 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) => { | |||
| 249 | 250 | ||
| 250 | // Remove existing sprite with the same label | 251 | // 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 | } |
| 256 | 257 | ||
| 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 folder | 261 | // Copy uploaded file to sprites folder |