Custom char expressions
This commit is contained in:
parent
9fb4b3425e
commit
7553efc308
|
@ -134,7 +134,10 @@ const extension_settings = {
|
|||
caption: {
|
||||
refine_mode: false,
|
||||
},
|
||||
expressions: {},
|
||||
expressions: {
|
||||
/** @type {string[]} */
|
||||
custom: [],
|
||||
},
|
||||
dice: {},
|
||||
regex: [],
|
||||
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) => {
|
||||
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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
<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" />
|
||||
|
|
|
@ -123,6 +123,8 @@ img.expression.default {
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.expression_list_buttons {
|
||||
|
|
Loading…
Reference in New Issue