mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Adds the option to override the chat's author's note with one set for each character. This saves the pain of having to copy and paste author's notes for every chat. Signed-off-by: kingbri <bdashore3@proton.me>
981 lines
32 KiB
JavaScript
981 lines
32 KiB
JavaScript
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
|
import { dragElement, isMobile } from "../../RossAscends-mods.js";
|
|
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
|
import { power_user } from "../../power-user.js";
|
|
import { onlyUnique, debounce, getCharaFilename } from "../../utils.js";
|
|
export { MODULE_NAME };
|
|
|
|
const MODULE_NAME = 'expressions';
|
|
const UPDATE_INTERVAL = 2000;
|
|
const FALLBACK_EXPRESSION = 'joy';
|
|
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 isVisualNovelMode() {
|
|
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
|
|
}
|
|
|
|
async function forceUpdateVisualNovelMode() {
|
|
if (isVisualNovelMode()) {
|
|
await updateVisualNovelMode();
|
|
}
|
|
}
|
|
|
|
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
|
|
|
|
async function updateVisualNovelMode(name, expression) {
|
|
const container = $('#visual-novel-wrapper');
|
|
|
|
await visualNovelRemoveInactive(container);
|
|
|
|
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);
|
|
|
|
// calculate layer indices based on recent messages
|
|
await visualNovelUpdateLayers(container);
|
|
|
|
await Promise.allSettled(setSpritePromises);
|
|
|
|
// update again based on new sprites
|
|
if (setSpritePromises.length > 0) {
|
|
await visualNovelUpdateLayers(container);
|
|
}
|
|
}
|
|
|
|
async function visualNovelRemoveInactive(container) {
|
|
const context = getContext();
|
|
const group = context.groups.find(x => x.id == context.groupId);
|
|
const removeInactiveCharactersPromises = [];
|
|
|
|
// remove inactive characters after 1 second
|
|
container.find('.expression-holder').each((_, current) => {
|
|
const promise = new Promise(resolve => {
|
|
const element = $(current);
|
|
const avatar = element.data('avatar');
|
|
|
|
if (!group.members.includes(avatar) || group.disabled_members.includes(avatar)) {
|
|
element.fadeOut(250, () => {
|
|
element.remove();
|
|
resolve();
|
|
});
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
|
|
removeInactiveCharactersPromises.push(promise);
|
|
});
|
|
|
|
await Promise.allSettled(removeInactiveCharactersPromises);
|
|
}
|
|
|
|
async function visualNovelSetCharacterSprites(container, name, expression) {
|
|
const context = getContext();
|
|
const group = context.groups.find(x => x.id == context.groupId);
|
|
const labels = await getExpressionsList();
|
|
|
|
const createCharacterPromises = [];
|
|
const setSpritePromises = [];
|
|
|
|
for (const avatar of group.members) {
|
|
const isDisabled = group.disabled_members.includes(avatar);
|
|
|
|
// skip disabled characters
|
|
if (isDisabled) {
|
|
continue;
|
|
}
|
|
|
|
const character = context.characters.find(x => x.avatar == avatar);
|
|
|
|
if (!character) {
|
|
continue;
|
|
}
|
|
|
|
let spriteFolderName = character.name;
|
|
const avatarFileName = getSpriteFolderName({ original_avatar: character.avatar });
|
|
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
|
e.name == avatarFileName
|
|
);
|
|
|
|
if (expressionOverride && expressionOverride.path) {
|
|
spriteFolderName = expressionOverride.path;
|
|
}
|
|
|
|
// download images if not downloaded yet
|
|
if (spriteCache[spriteFolderName] === undefined) {
|
|
spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName);
|
|
}
|
|
|
|
const sprites = spriteCache[spriteFolderName];
|
|
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
|
|
const defaultSpritePath = sprites.find(x => x.label === FALLBACK_EXPRESSION)?.path;
|
|
const noSprites = sprites.length === 0;
|
|
|
|
if (expressionImage.length > 0) {
|
|
if (name == spriteFolderName) {
|
|
await validateImages(spriteFolderName, true);
|
|
setExpressionOverrideHtml(true); // <= force clear expression override input
|
|
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
|
|
|
|
const path = currentSpritePath || defaultSpritePath || '';
|
|
const img = expressionImage.find('img');
|
|
setImage(img, path);
|
|
}
|
|
expressionImage.toggleClass('hidden', noSprites);
|
|
} else {
|
|
const template = $('#expression-holder').clone();
|
|
template.attr('id', `expression-${avatar}`);
|
|
template.attr('data-avatar', avatar);
|
|
template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
|
|
$('#visual-novel-wrapper').append(template);
|
|
dragElement(template[0]);
|
|
template.toggleClass('hidden', noSprites);
|
|
setImage(template.find('img'), defaultSpritePath || '');
|
|
const fadeInPromise = new Promise(resolve => {
|
|
template.fadeIn(250, () => resolve());
|
|
});
|
|
createCharacterPromises.push(fadeInPromise);
|
|
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
|
|
setSpritePromises.push(setSpritePromise);
|
|
}
|
|
}
|
|
|
|
await Promise.allSettled(createCharacterPromises);
|
|
return setSpritePromises;
|
|
}
|
|
|
|
async function visualNovelUpdateLayers(container) {
|
|
const context = getContext();
|
|
const group = context.groups.find(x => x.id == context.groupId);
|
|
const recentMessages = context.chat.map(x => x.original_avatar).filter(x => x).reverse().filter(onlyUnique);
|
|
const filteredMembers = group.members.filter(x => !group.disabled_members.includes(x));
|
|
const layerIndices = filteredMembers.slice().sort((a, b) => {
|
|
const aRecentIndex = recentMessages.indexOf(a);
|
|
const bRecentIndex = recentMessages.indexOf(b);
|
|
const aFilteredIndex = filteredMembers.indexOf(a);
|
|
const bFilteredIndex = filteredMembers.indexOf(b);
|
|
|
|
if (aRecentIndex !== -1 && bRecentIndex !== -1) {
|
|
return bRecentIndex - aRecentIndex;
|
|
} else if (aRecentIndex !== -1) {
|
|
return 1;
|
|
} else if (bRecentIndex !== -1) {
|
|
return -1;
|
|
} else {
|
|
return aFilteredIndex - bFilteredIndex;
|
|
}
|
|
});
|
|
|
|
const setLayerIndicesPromises = [];
|
|
|
|
const sortFunction = (a, b) => {
|
|
const avatarA = $(a).data('avatar');
|
|
const avatarB = $(b).data('avatar');
|
|
const indexA = filteredMembers.indexOf(avatarA);
|
|
const indexB = filteredMembers.indexOf(avatarB);
|
|
return indexA - indexB;
|
|
};
|
|
|
|
const containerWidth = container.width();
|
|
const pivotalPoint = containerWidth * 0.5;
|
|
|
|
let images = $('.expression-holder');
|
|
let imagesWidth = [];
|
|
|
|
images.sort(sortFunction).each(function () {
|
|
imagesWidth.push($(this).width());
|
|
});
|
|
|
|
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
|
|
let currentPosition = pivotalPoint - (totalWidth / 2);
|
|
|
|
if (totalWidth > containerWidth) {
|
|
let totalOverlap = totalWidth - containerWidth;
|
|
let totalWidthWithoutWidest = imagesWidth.reduce((a, b) => a + b, 0) - Math.max(...imagesWidth);
|
|
let overlaps = imagesWidth.map(width => (width / totalWidthWithoutWidest) * totalOverlap);
|
|
imagesWidth = imagesWidth.map((width, index) => width - overlaps[index]);
|
|
currentPosition = 0; // Reset the initial position to 0
|
|
}
|
|
|
|
images.sort(sortFunction).each((index, current) => {
|
|
const element = $(current);
|
|
|
|
// skip repositioning of dragged elements
|
|
if (element.data('dragged')) {
|
|
currentPosition += imagesWidth[index];
|
|
return;
|
|
}
|
|
|
|
const avatar = element.data('avatar');
|
|
const layerIndex = layerIndices.indexOf(avatar);
|
|
element.css('z-index', layerIndex);
|
|
element.show();
|
|
|
|
const promise = new Promise(resolve => {
|
|
element.animate({ left: currentPosition + 'px' }, 500, () => {
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
currentPosition += imagesWidth[index];
|
|
|
|
setLayerIndicesPromises.push(promise);
|
|
});
|
|
|
|
await Promise.allSettled(setLayerIndicesPromises);
|
|
}
|
|
|
|
async function setLastMessageSprite(img, avatar, labels) {
|
|
const context = getContext();
|
|
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
|
|
|
|
if (lastMessage) {
|
|
const text = lastMessage.mes || '';
|
|
let spriteFolderName = lastMessage.name;
|
|
const avatarFileName = getSpriteFolderName(lastMessage);
|
|
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
|
e.name == avatarFileName
|
|
);
|
|
|
|
if (expressionOverride && expressionOverride.path) {
|
|
spriteFolderName = expressionOverride.path;
|
|
}
|
|
|
|
const sprites = spriteCache[spriteFolderName] || [];
|
|
const label = await getExpressionLabel(text);
|
|
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
|
|
|
|
if (path) {
|
|
setImage(img, path);
|
|
}
|
|
}
|
|
}
|
|
|
|
function setImage(img, path) {
|
|
img.attr('src', path);
|
|
img.removeClass('default');
|
|
img.off('error');
|
|
img.on('error', function () {
|
|
console.debug('Error loading image', path);
|
|
$(this).off('error');
|
|
$(this).attr('src', '');
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 vnMode = isVisualNovelMode();
|
|
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
|
|
|
|
if (vnMode) {
|
|
$('#expression-wrapper').hide();
|
|
$('#visual-novel-wrapper').show();
|
|
} else {
|
|
$('#expression-wrapper').show();
|
|
$('#visual-novel-wrapper').hide();
|
|
}
|
|
|
|
const vnStateChanged = vnMode !== vnWrapperVisible;
|
|
|
|
if (vnStateChanged) {
|
|
lastMessage = null;
|
|
$('#visual-novel-wrapper').empty();
|
|
$("#expression-holder").css({ top: '', left: '', right: '', bottom: '', height: '', width: '', margin: '' });
|
|
}
|
|
|
|
const currentLastMessage = getLastCharacterMessage();
|
|
let spriteFolderName = currentLastMessage.name;
|
|
const avatarFileName = getSpriteFolderName(currentLastMessage);
|
|
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
|
e.name == avatarFileName
|
|
);
|
|
|
|
if (expressionOverride && expressionOverride.path) {
|
|
spriteFolderName = expressionOverride.path;
|
|
}
|
|
|
|
// character has no expressions or it is not loaded
|
|
if (Object.keys(spriteCache).length === 0) {
|
|
await validateImages(spriteFolderName);
|
|
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(spriteFolderName, true);
|
|
await forceUpdateVisualNovelMode();
|
|
}
|
|
|
|
return;
|
|
}
|
|
else {
|
|
// force reload expressions list on connect to API
|
|
if (offlineMode.is(':visible')) {
|
|
expressionsList = null;
|
|
spriteCache = {};
|
|
expressionsList = await getExpressionsList();
|
|
await validateImages(spriteFolderName, true);
|
|
await forceUpdateVisualNovelMode();
|
|
}
|
|
|
|
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;
|
|
let expression = await getExpressionLabel(currentLastMessage.mes);
|
|
|
|
// If we're not already overriding the folder name, account for group chats.
|
|
if (spriteFolderName === currentLastMessage.name && !context.groupId) {
|
|
spriteFolderName = context.name2;
|
|
}
|
|
|
|
const force = !!context.groupId;
|
|
|
|
// Character won't be angry on you for swiping
|
|
if (currentLastMessage.mes == '...' && expressionsList.includes(FALLBACK_EXPRESSION)) {
|
|
expression = FALLBACK_EXPRESSION;
|
|
}
|
|
|
|
await sendExpressionCall(spriteFolderName, expression, force, vnMode);
|
|
|
|
}
|
|
catch (error) {
|
|
console.log(error);
|
|
}
|
|
finally {
|
|
inApiCall = false;
|
|
lastCharacter = context.groupId || context.characterId;
|
|
lastMessage = currentLastMessage.mes;
|
|
}
|
|
}
|
|
|
|
function getSpriteFolderName(message) {
|
|
const context = getContext();
|
|
let avatarPath = '';
|
|
|
|
if (context.groupId) {
|
|
avatarPath = message.original_avatar || context.characters.find(x => message.force_avatar && message.force_avatar.includes(encodeURIComponent(x.avatar)))?.avatar;
|
|
}
|
|
else if (context.characterId) {
|
|
avatarPath = getCharaFilename();
|
|
}
|
|
|
|
if (!avatarPath) {
|
|
return '';
|
|
}
|
|
|
|
const folderName = avatarPath.replace(/\.[^/.]+$/, "");
|
|
return folderName;
|
|
}
|
|
|
|
async function sendExpressionCall(name, expression, force, vnMode) {
|
|
if (!vnMode) {
|
|
vnMode = isVisualNovelMode();
|
|
}
|
|
|
|
if (vnMode) {
|
|
await updateVisualNovelMode(name, expression);
|
|
} else {
|
|
setExpression(name, expression, force);
|
|
}
|
|
}
|
|
|
|
async function getExpressionLabel(text) {
|
|
// Return if text is undefined, saving a costly fetch request
|
|
if (!modules.includes('classify') || !text) {
|
|
return FALLBACK_EXPRESSION;
|
|
}
|
|
|
|
const url = new URL(getApiUrl());
|
|
url.pathname = '/api/classify';
|
|
|
|
const apiResult = await doExtrasFetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Bypass-Tunnel-Reminder': 'bypass',
|
|
},
|
|
body: JSON.stringify({ text: text }),
|
|
});
|
|
|
|
if (apiResult.ok) {
|
|
const data = await apiResult.json();
|
|
return data.classification[0].label;
|
|
}
|
|
}
|
|
|
|
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, original_avatar: mes.original_avatar, force_avatar: mes.force_avatar };
|
|
}
|
|
|
|
return { mes: '', name: null, original_avatar: null, force_avatar: 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.debug('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 `
|
|
<div id="${item}" class="expression_list_item">
|
|
<div class="expression_list_buttons">
|
|
<div class="menu_button expression_list_upload" title="Upload image">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
<span class="expression_list_title ${textClass}">${item}</span>
|
|
<img class="expression_list_image" src="${imageSrc}" />
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
async function getSpritesList(name) {
|
|
console.debug('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 doExtrasFetch(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.debug('entered setExpressions');
|
|
await validateImages(character);
|
|
const img = $('img.expression');
|
|
|
|
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
|
console.debug('checking for expression images to show..');
|
|
if (sprite) {
|
|
console.debug('setting expression from character images folder');
|
|
|
|
if (force && isVisualNovelMode()) {
|
|
const context = getContext();
|
|
const group = context.groups.find(x => x.id === context.groupId);
|
|
|
|
for (const member of group.members) {
|
|
const groupMember = context.characters.find(x => x.avatar === member);
|
|
|
|
if (!groupMember) {
|
|
continue;
|
|
}
|
|
|
|
if (groupMember.name == character) {
|
|
setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
img.attr('src', sprite.path);
|
|
img.removeClass('default');
|
|
img.off('error');
|
|
img.on('error', function () {
|
|
console.debug('Expression image error', sprite.path);
|
|
$(this).attr('src', '');
|
|
$(this).off('error');
|
|
if (force && extension_settings.expressions.showDefault) {
|
|
setDefault();
|
|
}
|
|
});
|
|
} else {
|
|
if (extension_settings.expressions.showDefault) {
|
|
setDefault();
|
|
}
|
|
}
|
|
|
|
function setDefault() {
|
|
console.debug('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 handleFileUpload(url, formData) {
|
|
try {
|
|
const data = await jQuery.ajax({
|
|
type: "POST",
|
|
url: url,
|
|
data: formData,
|
|
beforeSend: function () { },
|
|
cache: false,
|
|
contentType: false,
|
|
processData: false,
|
|
});
|
|
|
|
// Refresh sprites list
|
|
const name = formData.get('name');
|
|
delete spriteCache[name];
|
|
await validateImages(name);
|
|
|
|
return data;
|
|
} catch (error) {
|
|
toastr.error('Failed to upload image');
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
await handleFileUpload('/upload_sprite', formData);
|
|
|
|
// Reset the input
|
|
e.target.form.reset();
|
|
};
|
|
|
|
$('#expression_upload')
|
|
.off('change')
|
|
.on('change', handleExpressionUploadChange)
|
|
.trigger('click');
|
|
}
|
|
|
|
async function onClickExpressionOverrideButton() {
|
|
const context = getContext();
|
|
const currentLastMessage = getLastCharacterMessage();
|
|
const avatarFileName = getSpriteFolderName(currentLastMessage);
|
|
|
|
// If the avatar name couldn't be found, abort.
|
|
if (!avatarFileName) {
|
|
console.debug(`Could not find filename for character with name ${currentLastMessage.name} and ID ${context.characterId}`);
|
|
|
|
return;
|
|
}
|
|
|
|
const overridePath = $("#expression_override").val();
|
|
const existingOverrideIndex = extension_settings.expressionOverrides.findIndex((e) =>
|
|
e.name == avatarFileName
|
|
);
|
|
|
|
// If the path is empty, delete the entry from overrides
|
|
if (overridePath === undefined || overridePath.length === 0) {
|
|
if (existingOverrideIndex === -1) {
|
|
return;
|
|
}
|
|
|
|
extension_settings.expressionOverrides.splice(existingOverrideIndex, 1);
|
|
console.debug(`Removed existing override for ${avatarFileName}`);
|
|
} else {
|
|
// Properly override objects and clear the sprite cache of the previously set names
|
|
const existingOverride = extension_settings.expressionOverrides[existingOverrideIndex];
|
|
if (existingOverride) {
|
|
Object.assign(existingOverride, { path: overridePath });
|
|
delete spriteCache[existingOverride.name];
|
|
} else {
|
|
const characterOverride = { name: avatarFileName, path: overridePath };
|
|
extension_settings.expressionOverrides.push(characterOverride);
|
|
delete spriteCache[currentLastMessage.name];
|
|
}
|
|
|
|
console.debug(`Added/edited expression override for character with filename ${avatarFileName} to folder ${overridePath}`);
|
|
}
|
|
|
|
saveSettingsDebounced();
|
|
|
|
// Refresh sprites list. Assume the override path has been properly handled.
|
|
try {
|
|
$('#visual-novel-wrapper').empty();
|
|
await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
|
|
const expression = await getExpressionLabel(currentLastMessage.mes);
|
|
await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true);
|
|
forceUpdateVisualNovelMode();
|
|
} catch (error) {
|
|
console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
|
|
}
|
|
}
|
|
|
|
async function onClickExpressionOverrideRemoveAllButton() {
|
|
// Remove all the overrided entries from sprite cache
|
|
for (const element of extension_settings.expressionOverrides) {
|
|
delete spriteCache[element.name];
|
|
}
|
|
|
|
extension_settings.expressionOverrides = [];
|
|
saveSettingsDebounced();
|
|
|
|
console.debug("All expression image overrides have been cleared.");
|
|
|
|
// Refresh sprites list to use the default name if applicable
|
|
try {
|
|
$('#visual-novel-wrapper').empty();
|
|
const currentLastMessage = getLastCharacterMessage();
|
|
await validateImages(currentLastMessage.name, true);
|
|
const expression = await getExpressionLabel(currentLastMessage.mes);
|
|
await sendExpressionCall(currentLastMessage.name, expression, true);
|
|
forceUpdateVisualNovelMode();
|
|
|
|
console.debug(extension_settings.expressionOverrides);
|
|
} catch (error) {
|
|
console.debug(`The current expression could not be set because of error: ${error}`);
|
|
}
|
|
}
|
|
|
|
async function onClickExpressionUploadPackButton() {
|
|
const name = $('#image_list').data('name');
|
|
|
|
const handleFileUploadChange = async (e) => {
|
|
const file = e.target.files[0];
|
|
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
formData.append('avatar', file);
|
|
|
|
const { count } = await handleFileUpload('/upload_sprite_pack', formData);
|
|
toastr.success(`Uploaded ${count} image(s) for ${name}`);
|
|
|
|
// Reset the input
|
|
e.target.form.reset();
|
|
};
|
|
|
|
$('#expression_upload_pack')
|
|
.off('change')
|
|
.on('change', handleFileUploadChange)
|
|
.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 setExpressionOverrideHtml(forceClear = false) {
|
|
const currentLastMessage = getLastCharacterMessage();
|
|
const avatarFileName = getSpriteFolderName(currentLastMessage);
|
|
if (!avatarFileName) {
|
|
return;
|
|
}
|
|
|
|
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
|
e.name == avatarFileName
|
|
);
|
|
|
|
if (expressionOverride && expressionOverride.path) {
|
|
$("#expression_override").val(expressionOverride.path);
|
|
} else if (expressionOverride) {
|
|
delete extension_settings.expressionOverrides[expressionOverride.name];
|
|
}
|
|
|
|
if (forceClear && !expressionOverride) {
|
|
$("#expression_override").val("");
|
|
}
|
|
}
|
|
|
|
(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 addVisualNovelMode() {
|
|
const html = `
|
|
<div id="visual-novel-wrapper">
|
|
</div>`
|
|
const element = $(html);
|
|
element.hide();
|
|
$('body').append(element);
|
|
}
|
|
function addSettings() {
|
|
|
|
const html = `
|
|
<div class="expression_settings">
|
|
<div class="inline-drawer">
|
|
<div class="inline-drawer-toggle inline-drawer-header">
|
|
<b>Character Expressions</b>
|
|
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
|
</div>
|
|
<div class="inline-drawer-content">
|
|
<div class="offline_mode">
|
|
<small>You are in offline mode. Click on the image below to set the expression.</small>
|
|
</div>
|
|
<div class="flex-container flexnowrap">
|
|
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
|
|
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
|
|
</div>
|
|
<div id="image_list"></div>
|
|
<div class="expression_buttons flex-container spaceEvenly">
|
|
<div id="expression_upload_pack_button" class="menu_button">
|
|
<i class="fa-solid fa-file-zipper"></i>
|
|
<span>Upload sprite pack (ZIP)</span>
|
|
</div>
|
|
<div id="expression_override_cleanup_button" class="menu_button">
|
|
<i class="fa-solid fa-trash-can"></i>
|
|
<span>Remove all image overrides</span>
|
|
</div>
|
|
</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_pack" name="expression_upload_pack" accept="application/zip" hidden>
|
|
<input type="file" id="expression_upload" name="expression_upload" accept="image/*" hidden>
|
|
</form>
|
|
</div>
|
|
`;
|
|
$('#extensions_settings').append(html);
|
|
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
|
|
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
|
|
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);
|
|
$('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input');
|
|
$('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
|
|
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
|
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
|
|
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
|
$(window).on("resize", updateVisualNovelModeDebounced);
|
|
$('.expression_settings').hide();
|
|
}
|
|
|
|
addExpressionImage();
|
|
addVisualNovelMode();
|
|
addSettings();
|
|
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
|
const updateFunction = wrapper.update.bind(wrapper);
|
|
setInterval(updateFunction, UPDATE_INTERVAL);
|
|
moduleWorker();
|
|
eventSource.on(event_types.CHAT_CHANGED, () => {
|
|
setExpressionOverrideHtml();
|
|
|
|
if (isVisualNovelMode()) {
|
|
$('#visual-novel-wrapper').empty();
|
|
}
|
|
});
|
|
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
|
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
|
})();
|