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 = `
-
-
-
-
-
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]
-
Show default images (emojis) if missing
-
-
-
- `;
- $('#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 = `
+
+
+
+
+
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]
+
Show default images (emojis) if missing
+
+
+
+
+ `;
+ $('#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({});