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 | import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js'; | 3 | import { characters, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types, this_chid } from '../../../script.js'; |
| 4 | import { dragElement, isMobile } from '../../RossAscends-mods.js'; | 4 | import { dragElement, isMobile } from '../../RossAscends-mods.js'; |
| 5 | import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; | 5 | import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 6 | import { loadMovingUIState, power_user } from '../../power-user.js'; | 6 | import { loadMovingUIState, performFuzzySearch, power_user } from '../../power-user.js'; |
| 7 | import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js'; | 7 | import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js'; |
| 8 | import { hideMutedSprites, selected_group } from '../../group-chats.js'; | 8 | import { hideMutedSprites, selected_group } from '../../group-chats.js'; |
| 9 | import { isJsonSchemaSupported } from '../../textgen-settings.js'; | 9 | import { isJsonSchemaSupported } from '../../textgen-settings.js'; |
| @@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | |||
| 12 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 12 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 13 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 13 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 14 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; | 14 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 15 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 15 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; | 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; | 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 18 | import { Popup, POPUP_RESULT } from '../../popup.js'; | 18 | import { Popup, POPUP_RESULT } from '../../popup.js'; |
| @@ -27,8 +27,8 @@ export { MODULE_NAME }; | |||
| 27 | 27 | ||
| 28 | /** | 28 | /** |
| 29 | * @typedef {object} ExpressionImage An expression image | 29 | * @typedef {object} ExpressionImage An expression image |
| 30 | * @property {string?} [expression=null] - The expression | 30 | * @property {string} expression - The expression |
| 31 | * @property {boolean?} [isCustom=null] - If the expression is added by user | 31 | * @property {boolean} [isCustom=false] - If the expression is added by user |
| 32 | * @property {string} fileName - The filename with extension | 32 | * @property {string} fileName - The filename with extension |
| 33 | * @property {string} title - The title for the image | 33 | * @property {string} title - The title for the image |
| 34 | * @property {string} imageSrc - The image source / full path | 34 | * @property {string} imageSrc - The image source / full path |
| @@ -78,9 +78,6 @@ const EXPRESSION_API = { | |||
| 78 | webllm: 3, | 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 | let expressionsList = null; | 81 | let expressionsList = null; |
| 85 | let lastCharacter = undefined; | 82 | let lastCharacter = undefined; |
| 86 | let lastMessage = null; | 83 | let lastMessage = null; |
| @@ -93,6 +90,24 @@ let lastServerResponseTime = 0; | |||
| 93 | export let lastExpression = {}; | 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 | * Returns the fallback expression if explicitly chosen, otherwise the default one | 111 | * Returns the fallback expression if explicitly chosen, otherwise the default one |
| 97 | * @returns {string} expression name | 112 | * @returns {string} expression name |
| 98 | */ | 113 | */ |
| @@ -189,6 +204,7 @@ async function visualNovelSetCharacterSprites(container, name, expression) { | |||
| 189 | const sprites = spriteCache[spriteFolderName]; | 204 | const sprites = spriteCache[spriteFolderName]; |
| 190 | const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`); | 205 | const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`); |
| 191 | const defaultExpression = getFallbackExpression(); | 206 | const defaultExpression = getFallbackExpression(); |
| 207 | // TODO: Visual novel sprites need fixing, currently do not update based on multiple sprites, etc | ||
| 192 | const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path; | 208 | const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path; |
| 193 | const noSprites = sprites.length === 0; | 209 | const noSprites = sprites.length === 0; |
| 194 | 210 | ||
| @@ -460,7 +476,7 @@ async function moduleWorker() { | |||
| 460 | } | 476 | } |
| 461 | 477 | ||
| 462 | const currentLastMessage = getLastCharacterMessage(); | 478 | const currentLastMessage = getLastCharacterMessage(); |
| 463 | let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName(); | 479 | let spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name); |
| 464 | 480 | ||
| 465 | // character has no expressions or it is not loaded | 481 | // character has no expressions or it is not loaded |
| 466 | if (Object.keys(spriteCache).length === 0) { | 482 | if (Object.keys(spriteCache).length === 0) { |
| @@ -550,7 +566,7 @@ async function moduleWorker() { | |||
| 550 | expression = getFallbackExpression(); | 566 | expression = getFallbackExpression(); |
| 551 | } | 567 | } |
| 552 | 568 | ||
| 553 | await sendExpressionCall(spriteFolderName, expression, force, vnMode); | 569 | await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode }); |
| 554 | } | 570 | } |
| 555 | catch (error) { | 571 | catch (error) { |
| 556 | console.log(error); | 572 | console.log(error); |
| @@ -596,48 +612,55 @@ function getFolderNameByMessage(message) { | |||
| 596 | return folderName; | 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 | lastExpression[name.split('/')[0]] = expression; | 626 | lastExpression[name.split('/')[0]] = expression; |
| 601 | if (!vnMode) { | 627 | if (vnMode === null) { |
| 602 | vnMode = isVisualNovelMode(); | 628 | vnMode = isVisualNovelMode(); |
| 603 | } | 629 | } |
| 604 | 630 | ||
| 605 | if (vnMode) { | 631 | if (vnMode) { |
| 606 | await updateVisualNovelMode(name, expression); | 632 | await updateVisualNovelMode(name, expression); |
| 607 | } else { | 633 | } else { |
| 608 | setExpression(name, expression, force); | 634 | setExpression(name, expression, { force: force, overrideSpriteFile: overrideSpriteFile }); |
| 609 | } | 635 | } |
| 610 | } | 636 | } |
| 611 | 637 | ||
| 612 | async function setSpriteSetCommand(_, folder) { | 638 | async function setSpriteFolderCommand(_, folder) { |
| 613 | if (!folder) { | 639 | if (!folder) { |
| 614 | console.log('Clearing sprite set'); | 640 | console.log('Clearing sprite set'); |
| 615 | folder = ''; | 641 | folder = ''; |
| 616 | } | 642 | } |
| 617 | 643 | ||
| 618 | if (folder.startsWith('/') || folder.startsWith('\\')) { | 644 | if (folder.startsWith('/') || folder.startsWith('\\')) { |
| 619 | folder = folder.slice(1); | ||
| 620 | |||
| 621 | const currentLastMessage = getLastCharacterMessage(); | 645 | const currentLastMessage = getLastCharacterMessage(); |
| 646 | folder = folder.slice(1); | ||
| 622 | folder = `${currentLastMessage.name}/${folder}`; | 647 | folder = `${currentLastMessage.name}/${folder}`; |
| 623 | } | 648 | } |
| 624 | 649 | ||
| 625 | $('#expression_override').val(folder.trim()); | 650 | $('#expression_override').val(folder.trim()); |
| 626 | onClickExpressionOverrideButton(); | 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 | return ''; | 654 | return ''; |
| 632 | } | 655 | } |
| 633 | 656 | ||
| 634 | async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) { | 657 | async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) { |
| 635 | if (!text) { | 658 | if (!text) { |
| 636 | toastr.warning('No text provided'); | 659 | toastr.error('No text provided'); |
| 637 | return ''; | 660 | return ''; |
| 638 | } | 661 | } |
| 639 | if (api && !Object.keys(EXPRESSION_API).includes(api)) { | 662 | if (api && !Object.keys(EXPRESSION_API).includes(api)) { |
| 640 | toastr.warning('Invalid API provided'); | 663 | toastr.error('Invalid API provided'); |
| 641 | return ''; | 664 | return ''; |
| 642 | } | 665 | } |
| 643 | 666 | ||
| @@ -653,31 +676,68 @@ async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { | |||
| 653 | return label; | 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 | return ''; | 685 | return ''; |
| 661 | } | 686 | } |
| 662 | 687 | ||
| 663 | const spriteFolderName = getSpriteFolderName(); | 688 | const spriteFolderName = getSpriteFolderName(); |
| 664 | 689 | ||
| 690 | let label = searchTerm; | ||
| 691 | |||
| 692 | /** @type {string?} */ | ||
| 693 | let spriteFile = null; | ||
| 694 | |||
| 665 | await validateImages(spriteFolderName); | 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 | return label; | 741 | return label; |
| 682 | } | 742 | } |
| 683 | 743 | ||
| @@ -714,13 +774,22 @@ function spriteFolderNameFromCharacter(char) { | |||
| 714 | */ | 774 | */ |
| 715 | async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) { | 775 | async function uploadSpriteCommand({ name, label, folder = null, spriteName = null }, imageUrl) { |
| 716 | if (!imageUrl) throw new Error('Image URL is required'); | 776 | if (!imageUrl) throw new Error('Image URL is required'); |
| 717 | if (!label || typeof label !== 'string') throw new Error('Expression label is required'); | 777 | if (!label || typeof label !== 'string') { |
| 778 | toastr.error(t`Expression label is required`, t`Error Uploading Sprite`); | ||
| 779 | return ''; | ||
| 780 | } | ||
| 718 | 781 | ||
| 719 | label = label.replace(/[^a-z]/gi, '').toLowerCase().trim(); | 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 | spriteName = spriteName || label; | 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 | name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; | 794 | name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name; |
| 726 | const char = findChar({ name }); | 795 | const char = findChar({ name }); |
| @@ -1066,7 +1135,7 @@ async function drawSpritesList(character, labels, sprites) { | |||
| 1066 | if (images.length === 0) { | 1135 | if (images.length === 0) { |
| 1067 | const listItem = await getListItem(expression, { | 1136 | const listItem = await getListItem(expression, { |
| 1068 | isCustom, | 1137 | isCustom, |
| 1069 | images: [{ expression, isCustom, ...NO_IMAGE_PLACEHOLDER }], | 1138 | images: [getPlaceholderImage(expression, isCustom)], |
| 1070 | }); | 1139 | }); |
| 1071 | $('#image_list').append(listItem); | 1140 | $('#image_list').append(listItem); |
| 1072 | continue; | 1141 | continue; |
| @@ -1264,12 +1333,13 @@ export async function getExpressionsList() { | |||
| 1264 | /** | 1333 | /** |
| 1265 | * Set the expression of a character. | 1334 | * Set the expression of a character. |
| 1266 | * @param {string} character - The name of the character | 1335 | * @param {string} character - The name of the character |
| 1267 | * @param {string} expression - The expression to set | 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 | * @returns {Promise<void>} A promise that resolves when the expression has been set. | 1340 | * @returns {Promise<void>} A promise that resolves when the expression has been set. |
| 1270 | */ | 1341 | */ |
| 1271 | async function setExpression(character, expression, force = false) { | 1342 | async function setExpression(character, expression, { force = false, overrideSpriteFile = null } = {}) { |
| 1272 | console.debug('entered setExpressions'); | ||
| 1273 | await validateImages(character); | 1343 | await validateImages(character); |
| 1274 | const img = $('img.expression'); | 1344 | const img = $('img.expression'); |
| 1275 | const prevExpressionSrc = img.attr('src'); | 1345 | const prevExpressionSrc = img.attr('src'); |
| @@ -1277,14 +1347,17 @@ async function setExpression(character, expression, force = false) { | |||
| 1277 | 1347 | ||
| 1278 | /** @type {Expression} */ | 1348 | /** @type {Expression} */ |
| 1279 | const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); | 1349 | const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); |
| 1280 | console.debug('checking for expression images to show..'); | ||
| 1281 | if (sprite && sprite.files.length > 0) { | 1350 | if (sprite && sprite.files.length > 0) { |
| 1282 | console.debug('setting expression from character images folder'); | ||
| 1283 | |||
| 1284 | let spriteFile = sprite.files[0]; | 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 | let possibleFiles = sprite.files; | 1361 | let possibleFiles = sprite.files; |
| 1289 | if (extension_settings.expressions.rerollIfSame) { | 1362 | if (extension_settings.expressions.rerollIfSame) { |
| 1290 | possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc); | 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 | //only swap expressions when necessary | 1386 | //only swap expressions when necessary |
| 1313 | if (prevExpressionSrc !== spriteFile.imageSrc | 1387 | if (prevExpressionSrc !== spriteFile.imageSrc |
| 1314 | && !img.hasClass('expression-animating')) { | 1388 | && !img.hasClass('expression-animating')) { |
| @@ -1360,7 +1434,6 @@ async function setExpression(character, expression, force = false) { | |||
| 1360 | expressionHolder.css('min-height', 100); | 1434 | expressionHolder.css('min-height', 100); |
| 1361 | }); | 1435 | }); |
| 1362 | 1436 | ||
| 1363 | |||
| 1364 | expressionClone.removeClass('expression-clone'); | 1437 | expressionClone.removeClass('expression-clone'); |
| 1365 | 1438 | ||
| 1366 | expressionClone.removeClass('default'); | 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 | else { | 1453 | else { |
| 1379 | if (extension_settings.expressions.showDefault) { | 1454 | if (extension_settings.expressions.showDefault) { |
| 1380 | setDefault(); | 1455 | setDefault(); |
| 1456 | } else { | ||
| 1457 | setNone(); | ||
| 1381 | } | 1458 | } |
| 1459 | console.debug('Expression unset'); | ||
| 1382 | } | 1460 | } |
| 1383 | 1461 | ||
| 1384 | function setDefault() { | 1462 | function setDefault() { |
| 1385 | console.debug('setting default'); | 1463 | console.debug('setting default expression'); |
| 1386 | const defImgUrl = `/img/default-expressions/${expression}.png`; | 1464 | const defImgUrl = `/img/default-expressions/${expression}.png`; |
| 1387 | //console.log(defImgUrl); | 1465 | //console.log(defImgUrl); |
| 1388 | img.attr('src', defImgUrl); | 1466 | img.attr('src', defImgUrl); |
| 1389 | img.addClass('default'); | 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 | document.getElementById('expression-holder').style.display = ''; | 1475 | document.getElementById('expression-holder').style.display = ''; |
| 1392 | } | 1476 | } |
| 1393 | 1477 | ||
| 1394 | function onClickExpressionImage() { | 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 | async function onClickExpressionAddCustom() { | 1490 | async function onClickExpressionAddCustom() { |
| @@ -1667,8 +1758,9 @@ async function onClickExpressionOverrideButton() { | |||
| 1667 | inApiCall = true; | 1758 | inApiCall = true; |
| 1668 | $('#visual-novel-wrapper').empty(); | 1759 | $('#visual-novel-wrapper').empty(); |
| 1669 | await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true); | 1760 | await validateImages(overridePath.length === 0 ? currentLastMessage.name : overridePath, true); |
| 1761 | const name = overridePath.length === 0 ? currentLastMessage.name : overridePath; | ||
| 1670 | const expression = await getExpressionLabel(currentLastMessage.mes); | 1762 | const expression = await getExpressionLabel(currentLastMessage.mes); |
| 1671 | await sendExpressionCall(overridePath.length === 0 ? currentLastMessage.name : overridePath, expression, true); | 1763 | await sendExpressionCall(name, expression, { force: true }); |
| 1672 | forceUpdateVisualNovelMode(); | 1764 | forceUpdateVisualNovelMode(); |
| 1673 | } catch (error) { | 1765 | } catch (error) { |
| 1674 | console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`); | 1766 | console.debug(`Setting expression override for ${avatarFileName} failed with error: ${error}`); |
| @@ -1694,7 +1786,7 @@ async function onClickExpressionOverrideRemoveAllButton() { | |||
| 1694 | const currentLastMessage = getLastCharacterMessage(); | 1786 | const currentLastMessage = getLastCharacterMessage(); |
| 1695 | await validateImages(currentLastMessage.name, true); | 1787 | await validateImages(currentLastMessage.name, true); |
| 1696 | const expression = await getExpressionLabel(currentLastMessage.mes); | 1788 | const expression = await getExpressionLabel(currentLastMessage.mes); |
| 1697 | await sendExpressionCall(currentLastMessage.name, expression, true); | 1789 | await sendExpressionCall(currentLastMessage.name, expression, { force: true }); |
| 1698 | forceUpdateVisualNovelMode(); | 1790 | forceUpdateVisualNovelMode(); |
| 1699 | 1791 | ||
| 1700 | console.debug(extension_settings.expressionOverrides); | 1792 | console.debug(extension_settings.expressionOverrides); |
| @@ -1933,22 +2025,60 @@ function migrateSettings() { | |||
| 1933 | eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); | 2025 | eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced); |
| 1934 | 2026 | ||
| 1935 | const localEnumProviders = { | 2027 | const localEnumProviders = { |
| 1936 | expressions: () => getCachedExpressions().map(expression => { | 2028 | expressions: () => { |
| 1937 | const isCustom = extension_settings.expressions.custom?.includes(expression); | 2029 | const spriteFolderName = getSpriteFolderName(); |
| 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 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2054 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1943 | name: 'expression-set', | 2055 | name: 'expression-set', |
| 1944 | aliases: ['sprite', 'emote'], | 2056 | aliases: ['sprite', 'emote'], |
| 1945 | callback: setSpriteSlashCommand, | 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 | unnamedArgumentList: [ | 2068 | unnamedArgumentList: [ |
| 1947 | SlashCommandArgument.fromProps({ | 2069 | SlashCommandArgument.fromProps({ |
| 1948 | description: 'expression label to set', | 2070 | description: 'expression label to set', |
| 1949 | typeList: [ARGUMENT_TYPE.STRING], | 2071 | typeList: [ARGUMENT_TYPE.STRING], |
| 1950 | isRequired: true, | 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 | helpString: 'Force sets the expression for the current character.', | 2084 | helpString: 'Force sets the expression for the current character.', |
| @@ -1957,13 +2087,21 @@ function migrateSettings() { | |||
| 1957 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2087 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1958 | name: 'expression-folder-override', | 2088 | name: 'expression-folder-override', |
| 1959 | aliases: ['spriteoverride', 'costume'], | 2089 | aliases: ['spriteoverride', 'costume'], |
| 1960 | callback: setSpriteSetCommand, | 2090 | callback: setSpriteFolderCommand, |
| 1961 | unnamedArgumentList: [ | 2091 | unnamedArgumentList: [ |
| 1962 | new SlashCommandArgument( | 2092 | new SlashCommandArgument( |
| 1963 | 'optional folder', [ARGUMENT_TYPE.STRING], false, | 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 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2106 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1969 | name: 'expression-last', | 2107 | name: 'expression-last', |
| @@ -1997,8 +2135,8 @@ function migrateSettings() { | |||
| 1997 | helpString: 'Returns the last set expression for the named character.', | 2135 | helpString: 'Returns the last set expression for the named character.', |
| 1998 | })); | 2136 | })); |
| 1999 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 2137 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 2000 | name: 'expression-classify', | 2138 | name: 'expression-list', |
| 2001 | aliases: ['classify-expressions', 'expressions'], | 2139 | aliases: ['expressions'], |
| 2002 | /** @type {(args: {return: string}) => Promise<string>} */ | 2140 | /** @type {(args: {return: string}) => Promise<string>} */ |
| 2003 | callback: async (args) => { | 2141 | callback: async (args) => { |
| 2004 | let returnType = | 2142 | let returnType = |
| @@ -109,6 +109,9 @@ img.expression.default { | |||
| 109 | flex-direction: column; | 109 | flex-direction: column; |
| 110 | align-items: center; | 110 | align-items: center; |
| 111 | justify-content: center; | 111 | justify-content: center; |
| 112 | } | ||
| 113 | |||
| 114 | .expression_list_image_container { | ||
| 112 | overflow: hidden; | 115 | overflow: hidden; |
| 113 | } | 116 | } |
| 114 | 117 | ||
| @@ -1833,14 +1833,15 @@ async function loadContextSettings() { | |||
| 1833 | 1833 | ||
| 1834 | /** | 1834 | /** |
| 1835 | * Common function to perform fuzzy search with optional caching | 1835 | * Common function to perform fuzzy search with optional caching |
| 1836 | * @template T | ||
| 1836 | * @param {string} type - Type of search from fuzzySearchCategories | 1837 | * @param {string} type - Type of search from fuzzySearchCategories |
| 1837 | * @param {any[]} data - Data array to search in | 1838 | * @param {T[]} data - Data array to search in |
| 1838 | * @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration | 1839 | * @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration |
| 1839 | * @param {string} searchValue - The search term | 1840 | * @param {string} searchValue - The search term |
| 1840 | * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches | 1841 | * @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches |
| 1841 | * @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score | 1842 | * @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score |
| 1842 | */ | 1843 | */ |
| 1843 | function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) { | 1844 | export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) { |
| 1844 | // Check cache if provided | 1845 | // Check cache if provided |
| 1845 | if (fuzzySearchCaches) { | 1846 | if (fuzzySearchCaches) { |
| 1846 | const cache = fuzzySearchCaches[type]; | 1847 | const cache = fuzzySearchCaches[type]; |