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: { caption: {
refine_mode: false, refine_mode: false,
}, },
expressions: {}, expressions: {
/** @type {string[]} */
custom: [],
},
dice: {}, dice: {},
regex: [], regex: [],
tts: {}, 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) => { labels.sort().forEach((item) => {
const sprite = sprites.find(x => x.label == item); const sprite = sprites.find(x => x.label == item);
const isCustom = extension_settings.expressions.custom.includes(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', isCustom));
} }
else { 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; 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) { async function getSpritesList(name) {
@ -917,16 +926,37 @@ async function getSpritesList(name) {
} }
} }
async function getExpressionsList() { function renderCustomExpressions() {
// get something for offline mode (default images) if (!Array.isArray(extension_settings.expressions.custom)) {
if (!modules.includes('classify') && !extension_settings.expressions.local) { extension_settings.expressions.custom = [];
return DEFAULT_EXPRESSIONS;
} }
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)) { if (Array.isArray(expressionsList)) {
return 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 { try {
if (extension_settings.expressions.local) { if (extension_settings.expressions.local) {
@ -963,6 +993,11 @@ async function getExpressionsList() {
} }
} }
const result = await resolveExpressionsList();
result.push(...extension_settings.expressions.custom);
return result;
}
async function setExpression(character, expression, force) { async function setExpression(character, expression, force) {
if (extension_settings.expressions.local || !extension_settings.expressions.talkinghead) { if (extension_settings.expressions.local || !extension_settings.expressions.talkinghead) {
console.debug('entered setExpressions'); console.debug('entered setExpressions');
@ -1101,6 +1136,72 @@ function onClickExpressionImage() {
setSpriteSlashCommand({}, expression); 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) { async function handleFileUpload(url, formData) {
try { try {
const data = await jQuery.ajax({ const data = await jQuery.ajax({
@ -1359,6 +1460,11 @@ function setExpressionOverrideHtml(forceClear = false) {
setTalkingHeadState(this.checked); setTalkingHeadState(this.checked);
} }
}); });
renderCustomExpressions();
$('#expression_custom_add').on('click', onClickExpressionAddCustom);
$('#expression_custom_remove').on('click', onClickExpressionRemoveCustom);
} }
addExpressionImage(); addExpressionImage();

View File

@ -7,6 +7,11 @@
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</div> </div>
</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}}" /> <img class="expression_list_image" src="{{imageSrc}}" />
</div> </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"> <input id="image_type_toggle" type="checkbox">
<span>Image Type - talkinghead (extras)</span> <span>Image Type - talkinghead (extras)</span>
</label> </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"> <div id="no_chat_expressions">
Open a chat to see the character expressions. Open a chat to see the character expressions.
</div> </div>
@ -25,6 +34,8 @@
<div class="offline_mode"> <div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small> <small>You are in offline mode. Click on the image below to set the expression.</small>
</div> </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"> <div class="flex-container flexnowrap">
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" /> <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" /> <input id="expression_override_button" class="menu_button" type="submit" value="Submit" />

View File

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