mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Upload or delete a sprite image.
This commit is contained in:
@ -1,385 +1,470 @@
|
|||||||
import { saveSettingsDebounced } from "../../../script.js";
|
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
||||||
import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js";
|
import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js";
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
const MODULE_NAME = 'expressions';
|
const MODULE_NAME = 'expressions';
|
||||||
const UPDATE_INTERVAL = 2000;
|
const UPDATE_INTERVAL = 2000;
|
||||||
const DEFAULT_EXPRESSIONS = [
|
const DEFAULT_EXPRESSIONS = [
|
||||||
"admiration",
|
"admiration",
|
||||||
"amusement",
|
"amusement",
|
||||||
"anger",
|
"anger",
|
||||||
"annoyance",
|
"annoyance",
|
||||||
"approval",
|
"approval",
|
||||||
"caring",
|
"caring",
|
||||||
"confusion",
|
"confusion",
|
||||||
"curiosity",
|
"curiosity",
|
||||||
"desire",
|
"desire",
|
||||||
"disappointment",
|
"disappointment",
|
||||||
"disapproval",
|
"disapproval",
|
||||||
"disgust",
|
"disgust",
|
||||||
"embarrassment",
|
"embarrassment",
|
||||||
"excitement",
|
"excitement",
|
||||||
"fear",
|
"fear",
|
||||||
"gratitude",
|
"gratitude",
|
||||||
"grief",
|
"grief",
|
||||||
"joy",
|
"joy",
|
||||||
"love",
|
"love",
|
||||||
"nervousness",
|
"nervousness",
|
||||||
"optimism",
|
"optimism",
|
||||||
"pride",
|
"pride",
|
||||||
"realization",
|
"realization",
|
||||||
"relief",
|
"relief",
|
||||||
"remorse",
|
"remorse",
|
||||||
"sadness",
|
"sadness",
|
||||||
"surprise",
|
"surprise",
|
||||||
"neutral"
|
"neutral"
|
||||||
];
|
];
|
||||||
|
|
||||||
let expressionsList = null;
|
let expressionsList = null;
|
||||||
let lastCharacter = undefined;
|
let lastCharacter = undefined;
|
||||||
let lastMessage = null;
|
let lastMessage = null;
|
||||||
let spriteCache = {};
|
let spriteCache = {};
|
||||||
let inApiCall = false;
|
let inApiCall = false;
|
||||||
|
|
||||||
function onExpressionsShowDefaultInput() {
|
function onExpressionsShowDefaultInput() {
|
||||||
const value = $(this).prop('checked');
|
const value = $(this).prop('checked');
|
||||||
extension_settings.expressions.showDefault = value;
|
extension_settings.expressions.showDefault = value;
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
|
|
||||||
const existingImageSrc = $('img.expression').prop('src');
|
const existingImageSrc = $('img.expression').prop('src');
|
||||||
if (existingImageSrc !== undefined) { //if we have an image in 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)
|
if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default)
|
||||||
$('img.expression').prop('src', ''); //remove it
|
$('img.expression').prop('src', ''); //remove it
|
||||||
lastMessage = null;
|
lastMessage = null;
|
||||||
}
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
lastMessage = null;
|
lastMessage = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isWorkerBusy = false;
|
let isWorkerBusy = false;
|
||||||
|
|
||||||
async function moduleWorkerWrapper() {
|
async function moduleWorkerWrapper() {
|
||||||
// Don't touch me I'm busy...
|
// Don't touch me I'm busy...
|
||||||
if (isWorkerBusy) {
|
if (isWorkerBusy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// I'm free. Let's update!
|
// I'm free. Let's update!
|
||||||
try {
|
try {
|
||||||
isWorkerBusy = true;
|
isWorkerBusy = true;
|
||||||
await moduleWorker();
|
await moduleWorker();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
isWorkerBusy = false;
|
isWorkerBusy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moduleWorker() {
|
async function moduleWorker() {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
|
|
||||||
// non-characters not supported
|
// non-characters not supported
|
||||||
if (!context.groupId && context.characterId === undefined) {
|
if (!context.groupId && context.characterId === undefined) {
|
||||||
removeExpression();
|
removeExpression();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// character changed
|
// character changed
|
||||||
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
|
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
|
||||||
removeExpression();
|
removeExpression();
|
||||||
spriteCache = {};
|
spriteCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLastMessage = getLastCharacterMessage();
|
const currentLastMessage = getLastCharacterMessage();
|
||||||
|
|
||||||
// character has no expressions or it is not loaded
|
// character has no expressions or it is not loaded
|
||||||
if (Object.keys(spriteCache).length === 0) {
|
if (Object.keys(spriteCache).length === 0) {
|
||||||
await validateImages(currentLastMessage.name);
|
await validateImages(currentLastMessage.name);
|
||||||
lastCharacter = context.groupId || context.characterId;
|
lastCharacter = context.groupId || context.characterId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const offlineMode = $('.expression_settings .offline_mode');
|
const offlineMode = $('.expression_settings .offline_mode');
|
||||||
if (!modules.includes('classify')) {
|
if (!modules.includes('classify')) {
|
||||||
$('.expression_settings').show();
|
$('.expression_settings').show();
|
||||||
offlineMode.css('display', 'block');
|
offlineMode.css('display', 'block');
|
||||||
lastCharacter = context.groupId || context.characterId;
|
lastCharacter = context.groupId || context.characterId;
|
||||||
|
|
||||||
if (context.groupId) {
|
if (context.groupId) {
|
||||||
await validateImages(currentLastMessage.name, true);
|
await validateImages(currentLastMessage.name, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// force reload expressions list on connect to API
|
// force reload expressions list on connect to API
|
||||||
if (offlineMode.is(':visible')) {
|
if (offlineMode.is(':visible')) {
|
||||||
expressionsList = null;
|
expressionsList = null;
|
||||||
spriteCache = {};
|
spriteCache = {};
|
||||||
expressionsList = await getExpressionsList();
|
expressionsList = await getExpressionsList();
|
||||||
await validateImages(currentLastMessage.name, true);
|
await validateImages(currentLastMessage.name, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
offlineMode.css('display', 'none');
|
offlineMode.css('display', 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// check if last message changed
|
// check if last message changed
|
||||||
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
|
if ((lastCharacter === context.characterId || lastCharacter === context.groupId)
|
||||||
&& lastMessage === currentLastMessage.mes) {
|
&& lastMessage === currentLastMessage.mes) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API is busy
|
// API is busy
|
||||||
if (inApiCall) {
|
if (inApiCall) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
inApiCall = true;
|
inApiCall = true;
|
||||||
const url = new URL(getApiUrl());
|
const url = new URL(getApiUrl());
|
||||||
url.pathname = '/api/classify';
|
url.pathname = '/api/classify';
|
||||||
|
|
||||||
const apiResult = await fetch(url, {
|
const apiResult = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Bypass-Tunnel-Reminder': 'bypass',
|
'Bypass-Tunnel-Reminder': 'bypass',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ text: currentLastMessage.mes })
|
body: JSON.stringify({ text: currentLastMessage.mes })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (apiResult.ok) {
|
if (apiResult.ok) {
|
||||||
const name = context.groupId ? currentLastMessage.name : context.name2;
|
const name = context.groupId ? currentLastMessage.name : context.name2;
|
||||||
const force = !!context.groupId;
|
const force = !!context.groupId;
|
||||||
const data = await apiResult.json();
|
const data = await apiResult.json();
|
||||||
let expression = data.classification[0].label;
|
let expression = data.classification[0].label;
|
||||||
|
|
||||||
// Character won't be angry on you for swiping
|
// Character won't be angry on you for swiping
|
||||||
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
|
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
|
||||||
expression = 'joy';
|
expression = 'joy';
|
||||||
}
|
}
|
||||||
|
|
||||||
setExpression(name, expression, force);
|
setExpression(name, expression, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
inApiCall = false;
|
inApiCall = false;
|
||||||
lastCharacter = context.groupId || context.characterId;
|
lastCharacter = context.groupId || context.characterId;
|
||||||
lastMessage = currentLastMessage.mes;
|
lastMessage = currentLastMessage.mes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLastCharacterMessage() {
|
function getLastCharacterMessage() {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const reversedChat = context.chat.slice().reverse();
|
const reversedChat = context.chat.slice().reverse();
|
||||||
|
|
||||||
for (let mes of reversedChat) {
|
for (let mes of reversedChat) {
|
||||||
if (mes.is_user || mes.is_system) {
|
if (mes.is_user || mes.is_system) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mes: mes.mes, name: mes.name };
|
return { mes: mes.mes, name: mes.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mes: '', name: null };
|
return { mes: '', name: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeExpression() {
|
function removeExpression() {
|
||||||
lastMessage = null;
|
lastMessage = null;
|
||||||
$('img.expression').off('error');
|
$('img.expression').off('error');
|
||||||
$('img.expression').prop('src', '');
|
$('img.expression').prop('src', '');
|
||||||
$('img.expression').removeClass('default');
|
$('img.expression').removeClass('default');
|
||||||
$('.expression_settings').hide();
|
$('.expression_settings').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateImages(character, forceRedrawCached) {
|
async function validateImages(character, forceRedrawCached) {
|
||||||
if (!character) {
|
if (!character) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const labels = await getExpressionsList();
|
const labels = await getExpressionsList();
|
||||||
|
|
||||||
if (spriteCache[character]) {
|
if (spriteCache[character]) {
|
||||||
if (forceRedrawCached && $('#image_list').data('name') !== character) {
|
if (forceRedrawCached && $('#image_list').data('name') !== character) {
|
||||||
console.log('force redrawing character sprites list')
|
console.log('force redrawing character sprites list')
|
||||||
drawSpritesList(character, labels, spriteCache[character]);
|
drawSpritesList(character, labels, spriteCache[character]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sprites = await getSpritesList(character);
|
const sprites = await getSpritesList(character);
|
||||||
let validExpressions = drawSpritesList(character, labels, sprites);
|
let validExpressions = drawSpritesList(character, labels, sprites);
|
||||||
spriteCache[character] = validExpressions;
|
spriteCache[character] = validExpressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSpritesList(character, labels, sprites) {
|
function drawSpritesList(character, labels, sprites) {
|
||||||
let validExpressions = [];
|
let validExpressions = [];
|
||||||
$('.expression_settings').show();
|
$('.expression_settings').show();
|
||||||
$('#image_list').empty();
|
$('#image_list').empty();
|
||||||
$('#image_list').data('name', character);
|
$('#image_list').data('name', character);
|
||||||
labels.sort().forEach((item) => {
|
labels.sort().forEach((item) => {
|
||||||
const sprite = sprites.find(x => x.label == item);
|
const sprite = sprites.find(x => x.label == item);
|
||||||
|
|
||||||
if (sprite) {
|
if (sprite) {
|
||||||
validExpressions.push(sprite);
|
validExpressions.push(sprite);
|
||||||
$('#image_list').append(getListItem(item, sprite.path, 'success'));
|
$('#image_list').append(getListItem(item, sprite.path, 'success'));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure'));
|
$('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return validExpressions;
|
return validExpressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getListItem(item, imageSrc, textClass) {
|
function getListItem(item, imageSrc, textClass) {
|
||||||
return `
|
return `
|
||||||
<div id="${item}" class="expression_list_item">
|
<div id="${item}" class="expression_list_item">
|
||||||
<span class="expression_list_title ${textClass}">${item}</span>
|
<div class="expression_list_buttons">
|
||||||
<img class="expression_list_image" src="${imageSrc}" />
|
<div class="menu_button expression_list_upload" title="Upload image">
|
||||||
</div>
|
<i class="fa-solid fa-upload"></i>
|
||||||
`;
|
</div>
|
||||||
}
|
<div class="menu_button expression_list_delete" title="Delete image">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
async function getSpritesList(name) {
|
</div>
|
||||||
console.log('getting sprites list');
|
</div>
|
||||||
|
<span class="expression_list_title ${textClass}">${item}</span>
|
||||||
try {
|
<img class="expression_list_image" src="${imageSrc}" />
|
||||||
const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`);
|
</div>
|
||||||
|
`;
|
||||||
let sprites = result.ok ? (await result.json()) : [];
|
}
|
||||||
return sprites;
|
|
||||||
}
|
async function getSpritesList(name) {
|
||||||
catch (err) {
|
console.log('getting sprites list');
|
||||||
console.log(err);
|
|
||||||
return [];
|
try {
|
||||||
}
|
const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`);
|
||||||
}
|
|
||||||
|
let sprites = result.ok ? (await result.json()) : [];
|
||||||
async function getExpressionsList() {
|
return sprites;
|
||||||
// get something for offline mode (default images)
|
}
|
||||||
if (!modules.includes('classify')) {
|
catch (err) {
|
||||||
return DEFAULT_EXPRESSIONS;
|
console.log(err);
|
||||||
}
|
return [];
|
||||||
|
}
|
||||||
if (Array.isArray(expressionsList)) {
|
}
|
||||||
return expressionsList;
|
|
||||||
}
|
async function getExpressionsList() {
|
||||||
|
// get something for offline mode (default images)
|
||||||
const url = new URL(getApiUrl());
|
if (!modules.includes('classify')) {
|
||||||
url.pathname = '/api/classify/labels';
|
return DEFAULT_EXPRESSIONS;
|
||||||
|
}
|
||||||
try {
|
|
||||||
const apiResult = await fetch(url, {
|
if (Array.isArray(expressionsList)) {
|
||||||
method: 'GET',
|
return expressionsList;
|
||||||
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
|
}
|
||||||
});
|
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
if (apiResult.ok) {
|
url.pathname = '/api/classify/labels';
|
||||||
|
|
||||||
const data = await apiResult.json();
|
try {
|
||||||
expressionsList = data.labels;
|
const apiResult = await fetch(url, {
|
||||||
return expressionsList;
|
method: 'GET',
|
||||||
}
|
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
|
||||||
}
|
});
|
||||||
catch (error) {
|
|
||||||
console.log(error);
|
if (apiResult.ok) {
|
||||||
return [];
|
|
||||||
}
|
const data = await apiResult.json();
|
||||||
}
|
expressionsList = data.labels;
|
||||||
|
return expressionsList;
|
||||||
async function setExpression(character, expression, force) {
|
}
|
||||||
console.log('entered setExpressions');
|
}
|
||||||
await validateImages(character);
|
catch (error) {
|
||||||
const img = $('img.expression');
|
console.log(error);
|
||||||
|
return [];
|
||||||
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');
|
async function setExpression(character, expression, force) {
|
||||||
img.attr('src', sprite.path);
|
console.log('entered setExpressions');
|
||||||
img.removeClass('default');
|
await validateImages(character);
|
||||||
img.off('error');
|
const img = $('img.expression');
|
||||||
img.on('error', function () {
|
|
||||||
$(this).attr('src', '');
|
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
||||||
if (force && extension_settings.expressions.showDefault) {
|
console.log('checking for expression images to show..');
|
||||||
setDefault();
|
if (sprite) {
|
||||||
}
|
console.log('setting expression from character images folder');
|
||||||
});
|
img.attr('src', sprite.path);
|
||||||
} else {
|
img.removeClass('default');
|
||||||
if (extension_settings.expressions.showDefault) {
|
img.off('error');
|
||||||
setDefault();
|
img.on('error', function () {
|
||||||
}
|
$(this).attr('src', '');
|
||||||
}
|
if (force && extension_settings.expressions.showDefault) {
|
||||||
|
setDefault();
|
||||||
function setDefault() {
|
}
|
||||||
console.log('setting default');
|
});
|
||||||
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
} else {
|
||||||
//console.log(defImgUrl);
|
if (extension_settings.expressions.showDefault) {
|
||||||
img.attr('src', defImgUrl);
|
setDefault();
|
||||||
img.addClass('default');
|
}
|
||||||
}
|
}
|
||||||
document.getElementById("expression-holder").style.display = '';
|
|
||||||
}
|
function setDefault() {
|
||||||
|
console.log('setting default');
|
||||||
function onClickExpressionImage() {
|
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
||||||
// online mode doesn't need force set
|
//console.log(defImgUrl);
|
||||||
if (modules.includes('classify')) {
|
img.attr('src', defImgUrl);
|
||||||
return;
|
img.addClass('default');
|
||||||
}
|
}
|
||||||
|
document.getElementById("expression-holder").style.display = '';
|
||||||
const expression = $(this).attr('id');
|
}
|
||||||
const name = getLastCharacterMessage().name;
|
|
||||||
|
function onClickExpressionImage() {
|
||||||
if ($(this).find('.failure').length === 0) {
|
// online mode doesn't need force set
|
||||||
setExpression(name, expression, true);
|
if (modules.includes('classify')) {
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(function () {
|
const expression = $(this).attr('id');
|
||||||
function addExpressionImage() {
|
const name = getLastCharacterMessage().name;
|
||||||
const html = `
|
|
||||||
<div id="expression-wrapper">
|
if ($(this).find('.failure').length === 0) {
|
||||||
<div id="expression-holder" class="expression-holder" style="display:none;">
|
setExpression(name, expression, true);
|
||||||
<div id="expression-holderheader" class="fa-solid fa-grip drag-grabber"></div>
|
}
|
||||||
<img id="expression-image" class="expression">
|
}
|
||||||
</div>
|
async function onClickExpressionUpload(event) {
|
||||||
</div>`;
|
// Prevents the expression from being set
|
||||||
$('body').append(html);
|
event.stopPropagation();
|
||||||
}
|
|
||||||
function addSettings() {
|
const id = $(this).closest('.expression_list_item').attr('id');
|
||||||
|
const name = $('#image_list').data('name');
|
||||||
const html = `
|
|
||||||
<div class="expression_settings">
|
const handleExpressionUploadChange = async (e) => {
|
||||||
<div class="inline-drawer">
|
const file = e.target.files[0];
|
||||||
<div class="inline-drawer-toggle inline-drawer-header">
|
|
||||||
<b>Expression images</b>
|
if (!file) {
|
||||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
return;
|
||||||
</div>
|
}
|
||||||
<div class="inline-drawer-content">
|
|
||||||
<p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p>
|
const formData = new FormData();
|
||||||
<div id="image_list"></div>
|
formData.append('name', name);
|
||||||
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
|
formData.append('label', id);
|
||||||
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
|
formData.append('avatar', file);
|
||||||
<label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label>
|
|
||||||
</div>
|
try {
|
||||||
</div>
|
await jQuery.ajax({
|
||||||
</div>
|
type: "POST",
|
||||||
`;
|
url: "/upload_sprite",
|
||||||
$('#extensions_settings').append(html);
|
data: formData,
|
||||||
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
|
beforeSend: function () { },
|
||||||
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
|
cache: false,
|
||||||
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
contentType: false,
|
||||||
$('.expression_settings').hide();
|
processData: false,
|
||||||
}
|
});
|
||||||
|
|
||||||
addExpressionImage();
|
// Refresh sprites list
|
||||||
addSettings();
|
delete spriteCache[name];
|
||||||
setInterval(moduleWorkerWrapper, UPDATE_INTERVAL);
|
await validateImages(name);
|
||||||
moduleWorkerWrapper();
|
} 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("<h3>Are you sure?</h3>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 = `
|
||||||
|
<div id="expression-wrapper">
|
||||||
|
<div id="expression-holder" class="expression-holder" style="display:none;">
|
||||||
|
<div id="expression-holderheader" class="fa-solid fa-grip drag-grabber"></div>
|
||||||
|
<img id="expression-image" class="expression">
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
$('body').append(html);
|
||||||
|
}
|
||||||
|
function addSettings() {
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="expression_settings">
|
||||||
|
<div class="inline-drawer">
|
||||||
|
<div class="inline-drawer-toggle inline-drawer-header">
|
||||||
|
<b>Expression images</b>
|
||||||
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||||
|
</div>
|
||||||
|
<div class="inline-drawer-content">
|
||||||
|
<p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p>
|
||||||
|
<div id="image_list"></div>
|
||||||
|
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
|
||||||
|
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
|
||||||
|
<label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form><input type="file" id="expression_upload" name="expression_upload" accept="image/*" hidden></form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$('#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();
|
||||||
|
})();
|
||||||
|
@ -1,124 +1,138 @@
|
|||||||
.expression-helper {
|
.expression-helper {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#expression-wrapper {
|
#expression-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 40px);
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expression-holder {
|
.expression-holder {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
max-width: 90vh;
|
max-width: 90vh;
|
||||||
width: calc((100vw - var(--sheldWidth)) /2);
|
width: calc((100vw - var(--sheldWidth)) /2);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
filter: drop-shadow(2px 2px 2px #51515199);
|
filter: drop-shadow(2px 2px 2px #51515199);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img.expression {
|
img.expression {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.expression[src=""] {
|
img.expression[src=""] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.expression.default {
|
img.expression.default {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
max-height: 120px;
|
max-height: 120px;
|
||||||
object-fit: contain !important;
|
object-fit: contain !important;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.debug-image {
|
.debug-image {
|
||||||
display: none;
|
display: none;
|
||||||
visibility: collapse;
|
visibility: collapse;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
height: 0px;
|
height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expression_list_item {
|
.expression_list_item {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
background-color: #515151b0;
|
background-color: #515151b0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expression_list_title {
|
.expression_list_title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: #000000a8;
|
background-color: #000000a8;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 20%;
|
height: 20%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expression_list_image {
|
.expression_list_buttons {
|
||||||
max-width: 100%;
|
position: absolute;
|
||||||
height: 100%;
|
top: 0;
|
||||||
}
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
#image_list {
|
display: flex;
|
||||||
display: flex;
|
flex-direction: row;
|
||||||
flex-direction: row;
|
justify-content: space-between;
|
||||||
column-gap: 1rem;
|
align-items: center;
|
||||||
margin: 1rem;
|
height: 20%;
|
||||||
flex-wrap: wrap;
|
padding: 0.25rem;
|
||||||
justify-content: space-evenly;
|
}
|
||||||
row-gap: 1rem;
|
|
||||||
}
|
.expression_list_image {
|
||||||
|
max-width: 100%;
|
||||||
#image_list .success {
|
height: 100%;
|
||||||
color: green;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image_list .failure {
|
#image_list {
|
||||||
color: red;
|
display: flex;
|
||||||
}
|
flex-direction: row;
|
||||||
|
column-gap: 1rem;
|
||||||
.expression_settings p {
|
margin: 1rem;
|
||||||
margin-top: 0.5rem;
|
flex-wrap: wrap;
|
||||||
margin-bottom: 0.5rem;
|
justify-content: space-evenly;
|
||||||
}
|
row-gap: 1rem;
|
||||||
|
}
|
||||||
.expression_settings label {
|
|
||||||
display: flex;
|
#image_list .success {
|
||||||
align-items: center;
|
color: green;
|
||||||
flex-direction: row;
|
}
|
||||||
margin-left: 0px;
|
|
||||||
}
|
#image_list .failure {
|
||||||
|
color: red;
|
||||||
.expression_settings label input {
|
}
|
||||||
margin-left: 0px !important;
|
|
||||||
}
|
.expression_settings p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
@media screen and (max-width:1200px) {
|
margin-bottom: 0.5rem;
|
||||||
div.expression {
|
}
|
||||||
display: none;
|
|
||||||
}
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
77
server.js
77
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) {
|
function writeSecret(key, value) {
|
||||||
if (!fs.existsSync(SECRETS_FILE)) {
|
if (!fs.existsSync(SECRETS_FILE)) {
|
||||||
const emptyFile = JSON.stringify({});
|
const emptyFile = JSON.stringify({});
|
||||||
|
Reference in New Issue
Block a user