mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Allow setting specific sprites as expressions
- Update /expression-set command to allow setting specific sprites - Enhance enum completion for /expression-set to show expressions/sprites and more their info - Fix setting sprite folder reprinting stuff double - Fix not being able to unset expressions
This commit is contained in:
@ -3,7 +3,7 @@ import { Fuse } from '../../../lib.js';
|
|||||||
import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js';
|
import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js';
|
||||||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
||||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||||
import { loadMovingUIState, power_user } from '../../power-user.js';
|
import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js';
|
||||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
|
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
|
||||||
import { hideMutedSprites, selected_group } from '../../group-chats.js';
|
import { hideMutedSprites, selected_group } from '../../group-chats.js';
|
||||||
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
||||||
@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
|||||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
|
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
|
||||||
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
|
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
|
||||||
import { Popup, POPUP_RESULT } from '../../popup.js';
|
import { Popup, POPUP_RESULT } from '../../popup.js';
|
||||||
@ -27,8 +27,8 @@ export { MODULE_NAME };
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} ExpressionImage An expression image
|
* @typedef {object} ExpressionImage An expression image
|
||||||
* @property {string?} [expression=null] - The expression
|
* @property {string} expression - The expression
|
||||||
* @property {boolean?} [isCustom=null] - If the expression is added by user
|
* @property {boolean} [isCustom=false] - If the expression is added by user
|
||||||
* @property {string} fileName - The filename with extension
|
* @property {string} fileName - The filename with extension
|
||||||
* @property {string} title - The title for the image
|
* @property {string} title - The title for the image
|
||||||
* @property {string} imageSrc - The image source / full path
|
* @property {string} imageSrc - The image source / full path
|
||||||
@ -78,9 +78,6 @@ const EXPRESSION_API = {
|
|||||||
webllm: 3,
|
webllm: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {ExpressionImage} */
|
|
||||||
const NO_IMAGE_PLACEHOLDER = { title: 'No Image', type: 'failure', fileName: 'No-Image-Placeholder.svg', imageSrc: '/img/No-Image-Placeholder.svg' };
|
|
||||||
|
|
||||||
let expressionsList = null;
|
let expressionsList = null;
|
||||||
let lastCharacter = undefined;
|
let lastCharacter = undefined;
|
||||||
let lastMessage = null;
|
let lastMessage = null;
|
||||||
@ -92,6 +89,24 @@ let lastServerResponseTime = 0;
|
|||||||
/** @type {{[characterName: string]: string}} */
|
/** @type {{[characterName: string]: string}} */
|
||||||
export let lastExpression = {};
|
export let lastExpression = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a placeholder image object for a given expression
|
||||||
|
* @param {string} expression - The expression label
|
||||||
|
* @param {boolean} [isCustom=false] - Whether the expression is custom
|
||||||
|
* @returns {ExpressionImage} The placeholder image object
|
||||||
|
*/
|
||||||
|
function getPlaceholderImage(expression, isCustom = false) {
|
||||||
|
return {
|
||||||
|
expression: expression,
|
||||||
|
isCustom: isCustom,
|
||||||
|
title: 'No Image',
|
||||||
|
type: 'failure',
|
||||||
|
fileName: 'No-Image-Placeholder.svg',
|
||||||
|
imageSrc: '/img/No-Image-Placeholder.svg',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the fallback expression if explicitly chosen, otherwise the default one
|
* Returns the fallback expression if explicitly chosen, otherwise the default one
|
||||||
* @returns {string} expression name
|
* @returns {string} expression name
|
||||||
@ -189,6 +204,7 @@ async function visualNovelSetCharacterSprites(container, name, expression) {
|
|||||||
const sprites = spriteCache[spriteFolderName];
|
const sprites = spriteCache[spriteFolderName];
|
||||||
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
|
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
|
||||||
const defaultExpression = getFallbackExpression();
|
const defaultExpression = getFallbackExpression();
|
||||||
|
// TODO: Visual novel sprites need fixing, currently do not update based on multiple sprites, etc
|
||||||
const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path;
|
const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path;
|
||||||
const noSprites = sprites.length === 0;
|
const noSprites = sprites.length === 0;
|
||||||
|
|
||||||
@ -460,7 +476,7 @@ async function moduleWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentLastMessage = getLastCharacterMessage();
|
const currentLastMessage = getLastCharacterMessage();
|
||||||
let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName();
|
let spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name);
|
||||||
|
|
||||||
// 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) {
|
||||||
@ -550,7 +566,7 @@ async function moduleWorker() {
|
|||||||
expression = getFallbackExpression();
|
expression = getFallbackExpression();
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendExpressionCall(spriteFolderName, expression, force, vnMode);
|
await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode });
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -596,48 +612,55 @@ function getFolderNameByMessage(message) {
|
|||||||
return folderName;
|
return folderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendExpressionCall(name, expression, force, vnMode) {
|
/**
|
||||||
|
* Update the expression for the given character.
|
||||||
|
*
|
||||||
|
* @param {string} name The character name, optionally with a sprite folder override, e.g. "folder/expression".
|
||||||
|
* @param {string} expression The expression label, e.g. "amusement", "joy", etc.
|
||||||
|
* @param {Object} [options] Additional options
|
||||||
|
* @param {boolean} [options.force=false] If true, the expression will be sent even if it is the same as the current expression.
|
||||||
|
* @param {boolean} [options.vnMode=null] If true, the expression will be sent in Visual Novel mode. If null, it will be determined by the current chat mode.
|
||||||
|
* @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
|
||||||
|
*/
|
||||||
|
async function sendExpressionCall(name, expression, { force = false, vnMode = null, overrideSpriteFile = null } = {}) {
|
||||||
lastExpression[name.split('/')[0]] = expression;
|
lastExpression[name.split('/')[0]] = expression;
|
||||||
if (!vnMode) {
|
if (vnMode === null) {
|
||||||
vnMode = isVisualNovelMode();
|
vnMode = isVisualNovelMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vnMode) {
|
if (vnMode) {
|
||||||
await updateVisualNovelMode(name, expression);
|
await updateVisualNovelMode(name, expression);
|
||||||
} else {
|
} else {
|
||||||
setExpression(name, expression, force);
|
setExpression(name, expression, { force: force, overrideSpriteFile: overrideSpriteFile });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSpriteSetCommand(_, folder) {
|
async function setSpriteFolderCommand(_, folder) {
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
console.log('Clearing sprite set');
|
console.log('Clearing sprite set');
|
||||||
folder = '';
|
folder = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folder.startsWith('/') || folder.startsWith('\\')) {
|
if (folder.startsWith('/') || folder.startsWith('\\')) {
|
||||||
folder = folder.slice(1);
|
|
||||||
|
|
||||||
const currentLastMessage = getLastCharacterMessage();
|
const currentLastMessage = getLastCharacterMessage();
|
||||||
|
folder = folder.slice(1);
|
||||||
folder = `${currentLastMessage.name}/${folder}`;
|
folder = `${currentLastMessage.name}/${folder}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#expression_override').val(folder.trim());
|
$('#expression_override').val(folder.trim());
|
||||||
onClickExpressionOverrideButton();
|
onClickExpressionOverrideButton();
|
||||||
// removeExpression();
|
|
||||||
// moduleWorker();
|
// No need to resend the expression, the folder override will automatically update the currently displayed one.
|
||||||
const vnMode = isVisualNovelMode();
|
|
||||||
await sendExpressionCall(folder, lastExpression, true, vnMode);
|
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
|
async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
toastr.warning('No text provided');
|
toastr.error('No text provided');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
if (api && !Object.keys(EXPRESSION_API).includes(api)) {
|
if (api && !Object.keys(EXPRESSION_API).includes(api)) {
|
||||||
toastr.warning('Invalid API provided');
|
toastr.error('Invalid API provided');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -653,31 +676,68 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSpriteSlashCommand(_, spriteId) {
|
/** @type {(args: {type: 'expression' | 'sprite'}, searchTerm: string) => Promise<string>} */
|
||||||
spriteId = spriteId.trim().toLowerCase();
|
async function setSpriteSlashCommand({ type }, searchTerm) {
|
||||||
if (!spriteId) {
|
type ??= 'expression';
|
||||||
console.log('No sprite id provided');
|
searchTerm = searchTerm.trim().toLowerCase();
|
||||||
|
if (!searchTerm) {
|
||||||
|
toastr.error(t`No expression or sprite name provided`, t`Set Sprite`);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const spriteFolderName = getSpriteFolderName();
|
const spriteFolderName = getSpriteFolderName();
|
||||||
|
|
||||||
|
let label = searchTerm;
|
||||||
|
|
||||||
|
/** @type {string?} */
|
||||||
|
let spriteFile = null;
|
||||||
|
|
||||||
await validateImages(spriteFolderName);
|
await validateImages(spriteFolderName);
|
||||||
|
|
||||||
// Fuzzy search for sprite
|
// Handle reset as a special term and just reset the sprite via expression call
|
||||||
const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] });
|
if (searchTerm === '#reset') {
|
||||||
const results = fuse.search(spriteId);
|
await sendExpressionCall(spriteFolderName, label, { force: true });
|
||||||
const spriteItem = results[0]?.item;
|
return lastExpression[spriteFolderName] ?? '';
|
||||||
|
|
||||||
if (!spriteItem) {
|
|
||||||
console.log('No sprite found for search term ' + spriteId);
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = spriteItem.label;
|
switch (type) {
|
||||||
|
case 'expression': {
|
||||||
|
// Fuzzy search for expression
|
||||||
|
const existingExpressions = getCachedExpressions().map(x => ({ label: x }));
|
||||||
|
const results = performFuzzySearch('expression-expressions', existingExpressions, [
|
||||||
|
{ name: 'label', weight: 1 },
|
||||||
|
], searchTerm);
|
||||||
|
const matchedExpression = results[0]?.item;
|
||||||
|
if (!matchedExpression) {
|
||||||
|
toastr.warning(t`No expression found for search term ${searchTerm}`, t`Set Sprite`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
label = matchedExpression.label;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'sprite': {
|
||||||
|
// Fuzzy search for sprite file
|
||||||
|
const sprites = spriteCache[spriteFolderName].map(x => x.files).flat();
|
||||||
|
const results = performFuzzySearch('expression-expressions', sprites, [
|
||||||
|
{ name: 'title', weight: 1 },
|
||||||
|
{ name: 'fileName', weight: 1 },
|
||||||
|
], searchTerm);
|
||||||
|
const matchedSprite = results[0]?.item;
|
||||||
|
if (!matchedSprite) {
|
||||||
|
toastr.warning(t`No sprite file found for search term ${searchTerm}`, t`Set Sprite`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
label = matchedSprite.expression;
|
||||||
|
spriteFile = matchedSprite.fileName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: throw Error('Invalid sprite set type: ' + type);
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendExpressionCall(spriteFolderName, label, { force: true, overrideSpriteFile: spriteFile });
|
||||||
|
|
||||||
const vnMode = isVisualNovelMode();
|
|
||||||
await sendExpressionCall(spriteFolderName, label, true, vnMode);
|
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -714,13 +774,22 @@ function spriteFolderNameFromCharacter(char) {
|
|||||||
*/
|
*/
|
||||||
async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) {
|
async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) {
|
||||||
if (!imageUrl) throw new Error('Image URL is required');
|
if (!imageUrl) throw new Error('Image URL is required');
|
||||||
if (!label || typeof label !== 'string') throw new Error('Expression label is required');
|
if (!label || typeof label !== 'string') {
|
||||||
|
toastr.error(t`Expression label is required`, t`Error Uploading Sprite`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
label = label.replace(/[^a-z]/gi, '').toLowerCase().trim();
|
label = label.replace(/[^a-z]/gi, '').toLowerCase().trim();
|
||||||
if (!label) throw new Error('Expression label must contain at least one letter');
|
if (!label) {
|
||||||
|
toastr.error(t`Expression label must contain at least one letter`, t`Error Uploading Sprite`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
spriteName = spriteName || label;
|
spriteName = spriteName || label;
|
||||||
if (!validateExpressionSpriteName(label, spriteName)) throw new Error('Invalid sprite name. Must follow the naming pattern for expression sprites.');
|
if (!validateExpressionSpriteName(label, spriteName)) {
|
||||||
|
toastr.error(t`Invalid sprite name. Must follow the naming pattern for expression sprites.`, t`Error Uploading Sprite`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name;
|
name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name;
|
||||||
const char = findChar({ name });
|
const char = findChar({ name });
|
||||||
@ -1066,7 +1135,7 @@ async function drawSpritesList(character, labels, sprites) {
|
|||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
const listItem = await getListItem(expression, {
|
const listItem = await getListItem(expression, {
|
||||||
isCustom,
|
isCustom,
|
||||||
images: [{ expression, isCustom, ...NO_IMAGE_PLACEHOLDER }],
|
images: [getPlaceholderImage(expression, isCustom)],
|
||||||
});
|
});
|
||||||
$('#image_list').append(listItem);
|
$('#image_list').append(listItem);
|
||||||
continue;
|
continue;
|
||||||
@ -1264,12 +1333,13 @@ export async function getExpressionsList() {
|
|||||||
/**
|
/**
|
||||||
* Set the expression of a character.
|
* Set the expression of a character.
|
||||||
* @param {string} character - The name of the character
|
* @param {string} character - The name of the character
|
||||||
* @param {string} expression - The expression to set
|
* @param {string} expression - The expression or sprite name to set
|
||||||
* @param {boolean} [force=false] - Whether to force the expression change even if Visual Novel mode is on.
|
* @param {Object} options - Optional parameters
|
||||||
|
* @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on
|
||||||
|
* @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
|
||||||
* @returns {Promise<void>} A promise that resolves when the expression has been set.
|
* @returns {Promise<void>} A promise that resolves when the expression has been set.
|
||||||
*/
|
*/
|
||||||
async function setExpression(character, expression, force = false) {
|
async function setExpression(character, expression, { force = false, overrideSpriteFile = null } = {}) {
|
||||||
console.debug('entered setExpressions');
|
|
||||||
await validateImages(character);
|
await validateImages(character);
|
||||||
const img = $('img.expression');
|
const img = $('img.expression');
|
||||||
const prevExpressionSrc = img.attr('src');
|
const prevExpressionSrc = img.attr('src');
|
||||||
@ -1277,14 +1347,17 @@ async function setExpression(character, expression, force = false) {
|
|||||||
|
|
||||||
/** @type {Expression} */
|
/** @type {Expression} */
|
||||||
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
||||||
console.debug('checking for expression images to show..');
|
|
||||||
if (sprite && sprite.files.length > 0) {
|
if (sprite && sprite.files.length > 0) {
|
||||||
console.debug('setting expression from character images folder');
|
|
||||||
|
|
||||||
let spriteFile = sprite.files[0];
|
let spriteFile = sprite.files[0];
|
||||||
|
|
||||||
// Calculate next expression, if multiple are allowed
|
// If a specific sprite file should be set, we are looking it up here
|
||||||
if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
|
if (overrideSpriteFile) {
|
||||||
|
const searched = sprite.files.find(x => x.fileName === overrideSpriteFile);
|
||||||
|
if (searched) spriteFile = searched;
|
||||||
|
else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`);
|
||||||
|
}
|
||||||
|
// Else calculate next expression, if multiple are allowed
|
||||||
|
else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
|
||||||
let possibleFiles = sprite.files;
|
let possibleFiles = sprite.files;
|
||||||
if (extension_settings.expressions.rerollIfSame) {
|
if (extension_settings.expressions.rerollIfSame) {
|
||||||
possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc);
|
possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc);
|
||||||
@ -1309,6 +1382,7 @@ async function setExpression(character, expression, force = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//only swap expressions when necessary
|
//only swap expressions when necessary
|
||||||
if (prevExpressionSrc !== spriteFile.imageSrc
|
if (prevExpressionSrc !== spriteFile.imageSrc
|
||||||
&& !img.hasClass('expression-animating')) {
|
&& !img.hasClass('expression-animating')) {
|
||||||
@ -1360,7 +1434,6 @@ async function setExpression(character, expression, force = false) {
|
|||||||
expressionHolder.css('min-height', 100);
|
expressionHolder.css('min-height', 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
expressionClone.removeClass('expression-clone');
|
expressionClone.removeClass('expression-clone');
|
||||||
|
|
||||||
expressionClone.removeClass('default');
|
expressionClone.removeClass('default');
|
||||||
@ -1374,26 +1447,44 @@ async function setExpression(character, expression, force = false) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (extension_settings.expressions.showDefault) {
|
if (extension_settings.expressions.showDefault) {
|
||||||
setDefault();
|
setDefault();
|
||||||
|
} else {
|
||||||
|
setNone();
|
||||||
}
|
}
|
||||||
|
console.debug('Expression unset');
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDefault() {
|
function setDefault() {
|
||||||
console.debug('setting default');
|
console.debug('setting default expression');
|
||||||
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
const defImgUrl = `/img/default-expressions/${expression}.png`;
|
||||||
//console.log(defImgUrl);
|
//console.log(defImgUrl);
|
||||||
img.attr('src', defImgUrl);
|
img.attr('src', defImgUrl);
|
||||||
img.addClass('default');
|
img.addClass('default');
|
||||||
}
|
}
|
||||||
|
function setNone() {
|
||||||
|
console.debug('setting no expression');
|
||||||
|
img.attr('src', '');
|
||||||
|
img.removeClass('default');
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('expression-holder').style.display = '';
|
document.getElementById('expression-holder').style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickExpressionImage() {
|
function onClickExpressionImage() {
|
||||||
const expression = $(this).data('expression');
|
// If there is no expression image and we clicked on the placeholder, we remove the sprite by calling via the expression label
|
||||||
setSpriteSlashCommand({}, expression);
|
if ($(this).attr('data-expression-type') === 'failure') {
|
||||||
|
const label = $(this).attr('data-expression');
|
||||||
|
setSpriteSlashCommand({ type: 'expression' }, label);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spriteFile = $(this).attr('data-filename');
|
||||||
|
setSpriteSlashCommand({ type: 'sprite' }, spriteFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onClickExpressionAddCustom() {
|
async function onClickExpressionAddCustom() {
|
||||||
@ -1667,8 +1758,9 @@ async function onClickExpressionOverrideButton() {
|
|||||||
inApiCall = true;
|
inApiCall = true;
|
||||||
$('#visual-novel-wrapper').empty();
|
$('#visual-novel-wrapper').empty();
|
||||||
await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
|
await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true);
|
||||||
|
const name = overridePath.length === 0 ? currentLastMessage.name : overridePath;
|
||||||
const expression = await getExpressionLabel(currentLastMessage.mes);
|
const expression = await getExpressionLabel(currentLastMessage.mes);
|
||||||
await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true);
|
await sendExpressionCall(name, expression, { force: true });
|
||||||
forceUpdateVisualNovelMode();
|
forceUpdateVisualNovelMode();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
|
console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`);
|
||||||
@ -1694,7 +1786,7 @@ async function onClickExpressionOverrideRemoveAllButton() {
|
|||||||
const currentLastMessage = getLastCharacterMessage();
|
const currentLastMessage = getLastCharacterMessage();
|
||||||
await validateImages(currentLastMessage.name, true);
|
await validateImages(currentLastMessage.name, true);
|
||||||
const expression = await getExpressionLabel(currentLastMessage.mes);
|
const expression = await getExpressionLabel(currentLastMessage.mes);
|
||||||
await sendExpressionCall(currentLastMessage.name, expression, true);
|
await sendExpressionCall(currentLastMessage.name, expression, { force: true });
|
||||||
forceUpdateVisualNovelMode();
|
forceUpdateVisualNovelMode();
|
||||||
|
|
||||||
console.debug(extension_settings.expressionOverrides);
|
console.debug(extension_settings.expressionOverrides);
|
||||||
@ -1933,22 +2025,60 @@ function migrateSettings() {
|
|||||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||||
|
|
||||||
const localEnumProviders = {
|
const localEnumProviders = {
|
||||||
expressions: () => getCachedExpressions().map(expression => {
|
expressions: () => {
|
||||||
const isCustom = extension_settings.expressions.custom?.includes(expression);
|
const spriteFolderName = getSpriteFolderName();
|
||||||
return new SlashCommandEnumValue(expression, null, isCustom ? enumTypes.name : enumTypes.enum, isCustom ? 'C' : 'D');
|
const expressions = getCachedExpressions();
|
||||||
}),
|
return expressions.map(expression => {
|
||||||
|
const spriteCount = spriteCache[spriteFolderName]?.find(x => x.label === expression)?.files.length ?? 0;
|
||||||
|
const isCustom = extension_settings.expressions.custom?.includes(expression);
|
||||||
|
const subtitle = spriteCount == 0 ? '❌ No sprites available for this expression' :
|
||||||
|
spriteCount > 1 ? `${spriteCount} sprites` : null;
|
||||||
|
return new SlashCommandEnumValue(expression,
|
||||||
|
subtitle,
|
||||||
|
isCustom ? enumTypes.name : enumTypes.enum,
|
||||||
|
isCustom ? 'C' : 'D');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
sprites: () => {
|
||||||
|
const spriteFolderName = getSpriteFolderName();
|
||||||
|
const sprites = spriteCache[spriteFolderName]?.map(x => x.files)?.flat() ?? [];
|
||||||
|
return sprites.map(x => {
|
||||||
|
return new SlashCommandEnumValue(x.title,
|
||||||
|
x.title !== x.expression ? x.expression : null,
|
||||||
|
x.isCustom ? enumTypes.name : enumTypes.enum,
|
||||||
|
x.isCustom ? 'C' : 'D');
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'expression-set',
|
name: 'expression-set',
|
||||||
aliases: ['sprite', 'emote'],
|
aliases: ['sprite', 'emote'],
|
||||||
callback: setSpriteSlashCommand,
|
callback: setSpriteSlashCommand,
|
||||||
|
namedArgumentList: [
|
||||||
|
SlashCommandNamedArgument.fromProps({
|
||||||
|
name: 'type',
|
||||||
|
description: 'Whether to set an expression or a specific sprite.',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: false,
|
||||||
|
defaultValue: 'expression',
|
||||||
|
enumList: ['expression', 'sprite'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
unnamedArgumentList: [
|
unnamedArgumentList: [
|
||||||
SlashCommandArgument.fromProps({
|
SlashCommandArgument.fromProps({
|
||||||
description: 'expression label to set',
|
description: 'expression label to set',
|
||||||
typeList: [ARGUMENT_TYPE.STRING],
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
enumProvider: localEnumProviders.expressions,
|
enumProvider: (executor, _) => {
|
||||||
|
// Check if command is used to set a sprite, then use those enums
|
||||||
|
const type = executor.namedArgumentList.find(it => it.name == 'type')?.value || 'expression';
|
||||||
|
if (type == 'sprite') return localEnumProviders.sprites();
|
||||||
|
else return [
|
||||||
|
...localEnumProviders.expressions(),
|
||||||
|
new SlashCommandEnumValue('#reset', 'Resets the expression (to either default or no sprite)', enumTypes.enum, '❌'),
|
||||||
|
];
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
helpString: 'Force sets the expression for the current character.',
|
helpString: 'Force sets the expression for the current character.',
|
||||||
@ -1957,13 +2087,21 @@ function migrateSettings() {
|
|||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'expression-folder-override',
|
name: 'expression-folder-override',
|
||||||
aliases: ['spriteoverride', 'costume'],
|
aliases: ['spriteoverride', 'costume'],
|
||||||
callback: setSpriteSetCommand,
|
callback: setSpriteFolderCommand,
|
||||||
unnamedArgumentList: [
|
unnamedArgumentList: [
|
||||||
new SlashCommandArgument(
|
new SlashCommandArgument(
|
||||||
'optional folder', [ARGUMENT_TYPE.STRING], false,
|
'optional folder', [ARGUMENT_TYPE.STRING], false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets an override sprite folder for the current character.<br />
|
||||||
|
In groups, this will apply to the character who last sent a message.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
}));
|
}));
|
||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'expression-last',
|
name: 'expression-last',
|
||||||
@ -1997,8 +2135,8 @@ function migrateSettings() {
|
|||||||
helpString: 'Returns the last set expression for the named character.',
|
helpString: 'Returns the last set expression for the named character.',
|
||||||
}));
|
}));
|
||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'expression-classify',
|
name: 'expression-list',
|
||||||
aliases: ['classify-expressions', 'expressions'],
|
aliases: ['expressions'],
|
||||||
/** @type {(args: {return: string}) => Promise<string>} */
|
/** @type {(args: {return: string}) => Promise<string>} */
|
||||||
callback: async (args) => {
|
callback: async (args) => {
|
||||||
let returnType =
|
let returnType =
|
||||||
|
@ -109,6 +109,9 @@ img.expression.default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expression_list_image_container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1833,14 +1833,15 @@ async function loadContextSettings() {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Common function to perform fuzzy search with optional caching
|
* Common function to perform fuzzy search with optional caching
|
||||||
|
* @template T
|
||||||
* @param {string} type - Type of search from fuzzySearchCategories
|
* @param {string} type - Type of search from fuzzySearchCategories
|
||||||
* @param {any[]} data - Data array to search in
|
* @param {T[]} data - Data array to search in
|
||||||
* @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration
|
* @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
|
||||||
* @param {string} searchValue - The search term
|
* @param {string} searchValue - The search term
|
||||||
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
|
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
|
||||||
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
|
* @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
|
||||||
*/
|
*/
|
||||||
function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
|
export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
|
||||||
// Check cache if provided
|
// Check cache if provided
|
||||||
if (fuzzySearchCaches) {
|
if (fuzzySearchCaches) {
|
||||||
const cache = fuzzySearchCaches[type];
|
const cache = fuzzySearchCaches[type];
|
||||||
|
Reference in New Issue
Block a user