Custom char expressions

This commit is contained in:
Cohee 2023-09-14 21:30:02 +03:00
parent 9fb4b3425e
commit 7553efc308
7 changed files with 184 additions and 36 deletions

View File

@ -134,7 +134,10 @@ const extension_settings = {
caption: {
refine_mode: false,
},
expressions: {},
expressions: {
/** @type {string[]} */
custom: [],
},
dice: {},
regex: [],
tts: {},

View File

@ -0,0 +1,14 @@
<h3>
Enter a name for the custom expression:
</h3>
<h4>
Requirements:
</h4>
<ol class="justifyLeft">
<li>
The name must be unique and not already in use by the default expression.
</li>
<li>
The name must contain only letters, numbers, dashes and underscores. Don't include any file extensions.
</li>
</ol>

View File

@ -887,20 +887,29 @@ function drawSpritesList(character, labels, sprites) {
labels.sort().forEach((item) => {
const sprite = sprites.find(x => x.label == item);
const isCustom = extension_settings.expressions.custom.includes(item);
if (sprite) {
validExpressions.push(sprite);
$('#image_list').append(getListItem(item, sprite.path, 'success'));
$('#image_list').append(getListItem(item, sprite.path, 'success', isCustom));
}
else {
$('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure'));
$('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure', isCustom));
}
});
return validExpressions;
}
function getListItem(item, imageSrc, textClass) {
return renderExtensionTemplate(MODULE_NAME, 'list-item', { item, imageSrc, textClass });
/**
* Renders a list item template for the expressions list.
* @param {string} item Expression name
* @param {string} imageSrc Path to image
* @param {'success' | 'failure'} textClass 'success' or 'failure'
* @param {boolean} isCustom If expression is added by user
* @returns {string} Rendered list item template
*/
function getListItem(item, imageSrc, textClass, isCustom) {
return renderExtensionTemplate(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
}
async function getSpritesList(name) {
@ -917,50 +926,76 @@ async function getSpritesList(name) {
}
}
async function getExpressionsList() {
// get something for offline mode (default images)
if (!modules.includes('classify') && !extension_settings.expressions.local) {
return DEFAULT_EXPRESSIONS;
function renderCustomExpressions() {
if (!Array.isArray(extension_settings.expressions.custom)) {
extension_settings.expressions.custom = [];
}
const customExpressions = extension_settings.expressions.custom.sort((a, b) => a.localeCompare(b));
$('#expression_custom').empty();
for (const expression of customExpressions) {
const option = document.createElement('option');
option.value = expression;
option.text = expression;
$('#expression_custom').append(option);
}
}
async function getExpressionsList() {
// Return cached list if available
if (Array.isArray(expressionsList)) {
return expressionsList;
}
/**
* Returns the list of expressions from the API or fallback in offline mode.
* @returns {Promise<string[]>}
*/
async function resolveExpressionsList() {
// get something for offline mode (default images)
if (!modules.includes('classify') && !extension_settings.expressions.local) {
return DEFAULT_EXPRESSIONS;
}
try {
if (extension_settings.expressions.local) {
const apiResult = await fetch('/api/extra/classify/labels', {
method: 'POST',
headers: getRequestHeaders(),
});
try {
if (extension_settings.expressions.local) {
const apiResult = await fetch('/api/extra/classify/labels', {
method: 'POST',
headers: getRequestHeaders(),
});
if (apiResult.ok) {
const data = await apiResult.json();
expressionsList = data.labels;
return expressionsList;
}
} else {
const url = new URL(getApiUrl());
url.pathname = '/api/classify/labels';
if (apiResult.ok) {
const data = await apiResult.json();
expressionsList = data.labels;
return expressionsList;
}
} else {
const url = new URL(getApiUrl());
url.pathname = '/api/classify/labels';
const apiResult = await doExtrasFetch(url, {
method: 'GET',
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
});
const apiResult = await doExtrasFetch(url, {
method: 'GET',
headers: { 'Bypass-Tunnel-Reminder': 'bypass' },
});
if (apiResult.ok) {
if (apiResult.ok) {
const data = await apiResult.json();
expressionsList = data.labels;
return expressionsList;
const data = await apiResult.json();
expressionsList = data.labels;
return expressionsList;
}
}
}
catch (error) {
console.log(error);
return [];
}
}
catch (error) {
console.log(error);
return [];
}
const result = await resolveExpressionsList();
result.push(...extension_settings.expressions.custom);
return result;
}
async function setExpression(character, expression, force) {
@ -1101,6 +1136,72 @@ function onClickExpressionImage() {
setSpriteSlashCommand({}, expression);
}
async function onClickExpressionAddCustom() {
let expressionName = await callPopup(renderExtensionTemplate(MODULE_NAME, 'add-custom-expression'), 'input');
if (!expressionName) {
console.debug('No custom expression name provided');
return;
}
expressionName = expressionName.trim().toLowerCase();
// a-z, 0-9, dashes and underscores only
if (!/^[a-z0-9-_]+$/.test(expressionName)) {
toastr.info('Invalid custom expression name provided');
return;
}
// Check if expression name already exists in default expressions
if (DEFAULT_EXPRESSIONS.includes(expressionName)) {
toastr.info('Expression name already exists');
return;
}
// Check if expression name already exists in custom expressions
if (extension_settings.expressions.custom.includes(expressionName)) {
toastr.info('Custom expression already exists');
return;
}
// Add custom expression into settings
extension_settings.expressions.custom.push(expressionName);
renderCustomExpressions();
saveSettingsDebounced();
// Force refresh sprites list
expressionsList = null;
spriteCache = {};
moduleWorker();
}
async function onClickExpressionRemoveCustom() {
const selectedExpression = $('#expression_custom').val();
if (!selectedExpression) {
console.debug('No custom expression selected');
return;
}
const confirmation = await callPopup(renderExtensionTemplate(MODULE_NAME, 'remove-custom-expression', { expression: selectedExpression }), 'confirm');
if (!confirmation) {
console.debug('Custom expression removal cancelled');
return;
}
// Remove custom expression from settings
const index = extension_settings.expressions.custom.indexOf(selectedExpression);
extension_settings.expressions.custom.splice(index, 1);
renderCustomExpressions();
saveSettingsDebounced();
// Force refresh sprites list
expressionsList = null;
spriteCache = {};
moduleWorker();
}
async function handleFileUpload(url, formData) {
try {
const data = await jQuery.ajax({
@ -1359,6 +1460,11 @@ function setExpressionOverrideHtml(forceClear = false) {
setTalkingHeadState(this.checked);
}
});
renderCustomExpressions();
$('#expression_custom_add').on('click', onClickExpressionAddCustom);
$('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
}
addExpressionImage();

View File

@ -7,6 +7,11 @@
<i class="fa-solid fa-trash"></i>
</div>
</div>
<span class="expression_list_title {{textClass}}">{{item}}</span>
<div class="expression_list_title {{textClass}}">
<span>{{item}}</span>
{{#if isCustom}}
<small class="expression_list_custom">(custom)</small>
{{/if}}
</div>
<img class="expression_list_image" src="{{imageSrc}}" />
</div>

View File

@ -0,0 +1,7 @@
<h3>
Are you sure you want to remove the expression <tt>&quot;{{expression}}&quot;</tt>?
</h3>
<div>
Uploaded images will not be deleted, but will no longer be used by the extension.
</div>
<br>

View File

@ -18,6 +18,15 @@
<input id="image_type_toggle" type="checkbox">
<span>Image Type - talkinghead (extras)</span>
</label>
<div class="expression_custom_block">
<label for="expression_custom">Custom Expressions</label>
<small>Can be set manually or with an <tt>/emote</tt> slash command.</small>
<div class="flex-container alignitemscenter">
<select id="expression_custom" class="flex1 margin0"><select>
<i id="expression_custom_add" class="menu_button fa-solid fa-plus" title="Add"></i>
<i id="expression_custom_remove" class="menu_button fa-solid fa-xmark" title="Remove"></i>
</div>
</div>
<div id="no_chat_expressions">
Open a chat to see the character expressions.
</div>
@ -25,6 +34,8 @@
<div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small>
</div>
<label for="expression_override">Sprite Folder Override</label>
<small>Use a forward slash to specify a subfolder. Example: <tt>Bob/formal</tt></small>
<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" />

View File

@ -123,6 +123,8 @@ img.expression.default {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
line-height: 1;
}
.expression_list_buttons {