Custom char expressions
This commit is contained in:
parent
9fb4b3425e
commit
7553efc308
|
@ -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: {},
|
||||||
|
|
|
@ -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>
|
|
@ -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) {
|
||||||
|
@ -961,6 +991,11 @@ async function getExpressionsList() {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await resolveExpressionsList();
|
||||||
|
result.push(...extension_settings.expressions.custom);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setExpression(character, expression, force) {
|
async function setExpression(character, expression, force) {
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<h3>
|
||||||
|
Are you sure you want to remove the expression <tt>"{{expression}}"</tt>?
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
Uploaded images will not be deleted, but will no longer be used by the extension.
|
||||||
|
</div>
|
||||||
|
<br>
|
|
@ -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" />
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue