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
| @@ -3,7 +3,7 @@ import { Fuse } from '../../../lib.js'; | ||
| 3 | 3 | import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js'; |
| 4 | 4 | import { dragElement, isMobile } from '../../RossAscends-mods.js'; |
| 5 | 5 | import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 6 | 6 | import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js'; |
| 7 | 7 | import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js'; |
| 8 | 8 | import { hideMutedSprites, selected_group } from '../../group-chats.js'; |
| 9 | 9 | import { isJsonSchemaSupported } from '../../textgen-settings.js'; |
| @@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | ||
| 12 | 12 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 13 | 13 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 14 | 14 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 15 | 15 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 16 | 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 17 | 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 18 | 18 | import { Popup, POPUP_RESULT } from '../../popup.js'; |
| @@ -27,8 +27,8 @@ export { MODULE_NAME }; | ||
| 27 | 27 | |
| 28 | 28 | /** |
| 29 | 29 | * @typedef {object} ExpressionImage An expression image |
| 30 | 30 | * @property {string?} [expression=null] - The expression |
| 31 | 31 | * @property {boolean?} [isCustom=nullfalse] - If the expression is added by user |
| 32 | 32 | * @property {string} fileName - The filename with extension |
| 33 | 33 | * @property {string} title - The title for the image |
| 34 | 34 | * @property {string} imageSrc - The image source / full path |
| @@ -78,9 +78,6 @@ const EXPRESSION_API = { | ||
| 78 | 78 | webllm: 3, |
| 79 | 79 | }; |
| 80 | 80 | |
| 81 | -/** @type {ExpressionImage} */ | |
| 82 | -const NO_IMAGE_PLACEHOLDER = { title: 'No Image', type: 'failure', fileName: 'No-Image-Placeholder.svg', imageSrc: '/img/No-Image-Placeholder.svg' }; | |
| 83 | - | |
| 84 | 81 | let expressionsList = null; |
| 85 | 82 | let lastCharacter = undefined; |
| 86 | 83 | let lastMessage = null; |
| @@ -93,6 +90,24 @@ let lastServerResponseTime = 0; | ||
| 93 | 90 | export let lastExpression = {}; |
| 94 | 91 | |
| 95 | 92 | /** |
| 93 | + * Returns a placeholder image object for a given expression | |
| 94 | + * @param {string} expression - The expression label | |
| 95 | + * @param {boolean} [isCustom=false] - Whether the expression is custom | |
| 96 | + * @returns {ExpressionImage} The placeholder image object | |
| 97 | + */ | |
| 98 | +function getPlaceholderImage(expression, isCustom = false) { | |
| 99 | + return { | |
| 100 | + expression: expression, | |
| 101 | + isCustom: isCustom, | |
| 102 | + title: 'No Image', | |
| 103 | + type: 'failure', | |
| 104 | + fileName: 'No-Image-Placeholder.svg', | |
| 105 | + imageSrc: '/img/No-Image-Placeholder.svg', | |
| 106 | + }; | |
| 107 | +} | |
| 108 | + | |
| 109 | + | |
| 110 | +/** | |
| 96 | 111 | * Returns the fallback expression if explicitly chosen, otherwise the default one |
| 97 | 112 | * @returns {string} expression name |
| 98 | 113 | */ |
| @@ -189,6 +204,7 @@ async function visualNovelSetCharacterSprites(container, name, expression) { | ||
| 189 | 204 | const sprites = spriteCache[spriteFolderName]; |
| 190 | 205 | const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`); |
| 191 | 206 | const defaultExpression = getFallbackExpression(); |
| 207 | + // TODO: Visual novel sprites need fixing, currently do not update based on multiple sprites, etc | |
| 192 | 208 | const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path; |
| 193 | 209 | const noSprites = sprites.length === 0; |
| 194 | 210 | |
| @@ -460,7 +476,7 @@ async function moduleWorker() { | ||
| 460 | 476 | } |
| 461 | 477 | |
| 462 | 478 | const currentLastMessage = getLastCharacterMessage(); |
| 463 | 479 | let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName(); |
| 464 | 480 | |
| 465 | 481 | // character has no expressions or it is not loaded |
| 466 | 482 | if (Object.keys(spriteCache).length === 0) { |
| @@ -550,7 +566,7 @@ async function moduleWorker() { | ||
| 550 | 566 | expression = getFallbackExpression(); |
| 551 | 567 | } |
| 552 | 568 | |
| 553 | 569 | await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode }); |
| 554 | 570 | } |
| 555 | 571 | catch (error) { |
| 556 | 572 | console.log(error); |
| @@ -596,48 +612,55 @@ function getFolderNameByMessage(message) { | ||
| 596 | 612 | return folderName; |
| 597 | 613 | } |
| 598 | 614 | |
| 599 | -async function sendExpressionCall(name, expression, force, vnMode) { | |
| 615 | +/** | |
| 616 | + * Update the expression for the given character. | |
| 617 | + * | |
| 618 | + * @param {string} name The character name, optionally with a sprite folder override, e.g. "folder/expression". | |
| 619 | + * @param {string} expression The expression label, e.g. "amusement", "joy", etc. | |
| 620 | + * @param {Object} [options] Additional options | |
| 621 | + * @param {boolean} [options.force=false] If true, the expression will be sent even if it is the same as the current expression. | |
| 622 | + * @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. | |
| 623 | + * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name. | |
| 624 | + */ | |
| 625 | +async function sendExpressionCall(name, expression, { force = false, vnMode = null, overrideSpriteFile = null } = {}) { | |
| 600 | 626 | lastExpression[name.split('/')[0]] = expression; |
| 601 | 627 | if (!vnMode === null) { |
| 602 | 628 | vnMode = isVisualNovelMode(); |
| 603 | 629 | } |
| 604 | 630 | |
| 605 | 631 | if (vnMode) { |
| 606 | 632 | await updateVisualNovelMode(name, expression); |
| 607 | 633 | } else { |
| 608 | 634 | setExpression(name, expression, { force: force, overrideSpriteFile: overrideSpriteFile }); |
| 609 | 635 | } |
| 610 | 636 | } |
| 611 | 637 | |
| 612 | 638 | async function setSpriteSetCommandsetSpriteFolderCommand(_, folder) { |
| 613 | 639 | if (!folder) { |
| 614 | 640 | console.log('Clearing sprite set'); |
| 615 | 641 | folder = ''; |
| 616 | 642 | } |
| 617 | 643 | |
| 618 | 644 | if (folder.startsWith('/') || folder.startsWith('\\')) { |
| 619 | - folder = folder.slice(1); | |
| 620 | - | |
| 621 | 645 | const currentLastMessage = getLastCharacterMessage(); |
| 646 | + folder = folder.slice(1); | |
| 622 | 647 | folder = `${currentLastMessage.name}/${folder}`; |
| 623 | 648 | } |
| 624 | 649 | |
| 625 | 650 | $('#expression_override').val(folder.trim()); |
| 626 | 651 | onClickExpressionOverrideButton(); |
| 627 | - // removeExpression(); | |
| 652 | + | |
| 628 | - // moduleWorker(); | |
| 653 | + // No need to resend the expression, the folder override will automatically update the currently displayed one. | |
| 629 | - const vnMode = isVisualNovelMode(); | |
| 630 | - await sendExpressionCall(folder, lastExpression, true, vnMode); | |
| 631 | 654 | return ''; |
| 632 | 655 | } |
| 633 | 656 | |
| 634 | 657 | async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) { |
| 635 | 658 | if (!text) { |
| 636 | 659 | toastr.warningerror('No text provided'); |
| 637 | 660 | return ''; |
| 638 | 661 | } |
| 639 | 662 | if (api && !Object.keys(EXPRESSION_API).includes(api)) { |
| 640 | 663 | toastr.warningerror('Invalid API provided'); |
| 641 | 664 | return ''; |
| 642 | 665 | } |
| 643 | 666 | |
| @@ -653,31 +676,68 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { | ||
| 653 | 676 | return label; |
| 654 | 677 | } |
| 655 | 678 | |
| 656 | -async function setSpriteSlashCommand(_, spriteId) { | |
| 679 | +/** @type {(args: {type: 'expression' | 'sprite'}, searchTerm: string) => Promise<string>} */ | |
| 657 | - spriteId = spriteId.trim().toLowerCase(); | |
| 680 | +async function setSpriteSlashCommand({ type }, searchTerm) { | |
| 658 | - if (!spriteId) { | |
| 681 | + type ??= 'expression'; | |
| 659 | - console.log('No sprite id provided'); | |
| 682 | + searchTerm = searchTerm.trim().toLowerCase(); | |
| 683 | + if (!searchTerm) { | |
| 684 | + toastr.error(t`No expression or sprite name provided`, t`Set Sprite`); | |
| 660 | 685 | return ''; |
| 661 | 686 | } |
| 662 | 687 | |
| 663 | 688 | const spriteFolderName = getSpriteFolderName(); |
| 664 | 689 | |
| 690 | + let label = searchTerm; | |
| 691 | + | |
| 692 | + /** @type {string?} */ | |
| 693 | + let spriteFile = null; | |
| 694 | + | |
| 665 | 695 | await validateImages(spriteFolderName); |
| 666 | 696 | |
| 667 | - // Fuzzy search for sprite | |
| 697 | + // Handle reset as a special term and just reset the sprite via expression call | |
| 668 | - const fuse = new Fuse(spriteCache[spriteFolderName], { keys: ['label'] }); | |
| 698 | + if (searchTerm === '#reset') { | |
| 669 | - const results = fuse.search(spriteId); | |
| 699 | + await sendExpressionCall(spriteFolderName, label, { force: true }); | |
| 670 | - const spriteItem = results[0]?.item; | |
| 700 | + return lastExpression[spriteFolderName] ?? ''; | |
| 701 | + } | |
| 702 | + | |
| 703 | + switch (type) { | |
| 704 | + case 'expression': { | |
| 705 | + // Fuzzy search for expression | |
| 706 | + const existingExpressions = getCachedExpressions().map(x => ({ label: x })); | |
| 707 | + const results = performFuzzySearch('expression-expressions', existingExpressions, [ | |
| 708 | + { name: 'label', weight: 1 }, | |
| 709 | + ], searchTerm); | |
| 710 | + const matchedExpression = results[0]?.item; | |
| 711 | + if (!matchedExpression) { | |
| 712 | + toastr.warning(t`No expression found for search term ${searchTerm}`, t`Set Sprite`); | |
| 713 | + return ''; | |
| 714 | + } | |
| 715 | + | |
| 716 | + label = matchedExpression.label; | |
| 717 | + break; | |
| 718 | + } | |
| 719 | + case 'sprite': { | |
| 720 | + // Fuzzy search for sprite file | |
| 721 | + const sprites = spriteCache[spriteFolderName].map(x => x.files).flat(); | |
| 722 | + const results = performFuzzySearch('expression-expressions', sprites, [ | |
| 723 | + { name: 'title', weight: 1 }, | |
| 724 | + { name: 'fileName', weight: 1 }, | |
| 725 | + ], searchTerm); | |
| 726 | + const matchedSprite = results[0]?.item; | |
| 727 | + if (!matchedSprite) { | |
| 728 | + toastr.warning(t`No sprite file found for search term ${searchTerm}`, t`Set Sprite`); | |
| 729 | + return ''; | |
| 730 | + } | |
| 671 | 731 | |
| 672 | - if (!spriteItem) { | |
| 732 | + label = matchedSprite.expression; | |
| 673 | - console.log('No sprite found for search term ' + spriteId); | |
| 733 | + spriteFile = matchedSprite.fileName; | |
| 674 | - return ''; | |
| 734 | + break; | |
| 735 | + } | |
| 736 | + default: throw Error('Invalid sprite set type: ' + type); | |
| 675 | 737 | } |
| 676 | 738 | |
| 677 | - const label = spriteItem.label; | |
| 739 | + await sendExpressionCall(spriteFolderName, label, { force: true, overrideSpriteFile: spriteFile }); | |
| 678 | 740 | |
| 679 | - const vnMode = isVisualNovelMode(); | |
| 680 | - await sendExpressionCall(spriteFolderName, label, true, vnMode); | |
| 681 | 741 | return label; |
| 682 | 742 | } |
| 683 | 743 | |
| @@ -714,13 +774,22 @@ function spriteFolderNameFromCharacter(char) { | ||
| 714 | 774 | */ |
| 715 | 775 | async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) { |
| 716 | 776 | if (!imageUrl) throw new Error('Image URL is required'); |
| 717 | 777 | if (!label || typeof label !== 'string') throw new Error('Expression label is required');{ |
| 778 | + toastr.error(t`Expression label is required`, t`Error Uploading Sprite`); | |
| 779 | + return ''; | |
| 780 | + } | |
| 718 | 781 | |
| 719 | 782 | label = label.replace(/[^a-z]/gi, '').toLowerCase().trim(); |
| 720 | - if (!label) throw new Error('Expression label must contain at least one letter'); | |
| 783 | + if (!label) { | |
| 784 | + toastr.error(t`Expression label must contain at least one letter`, t`Error Uploading Sprite`); | |
| 785 | + return ''; | |
| 786 | + } | |
| 721 | 787 | |
| 722 | 788 | spriteName = spriteName || label; |
| 723 | - if (!validateExpressionSpriteName(label, spriteName)) throw new Error('Invalid sprite name. Must follow the naming pattern for expression sprites.'); | |
| 789 | + if (!validateExpressionSpriteName(label, spriteName)) { | |
| 790 | + toastr.error(t`Invalid sprite name. Must follow the naming pattern for expression sprites.`, t`Error Uploading Sprite`); | |
| 791 | + return ''; | |
| 792 | + } | |
| 724 | 793 | |
| 725 | 794 | name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; |
| 726 | 795 | const char = findChar({ name }); |
| @@ -1066,7 +1135,7 @@ async function drawSpritesList(character, labels, sprites) { | ||
| 1066 | 1135 | if (images.length === 0) { |
| 1067 | 1136 | const listItem = await getListItem(expression, { |
| 1068 | 1137 | isCustom, |
| 1069 | 1138 | images: [{ getPlaceholderImage(expression, isCustom, ...NO_IMAGE_PLACEHOLDER })], |
| 1070 | 1139 | }); |
| 1071 | 1140 | $('#image_list').append(listItem); |
| 1072 | 1141 | continue; |
| @@ -1264,12 +1333,13 @@ export async function getExpressionsList() { | ||
| 1264 | 1333 | /** |
| 1265 | 1334 | * Set the expression of a character. |
| 1266 | 1335 | * @param {string} character - The name of the character |
| 1267 | 1336 | * @param {string} expression - The expression or sprite name to set |
| 1268 | - * @param {boolean} [force=false] - Whether to force the expression change even if Visual Novel mode is on. | |
| 1337 | + * @param {Object} options - Optional parameters | |
| 1338 | + * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on | |
| 1339 | + * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name. | |
| 1269 | 1340 | * @returns {Promise<void>} A promise that resolves when the expression has been set. |
| 1270 | 1341 | */ |
| 1271 | 1342 | async function setExpression(character, expression, { force = false, overrideSpriteFile = null } = {}) { |
| 1272 | - console.debug('entered setExpressions'); | |
| 1273 | 1343 | await validateImages(character); |
| 1274 | 1344 | const img = $('img.expression'); |
| 1275 | 1345 | const prevExpressionSrc = img.attr('src'); |
| @@ -1277,14 +1347,17 @@ async function setExpression(character, expression, force = false) { | ||
| 1277 | 1347 | |
| 1278 | 1348 | /** @type {Expression} */ |
| 1279 | 1349 | const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); |
| 1280 | - console.debug('checking for expression images to show..'); | |
| 1281 | 1350 | if (sprite && sprite.files.length > 0) { |
| 1282 | - console.debug('setting expression from character images folder'); | |
| 1283 | - | |
| 1284 | 1351 | let spriteFile = sprite.files[0]; |
| 1285 | 1352 | |
| 1286 | - // Calculate next expression, if multiple are allowed | |
| 1353 | + // If a specific sprite file should be set, we are looking it up here | |
| 1287 | - if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) { | |
| 1354 | + if (overrideSpriteFile) { | |
| 1355 | + const searched = sprite.files.find(x => x.fileName === overrideSpriteFile); | |
| 1356 | + if (searched) spriteFile = searched; | |
| 1357 | + else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`); | |
| 1358 | + } | |
| 1359 | + // Else calculate next expression, if multiple are allowed | |
| 1360 | + else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) { | |
| 1288 | 1361 | let possibleFiles = sprite.files; |
| 1289 | 1362 | if (extension_settings.expressions.rerollIfSame) { |
| 1290 | 1363 | possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc); |
| @@ -1309,6 +1382,7 @@ async function setExpression(character, expression, force = false) { | ||
| 1309 | 1382 | } |
| 1310 | 1383 | } |
| 1311 | 1384 | } |
| 1385 | + | |
| 1312 | 1386 | //only swap expressions when necessary |
| 1313 | 1387 | if (prevExpressionSrc !== spriteFile.imageSrc |
| 1314 | 1388 | && !img.hasClass('expression-animating')) { |
| @@ -1360,7 +1434,6 @@ async function setExpression(character, expression, force = false) { | ||
| 1360 | 1434 | expressionHolder.css('min-height', 100); |
| 1361 | 1435 | }); |
| 1362 | 1436 | |
| 1363 | - | |
| 1364 | 1437 | expressionClone.removeClass('expression-clone'); |
| 1365 | 1438 | |
| 1366 | 1439 | expressionClone.removeClass('default'); |
| @@ -1374,26 +1447,44 @@ async function setExpression(character, expression, force = false) { | ||
| 1374 | 1447 | } |
| 1375 | 1448 | }); |
| 1376 | 1449 | } |
| 1450 | + | |
| 1451 | + console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName }); | |
| 1377 | 1452 | } |
| 1378 | 1453 | else { |
| 1379 | 1454 | if (extension_settings.expressions.showDefault) { |
| 1380 | 1455 | setDefault(); |
| 1456 | + } else { | |
| 1457 | + setNone(); | |
| 1381 | 1458 | } |
| 1459 | + console.debug('Expression unset'); | |
| 1382 | 1460 | } |
| 1383 | 1461 | |
| 1384 | 1462 | function setDefault() { |
| 1385 | 1463 | console.debug('setting default expression'); |
| 1386 | 1464 | const defImgUrl = `/img/default-expressions/${expression}.png`; |
| 1387 | 1465 | //console.log(defImgUrl); |
| 1388 | 1466 | img.attr('src', defImgUrl); |
| 1389 | 1467 | img.addClass('default'); |
| 1390 | 1468 | } |
| 1469 | + function setNone() { | |
| 1470 | + console.debug('setting no expression'); | |
| 1471 | + img.attr('src', ''); | |
| 1472 | + img.removeClass('default'); | |
| 1473 | + } | |
| 1474 | + | |
| 1391 | 1475 | document.getElementById('expression-holder').style.display = ''; |
| 1392 | 1476 | } |
| 1393 | 1477 | |
| 1394 | 1478 | function onClickExpressionImage() { |
| 1395 | - const expression = $(this).data('expression'); | |
| 1479 | + // If there is no expression image and we clicked on the placeholder, we remove the sprite by calling via the expression label | |
| 1396 | - setSpriteSlashCommand({}, expression); | |
| 1480 | + if ($(this).attr('data-expression-type') === 'failure') { | |
| 1481 | + const label = $(this).attr('data-expression'); | |
| 1482 | + setSpriteSlashCommand({ type: 'expression' }, label); | |
| 1483 | + return; | |
| 1484 | + } | |
| 1485 | + | |
| 1486 | + const spriteFile = $(this).attr('data-filename'); | |
| 1487 | + setSpriteSlashCommand({ type: 'sprite' }, spriteFile); | |
| 1397 | 1488 | } |
| 1398 | 1489 | |
| 1399 | 1490 | async function onClickExpressionAddCustom() { |
| @@ -1667,8 +1758,9 @@ async function onClickExpressionOverrideButton() { | ||
| 1667 | 1758 | inApiCall = true; |
| 1668 | 1759 | $('#visual-novel-wrapper').empty(); |
| 1669 | 1760 | await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true); |
| 1761 | + const name = overridePath.length === 0 ? currentLastMessage.name : overridePath; | |
| 1670 | 1762 | const expression = await getExpressionLabel(currentLastMessage.mes); |
| 1671 | 1763 | await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, { force: true }); |
| 1672 | 1764 | forceUpdateVisualNovelMode(); |
| 1673 | 1765 | } catch (error) { |
| 1674 | 1766 | console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`); |
| @@ -1694,7 +1786,7 @@ async function onClickExpressionOverrideRemoveAllButton() { | ||
| 1694 | 1786 | const currentLastMessage = getLastCharacterMessage(); |
| 1695 | 1787 | await validateImages(currentLastMessage.name, true); |
| 1696 | 1788 | const expression = await getExpressionLabel(currentLastMessage.mes); |
| 1697 | 1789 | await sendExpressionCall(currentLastMessage.name, expression, { force: true }); |
| 1698 | 1790 | forceUpdateVisualNovelMode(); |
| 1699 | 1791 | |
| 1700 | 1792 | console.debug(extension_settings.expressionOverrides); |
| @@ -1933,22 +2025,60 @@ function migrateSettings() { | ||
| 1933 | 2025 | eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); |
| 1934 | 2026 | |
| 1935 | 2027 | const localEnumProviders = { |
| 1936 | 2028 | expressions: () => getCachedExpressions().map(expression => { |
| 1937 | 2029 | const isCustomspriteFolderName = extension_settings.expressions.custom?.includesgetSpriteFolderName(expression); |
| 1938 | - return new SlashCommandEnumValue(expression, null, isCustom ? enumTypes.name : enumTypes.enum, isCustom ? 'C' : 'D'); | |
| 2030 | + const expressions = getCachedExpressions(); | |
| 1939 | - }), | |
| 2031 | + return expressions.map(expression => { | |
| 2032 | + const spriteCount = spriteCache[spriteFolderName]?.find(x => x.label === expression)?.files.length ?? 0; | |
| 2033 | + const isCustom = extension_settings.expressions.custom?.includes(expression); | |
| 2034 | + const subtitle = spriteCount == 0 ? '❌ No sprites available for this expression' : | |
| 2035 | + spriteCount > 1 ? `${spriteCount} sprites` : null; | |
| 2036 | + return new SlashCommandEnumValue(expression, | |
| 2037 | + subtitle, | |
| 2038 | + isCustom ? enumTypes.name : enumTypes.enum, | |
| 2039 | + isCustom ? 'C' : 'D'); | |
| 2040 | + }); | |
| 2041 | + }, | |
| 2042 | + sprites: () => { | |
| 2043 | + const spriteFolderName = getSpriteFolderName(); | |
| 2044 | + const sprites = spriteCache[spriteFolderName]?.map(x => x.files)?.flat() ?? []; | |
| 2045 | + return sprites.map(x => { | |
| 2046 | + return new SlashCommandEnumValue(x.title, | |
| 2047 | + x.title !== x.expression ? x.expression : null, | |
| 2048 | + x.isCustom ? enumTypes.name : enumTypes.enum, | |
| 2049 | + x.isCustom ? 'C' : 'D'); | |
| 2050 | + }); | |
| 2051 | + }, | |
| 1940 | 2052 | }; |
| 1941 | 2053 | |
| 1942 | 2054 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1943 | 2055 | name: 'expression-set', |
| 1944 | 2056 | aliases: ['sprite', 'emote'], |
| 1945 | 2057 | callback: setSpriteSlashCommand, |
| 2058 | + namedArgumentList: [ | |
| 2059 | + SlashCommandNamedArgument.fromProps({ | |
| 2060 | + name: 'type', | |
| 2061 | + description: 'Whether to set an expression or a specific sprite.', | |
| 2062 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 2063 | + isRequired: false, | |
| 2064 | + defaultValue: 'expression', | |
| 2065 | + enumList: ['expression', 'sprite'], | |
| 2066 | + }), | |
| 2067 | + ], | |
| 1946 | 2068 | unnamedArgumentList: [ |
| 1947 | 2069 | SlashCommandArgument.fromProps({ |
| 1948 | 2070 | description: 'expression label to set', |
| 1949 | 2071 | typeList: [ARGUMENT_TYPE.STRING], |
| 1950 | 2072 | isRequired: true, |
| 1951 | - enumProvider: localEnumProviders.expressions, | |
| 2073 | + enumProvider: (executor, _) => { | |
| 2074 | + // Check if command is used to set a sprite, then use those enums | |
| 2075 | + const type = executor.namedArgumentList.find(it => it.name == 'type')?.value || 'expression'; | |
| 2076 | + if (type == 'sprite') return localEnumProviders.sprites(); | |
| 2077 | + else return [ | |
| 2078 | + ...localEnumProviders.expressions(), | |
| 2079 | + new SlashCommandEnumValue('#reset', 'Resets the expression (to either default or no sprite)', enumTypes.enum, '❌'), | |
| 2080 | + ]; | |
| 2081 | + }, | |
| 1952 | 2082 | }), |
| 1953 | 2083 | ], |
| 1954 | 2084 | helpString: 'Force sets the expression for the current character.', |
| @@ -1957,13 +2087,21 @@ function migrateSettings() { | ||
| 1957 | 2087 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1958 | 2088 | name: 'expression-folder-override', |
| 1959 | 2089 | aliases: ['spriteoverride', 'costume'], |
| 1960 | 2090 | callback: setSpriteSetCommandsetSpriteFolderCommand, |
| 1961 | 2091 | unnamedArgumentList: [ |
| 1962 | 2092 | new SlashCommandArgument( |
| 1963 | 2093 | 'optional folder', [ARGUMENT_TYPE.STRING], false, |
| 1964 | 2094 | ), |
| 1965 | 2095 | ], |
| 1966 | - 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.', | |
| 2096 | + helpString: ` | |
| 2097 | + <div> | |
| 2098 | + Sets an override sprite folder for the current character.<br /> | |
| 2099 | + In groups, this will apply to the character who last sent a message. | |
| 2100 | + </div> | |
| 2101 | + <div> | |
| 2102 | + 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. | |
| 2103 | + </div> | |
| 2104 | + `, | |
| 1967 | 2105 | })); |
| 1968 | 2106 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1969 | 2107 | name: 'expression-last', |
| @@ -1997,8 +2135,8 @@ function migrateSettings() { | ||
| 1997 | 2135 | helpString: 'Returns the last set expression for the named character.', |
| 1998 | 2136 | })); |
| 1999 | 2137 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2000 | 2138 | name: 'expression-classifylist', |
| 2001 | 2139 | aliases: ['classify-expressions', 'expressions'], |
| 2002 | 2140 | /** @type {(args: {return: string}) => Promise<string>} */ |
| 2003 | 2141 | callback: async (args) => { |
| 2004 | 2142 | let returnType = |
| @@ -109,6 +109,9 @@ img.expression.default { | ||
| 109 | 109 | flex-direction: column; |
| 110 | 110 | align-items: center; |
| 111 | 111 | justify-content: center; |
| 112 | +} | |
| 113 | + | |
| 114 | +.expression_list_image_container { | |
| 112 | 115 | overflow: hidden; |
| 113 | 116 | } |
| 114 | 117 | |
| @@ -1833,14 +1833,15 @@ async function loadContextSettings() { | ||
| 1833 | 1833 | |
| 1834 | 1834 | /** |
| 1835 | 1835 | * Common function to perform fuzzy search with optional caching |
| 1836 | + * @template T | |
| 1836 | 1837 | * @param {string} type - Type of search from fuzzySearchCategories |
| 1837 | 1838 | * @param {anyT[]} data - Data array to search in |
| 1838 | 1839 | * @param {Array<{name: string, weight: number, getFn?: (obj: anyT) => string}>} keys - Fuse.js keys configuration |
| 1839 | 1840 | * @param {string} searchValue - The search term |
| 1840 | 1841 | * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches |
| 1841 | 1842 | * @returns {import('fuse.js').FuseResult<anyT>[]} Results as items with their score |
| 1842 | 1843 | */ |
| 1843 | 1844 | export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) { |
| 1844 | 1845 | // Check cache if provided |
| 1845 | 1846 | if (fuzzySearchCaches) { |
| 1846 | 1847 | const cache = fuzzySearchCaches[type]; |