From 602bceb26e3c1950a12c87c08727a63c8a89ea81 Mon Sep 17 00:00:00 2001 From: SillyLossy Date: Mon, 29 May 2023 01:17:52 +0300 Subject: [PATCH] Upload or delete a sprite image. --- .../scripts/extensions/expressions/index.js | 855 ++++++++++-------- .../scripts/extensions/expressions/style.css | 262 +++--- server.js | 77 ++ 3 files changed, 685 insertions(+), 509 deletions(-) diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 89b789139..594e33bc2 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1,385 +1,470 @@ -import { saveSettingsDebounced } from "../../../script.js"; -import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js"; -export { MODULE_NAME }; - -const MODULE_NAME = 'expressions'; -const UPDATE_INTERVAL = 2000; -const DEFAULT_EXPRESSIONS = [ - "admiration", - "amusement", - "anger", - "annoyance", - "approval", - "caring", - "confusion", - "curiosity", - "desire", - "disappointment", - "disapproval", - "disgust", - "embarrassment", - "excitement", - "fear", - "gratitude", - "grief", - "joy", - "love", - "nervousness", - "optimism", - "pride", - "realization", - "relief", - "remorse", - "sadness", - "surprise", - "neutral" -]; - -let expressionsList = null; -let lastCharacter = undefined; -let lastMessage = null; -let spriteCache = {}; -let inApiCall = false; - -function onExpressionsShowDefaultInput() { - const value = $(this).prop('checked'); - extension_settings.expressions.showDefault = value; - saveSettingsDebounced(); - - const existingImageSrc = $('img.expression').prop('src'); - if (existingImageSrc !== undefined) { //if we have an image in src - if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default) - $('img.expression').prop('src', ''); //remove it - lastMessage = null; - } - if (value) { - lastMessage = null; - } - } -} - -let isWorkerBusy = false; - -async function moduleWorkerWrapper() { - // Don't touch me I'm busy... - if (isWorkerBusy) { - return; - } - - // I'm free. Let's update! - try { - isWorkerBusy = true; - await moduleWorker(); - } - finally { - isWorkerBusy = false; - } -} - -async function moduleWorker() { - const context = getContext(); - - // non-characters not supported - if (!context.groupId && context.characterId === undefined) { - removeExpression(); - return; - } - - // character changed - if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { - removeExpression(); - spriteCache = {}; - } - - const currentLastMessage = getLastCharacterMessage(); - - // character has no expressions or it is not loaded - if (Object.keys(spriteCache).length === 0) { - await validateImages(currentLastMessage.name); - lastCharacter = context.groupId || context.characterId; - } - - const offlineMode = $('.expression_settings .offline_mode'); - if (!modules.includes('classify')) { - $('.expression_settings').show(); - offlineMode.css('display', 'block'); - lastCharacter = context.groupId || context.characterId; - - if (context.groupId) { - await validateImages(currentLastMessage.name, true); - } - - return; - } - else { - // force reload expressions list on connect to API - if (offlineMode.is(':visible')) { - expressionsList = null; - spriteCache = {}; - expressionsList = await getExpressionsList(); - await validateImages(currentLastMessage.name, true); - } - - offlineMode.css('display', 'none'); - } - - - // check if last message changed - if ((lastCharacter === context.characterId || lastCharacter === context.groupId) - && lastMessage === currentLastMessage.mes) { - return; - } - - // API is busy - if (inApiCall) { - return; - } - - try { - inApiCall = true; - const url = new URL(getApiUrl()); - url.pathname = '/api/classify'; - - const apiResult = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Bypass-Tunnel-Reminder': 'bypass', - }, - body: JSON.stringify({ text: currentLastMessage.mes }) - }); - - if (apiResult.ok) { - const name = context.groupId ? currentLastMessage.name : context.name2; - const force = !!context.groupId; - const data = await apiResult.json(); - let expression = data.classification[0].label; - - // Character won't be angry on you for swiping - if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { - expression = 'joy'; - } - - setExpression(name, expression, force); - } - - } - catch (error) { - console.log(error); - } - finally { - inApiCall = false; - lastCharacter = context.groupId || context.characterId; - lastMessage = currentLastMessage.mes; - } -} - -function getLastCharacterMessage() { - const context = getContext(); - const reversedChat = context.chat.slice().reverse(); - - for (let mes of reversedChat) { - if (mes.is_user || mes.is_system) { - continue; - } - - return { mes: mes.mes, name: mes.name }; - } - - return { mes: '', name: null }; -} - -function removeExpression() { - lastMessage = null; - $('img.expression').off('error'); - $('img.expression').prop('src', ''); - $('img.expression').removeClass('default'); - $('.expression_settings').hide(); -} - -async function validateImages(character, forceRedrawCached) { - if (!character) { - return; - } - - const labels = await getExpressionsList(); - - if (spriteCache[character]) { - if (forceRedrawCached && $('#image_list').data('name') !== character) { - console.log('force redrawing character sprites list') - drawSpritesList(character, labels, spriteCache[character]); - } - - return; - } - - const sprites = await getSpritesList(character); - let validExpressions = drawSpritesList(character, labels, sprites); - spriteCache[character] = validExpressions; -} - -function drawSpritesList(character, labels, sprites) { - let validExpressions = []; - $('.expression_settings').show(); - $('#image_list').empty(); - $('#image_list').data('name', character); - labels.sort().forEach((item) => { - const sprite = sprites.find(x => x.label == item); - - if (sprite) { - validExpressions.push(sprite); - $('#image_list').append(getListItem(item, sprite.path, 'success')); - } - else { - $('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure')); - } - }); - return validExpressions; -} - -function getListItem(item, imageSrc, textClass) { - return ` -
- ${item} - -
- `; -} - -async function getSpritesList(name) { - console.log('getting sprites list'); - - try { - const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`); - - let sprites = result.ok ? (await result.json()) : []; - return sprites; - } - catch (err) { - console.log(err); - return []; - } -} - -async function getExpressionsList() { - // get something for offline mode (default images) - if (!modules.includes('classify')) { - return DEFAULT_EXPRESSIONS; - } - - if (Array.isArray(expressionsList)) { - return expressionsList; - } - - const url = new URL(getApiUrl()); - url.pathname = '/api/classify/labels'; - - try { - const apiResult = await fetch(url, { - method: 'GET', - headers: { 'Bypass-Tunnel-Reminder': 'bypass' }, - }); - - if (apiResult.ok) { - - const data = await apiResult.json(); - expressionsList = data.labels; - return expressionsList; - } - } - catch (error) { - console.log(error); - return []; - } -} - -async function setExpression(character, expression, force) { - console.log('entered setExpressions'); - await validateImages(character); - const img = $('img.expression'); - - const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); - console.log('checking for expression images to show..'); - if (sprite) { - console.log('setting expression from character images folder'); - img.attr('src', sprite.path); - img.removeClass('default'); - img.off('error'); - img.on('error', function () { - $(this).attr('src', ''); - if (force && extension_settings.expressions.showDefault) { - setDefault(); - } - }); - } else { - if (extension_settings.expressions.showDefault) { - setDefault(); - } - } - - function setDefault() { - console.log('setting default'); - const defImgUrl = `/img/default-expressions/${expression}.png`; - //console.log(defImgUrl); - img.attr('src', defImgUrl); - img.addClass('default'); - } - document.getElementById("expression-holder").style.display = ''; -} - -function onClickExpressionImage() { - // online mode doesn't need force set - if (modules.includes('classify')) { - return; - } - - const expression = $(this).attr('id'); - const name = getLastCharacterMessage().name; - - if ($(this).find('.failure').length === 0) { - setExpression(name, expression, true); - } -} - -(function () { - function addExpressionImage() { - const html = ` -
- -
`; - $('body').append(html); - } - function addSettings() { - - const html = ` -
-
-
- Expression images -
-
-
-

You are in offline mode. Click on the image below to set the expression.

-
-

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

- -
-
-
- `; - $('#extensions_settings').append(html); - $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); - $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); - $(document).on('click', '.expression_list_item', onClickExpressionImage); - $('.expression_settings').hide(); - } - - addExpressionImage(); - addSettings(); - setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); - moduleWorkerWrapper(); -})(); \ No newline at end of file +import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js"; +import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js"; +export { MODULE_NAME }; + +const MODULE_NAME = 'expressions'; +const UPDATE_INTERVAL = 2000; +const DEFAULT_EXPRESSIONS = [ + "admiration", + "amusement", + "anger", + "annoyance", + "approval", + "caring", + "confusion", + "curiosity", + "desire", + "disappointment", + "disapproval", + "disgust", + "embarrassment", + "excitement", + "fear", + "gratitude", + "grief", + "joy", + "love", + "nervousness", + "optimism", + "pride", + "realization", + "relief", + "remorse", + "sadness", + "surprise", + "neutral" +]; + +let expressionsList = null; +let lastCharacter = undefined; +let lastMessage = null; +let spriteCache = {}; +let inApiCall = false; + +function onExpressionsShowDefaultInput() { + const value = $(this).prop('checked'); + extension_settings.expressions.showDefault = value; + saveSettingsDebounced(); + + const existingImageSrc = $('img.expression').prop('src'); + if (existingImageSrc !== undefined) { //if we have an image in src + if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default) + $('img.expression').prop('src', ''); //remove it + lastMessage = null; + } + if (value) { + lastMessage = null; + } + } +} + +let isWorkerBusy = false; + +async function moduleWorkerWrapper() { + // Don't touch me I'm busy... + if (isWorkerBusy) { + return; + } + + // I'm free. Let's update! + try { + isWorkerBusy = true; + await moduleWorker(); + } + finally { + isWorkerBusy = false; + } +} + +async function moduleWorker() { + const context = getContext(); + + // non-characters not supported + if (!context.groupId && context.characterId === undefined) { + removeExpression(); + return; + } + + // character changed + if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { + removeExpression(); + spriteCache = {}; + } + + const currentLastMessage = getLastCharacterMessage(); + + // character has no expressions or it is not loaded + if (Object.keys(spriteCache).length === 0) { + await validateImages(currentLastMessage.name); + lastCharacter = context.groupId || context.characterId; + } + + const offlineMode = $('.expression_settings .offline_mode'); + if (!modules.includes('classify')) { + $('.expression_settings').show(); + offlineMode.css('display', 'block'); + lastCharacter = context.groupId || context.characterId; + + if (context.groupId) { + await validateImages(currentLastMessage.name, true); + } + + return; + } + else { + // force reload expressions list on connect to API + if (offlineMode.is(':visible')) { + expressionsList = null; + spriteCache = {}; + expressionsList = await getExpressionsList(); + await validateImages(currentLastMessage.name, true); + } + + offlineMode.css('display', 'none'); + } + + + // check if last message changed + if ((lastCharacter === context.characterId || lastCharacter === context.groupId) + && lastMessage === currentLastMessage.mes) { + return; + } + + // API is busy + if (inApiCall) { + return; + } + + try { + inApiCall = true; + const url = new URL(getApiUrl()); + url.pathname = '/api/classify'; + + const apiResult = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Bypass-Tunnel-Reminder': 'bypass', + }, + body: JSON.stringify({ text: currentLastMessage.mes }) + }); + + if (apiResult.ok) { + const name = context.groupId ? currentLastMessage.name : context.name2; + const force = !!context.groupId; + const data = await apiResult.json(); + let expression = data.classification[0].label; + + // Character won't be angry on you for swiping + if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { + expression = 'joy'; + } + + setExpression(name, expression, force); + } + + } + catch (error) { + console.log(error); + } + finally { + inApiCall = false; + lastCharacter = context.groupId || context.characterId; + lastMessage = currentLastMessage.mes; + } +} + +function getLastCharacterMessage() { + const context = getContext(); + const reversedChat = context.chat.slice().reverse(); + + for (let mes of reversedChat) { + if (mes.is_user || mes.is_system) { + continue; + } + + return { mes: mes.mes, name: mes.name }; + } + + return { mes: '', name: null }; +} + +function removeExpression() { + lastMessage = null; + $('img.expression').off('error'); + $('img.expression').prop('src', ''); + $('img.expression').removeClass('default'); + $('.expression_settings').hide(); +} + +async function validateImages(character, forceRedrawCached) { + if (!character) { + return; + } + + const labels = await getExpressionsList(); + + if (spriteCache[character]) { + if (forceRedrawCached && $('#image_list').data('name') !== character) { + console.log('force redrawing character sprites list') + drawSpritesList(character, labels, spriteCache[character]); + } + + return; + } + + const sprites = await getSpritesList(character); + let validExpressions = drawSpritesList(character, labels, sprites); + spriteCache[character] = validExpressions; +} + +function drawSpritesList(character, labels, sprites) { + let validExpressions = []; + $('.expression_settings').show(); + $('#image_list').empty(); + $('#image_list').data('name', character); + labels.sort().forEach((item) => { + const sprite = sprites.find(x => x.label == item); + + if (sprite) { + validExpressions.push(sprite); + $('#image_list').append(getListItem(item, sprite.path, 'success')); + } + else { + $('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure')); + } + }); + return validExpressions; +} + +function getListItem(item, imageSrc, textClass) { + return ` +
+
+ + +
+ ${item} + +
+ `; +} + +async function getSpritesList(name) { + console.log('getting sprites list'); + + try { + const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`); + + let sprites = result.ok ? (await result.json()) : []; + return sprites; + } + catch (err) { + console.log(err); + return []; + } +} + +async function getExpressionsList() { + // get something for offline mode (default images) + if (!modules.includes('classify')) { + return DEFAULT_EXPRESSIONS; + } + + if (Array.isArray(expressionsList)) { + return expressionsList; + } + + const url = new URL(getApiUrl()); + url.pathname = '/api/classify/labels'; + + try { + const apiResult = await fetch(url, { + method: 'GET', + headers: { 'Bypass-Tunnel-Reminder': 'bypass' }, + }); + + if (apiResult.ok) { + + const data = await apiResult.json(); + expressionsList = data.labels; + return expressionsList; + } + } + catch (error) { + console.log(error); + return []; + } +} + +async function setExpression(character, expression, force) { + console.log('entered setExpressions'); + await validateImages(character); + const img = $('img.expression'); + + const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); + console.log('checking for expression images to show..'); + if (sprite) { + console.log('setting expression from character images folder'); + img.attr('src', sprite.path); + img.removeClass('default'); + img.off('error'); + img.on('error', function () { + $(this).attr('src', ''); + if (force && extension_settings.expressions.showDefault) { + setDefault(); + } + }); + } else { + if (extension_settings.expressions.showDefault) { + setDefault(); + } + } + + function setDefault() { + console.log('setting default'); + const defImgUrl = `/img/default-expressions/${expression}.png`; + //console.log(defImgUrl); + img.attr('src', defImgUrl); + img.addClass('default'); + } + document.getElementById("expression-holder").style.display = ''; +} + +function onClickExpressionImage() { + // online mode doesn't need force set + if (modules.includes('classify')) { + return; + } + + const expression = $(this).attr('id'); + const name = getLastCharacterMessage().name; + + if ($(this).find('.failure').length === 0) { + setExpression(name, expression, true); + } +} +async function onClickExpressionUpload(event) { + // Prevents the expression from being set + event.stopPropagation(); + + const id = $(this).closest('.expression_list_item').attr('id'); + const name = $('#image_list').data('name'); + + const handleExpressionUploadChange = async (e) => { + const file = e.target.files[0]; + + if (!file) { + return; + } + + const formData = new FormData(); + formData.append('name', name); + formData.append('label', id); + formData.append('avatar', file); + + try { + await jQuery.ajax({ + type: "POST", + url: "/upload_sprite", + data: formData, + beforeSend: function () { }, + cache: false, + contentType: false, + processData: false, + }); + + // Refresh sprites list + delete spriteCache[name]; + await validateImages(name); + } catch (error) { + toastr.error('Failed to upload image'); + } + + // Reset the input + e.target.form.reset(); + }; + + $('#expression_upload') + .off('change') + .on('change', handleExpressionUploadChange) + .trigger('click'); +} + +async function onClickExpressionDelete(event) { + // Prevents the expression from being set + event.stopPropagation(); + + const confirmation = await callPopup("

Are you sure?

Once deleted, it's gone forever!", 'confirm'); + + if (!confirmation) { + return; + } + + const id = $(this).closest('.expression_list_item').attr('id'); + const name = $('#image_list').data('name'); + + try { + await fetch('/delete_sprite', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name, label: id }), + }); + } catch (error) { + toastr.error('Failed to delete image. Try again later.'); + } + + // Refresh sprites list + delete spriteCache[name]; + await validateImages(name); +} + +(function () { + function addExpressionImage() { + const html = ` +
+ +
`; + $('body').append(html); + } + function addSettings() { + + const html = ` +
+
+
+ Expression images +
+
+
+

You are in offline mode. Click on the image below to set the expression.

+
+

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

+ +
+
+
+
+ `; + $('#extensions_settings').append(html); + $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); + $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); + $(document).on('click', '.expression_list_item', onClickExpressionImage); + $(document).on('click', '.expression_list_upload', onClickExpressionUpload); + $(document).on('click', '.expression_list_delete', onClickExpressionDelete); + $('.expression_settings').hide(); + } + + addExpressionImage(); + addSettings(); + setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); + moduleWorkerWrapper(); +})(); diff --git a/public/scripts/extensions/expressions/style.css b/public/scripts/extensions/expressions/style.css index 53e5745a3..979a5d083 100644 --- a/public/scripts/extensions/expressions/style.css +++ b/public/scripts/extensions/expressions/style.css @@ -1,124 +1,138 @@ -.expression-helper { - display: inline-block; - height: 100%; - vertical-align: middle; -} - -#expression-wrapper { - display: flex; - height: calc(100vh - 40px); - width: 100vw; -} - -.expression-holder { - min-width: 100px; - min-height: 100px; - max-height: 90vh; - max-width: 90vh; - width: calc((100vw - var(--sheldWidth)) /2); - position: absolute; - bottom: 1px; - padding: 0; - filter: drop-shadow(2px 2px 2px #51515199); - z-index: 2; - overflow: hidden; - -} - -img.expression { - width: 100%; - height: 100%; - vertical-align: bottom; - object-fit: contain; -} - -img.expression[src=""] { - visibility: hidden; -} - -img.expression.default { - vertical-align: middle; - max-height: 120px; - object-fit: contain !important; - margin-top: 50px; -} - -.debug-image { - display: none; - visibility: collapse; - opacity: 0; - width: 0px; - height: 0px; -} - -.expression_list_item { - position: relative; - max-width: 20%; - max-height: 200px; - background-color: #515151b0; - border-radius: 10px; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.expression_list_title { - position: absolute; - bottom: 0; - left: 0; - text-align: center; - font-weight: 600; - background-color: #000000a8; - width: 100%; - height: 20%; - display: flex; - justify-content: center; - align-items: center; -} - -.expression_list_image { - max-width: 100%; - height: 100%; -} - -#image_list { - display: flex; - flex-direction: row; - column-gap: 1rem; - margin: 1rem; - flex-wrap: wrap; - justify-content: space-evenly; - row-gap: 1rem; -} - -#image_list .success { - color: green; -} - -#image_list .failure { - color: red; -} - -.expression_settings p { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} - -.expression_settings label { - display: flex; - align-items: center; - flex-direction: row; - margin-left: 0px; -} - -.expression_settings label input { - margin-left: 0px !important; -} - -@media screen and (max-width:1200px) { - div.expression { - display: none; - } -} \ No newline at end of file +.expression-helper { + display: inline-block; + height: 100%; + vertical-align: middle; +} + +#expression-wrapper { + display: flex; + height: calc(100vh - 40px); + width: 100vw; +} + +.expression-holder { + min-width: 100px; + min-height: 100px; + max-height: 90vh; + max-width: 90vh; + width: calc((100vw - var(--sheldWidth)) /2); + position: absolute; + bottom: 1px; + padding: 0; + filter: drop-shadow(2px 2px 2px #51515199); + z-index: 2; + overflow: hidden; + +} + +img.expression { + width: 100%; + height: 100%; + vertical-align: bottom; + object-fit: contain; +} + +img.expression[src=""] { + visibility: hidden; +} + +img.expression.default { + vertical-align: middle; + max-height: 120px; + object-fit: contain !important; + margin-top: 50px; +} + +.debug-image { + display: none; + visibility: collapse; + opacity: 0; + width: 0px; + height: 0px; +} + +.expression_list_item { + position: relative; + max-width: 20%; + max-height: 200px; + background-color: #515151b0; + border-radius: 10px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.expression_list_title { + position: absolute; + bottom: 0; + left: 0; + text-align: center; + font-weight: 600; + background-color: #000000a8; + width: 100%; + height: 20%; + display: flex; + justify-content: center; + align-items: center; +} + +.expression_list_buttons { + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 20%; + padding: 0.25rem; +} + +.expression_list_image { + max-width: 100%; + height: 100%; + object-fit: cover; +} + +#image_list { + display: flex; + flex-direction: row; + column-gap: 1rem; + margin: 1rem; + flex-wrap: wrap; + justify-content: space-evenly; + row-gap: 1rem; +} + +#image_list .success { + color: green; +} + +#image_list .failure { + color: red; +} + +.expression_settings p { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.expression_settings label { + display: flex; + align-items: center; + flex-direction: row; + margin-left: 0px; +} + +.expression_settings label input { + margin-left: 0px !important; +} + +@media screen and (max-width:1200px) { + div.expression { + display: none; + } +} diff --git a/server.js b/server.js index 03451fb50..2e7314303 100644 --- a/server.js +++ b/server.js @@ -3061,6 +3061,83 @@ app.post('/google_translate', jsonParser, async (request, response) => { } }); +app.post('/delete_sprite', jsonParser, async (request, response) => { + const label = request.body.label; + const name = request.body.name; + + if (!label || !name) { + return response.sendStatus(400); + } + + try { + const spritesPath = path.join(directories.characters, name); + + // No sprites folder exists, or not a directory + if (!fs.existsSync(spritesPath) || !fs.statSync(spritesPath).isDirectory()) { + return response.sendStatus(404); + } + + const files = fs.readdirSync(spritesPath); + + // Remove existing sprite with the same label + for (const file of files) { + if (path.parse(file).name === label) { + fs.rmSync(path.join(spritesPath, file)); + } + } + + return response.sendStatus(200); + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +}); + +app.post('/upload_sprite', urlencodedParser, async (request, response) => { + const file = request.file; + const label = request.body.label; + const name = request.body.name; + + if (!file || !label || !name) { + return response.sendStatus(400); + } + + try { + const spritesPath = path.join(directories.characters, name); + + // Path to sprites is not a directory. This should never happen. + if (!fs.statSync(spritesPath).isDirectory()) { + return response.sendStatus(404); + } + + // Create sprites folder if it doesn't exist + if (!fs.existsSync(spritesPath)) { + fs.mkdirSync(spritesPath); + } + + const files = fs.readdirSync(spritesPath); + + // Remove existing sprite with the same label + for (const file of files) { + if (path.parse(file).name === label) { + fs.rmSync(path.join(spritesPath, file)); + } + } + + const filename = label + path.parse(file.originalname).ext; + const spritePath = path.join("./uploads/", file.filename); + const pathToFile = path.join(spritesPath, filename); + // Copy uploaded file to sprites folder + fs.cpSync(spritePath, pathToFile); + // Remove uploaded file + fs.rmSync(spritePath); + return response.sendStatus(200); + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +}); + function writeSecret(key, value) { if (!fs.existsSync(SECRETS_FILE)) { const emptyFile = JSON.stringify({});