mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	Upload or delete a sprite image.
This commit is contained in:
		| @@ -1,385 +1,470 @@ | ||||
| import { saveSettingsDebounced } from "../../../script.js"; | ||||
| import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js"; | ||||
| export { MODULE_NAME }; | ||||
|  | ||||
| const MODULE_NAME = 'expressions'; | ||||
| const UPDATE_INTERVAL = 2000; | ||||
| const DEFAULT_EXPRESSIONS = [ | ||||
|     "admiration", | ||||
|     "amusement", | ||||
|     "anger", | ||||
|     "annoyance", | ||||
|     "approval", | ||||
|     "caring", | ||||
|     "confusion", | ||||
|     "curiosity", | ||||
|     "desire", | ||||
|     "disappointment", | ||||
|     "disapproval", | ||||
|     "disgust", | ||||
|     "embarrassment", | ||||
|     "excitement", | ||||
|     "fear", | ||||
|     "gratitude", | ||||
|     "grief", | ||||
|     "joy", | ||||
|     "love", | ||||
|     "nervousness", | ||||
|     "optimism", | ||||
|     "pride", | ||||
|     "realization", | ||||
|     "relief", | ||||
|     "remorse", | ||||
|     "sadness", | ||||
|     "surprise", | ||||
|     "neutral" | ||||
| ]; | ||||
|  | ||||
| let expressionsList = null; | ||||
| let lastCharacter = undefined; | ||||
| let lastMessage = null; | ||||
| let spriteCache = {}; | ||||
| let inApiCall = false; | ||||
|  | ||||
| function onExpressionsShowDefaultInput() { | ||||
|     const value = $(this).prop('checked'); | ||||
|     extension_settings.expressions.showDefault = value; | ||||
|     saveSettingsDebounced(); | ||||
|  | ||||
|     const existingImageSrc = $('img.expression').prop('src'); | ||||
|     if (existingImageSrc !== undefined) {                      //if we have an image in src | ||||
|         if (!value && existingImageSrc.includes('/img/default-expressions/')) {    //and that image is from /img/ (default) | ||||
|             $('img.expression').prop('src', '');               //remove it | ||||
|             lastMessage = null; | ||||
|         } | ||||
|         if (value) { | ||||
|             lastMessage = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| let isWorkerBusy = false; | ||||
|  | ||||
| async function moduleWorkerWrapper() { | ||||
|     // Don't touch me I'm busy... | ||||
|     if (isWorkerBusy) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // I'm free. Let's update! | ||||
|     try { | ||||
|         isWorkerBusy = true; | ||||
|         await moduleWorker(); | ||||
|     } | ||||
|     finally { | ||||
|         isWorkerBusy = false; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function moduleWorker() { | ||||
|     const context = getContext(); | ||||
|  | ||||
|     // non-characters not supported | ||||
|     if (!context.groupId && context.characterId === undefined) { | ||||
|         removeExpression(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // character changed | ||||
|     if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { | ||||
|         removeExpression(); | ||||
|         spriteCache = {}; | ||||
|     } | ||||
|  | ||||
|     const currentLastMessage = getLastCharacterMessage(); | ||||
|  | ||||
|     // character has no expressions or it is not loaded | ||||
|     if (Object.keys(spriteCache).length === 0) { | ||||
|         await validateImages(currentLastMessage.name); | ||||
|         lastCharacter = context.groupId || context.characterId; | ||||
|     } | ||||
|  | ||||
|     const offlineMode = $('.expression_settings .offline_mode'); | ||||
|     if (!modules.includes('classify')) { | ||||
|         $('.expression_settings').show(); | ||||
|         offlineMode.css('display', 'block'); | ||||
|         lastCharacter = context.groupId || context.characterId; | ||||
|  | ||||
|         if (context.groupId) { | ||||
|             await validateImages(currentLastMessage.name, true); | ||||
|         } | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|     else { | ||||
|         // force reload expressions list on connect to API | ||||
|         if (offlineMode.is(':visible')) { | ||||
|             expressionsList = null; | ||||
|             spriteCache = {}; | ||||
|             expressionsList = await getExpressionsList(); | ||||
|             await validateImages(currentLastMessage.name, true); | ||||
|         } | ||||
|  | ||||
|         offlineMode.css('display', 'none'); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // check if last message changed | ||||
|     if ((lastCharacter === context.characterId || lastCharacter === context.groupId) | ||||
|         && lastMessage === currentLastMessage.mes) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // API is busy | ||||
|     if (inApiCall) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         inApiCall = true; | ||||
|         const url = new URL(getApiUrl()); | ||||
|         url.pathname = '/api/classify'; | ||||
|  | ||||
|         const apiResult = await fetch(url, { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json', | ||||
|                 'Bypass-Tunnel-Reminder': 'bypass', | ||||
|             }, | ||||
|             body: JSON.stringify({ text: currentLastMessage.mes }) | ||||
|         }); | ||||
|  | ||||
|         if (apiResult.ok) { | ||||
|             const name = context.groupId ? currentLastMessage.name : context.name2; | ||||
|             const force = !!context.groupId; | ||||
|             const data = await apiResult.json(); | ||||
|             let expression = data.classification[0].label; | ||||
|  | ||||
|             // Character won't be angry on you for swiping | ||||
|             if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { | ||||
|                 expression = 'joy'; | ||||
|             } | ||||
|  | ||||
|             setExpression(name, expression, force); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|     catch (error) { | ||||
|         console.log(error); | ||||
|     } | ||||
|     finally { | ||||
|         inApiCall = false; | ||||
|         lastCharacter = context.groupId || context.characterId; | ||||
|         lastMessage = currentLastMessage.mes; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getLastCharacterMessage() { | ||||
|     const context = getContext(); | ||||
|     const reversedChat = context.chat.slice().reverse(); | ||||
|  | ||||
|     for (let mes of reversedChat) { | ||||
|         if (mes.is_user || mes.is_system) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         return { mes: mes.mes, name: mes.name }; | ||||
|     } | ||||
|  | ||||
|     return { mes: '', name: null }; | ||||
| } | ||||
|  | ||||
| function removeExpression() { | ||||
|     lastMessage = null; | ||||
|     $('img.expression').off('error'); | ||||
|     $('img.expression').prop('src', ''); | ||||
|     $('img.expression').removeClass('default'); | ||||
|     $('.expression_settings').hide(); | ||||
| } | ||||
|  | ||||
| async function validateImages(character, forceRedrawCached) { | ||||
|     if (!character) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const labels = await getExpressionsList(); | ||||
|  | ||||
|     if (spriteCache[character]) { | ||||
|         if (forceRedrawCached && $('#image_list').data('name') !== character) { | ||||
|             console.log('force redrawing character sprites list') | ||||
|             drawSpritesList(character, labels, spriteCache[character]); | ||||
|         } | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const sprites = await getSpritesList(character); | ||||
|     let validExpressions = drawSpritesList(character, labels, sprites); | ||||
|     spriteCache[character] = validExpressions; | ||||
| } | ||||
|  | ||||
| function drawSpritesList(character, labels, sprites) { | ||||
|     let validExpressions = []; | ||||
|     $('.expression_settings').show(); | ||||
|     $('#image_list').empty(); | ||||
|     $('#image_list').data('name', character); | ||||
|     labels.sort().forEach((item) => { | ||||
|         const sprite = sprites.find(x => x.label == item); | ||||
|  | ||||
|         if (sprite) { | ||||
|             validExpressions.push(sprite); | ||||
|             $('#image_list').append(getListItem(item, sprite.path, 'success')); | ||||
|         } | ||||
|         else { | ||||
|             $('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure')); | ||||
|         } | ||||
|     }); | ||||
|     return validExpressions; | ||||
| } | ||||
|  | ||||
| function getListItem(item, imageSrc, textClass) { | ||||
|     return ` | ||||
|         <div id="${item}" class="expression_list_item"> | ||||
|             <span class="expression_list_title ${textClass}">${item}</span> | ||||
|             <img class="expression_list_image" src="${imageSrc}" /> | ||||
|         </div> | ||||
|     `; | ||||
| } | ||||
|  | ||||
| async function getSpritesList(name) { | ||||
|     console.log('getting sprites list'); | ||||
|  | ||||
|     try { | ||||
|         const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`); | ||||
|  | ||||
|         let sprites = result.ok ? (await result.json()) : []; | ||||
|         return sprites; | ||||
|     } | ||||
|     catch (err) { | ||||
|         console.log(err); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function getExpressionsList() { | ||||
|     // get something for offline mode (default images) | ||||
|     if (!modules.includes('classify')) { | ||||
|         return DEFAULT_EXPRESSIONS; | ||||
|     } | ||||
|  | ||||
|     if (Array.isArray(expressionsList)) { | ||||
|         return expressionsList; | ||||
|     } | ||||
|  | ||||
|     const url = new URL(getApiUrl()); | ||||
|     url.pathname = '/api/classify/labels'; | ||||
|  | ||||
|     try { | ||||
|         const apiResult = await fetch(url, { | ||||
|             method: 'GET', | ||||
|             headers: { 'Bypass-Tunnel-Reminder': 'bypass' }, | ||||
|         }); | ||||
|  | ||||
|         if (apiResult.ok) { | ||||
|  | ||||
|             const data = await apiResult.json(); | ||||
|             expressionsList = data.labels; | ||||
|             return expressionsList; | ||||
|         } | ||||
|     } | ||||
|     catch (error) { | ||||
|         console.log(error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function setExpression(character, expression, force) { | ||||
|     console.log('entered setExpressions'); | ||||
|     await validateImages(character); | ||||
|     const img = $('img.expression'); | ||||
|  | ||||
|     const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); | ||||
|     console.log('checking for expression images to show..'); | ||||
|     if (sprite) { | ||||
|         console.log('setting expression from character images folder'); | ||||
|         img.attr('src', sprite.path); | ||||
|         img.removeClass('default'); | ||||
|         img.off('error'); | ||||
|         img.on('error', function () { | ||||
|             $(this).attr('src', ''); | ||||
|             if (force && extension_settings.expressions.showDefault) { | ||||
|                 setDefault(); | ||||
|             } | ||||
|         }); | ||||
|     } else { | ||||
|         if (extension_settings.expressions.showDefault) { | ||||
|             setDefault(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function setDefault() { | ||||
|         console.log('setting default'); | ||||
|         const defImgUrl = `/img/default-expressions/${expression}.png`; | ||||
|         //console.log(defImgUrl); | ||||
|         img.attr('src', defImgUrl); | ||||
|         img.addClass('default'); | ||||
|     } | ||||
|     document.getElementById("expression-holder").style.display = ''; | ||||
| } | ||||
|  | ||||
| function onClickExpressionImage() { | ||||
|     // online mode doesn't need force set | ||||
|     if (modules.includes('classify')) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const expression = $(this).attr('id'); | ||||
|     const name = getLastCharacterMessage().name; | ||||
|  | ||||
|     if ($(this).find('.failure').length === 0) { | ||||
|         setExpression(name, expression, true); | ||||
|     } | ||||
| } | ||||
|  | ||||
| (function () { | ||||
|     function addExpressionImage() { | ||||
|         const html = ` | ||||
|         <div id="expression-wrapper"> | ||||
|             <div id="expression-holder" class="expression-holder" style="display:none;"> | ||||
|                 <div id="expression-holderheader" class="fa-solid fa-grip drag-grabber"></div> | ||||
|                 <img id="expression-image" class="expression"> | ||||
|             </div> | ||||
|         </div>`; | ||||
|         $('body').append(html); | ||||
|     } | ||||
|     function addSettings() { | ||||
|  | ||||
|         const html = ` | ||||
|         <div class="expression_settings"> | ||||
|             <div class="inline-drawer"> | ||||
|             <div class="inline-drawer-toggle inline-drawer-header"> | ||||
|                 <b>Expression images</b> | ||||
|                 <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> | ||||
|             </div> | ||||
|             <div class="inline-drawer-content"> | ||||
|                 <p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p> | ||||
|                 <div id="image_list"></div> | ||||
|                 <p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character. | ||||
|                 Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p> | ||||
|                 <label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label> | ||||
|             </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         `; | ||||
|         $('#extensions_settings').append(html); | ||||
|         $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); | ||||
|         $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); | ||||
|         $(document).on('click', '.expression_list_item', onClickExpressionImage); | ||||
|         $('.expression_settings').hide(); | ||||
|     } | ||||
|  | ||||
|     addExpressionImage(); | ||||
|     addSettings(); | ||||
|     setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); | ||||
|     moduleWorkerWrapper(); | ||||
| })(); | ||||
| import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js"; | ||||
| import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js"; | ||||
| export { MODULE_NAME }; | ||||
|  | ||||
| const MODULE_NAME = 'expressions'; | ||||
| const UPDATE_INTERVAL = 2000; | ||||
| const DEFAULT_EXPRESSIONS = [ | ||||
|     "admiration", | ||||
|     "amusement", | ||||
|     "anger", | ||||
|     "annoyance", | ||||
|     "approval", | ||||
|     "caring", | ||||
|     "confusion", | ||||
|     "curiosity", | ||||
|     "desire", | ||||
|     "disappointment", | ||||
|     "disapproval", | ||||
|     "disgust", | ||||
|     "embarrassment", | ||||
|     "excitement", | ||||
|     "fear", | ||||
|     "gratitude", | ||||
|     "grief", | ||||
|     "joy", | ||||
|     "love", | ||||
|     "nervousness", | ||||
|     "optimism", | ||||
|     "pride", | ||||
|     "realization", | ||||
|     "relief", | ||||
|     "remorse", | ||||
|     "sadness", | ||||
|     "surprise", | ||||
|     "neutral" | ||||
| ]; | ||||
|  | ||||
| let expressionsList = null; | ||||
| let lastCharacter = undefined; | ||||
| let lastMessage = null; | ||||
| let spriteCache = {}; | ||||
| let inApiCall = false; | ||||
|  | ||||
| function onExpressionsShowDefaultInput() { | ||||
|     const value = $(this).prop('checked'); | ||||
|     extension_settings.expressions.showDefault = value; | ||||
|     saveSettingsDebounced(); | ||||
|  | ||||
|     const existingImageSrc = $('img.expression').prop('src'); | ||||
|     if (existingImageSrc !== undefined) {                      //if we have an image in src | ||||
|         if (!value && existingImageSrc.includes('/img/default-expressions/')) {    //and that image is from /img/ (default) | ||||
|             $('img.expression').prop('src', '');               //remove it | ||||
|             lastMessage = null; | ||||
|         } | ||||
|         if (value) { | ||||
|             lastMessage = null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| let isWorkerBusy = false; | ||||
|  | ||||
| async function moduleWorkerWrapper() { | ||||
|     // Don't touch me I'm busy... | ||||
|     if (isWorkerBusy) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // I'm free. Let's update! | ||||
|     try { | ||||
|         isWorkerBusy = true; | ||||
|         await moduleWorker(); | ||||
|     } | ||||
|     finally { | ||||
|         isWorkerBusy = false; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function moduleWorker() { | ||||
|     const context = getContext(); | ||||
|  | ||||
|     // non-characters not supported | ||||
|     if (!context.groupId && context.characterId === undefined) { | ||||
|         removeExpression(); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // character changed | ||||
|     if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { | ||||
|         removeExpression(); | ||||
|         spriteCache = {}; | ||||
|     } | ||||
|  | ||||
|     const currentLastMessage = getLastCharacterMessage(); | ||||
|  | ||||
|     // character has no expressions or it is not loaded | ||||
|     if (Object.keys(spriteCache).length === 0) { | ||||
|         await validateImages(currentLastMessage.name); | ||||
|         lastCharacter = context.groupId || context.characterId; | ||||
|     } | ||||
|  | ||||
|     const offlineMode = $('.expression_settings .offline_mode'); | ||||
|     if (!modules.includes('classify')) { | ||||
|         $('.expression_settings').show(); | ||||
|         offlineMode.css('display', 'block'); | ||||
|         lastCharacter = context.groupId || context.characterId; | ||||
|  | ||||
|         if (context.groupId) { | ||||
|             await validateImages(currentLastMessage.name, true); | ||||
|         } | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|     else { | ||||
|         // force reload expressions list on connect to API | ||||
|         if (offlineMode.is(':visible')) { | ||||
|             expressionsList = null; | ||||
|             spriteCache = {}; | ||||
|             expressionsList = await getExpressionsList(); | ||||
|             await validateImages(currentLastMessage.name, true); | ||||
|         } | ||||
|  | ||||
|         offlineMode.css('display', 'none'); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     // check if last message changed | ||||
|     if ((lastCharacter === context.characterId || lastCharacter === context.groupId) | ||||
|         && lastMessage === currentLastMessage.mes) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // API is busy | ||||
|     if (inApiCall) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         inApiCall = true; | ||||
|         const url = new URL(getApiUrl()); | ||||
|         url.pathname = '/api/classify'; | ||||
|  | ||||
|         const apiResult = await fetch(url, { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json', | ||||
|                 'Bypass-Tunnel-Reminder': 'bypass', | ||||
|             }, | ||||
|             body: JSON.stringify({ text: currentLastMessage.mes }) | ||||
|         }); | ||||
|  | ||||
|         if (apiResult.ok) { | ||||
|             const name = context.groupId ? currentLastMessage.name : context.name2; | ||||
|             const force = !!context.groupId; | ||||
|             const data = await apiResult.json(); | ||||
|             let expression = data.classification[0].label; | ||||
|  | ||||
|             // Character won't be angry on you for swiping | ||||
|             if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { | ||||
|                 expression = 'joy'; | ||||
|             } | ||||
|  | ||||
|             setExpression(name, expression, force); | ||||
|         } | ||||
|  | ||||
|     } | ||||
|     catch (error) { | ||||
|         console.log(error); | ||||
|     } | ||||
|     finally { | ||||
|         inApiCall = false; | ||||
|         lastCharacter = context.groupId || context.characterId; | ||||
|         lastMessage = currentLastMessage.mes; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getLastCharacterMessage() { | ||||
|     const context = getContext(); | ||||
|     const reversedChat = context.chat.slice().reverse(); | ||||
|  | ||||
|     for (let mes of reversedChat) { | ||||
|         if (mes.is_user || mes.is_system) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         return { mes: mes.mes, name: mes.name }; | ||||
|     } | ||||
|  | ||||
|     return { mes: '', name: null }; | ||||
| } | ||||
|  | ||||
| function removeExpression() { | ||||
|     lastMessage = null; | ||||
|     $('img.expression').off('error'); | ||||
|     $('img.expression').prop('src', ''); | ||||
|     $('img.expression').removeClass('default'); | ||||
|     $('.expression_settings').hide(); | ||||
| } | ||||
|  | ||||
| async function validateImages(character, forceRedrawCached) { | ||||
|     if (!character) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const labels = await getExpressionsList(); | ||||
|  | ||||
|     if (spriteCache[character]) { | ||||
|         if (forceRedrawCached && $('#image_list').data('name') !== character) { | ||||
|             console.log('force redrawing character sprites list') | ||||
|             drawSpritesList(character, labels, spriteCache[character]); | ||||
|         } | ||||
|  | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const sprites = await getSpritesList(character); | ||||
|     let validExpressions = drawSpritesList(character, labels, sprites); | ||||
|     spriteCache[character] = validExpressions; | ||||
| } | ||||
|  | ||||
| function drawSpritesList(character, labels, sprites) { | ||||
|     let validExpressions = []; | ||||
|     $('.expression_settings').show(); | ||||
|     $('#image_list').empty(); | ||||
|     $('#image_list').data('name', character); | ||||
|     labels.sort().forEach((item) => { | ||||
|         const sprite = sprites.find(x => x.label == item); | ||||
|  | ||||
|         if (sprite) { | ||||
|             validExpressions.push(sprite); | ||||
|             $('#image_list').append(getListItem(item, sprite.path, 'success')); | ||||
|         } | ||||
|         else { | ||||
|             $('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure')); | ||||
|         } | ||||
|     }); | ||||
|     return validExpressions; | ||||
| } | ||||
|  | ||||
| function getListItem(item, imageSrc, textClass) { | ||||
|     return ` | ||||
|         <div id="${item}" class="expression_list_item"> | ||||
|             <div class="expression_list_buttons"> | ||||
|                 <div class="menu_button expression_list_upload" title="Upload image"> | ||||
|                     <i class="fa-solid fa-upload"></i> | ||||
|                 </div> | ||||
|                 <div class="menu_button expression_list_delete" title="Delete image"> | ||||
|                     <i class="fa-solid fa-trash"></i> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <span class="expression_list_title ${textClass}">${item}</span> | ||||
|             <img class="expression_list_image" src="${imageSrc}" /> | ||||
|         </div> | ||||
|     `; | ||||
| } | ||||
|  | ||||
| async function getSpritesList(name) { | ||||
|     console.log('getting sprites list'); | ||||
|  | ||||
|     try { | ||||
|         const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`); | ||||
|  | ||||
|         let sprites = result.ok ? (await result.json()) : []; | ||||
|         return sprites; | ||||
|     } | ||||
|     catch (err) { | ||||
|         console.log(err); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function getExpressionsList() { | ||||
|     // get something for offline mode (default images) | ||||
|     if (!modules.includes('classify')) { | ||||
|         return DEFAULT_EXPRESSIONS; | ||||
|     } | ||||
|  | ||||
|     if (Array.isArray(expressionsList)) { | ||||
|         return expressionsList; | ||||
|     } | ||||
|  | ||||
|     const url = new URL(getApiUrl()); | ||||
|     url.pathname = '/api/classify/labels'; | ||||
|  | ||||
|     try { | ||||
|         const apiResult = await fetch(url, { | ||||
|             method: 'GET', | ||||
|             headers: { 'Bypass-Tunnel-Reminder': 'bypass' }, | ||||
|         }); | ||||
|  | ||||
|         if (apiResult.ok) { | ||||
|  | ||||
|             const data = await apiResult.json(); | ||||
|             expressionsList = data.labels; | ||||
|             return expressionsList; | ||||
|         } | ||||
|     } | ||||
|     catch (error) { | ||||
|         console.log(error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function setExpression(character, expression, force) { | ||||
|     console.log('entered setExpressions'); | ||||
|     await validateImages(character); | ||||
|     const img = $('img.expression'); | ||||
|  | ||||
|     const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); | ||||
|     console.log('checking for expression images to show..'); | ||||
|     if (sprite) { | ||||
|         console.log('setting expression from character images folder'); | ||||
|         img.attr('src', sprite.path); | ||||
|         img.removeClass('default'); | ||||
|         img.off('error'); | ||||
|         img.on('error', function () { | ||||
|             $(this).attr('src', ''); | ||||
|             if (force && extension_settings.expressions.showDefault) { | ||||
|                 setDefault(); | ||||
|             } | ||||
|         }); | ||||
|     } else { | ||||
|         if (extension_settings.expressions.showDefault) { | ||||
|             setDefault(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function setDefault() { | ||||
|         console.log('setting default'); | ||||
|         const defImgUrl = `/img/default-expressions/${expression}.png`; | ||||
|         //console.log(defImgUrl); | ||||
|         img.attr('src', defImgUrl); | ||||
|         img.addClass('default'); | ||||
|     } | ||||
|     document.getElementById("expression-holder").style.display = ''; | ||||
| } | ||||
|  | ||||
| function onClickExpressionImage() { | ||||
|     // online mode doesn't need force set | ||||
|     if (modules.includes('classify')) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const expression = $(this).attr('id'); | ||||
|     const name = getLastCharacterMessage().name; | ||||
|  | ||||
|     if ($(this).find('.failure').length === 0) { | ||||
|         setExpression(name, expression, true); | ||||
|     } | ||||
| } | ||||
| async function onClickExpressionUpload(event) { | ||||
|     // Prevents the expression from being set | ||||
|     event.stopPropagation(); | ||||
|  | ||||
|     const id = $(this).closest('.expression_list_item').attr('id'); | ||||
|     const name = $('#image_list').data('name'); | ||||
|  | ||||
|     const handleExpressionUploadChange = async (e) => { | ||||
|         const file = e.target.files[0]; | ||||
|  | ||||
|         if (!file) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const formData = new FormData(); | ||||
|         formData.append('name', name); | ||||
|         formData.append('label', id); | ||||
|         formData.append('avatar', file); | ||||
|  | ||||
|         try { | ||||
|             await jQuery.ajax({ | ||||
|                 type: "POST", | ||||
|                 url: "/upload_sprite", | ||||
|                 data: formData, | ||||
|                 beforeSend: function () { }, | ||||
|                 cache: false, | ||||
|                 contentType: false, | ||||
|                 processData: false, | ||||
|             }); | ||||
|  | ||||
|             // Refresh sprites list | ||||
|             delete spriteCache[name]; | ||||
|             await validateImages(name); | ||||
|         } catch (error) { | ||||
|             toastr.error('Failed to upload image'); | ||||
|         } | ||||
|  | ||||
|         // Reset the input | ||||
|         e.target.form.reset(); | ||||
|     }; | ||||
|  | ||||
|     $('#expression_upload') | ||||
|         .off('change') | ||||
|         .on('change', handleExpressionUploadChange) | ||||
|         .trigger('click'); | ||||
| } | ||||
|  | ||||
| async function onClickExpressionDelete(event) { | ||||
|     // Prevents the expression from being set | ||||
|     event.stopPropagation(); | ||||
|  | ||||
|     const confirmation = await callPopup("<h3>Are you sure?</h3>Once deleted, it's gone forever!", 'confirm'); | ||||
|  | ||||
|     if (!confirmation) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const id = $(this).closest('.expression_list_item').attr('id'); | ||||
|     const name = $('#image_list').data('name'); | ||||
|  | ||||
|     try { | ||||
|         await fetch('/delete_sprite', { | ||||
|             method: 'POST', | ||||
|             headers: getRequestHeaders(), | ||||
|             body: JSON.stringify({ name, label: id }), | ||||
|         }); | ||||
|     } catch (error) { | ||||
|         toastr.error('Failed to delete image. Try again later.'); | ||||
|     } | ||||
|  | ||||
|     // Refresh sprites list | ||||
|     delete spriteCache[name]; | ||||
|     await validateImages(name); | ||||
| } | ||||
|  | ||||
| (function () { | ||||
|     function addExpressionImage() { | ||||
|         const html = ` | ||||
|         <div id="expression-wrapper"> | ||||
|             <div id="expression-holder" class="expression-holder" style="display:none;"> | ||||
|                 <div id="expression-holderheader" class="fa-solid fa-grip drag-grabber"></div> | ||||
|                 <img id="expression-image" class="expression"> | ||||
|             </div> | ||||
|         </div>`; | ||||
|         $('body').append(html); | ||||
|     } | ||||
|     function addSettings() { | ||||
|  | ||||
|         const html = ` | ||||
|         <div class="expression_settings"> | ||||
|             <div class="inline-drawer"> | ||||
|                 <div class="inline-drawer-toggle inline-drawer-header"> | ||||
|                     <b>Expression images</b> | ||||
|                     <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div> | ||||
|                 </div> | ||||
|                 <div class="inline-drawer-content"> | ||||
|                     <p class="offline_mode">You are in offline mode. Click on the image below to set the expression.</p> | ||||
|                     <div id="image_list"></div> | ||||
|                     <p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character. | ||||
|                     Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p> | ||||
|                     <label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <form><input type="file" id="expression_upload" name="expression_upload" accept="image/*" hidden></form> | ||||
|         </div> | ||||
|         `; | ||||
|         $('#extensions_settings').append(html); | ||||
|         $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); | ||||
|         $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); | ||||
|         $(document).on('click', '.expression_list_item', onClickExpressionImage); | ||||
|         $(document).on('click', '.expression_list_upload', onClickExpressionUpload); | ||||
|         $(document).on('click', '.expression_list_delete', onClickExpressionDelete); | ||||
|         $('.expression_settings').hide(); | ||||
|     } | ||||
|  | ||||
|     addExpressionImage(); | ||||
|     addSettings(); | ||||
|     setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); | ||||
|     moduleWorkerWrapper(); | ||||
| })(); | ||||
|   | ||||
| @@ -1,124 +1,138 @@ | ||||
| .expression-helper { | ||||
|     display: inline-block; | ||||
|     height: 100%; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| #expression-wrapper { | ||||
|     display: flex; | ||||
|     height: calc(100vh - 40px); | ||||
|     width: 100vw; | ||||
| } | ||||
|  | ||||
| .expression-holder { | ||||
|     min-width: 100px; | ||||
|     min-height: 100px; | ||||
|     max-height: 90vh; | ||||
|     max-width: 90vh; | ||||
|     width: calc((100vw - var(--sheldWidth)) /2); | ||||
|     position: absolute; | ||||
|     bottom: 1px; | ||||
|     padding: 0; | ||||
|     filter: drop-shadow(2px 2px 2px #51515199); | ||||
|     z-index: 2; | ||||
|     overflow: hidden; | ||||
|  | ||||
| } | ||||
|  | ||||
| img.expression { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     vertical-align: bottom; | ||||
|     object-fit: contain; | ||||
| } | ||||
|  | ||||
| img.expression[src=""] { | ||||
|     visibility: hidden; | ||||
| } | ||||
|  | ||||
| img.expression.default { | ||||
|     vertical-align: middle; | ||||
|     max-height: 120px; | ||||
|     object-fit: contain !important; | ||||
|     margin-top: 50px; | ||||
| } | ||||
|  | ||||
| .debug-image { | ||||
|     display: none; | ||||
|     visibility: collapse; | ||||
|     opacity: 0; | ||||
|     width: 0px; | ||||
|     height: 0px; | ||||
| } | ||||
|  | ||||
| .expression_list_item { | ||||
|     position: relative; | ||||
|     max-width: 20%; | ||||
|     max-height: 200px; | ||||
|     background-color: #515151b0; | ||||
|     border-radius: 10px; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .expression_list_title { | ||||
|     position: absolute; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     text-align: center; | ||||
|     font-weight: 600; | ||||
|     background-color: #000000a8; | ||||
|     width: 100%; | ||||
|     height: 20%; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| .expression_list_image { | ||||
|     max-width: 100%; | ||||
|     height: 100%; | ||||
| } | ||||
|  | ||||
| #image_list { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     column-gap: 1rem; | ||||
|     margin: 1rem; | ||||
|     flex-wrap: wrap; | ||||
|     justify-content: space-evenly; | ||||
|     row-gap: 1rem; | ||||
| } | ||||
|  | ||||
| #image_list .success { | ||||
|     color: green; | ||||
| } | ||||
|  | ||||
| #image_list .failure { | ||||
|     color: red; | ||||
| } | ||||
|  | ||||
| .expression_settings p { | ||||
|     margin-top: 0.5rem; | ||||
|     margin-bottom: 0.5rem; | ||||
| } | ||||
|  | ||||
| .expression_settings label { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     flex-direction: row; | ||||
|     margin-left: 0px; | ||||
| } | ||||
|  | ||||
| .expression_settings label input { | ||||
|     margin-left: 0px !important; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width:1200px) { | ||||
|     div.expression { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
| .expression-helper { | ||||
|     display: inline-block; | ||||
|     height: 100%; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| #expression-wrapper { | ||||
|     display: flex; | ||||
|     height: calc(100vh - 40px); | ||||
|     width: 100vw; | ||||
| } | ||||
|  | ||||
| .expression-holder { | ||||
|     min-width: 100px; | ||||
|     min-height: 100px; | ||||
|     max-height: 90vh; | ||||
|     max-width: 90vh; | ||||
|     width: calc((100vw - var(--sheldWidth)) /2); | ||||
|     position: absolute; | ||||
|     bottom: 1px; | ||||
|     padding: 0; | ||||
|     filter: drop-shadow(2px 2px 2px #51515199); | ||||
|     z-index: 2; | ||||
|     overflow: hidden; | ||||
|  | ||||
| } | ||||
|  | ||||
| img.expression { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     vertical-align: bottom; | ||||
|     object-fit: contain; | ||||
| } | ||||
|  | ||||
| img.expression[src=""] { | ||||
|     visibility: hidden; | ||||
| } | ||||
|  | ||||
| img.expression.default { | ||||
|     vertical-align: middle; | ||||
|     max-height: 120px; | ||||
|     object-fit: contain !important; | ||||
|     margin-top: 50px; | ||||
| } | ||||
|  | ||||
| .debug-image { | ||||
|     display: none; | ||||
|     visibility: collapse; | ||||
|     opacity: 0; | ||||
|     width: 0px; | ||||
|     height: 0px; | ||||
| } | ||||
|  | ||||
| .expression_list_item { | ||||
|     position: relative; | ||||
|     max-width: 20%; | ||||
|     max-height: 200px; | ||||
|     background-color: #515151b0; | ||||
|     border-radius: 10px; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
| .expression_list_title { | ||||
|     position: absolute; | ||||
|     bottom: 0; | ||||
|     left: 0; | ||||
|     text-align: center; | ||||
|     font-weight: 600; | ||||
|     background-color: #000000a8; | ||||
|     width: 100%; | ||||
|     height: 20%; | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
| } | ||||
|  | ||||
| .expression_list_buttons { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     height: 20%; | ||||
|     padding: 0.25rem; | ||||
| } | ||||
|  | ||||
| .expression_list_image { | ||||
|     max-width: 100%; | ||||
|     height: 100%; | ||||
|     object-fit: cover; | ||||
| } | ||||
|  | ||||
| #image_list { | ||||
|     display: flex; | ||||
|     flex-direction: row; | ||||
|     column-gap: 1rem; | ||||
|     margin: 1rem; | ||||
|     flex-wrap: wrap; | ||||
|     justify-content: space-evenly; | ||||
|     row-gap: 1rem; | ||||
| } | ||||
|  | ||||
| #image_list .success { | ||||
|     color: green; | ||||
| } | ||||
|  | ||||
| #image_list .failure { | ||||
|     color: red; | ||||
| } | ||||
|  | ||||
| .expression_settings p { | ||||
|     margin-top: 0.5rem; | ||||
|     margin-bottom: 0.5rem; | ||||
| } | ||||
|  | ||||
| .expression_settings label { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     flex-direction: row; | ||||
|     margin-left: 0px; | ||||
| } | ||||
|  | ||||
| .expression_settings label input { | ||||
|     margin-left: 0px !important; | ||||
| } | ||||
|  | ||||
| @media screen and (max-width:1200px) { | ||||
|     div.expression { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user