Update and fix default expressions + emojis - Move "use default emojis" from its own toggle into the fallback dropdown - Add "no fallback" to the fallback dropdown - fix fallback expressions not being consistently used - Switching fallback will now reroll/reset the relevant expression - Add html attributes on the expression image
| @@ -70,6 +70,12 @@ const DEFAULT_EXPRESSIONS = [ | ||
| 70 | 70 | 'surprise', |
| 71 | 71 | 'neutral', |
| 72 | 72 | ]; |
| 73 | + | |
| 74 | +const OPTION_NO_FALLBACK = '#none'; | |
| 75 | +const OPTION_EMOJI_FALLBACK = '#emoji'; | |
| 76 | +const RESET_SPRITE_LABEL = '#reset'; | |
| 77 | + | |
| 78 | + | |
| 73 | 79 | /** @enum {number} */ |
| 74 | 80 | const EXPRESSION_API = { |
| 75 | 81 | local: 0, |
| @@ -106,15 +112,6 @@ function getPlaceholderImage(expression, isCustom = false) { | ||
| 106 | 112 | }; |
| 107 | 113 | } |
| 108 | 114 | |
| 109 | - | |
| 110 | -/** | |
| 111 | - * Returns the fallback expression if explicitly chosen, otherwise the default one | |
| 112 | - * @returns {string} expression name | |
| 113 | - */ | |
| 114 | -function getFallbackExpression() { | |
| 115 | - return extension_settings.expressions.fallback_expression ?? DEFAULT_FALLBACK_EXPRESSION; | |
| 116 | -} | |
| 117 | - | |
| 118 | 115 | function isVisualNovelMode() { |
| 119 | 116 | return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId); |
| 120 | 117 | } |
| @@ -127,12 +124,12 @@ async function forceUpdateVisualNovelMode() { | ||
| 127 | 124 | |
| 128 | 125 | const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick); |
| 129 | 126 | |
| 130 | 127 | async function updateVisualNovelMode(namespriteFolderName, expression) { |
| 131 | 128 | const vnContainer = $('#visual-novel-wrapper'); |
| 132 | 129 | |
| 133 | 130 | await visualNovelRemoveInactive(vnContainer); |
| 134 | 131 | |
| 135 | 132 | const setSpritePromises = await visualNovelSetCharacterSprites(vnContainer, namespriteFolderName, expression); |
| 136 | 133 | |
| 137 | 134 | // calculate layer indices based on recent messages |
| 138 | 135 | await visualNovelUpdateLayers(vnContainer); |
| @@ -414,23 +411,6 @@ async function setImage(img, path) { | ||
| 414 | 411 | }); |
| 415 | 412 | } |
| 416 | 413 | |
| 417 | -function onExpressionsShowDefaultInput() { | |
| 418 | - const value = $(this).prop('checked'); | |
| 419 | - extension_settings.expressions.showDefault = value; | |
| 420 | - saveSettingsDebounced(); | |
| 421 | - | |
| 422 | - const existingImageSrc = $('img.expression').prop('src'); | |
| 423 | - if (existingImageSrc !== undefined) { //if we have an image in src | |
| 424 | - if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default) | |
| 425 | - $('img.expression').prop('src', ''); //remove it | |
| 426 | - lastMessage = null; | |
| 427 | - } | |
| 428 | - if (value) { | |
| 429 | - lastMessage = null; | |
| 430 | - } | |
| 431 | - } | |
| 432 | -} | |
| 433 | - | |
| 434 | 414 | async function moduleWorker() { |
| 435 | 415 | const context = getContext(); |
| 436 | 416 | |
| @@ -546,8 +526,8 @@ async function moduleWorker() { | ||
| 546 | 526 | const force = !!context.groupId; |
| 547 | 527 | |
| 548 | 528 | // Character won't be angry on you for swiping |
| 549 | 529 | if (currentLastMessage.mes == '...' && expressionsList.includes(getFallbackExpression()extension_settings.expressions.fallback_expression)) { |
| 550 | 530 | expression = getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 551 | 531 | } |
| 552 | 532 | |
| 553 | 533 | await sendExpressionCall(spriteFolderName, expression, { force: force, vnMode: vnMode }); |
| @@ -599,23 +579,23 @@ function getFolderNameByMessage(message) { | ||
| 599 | 579 | /** |
| 600 | 580 | * Update the expression for the given character. |
| 601 | 581 | * |
| 602 | 582 | * @param {string} namespriteFolderName The character name, optionally with a sprite folder override, e.g. "folder/expression". |
| 603 | 583 | * @param {string} expression The expression label, e.g. "amusement", "joy", etc. |
| 604 | 584 | * @param {Object} [options] Additional options |
| 605 | 585 | * @param {boolean} [options.force=false] If true, the expression will be sent even if it is the same as the current expression. |
| 606 | 586 | * @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. |
| 607 | 587 | * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name. |
| 608 | 588 | */ |
| 609 | 589 | async function sendExpressionCall(namespriteFolderName, expression, { force = false, vnMode = null, overrideSpriteFile = null } = {}) { |
| 610 | 590 | lastExpression[namespriteFolderName.split('/')[0]] = expression; |
| 611 | 591 | if (vnMode === null) { |
| 612 | 592 | vnMode = isVisualNovelMode(); |
| 613 | 593 | } |
| 614 | 594 | |
| 615 | 595 | if (vnMode) { |
| 616 | 596 | await updateVisualNovelMode(namespriteFolderName, expression); |
| 617 | 597 | } else { |
| 618 | 598 | setExpression(namespriteFolderName, expression, { force: force, overrideSpriteFile: overrideSpriteFile }); |
| 619 | 599 | } |
| 620 | 600 | } |
| 621 | 601 | |
| @@ -679,7 +659,7 @@ async function setSpriteSlashCommand({ type }, searchTerm) { | ||
| 679 | 659 | await validateImages(spriteFolderName); |
| 680 | 660 | |
| 681 | 661 | // Handle reset as a special term and just reset the sprite via expression call |
| 682 | 662 | if (searchTerm === '#reset'RESET_SPRITE_LABEL) { |
| 683 | 663 | await sendExpressionCall(spriteFolderName, label, { force: true }); |
| 684 | 664 | return lastExpression[spriteFolderName] ?? ''; |
| 685 | 665 | } |
| @@ -933,12 +913,12 @@ function onTextGenSettingsReady(args) { | ||
| 933 | 913 | * @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification. |
| 934 | 914 | * @param {object} [options={}] - Optional arguments. |
| 935 | 915 | * @param {string?} [options.customPrompt=null] - The custom prompt to use for classification. |
| 936 | 916 | * @returns {Promise<string?>} - The label of the expression. |
| 937 | 917 | */ |
| 938 | 918 | export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) { |
| 939 | 919 | // Return if text is undefined, saving a costly fetch request |
| 940 | 920 | if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) { |
| 941 | 921 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 942 | 922 | } |
| 943 | 923 | |
| 944 | 924 | if (extension_settings.expressions.translate && typeof window['translate'] === 'function') { |
| @@ -968,7 +948,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 968 | 948 | await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250); |
| 969 | 949 | } catch (error) { |
| 970 | 950 | console.warn('No LLM connection. Using fallback expression', error); |
| 971 | 951 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 972 | 952 | } |
| 973 | 953 | |
| 974 | 954 | const expressionsList = await getExpressionsList(); |
| @@ -981,7 +961,7 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 981 | 961 | case EXPRESSION_API.webllm: { |
| 982 | 962 | if (!isWebLlmSupported()) { |
| 983 | 963 | console.warn('WebLLM is not supported. Using fallback expression'); |
| 984 | 964 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 985 | 965 | } |
| 986 | 966 | |
| 987 | 967 | const expressionsList = await getExpressionsList(); |
| @@ -1014,9 +994,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin | ||
| 1014 | 994 | } break; |
| 1015 | 995 | } |
| 1016 | 996 | } catch (error) { |
| 1017 | 997 | toastr.infoerror('Could not classify expression. Check the console or your backend for more information.'); |
| 1018 | 998 | console.error(error); |
| 1019 | 999 | return getFallbackExpression()extension_settings.expressions.fallback_expression; |
| 1020 | 1000 | } |
| 1021 | 1001 | } |
| 1022 | 1002 | |
| @@ -1231,13 +1211,20 @@ async function renderFallbackExpressionPicker() { | ||
| 1231 | 1211 | const defaultPicker = $('#expression_fallback'); |
| 1232 | 1212 | defaultPicker.empty(); |
| 1233 | 1213 | |
| 1234 | - const fallbackExpression = getFallbackExpression(); | |
| 1214 | + | |
| 1215 | + addOption(OPTION_NO_FALLBACK, '[ No fallback ]', !extension_settings.expressions.fallback_expression); | |
| 1216 | + addOption(OPTION_EMOJI_FALLBACK, '[ Default emojis ]', !!extension_settings.expressions.showDefault); | |
| 1235 | 1217 | |
| 1236 | 1218 | for (const expression of expressions) { |
| 1219 | + addOption(expression, expression, expression == extension_settings.expressions.fallback_expression); | |
| 1220 | + } | |
| 1221 | + | |
| 1222 | + /** @type {(value: string, label: string, isSelected: boolean) => void} */ | |
| 1223 | + function addOption(value, label, isSelected) { | |
| 1237 | 1224 | const option = document.createElement('option'); |
| 1238 | 1225 | option.value = expressionvalue; |
| 1239 | 1226 | option.text = expressionlabel; |
| 1240 | 1227 | option.selected = expression == fallbackExpressionisSelected; |
| 1241 | 1228 | defaultPicker.append(option); |
| 1242 | 1229 | } |
| 1243 | 1230 | } |
| @@ -1326,12 +1313,19 @@ export async function getExpressionsList() { | ||
| 1326 | 1313 | * @param {object} [options] - Options to select the sprite |
| 1327 | 1314 | * @param {string} [options.prevExpressionSrc=null] - The source of the previous expression |
| 1328 | 1315 | * @param {string} [options.overrideSpriteFile=null] - The file name of the sprite to select |
| 1329 | 1316 | * @returns {ExpressionImage?} - The selected sprite |
| 1330 | 1317 | */ |
| 1331 | 1318 | function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc = null, overrideSpriteFile = null } = {}) { |
| 1332 | - const sprite = (spriteCache[spriteFolderName] && spriteCache[spriteFolderName].find(x => x.label === expression)); | |
| 1319 | + if (!spriteCache[spriteFolderName]) return null; | |
| 1333 | - if (!(sprite?.files.length > 0)) | |
| 1320 | + if (expression === RESET_SPRITE_LABEL) return null; | |
| 1334 | - return null; | |
| 1321 | + | |
| 1322 | + // Search for sprites of that expression - or fallback expression sprites if enabled | |
| 1323 | + let sprite = spriteCache[spriteFolderName].find(x => x.label === expression); | |
| 1324 | + if (!(sprite?.files.length > 0) && extension_settings.expressions.fallback_expression) { | |
| 1325 | + sprite = spriteCache[spriteFolderName].find(x => x.label === extension_settings.expressions.fallback_expression); | |
| 1326 | + console.debug('Expression', expression, 'not found. Using fallback expression', extension_settings.expressions.fallback_expression); | |
| 1327 | + } | |
| 1328 | + if (!(sprite?.files.length > 0)) return null; | |
| 1335 | 1329 | |
| 1336 | 1330 | let spriteFile = sprite.files[0]; |
| 1337 | 1331 | |
| @@ -1397,6 +1391,10 @@ async function setExpression(spriteFolderName, expression, { force = false, over | ||
| 1397 | 1391 | expressionClone.attr('id', '').css({ opacity: 0 }); |
| 1398 | 1392 | //add new sprite path to clone src |
| 1399 | 1393 | expressionClone.attr('src', spriteFile.imageSrc); |
| 1394 | + //set relevant data tags | |
| 1395 | + expressionClone.attr('data-sprite-folder-name', spriteFolderName); | |
| 1396 | + expressionClone.attr('data-expression', expression); | |
| 1397 | + expressionClone.attr('data-sprite-filename', spriteFile.fileName); | |
| 1400 | 1398 | //add invisible clone to html |
| 1401 | 1399 | expressionClone.appendTo($('#expression-holder')); |
| 1402 | 1400 | |
| @@ -1442,12 +1440,12 @@ async function setExpression(spriteFolderName, expression, { force = false, over | ||
| 1442 | 1440 | |
| 1443 | 1441 | expressionClone.removeClass('default'); |
| 1444 | 1442 | expressionClone.off('error'); |
| 1445 | 1443 | expressionClone.on('error', function (error) { |
| 1446 | 1444 | console.debug('Expression image error', spriteFile.imageSrc, error); |
| 1447 | 1445 | $(this).attr('src', ''); |
| 1448 | 1446 | $(this).off('error'); |
| 1449 | 1447 | if (force && extension_settings.expressions.showDefault) { |
| 1450 | 1448 | setDefaultsetDefaultEmojiForImage(img, expression); |
| 1451 | 1449 | } |
| 1452 | 1450 | }); |
| 1453 | 1451 | } |
| @@ -1455,28 +1453,50 @@ async function setExpression(spriteFolderName, expression, { force = false, over | ||
| 1455 | 1453 | console.info('Expression set', { expression: spriteFile.expression, file: spriteFile.fileName }); |
| 1456 | 1454 | } |
| 1457 | 1455 | else { |
| 1458 | - if (extension_settings.expressions.showDefault) { | |
| 1456 | + img.attr('data-sprite-folder-name', spriteFolderName); | |
| 1459 | - setDefault(); | |
| 1457 | + img.attr('data-expression', expression); | |
| 1458 | + | |
| 1459 | + $(img).off('error'); | |
| 1460 | + | |
| 1461 | + if (extension_settings.expressions.showDefault && expression !== RESET_SPRITE_LABEL) { | |
| 1462 | + setDefaultEmojiForImage(img, expression); | |
| 1460 | 1463 | } else { |
| 1461 | 1464 | setNonesetNoneForImage(img, expression); |
| 1462 | 1465 | } |
| 1463 | 1466 | console.debug('Expression unset - No sprite found', { expression: expression }); |
| 1464 | 1467 | } |
| 1465 | 1468 | |
| 1466 | - function setDefault() { | |
| 1469 | + document.getElementById('expression-holder').style.display = ''; | |
| 1467 | - console.debug('setting default expression'); | |
| 1470 | +} | |
| 1468 | - const defImgUrl = `/img/default-expressions/${expression}.png`; | |
| 1471 | + | |
| 1469 | - //console.log(defImgUrl); | |
| 1472 | +/** | |
| 1470 | - img.attr('src', defImgUrl); | |
| 1473 | + * Sets the default expression image for the given image element and expression | |
| 1471 | - img.addClass('default'); | |
| 1474 | + * @param {JQuery<HTMLElement>} img - The image element to set the default expression for | |
| 1472 | - } | |
| 1475 | + * @param {string} expression - The expression label to use for the default image | |
| 1473 | - function setNone() { | |
| 1476 | + */ | |
| 1474 | - console.debug('setting no expression'); | |
| 1477 | +function setDefaultEmojiForImage(img, expression) { | |
| 1475 | - img.attr('src', ''); | |
| 1478 | + if (extension_settings.expressions.custom?.includes(expression)) { | |
| 1476 | - img.removeClass('default'); | |
| 1479 | + console.debug(`Can't set default emoji for a custom expression (${expression}). setting to ${DEFAULT_FALLBACK_EXPRESSION} instead.`); | |
| 1480 | + expression = DEFAULT_FALLBACK_EXPRESSION; | |
| 1477 | 1481 | } |
| 1478 | 1482 | |
| 1479 | - document.getElementById('expression-holder').style.display = ''; | |
| 1483 | + const defImgUrl = `/img/default-expressions/${expression}.png`; | |
| 1484 | + img.attr('src', defImgUrl); | |
| 1485 | + img.attr('data-expression', expression); | |
| 1486 | + img.attr('data-sprite-filename', null); | |
| 1487 | + img.addClass('default'); | |
| 1488 | +} | |
| 1489 | + | |
| 1490 | +/** | |
| 1491 | + * Sets the image element to display no expression by clearing its source attribute. | |
| 1492 | + * @param {JQuery<HTMLElement>} img - The image element to clear the expression for | |
| 1493 | + * @param {string} expression - The expression label to use | |
| 1494 | + */ | |
| 1495 | +function setNoneForImage(img, expression) { | |
| 1496 | + img.attr('src', ''); | |
| 1497 | + img.attr('data-expression', expression); | |
| 1498 | + img.attr('data-sprite-filename', null); | |
| 1499 | + img.removeClass('default'); | |
| 1480 | 1500 | } |
| 1481 | 1501 | |
| 1482 | 1502 | function onClickExpressionImage() { |
| @@ -1504,19 +1524,15 @@ async function onClickExpressionAddCustom() { | ||
| 1504 | 1524 | |
| 1505 | 1525 | // a-z, 0-9, dashes and underscores only |
| 1506 | 1526 | if (!/^[a-z0-9-_]+$/.test(expressionName)) { |
| 1507 | 1527 | toastr.infowarning('Invalid custom expression name provided', 'Add Custom Expression'); |
| 1508 | 1528 | return; |
| 1509 | 1529 | } |
| 1510 | - | |
| 1511 | - // Check if expression name already exists in default expressions | |
| 1512 | 1530 | if (DEFAULT_EXPRESSIONS.includes(expressionName)) { |
| 1513 | 1531 | toastr.infowarning('Expression name already exists', 'Add Custom Expression'); |
| 1514 | 1532 | return; |
| 1515 | 1533 | } |
| 1516 | - | |
| 1517 | - // Check if expression name already exists in custom expressions | |
| 1518 | 1534 | if (extension_settings.expressions.custom.includes(expressionName)) { |
| 1519 | 1535 | toastr.infowarning('Custom expression already exists', 'Add Custom Expression'); |
| 1520 | 1536 | return; |
| 1521 | 1537 | } |
| 1522 | 1538 | |
| @@ -1550,8 +1566,8 @@ async function onClickExpressionRemoveCustom() { | ||
| 1550 | 1566 | // Remove custom expression from settings |
| 1551 | 1567 | const index = extension_settings.expressions.custom.indexOf(selectedExpression); |
| 1552 | 1568 | extension_settings.expressions.custom.splice(index, 1); |
| 1553 | 1569 | if (selectedExpression == getFallbackExpression()extension_settings.expressions.fallback_expression) { |
| 1554 | 1570 | toastr.warning(`Deleted custom expression '${selectedExpression}' that was also selected as the fallback expression.\nFallback expression has been reset to '${DEFAULT_FALLBACK_EXPRESSION}'.`, 'Remove Custom Expression'); |
| 1555 | 1571 | extension_settings.expressions.fallback_expression = DEFAULT_FALLBACK_EXPRESSION; |
| 1556 | 1572 | } |
| 1557 | 1573 | await renderAdditionalExpressionSettings(); |
| @@ -1575,12 +1591,35 @@ function onExpressionApiChanged() { | ||
| 1575 | 1591 | } |
| 1576 | 1592 | } |
| 1577 | 1593 | |
| 1578 | 1594 | async function onExpressionFallbackChanged() { |
| 1579 | - const expression = this.value; | |
| 1595 | + /** @type {HTMLSelectElement} */ | |
| 1580 | - if (expression) { | |
| 1596 | + const select = this; | |
| 1581 | - extension_settings.expressions.fallback_expression = expression; | |
| 1597 | + const selectedValue = select.value; | |
| 1582 | - saveSettingsDebounced(); | |
| 1598 | + | |
| 1599 | + switch (selectedValue) { | |
| 1600 | + case OPTION_NO_FALLBACK: | |
| 1601 | + extension_settings.expressions.fallback_expression = null; | |
| 1602 | + extension_settings.expressions.showDefault = false; | |
| 1603 | + break; | |
| 1604 | + case OPTION_EMOJI_FALLBACK: | |
| 1605 | + extension_settings.expressions.fallback_expression = null; | |
| 1606 | + extension_settings.expressions.showDefault = true; | |
| 1607 | + break; | |
| 1608 | + default: | |
| 1609 | + extension_settings.expressions.fallback_expression = selectedValue; | |
| 1610 | + extension_settings.expressions.showDefault = false; | |
| 1611 | + break; | |
| 1583 | 1612 | } |
| 1613 | + | |
| 1614 | + const img = $('img.expression'); | |
| 1615 | + const spriteFolderName = img.attr('data-sprite-folder-name'); | |
| 1616 | + const expression = img.attr('data-expression'); | |
| 1617 | + | |
| 1618 | + if (spriteFolderName && expression) { | |
| 1619 | + await sendExpressionCall(spriteFolderName, expression, { force: true }); | |
| 1620 | + } | |
| 1621 | + | |
| 1622 | + saveSettingsDebounced(); | |
| 1584 | 1623 | } |
| 1585 | 1624 | |
| 1586 | 1625 | async function handleFileUpload(url, formData) { |
| @@ -1924,6 +1963,16 @@ function migrateSettings() { | ||
| 1924 | 1963 | extension_settings.expressions.llmPrompt = DEFAULT_LLM_PROMPT; |
| 1925 | 1964 | saveSettingsDebounced(); |
| 1926 | 1965 | } |
| 1966 | + | |
| 1967 | + if (extension_settings.expressions.allowMultiple === undefined) { | |
| 1968 | + extension_settings.expressions.allowMultiple = true; | |
| 1969 | + saveSettingsDebounced(); | |
| 1970 | + } | |
| 1971 | + | |
| 1972 | + if (extension_settings.expressions.showDefault && extension_settings.expressions.fallback_expression !== undefined) { | |
| 1973 | + extension_settings.expressions.showDefault = false; | |
| 1974 | + saveSettingsDebounced(); | |
| 1975 | + } | |
| 1927 | 1976 | } |
| 1928 | 1977 | |
| 1929 | 1978 | (async function () { |
| @@ -1950,9 +1999,7 @@ function migrateSettings() { | ||
| 1950 | 1999 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'settings'); |
| 1951 | 2000 | $('#expressions_container').append(template); |
| 1952 | 2001 | $('#expression_override_button').on('click', onClickExpressionOverrideButton); |
| 1953 | - $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); | |
| 1954 | 2002 | $('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton); |
| 1955 | - $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); | |
| 1956 | 2003 | $('#expression_translate').prop('checked', extension_settings.expressions.translate).on('input', function () { |
| 1957 | 2004 | extension_settings.expressions.translate = !!$(this).prop('checked'); |
| 1958 | 2005 | saveSettingsDebounced(); |
| @@ -2080,7 +2127,7 @@ function migrateSettings() { | ||
| 2080 | 2127 | if (type == 'sprite') return localEnumProviders.sprites(); |
| 2081 | 2128 | else return [ |
| 2082 | 2129 | ...localEnumProviders.expressions(), |
| 2083 | 2130 | new SlashCommandEnumValue('#reset'RESET_SPRITE_LABEL, 'Resets the expression (to either default or no sprite)', enumTypes.enum, '❌'), |
| 2084 | 2131 | ]; |
| 2085 | 2132 | }, |
| 2086 | 2133 | }), |
| @@ -10,10 +10,6 @@ | ||
| 10 | 10 | <input id="expression_translate" type="checkbox"> |
| 11 | 11 | <span data-i18n="Translate text to English before classification">Translate text to English before classification</span> |
| 12 | 12 | </label> |
| 13 | - <label class="checkbox_label" for="expressions_show_default"> | |
| 14 | - <input id="expressions_show_default" type="checkbox"> | |
| 15 | - <span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span> | |
| 16 | - </label> | |
| 17 | 13 | <label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected."> |
| 18 | 14 | <input id="expressions_allow_multiple" type="checkbox"> |
| 19 | 15 | <span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span> |