2023-12-02 19:04:51 +01:00
import { callPopup , eventSource , event _types , getRequestHeaders , saveSettingsDebounced } from '../../../script.js' ;
import { dragElement , isMobile } from '../../RossAscends-mods.js' ;
import { getContext , getApiUrl , modules , extension _settings , ModuleWorkerWrapper , doExtrasFetch , renderExtensionTemplate } from '../../extensions.js' ;
import { loadMovingUIState , power _user } from '../../power-user.js' ;
import { registerSlashCommand } from '../../slash-commands.js' ;
import { onlyUnique , debounce , getCharaFilename , trimToEndSentence , trimToStartSentence } from '../../utils.js' ;
import { hideMutedSprites } from '../../group-chats.js' ;
2023-07-20 19:32:15 +02:00
export { MODULE _NAME } ;
const MODULE _NAME = 'expressions' ;
const UPDATE _INTERVAL = 2000 ;
2023-09-10 19:14:57 +02:00
const STREAMING _UPDATE _INTERVAL = 6000 ;
2023-07-20 19:32:15 +02:00
const FALLBACK _EXPRESSION = 'joy' ;
const DEFAULT _EXPRESSIONS = [
2023-12-02 19:04:51 +01:00
'talkinghead' ,
'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' ,
2023-12-02 21:06:57 +01:00
'neutral' ,
2023-07-20 19:32:15 +02:00
] ;
let expressionsList = null ;
let lastCharacter = undefined ;
let lastMessage = null ;
let spriteCache = { } ;
let inApiCall = false ;
2023-09-10 19:14:57 +02:00
let lastServerResponseTime = 0 ;
2023-12-30 12:35:10 +01:00
export let lastExpression = { } ;
2023-07-20 19:32:15 +02:00
function isVisualNovelMode ( ) {
return Boolean ( ! isMobile ( ) && power _user . waifuMode && getContext ( ) . groupId ) ;
}
async function forceUpdateVisualNovelMode ( ) {
if ( isVisualNovelMode ( ) ) {
await updateVisualNovelMode ( ) ;
}
}
const updateVisualNovelModeDebounced = debounce ( forceUpdateVisualNovelMode , 100 ) ;
async function updateVisualNovelMode ( name , expression ) {
const container = $ ( '#visual-novel-wrapper' ) ;
await visualNovelRemoveInactive ( container ) ;
const setSpritePromises = await visualNovelSetCharacterSprites ( container , name , expression ) ;
// calculate layer indices based on recent messages
await visualNovelUpdateLayers ( container ) ;
await Promise . allSettled ( setSpritePromises ) ;
// update again based on new sprites
if ( setSpritePromises . length > 0 ) {
await visualNovelUpdateLayers ( container ) ;
}
}
async function visualNovelRemoveInactive ( container ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id == context . groupId ) ;
const removeInactiveCharactersPromises = [ ] ;
// remove inactive characters after 1 second
container . find ( '.expression-holder' ) . each ( ( _ , current ) => {
const promise = new Promise ( resolve => {
const element = $ ( current ) ;
const avatar = element . data ( 'avatar' ) ;
if ( ! group . members . includes ( avatar ) || group . disabled _members . includes ( avatar ) ) {
element . fadeOut ( 250 , ( ) => {
element . remove ( ) ;
resolve ( ) ;
} ) ;
} else {
resolve ( ) ;
}
} ) ;
removeInactiveCharactersPromises . push ( promise ) ;
} ) ;
await Promise . allSettled ( removeInactiveCharactersPromises ) ;
}
async function visualNovelSetCharacterSprites ( container , name , expression ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id == context . groupId ) ;
const labels = await getExpressionsList ( ) ;
const createCharacterPromises = [ ] ;
const setSpritePromises = [ ] ;
for ( const avatar of group . members ) {
const isDisabled = group . disabled _members . includes ( avatar ) ;
// skip disabled characters
2023-11-21 23:48:35 +01:00
if ( isDisabled && hideMutedSprites ) {
2023-07-20 19:32:15 +02:00
continue ;
}
const character = context . characters . find ( x => x . avatar == avatar ) ;
if ( ! character ) {
continue ;
}
2023-09-07 16:48:12 +02:00
const spriteFolderName = getSpriteFolderName ( { original _avatar : character . avatar } , character . name ) ;
2023-07-20 19:32:15 +02:00
// download images if not downloaded yet
if ( spriteCache [ spriteFolderName ] === undefined ) {
spriteCache [ spriteFolderName ] = await getSpritesList ( spriteFolderName ) ;
}
const sprites = spriteCache [ spriteFolderName ] ;
const expressionImage = container . find ( ` .expression-holder[data-avatar=" ${ avatar } "] ` ) ;
const defaultSpritePath = sprites . find ( x => x . label === FALLBACK _EXPRESSION ) ? . path ;
const noSprites = sprites . length === 0 ;
if ( expressionImage . length > 0 ) {
if ( name == spriteFolderName ) {
await validateImages ( spriteFolderName , true ) ;
setExpressionOverrideHtml ( true ) ; // <= force clear expression override input
const currentSpritePath = labels . includes ( expression ) ? sprites . find ( x => x . label === expression ) ? . path : '' ;
const path = currentSpritePath || defaultSpritePath || '' ;
const img = expressionImage . find ( 'img' ) ;
await setImage ( img , path ) ;
}
expressionImage . toggleClass ( 'hidden' , noSprites ) ;
} else {
const template = $ ( '#expression-holder' ) . clone ( ) ;
template . attr ( 'id' , ` expression- ${ avatar } ` ) ;
template . attr ( 'data-avatar' , avatar ) ;
template . find ( '.drag-grabber' ) . attr ( 'id' , ` expression- ${ avatar } header ` ) ;
$ ( '#visual-novel-wrapper' ) . append ( template ) ;
dragElement ( $ ( template [ 0 ] ) ) ;
template . toggleClass ( 'hidden' , noSprites ) ;
await setImage ( template . find ( 'img' ) , defaultSpritePath || '' ) ;
const fadeInPromise = new Promise ( resolve => {
template . fadeIn ( 250 , ( ) => resolve ( ) ) ;
} ) ;
createCharacterPromises . push ( fadeInPromise ) ;
const setSpritePromise = setLastMessageSprite ( template . find ( 'img' ) , avatar , labels ) ;
setSpritePromises . push ( setSpritePromise ) ;
}
}
await Promise . allSettled ( createCharacterPromises ) ;
return setSpritePromises ;
}
async function visualNovelUpdateLayers ( container ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id == context . groupId ) ;
const recentMessages = context . chat . map ( x => x . original _avatar ) . filter ( x => x ) . reverse ( ) . filter ( onlyUnique ) ;
const filteredMembers = group . members . filter ( x => ! group . disabled _members . includes ( x ) ) ;
const layerIndices = filteredMembers . slice ( ) . sort ( ( a , b ) => {
const aRecentIndex = recentMessages . indexOf ( a ) ;
const bRecentIndex = recentMessages . indexOf ( b ) ;
const aFilteredIndex = filteredMembers . indexOf ( a ) ;
const bFilteredIndex = filteredMembers . indexOf ( b ) ;
if ( aRecentIndex !== - 1 && bRecentIndex !== - 1 ) {
return bRecentIndex - aRecentIndex ;
} else if ( aRecentIndex !== - 1 ) {
return 1 ;
} else if ( bRecentIndex !== - 1 ) {
return - 1 ;
} else {
return aFilteredIndex - bFilteredIndex ;
}
} ) ;
const setLayerIndicesPromises = [ ] ;
const sortFunction = ( a , b ) => {
const avatarA = $ ( a ) . data ( 'avatar' ) ;
const avatarB = $ ( b ) . data ( 'avatar' ) ;
const indexA = filteredMembers . indexOf ( avatarA ) ;
const indexB = filteredMembers . indexOf ( avatarB ) ;
return indexA - indexB ;
} ;
const containerWidth = container . width ( ) ;
const pivotalPoint = containerWidth * 0.5 ;
2023-11-28 10:40:39 +01:00
let images = $ ( '#visual-novel-wrapper .expression-holder' ) ;
2023-07-20 19:32:15 +02:00
let imagesWidth = [ ] ;
images . sort ( sortFunction ) . each ( function ( ) {
imagesWidth . push ( $ ( this ) . width ( ) ) ;
} ) ;
let totalWidth = imagesWidth . reduce ( ( a , b ) => a + b , 0 ) ;
let currentPosition = pivotalPoint - ( totalWidth / 2 ) ;
if ( totalWidth > containerWidth ) {
let totalOverlap = totalWidth - containerWidth ;
let totalWidthWithoutWidest = imagesWidth . reduce ( ( a , b ) => a + b , 0 ) - Math . max ( ... imagesWidth ) ;
let overlaps = imagesWidth . map ( width => ( width / totalWidthWithoutWidest ) * totalOverlap ) ;
imagesWidth = imagesWidth . map ( ( width , index ) => width - overlaps [ index ] ) ;
currentPosition = 0 ; // Reset the initial position to 0
}
images . sort ( sortFunction ) . each ( ( index , current ) => {
const element = $ ( current ) ;
2023-12-02 20:11:06 +01:00
const elementID = element . attr ( 'id' ) ;
2023-07-20 19:32:15 +02:00
// skip repositioning of dragged elements
if ( element . data ( 'dragged' )
|| ( power _user . movingUIState [ elementID ]
&& ( typeof power _user . movingUIState [ elementID ] === 'object' )
&& Object . keys ( power _user . movingUIState [ elementID ] ) . length > 0 ) ) {
2023-12-02 20:11:06 +01:00
loadMovingUIState ( ) ;
2023-07-20 19:32:15 +02:00
//currentPosition += imagesWidth[index];
return ;
}
const avatar = element . data ( 'avatar' ) ;
const layerIndex = layerIndices . indexOf ( avatar ) ;
element . css ( 'z-index' , layerIndex ) ;
element . show ( ) ;
const promise = new Promise ( resolve => {
element . animate ( { left : currentPosition + 'px' } , 500 , ( ) => {
resolve ( ) ;
} ) ;
} ) ;
currentPosition += imagesWidth [ index ] ;
setLayerIndicesPromises . push ( promise ) ;
} ) ;
await Promise . allSettled ( setLayerIndicesPromises ) ;
}
async function setLastMessageSprite ( img , avatar , labels ) {
const context = getContext ( ) ;
const lastMessage = context . chat . slice ( ) . reverse ( ) . find ( x => x . original _avatar == avatar || ( x . force _avatar && x . force _avatar . includes ( encodeURIComponent ( avatar ) ) ) ) ;
if ( lastMessage ) {
const text = lastMessage . mes || '' ;
2023-09-07 16:52:37 +02:00
const spriteFolderName = getSpriteFolderName ( lastMessage , lastMessage . name ) ;
2023-07-20 19:32:15 +02:00
const sprites = spriteCache [ spriteFolderName ] || [ ] ;
const label = await getExpressionLabel ( text ) ;
const path = labels . includes ( label ) ? sprites . find ( x => x . label === label ) ? . path : '' ;
if ( path ) {
setImage ( img , path ) ;
}
}
}
async function setImage ( img , path ) {
// Cohee: If something goes wrong, uncomment this to return to the old behavior
/ *
img . attr ( 'src' , path ) ;
img . removeClass ( 'default' ) ;
img . off ( 'error' ) ;
img . on ( 'error' , function ( ) {
console . debug ( 'Error loading image' , path ) ;
$ ( this ) . off ( 'error' ) ;
$ ( this ) . attr ( 'src' , '' ) ;
} ) ;
* /
return new Promise ( resolve => {
const prevExpressionSrc = img . attr ( 'src' ) ;
const expressionClone = img . clone ( ) ;
const originalId = img . attr ( 'id' ) ;
//only swap expressions when necessary
if ( prevExpressionSrc !== path && ! img . hasClass ( 'expression-animating' ) ) {
//clone expression
2023-12-02 20:11:06 +01:00
expressionClone . addClass ( 'expression-clone' ) ;
2023-07-20 19:32:15 +02:00
//make invisible and remove id to prevent double ids
//must be made invisible to start because they share the same Z-index
expressionClone . attr ( 'id' , '' ) . css ( { opacity : 0 } ) ;
//add new sprite path to clone src
expressionClone . attr ( 'src' , path ) ;
//add invisible clone to html
expressionClone . appendTo ( img . parent ( ) ) ;
const duration = 200 ;
//add animation flags to both images
//to prevent multiple expression changes happening simultaneously
img . addClass ( 'expression-animating' ) ;
// Set the parent container's min width and height before running the transition
const imgWidth = img . width ( ) ;
const imgHeight = img . height ( ) ;
const expressionHolder = img . parent ( ) ;
expressionHolder . css ( 'min-width' , imgWidth > 100 ? imgWidth : 100 ) ;
expressionHolder . css ( 'min-height' , imgHeight > 100 ? imgHeight : 100 ) ;
//position absolute prevent the original from jumping around during transition
2023-08-25 01:07:59 +02:00
img . css ( 'position' , 'absolute' ) . width ( imgWidth ) . height ( imgHeight ) ;
2023-07-20 19:32:15 +02:00
expressionClone . addClass ( 'expression-animating' ) ;
//fade the clone in
expressionClone . css ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-20 19:32:15 +02:00
} ) . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 1 ,
2023-07-20 19:32:15 +02:00
} , duration )
//when finshed fading in clone, fade out the original
. promise ( ) . done ( function ( ) {
img . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-20 19:32:15 +02:00
} , duration ) ;
//remove old expression
img . remove ( ) ;
//replace ID so it becomes the new 'original' expression for next change
expressionClone . attr ( 'id' , originalId ) ;
expressionClone . removeClass ( 'expression-animating' ) ;
// Reset the expression holder min height and width
expressionHolder . css ( 'min-width' , 100 ) ;
expressionHolder . css ( 'min-height' , 100 ) ;
resolve ( ) ;
} ) ;
expressionClone . removeClass ( 'expression-clone' ) ;
expressionClone . removeClass ( 'default' ) ;
expressionClone . off ( 'error' ) ;
expressionClone . on ( 'error' , function ( ) {
2023-09-07 16:48:12 +02:00
console . debug ( 'Expression image error' , path ) ;
2023-07-20 19:32:15 +02:00
$ ( this ) . attr ( 'src' , '' ) ;
$ ( this ) . off ( 'error' ) ;
resolve ( ) ;
} ) ;
} else {
resolve ( ) ;
}
} ) ;
}
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 ;
}
}
}
2023-08-04 01:29:39 +02:00
async function unloadLiveChar ( ) {
2023-08-29 20:50:26 +02:00
if ( ! modules . includes ( 'talkinghead' ) ) {
console . debug ( 'talkinghead module is disabled' ) ;
return ;
}
2023-08-04 01:29:39 +02:00
try {
const url = new URL ( getApiUrl ( ) ) ;
2023-08-10 23:52:14 +02:00
url . pathname = '/api/talkinghead/unload' ;
2023-08-04 02:27:01 +02:00
const loadResponse = await doExtrasFetch ( url ) ;
2023-08-04 01:29:39 +02:00
if ( ! loadResponse . ok ) {
throw new Error ( loadResponse . statusText ) ;
}
2023-08-04 07:54:01 +02:00
//console.log(`Response: ${loadResponseText}`);
2023-08-04 01:29:39 +02:00
} catch ( error ) {
2023-08-04 07:54:01 +02:00
//console.error(`Error unloading - ${error}`);
2023-08-04 01:29:39 +02:00
}
}
2023-08-01 22:57:04 +02:00
async function loadLiveChar ( ) {
2023-08-10 23:52:14 +02:00
if ( ! modules . includes ( 'talkinghead' ) ) {
console . debug ( 'talkinghead module is disabled' ) ;
2023-08-01 15:33:30 +02:00
return ;
}
2023-09-07 16:48:12 +02:00
const spriteFolderName = getSpriteFolderName ( ) ;
2023-08-01 22:57:04 +02:00
2023-08-10 23:52:14 +02:00
const talkingheadPath = ` /characters/ ${ encodeURIComponent ( spriteFolderName ) } /talkinghead.png ` ;
2023-08-01 22:57:04 +02:00
try {
2023-08-10 23:52:14 +02:00
const spriteResponse = await fetch ( talkingheadPath ) ;
2023-08-01 22:57:04 +02:00
if ( ! spriteResponse . ok ) {
throw new Error ( spriteResponse . statusText ) ;
}
const spriteBlob = await spriteResponse . blob ( ) ;
2023-08-10 23:52:14 +02:00
const spriteFile = new File ( [ spriteBlob ] , 'talkinghead.png' , { type : 'image/png' } ) ;
2023-08-01 22:57:04 +02:00
const formData = new FormData ( ) ;
formData . append ( 'file' , spriteFile ) ;
const url = new URL ( getApiUrl ( ) ) ;
2023-08-10 23:52:14 +02:00
url . pathname = '/api/talkinghead/load' ;
2023-08-01 22:57:04 +02:00
const loadResponse = await doExtrasFetch ( url , {
method : 'POST' ,
body : formData ,
} ) ;
if ( ! loadResponse . ok ) {
throw new Error ( loadResponse . statusText ) ;
}
const loadResponseText = await loadResponse . text ( ) ;
2023-08-10 23:52:14 +02:00
console . log ( ` Load talkinghead response: ${ loadResponseText } ` ) ;
2023-08-01 22:57:04 +02:00
} catch ( error ) {
2023-08-10 23:52:14 +02:00
console . error ( ` Error loading talkinghead image: ${ talkingheadPath } - ${ error } ` ) ;
2023-08-01 22:57:04 +02:00
}
2023-07-31 07:52:30 +02:00
}
2023-08-03 12:05:21 +02:00
function handleImageChange ( ) {
2023-07-31 07:52:30 +02:00
const imgElement = document . querySelector ( 'img#expression-image.expression' ) ;
2023-08-03 13:07:50 +02:00
2023-09-07 16:48:12 +02:00
if ( ! imgElement || ! ( imgElement instanceof HTMLImageElement ) ) {
2023-12-02 19:04:51 +01:00
console . log ( 'Cannot find addExpressionImage()' ) ;
2023-07-31 07:52:30 +02:00
return ;
}
2023-09-11 12:01:45 +02:00
if ( extension _settings . expressions . talkinghead && ! extension _settings . expressions . local ) {
2023-07-31 07:52:30 +02:00
// Method get IP of endpoint
2023-08-10 23:52:14 +02:00
const talkingheadResultFeedSrc = ` ${ getApiUrl ( ) } /api/talkinghead/result_feed ` ;
2023-08-03 13:07:50 +02:00
$ ( '#expression-holder' ) . css ( { display : '' } ) ;
2023-08-10 23:52:14 +02:00
if ( imgElement . src !== talkingheadResultFeedSrc ) {
2023-08-03 12:05:21 +02:00
const expressionImageElement = document . querySelector ( '.expression_list_image' ) ;
2023-09-07 16:48:12 +02:00
if ( expressionImageElement && expressionImageElement instanceof HTMLImageElement ) {
2023-08-03 12:05:21 +02:00
doExtrasFetch ( expressionImageElement . src , {
method : 'HEAD' ,
2023-07-31 19:56:05 +02:00
} )
2023-08-03 12:05:21 +02:00
. then ( response => {
if ( response . ok ) {
2023-08-10 23:52:14 +02:00
imgElement . src = talkingheadResultFeedSrc ;
2023-08-03 12:05:21 +02:00
}
} )
. catch ( error => {
console . error ( error ) ; // Log the error if necessary
} ) ;
}
2023-07-31 07:52:30 +02:00
}
2023-08-03 12:05:21 +02:00
} else {
2023-12-02 19:04:51 +01:00
imgElement . src = '' ; //remove incase char doesnt have expressions
2023-08-03 12:23:15 +02:00
setExpression ( getContext ( ) . name2 , FALLBACK _EXPRESSION , true ) ;
2023-07-31 07:52:30 +02:00
}
}
2023-07-20 19:32:15 +02:00
async function moduleWorker ( ) {
const context = getContext ( ) ;
2023-09-11 12:01:45 +02:00
// Hide and disable talkinghead while in local mode
$ ( '#image_type_block' ) . toggle ( ! extension _settings . expressions . local ) ;
if ( extension _settings . expressions . local && extension _settings . expressions . talkinghead ) {
$ ( '#image_type_toggle' ) . prop ( 'checked' , false ) ;
setTalkingHeadState ( false ) ;
}
2023-07-20 19:32:15 +02:00
// non-characters not supported
2023-08-20 12:15:02 +02:00
if ( ! context . groupId && ( context . characterId === undefined || context . characterId === 'invalid-safety-id' ) ) {
2023-07-20 19:32:15 +02:00
removeExpression ( ) ;
return ;
}
const vnMode = isVisualNovelMode ( ) ;
const vnWrapperVisible = $ ( '#visual-novel-wrapper' ) . is ( ':visible' ) ;
if ( vnMode ) {
$ ( '#expression-wrapper' ) . hide ( ) ;
$ ( '#visual-novel-wrapper' ) . show ( ) ;
} else {
$ ( '#expression-wrapper' ) . show ( ) ;
$ ( '#visual-novel-wrapper' ) . hide ( ) ;
}
const vnStateChanged = vnMode !== vnWrapperVisible ;
if ( vnStateChanged ) {
lastMessage = null ;
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
2023-12-02 19:04:51 +01:00
$ ( '#expression-holder' ) . css ( { top : '' , left : '' , right : '' , bottom : '' , height : '' , width : '' , margin : '' } ) ;
2023-07-20 19:32:15 +02:00
}
const currentLastMessage = getLastCharacterMessage ( ) ;
2023-10-16 08:12:12 +02:00
let spriteFolderName = context . groupId ? getSpriteFolderName ( currentLastMessage , currentLastMessage . name ) : getSpriteFolderName ( ) ;
2023-07-20 19:32:15 +02:00
// character has no expressions or it is not loaded
if ( Object . keys ( spriteCache ) . length === 0 ) {
await validateImages ( spriteFolderName ) ;
lastCharacter = context . groupId || context . characterId ;
}
const offlineMode = $ ( '.expression_settings .offline_mode' ) ;
2023-09-09 14:14:16 +02:00
if ( ! modules . includes ( 'classify' ) && ! extension _settings . expressions . local ) {
2023-09-13 14:19:10 +02:00
$ ( '#open_chat_expressions' ) . show ( ) ;
$ ( '#no_chat_expressions' ) . hide ( ) ;
2023-07-20 19:32:15 +02:00
offlineMode . css ( 'display' , 'block' ) ;
lastCharacter = context . groupId || context . characterId ;
if ( context . groupId ) {
await validateImages ( spriteFolderName , true ) ;
await forceUpdateVisualNovelMode ( ) ;
}
return ;
}
else {
// force reload expressions list on connect to API
if ( offlineMode . is ( ':visible' ) ) {
expressionsList = null ;
spriteCache = { } ;
expressionsList = await getExpressionsList ( ) ;
await validateImages ( spriteFolderName , true ) ;
await forceUpdateVisualNovelMode ( ) ;
}
2023-10-08 10:22:48 +02:00
if ( context . groupId && ! Array . isArray ( spriteCache [ spriteFolderName ] ) ) {
await validateImages ( spriteFolderName , true ) ;
await forceUpdateVisualNovelMode ( ) ;
}
2023-07-20 19:32:15 +02:00
offlineMode . css ( 'display' , 'none' ) ;
}
2023-09-18 00:49:00 +02:00
// Don't bother classifying if current char has no sprites and no default expressions are enabled
2023-09-21 20:15:05 +02:00
if ( ( ! Array . isArray ( spriteCache [ spriteFolderName ] ) || spriteCache [ spriteFolderName ] . length === 0 ) && ! extension _settings . expressions . showDefault ) {
2023-09-18 00:49:00 +02:00
return ;
}
2023-07-20 19:32:15 +02:00
// check if last message changed
if ( ( lastCharacter === context . characterId || lastCharacter === context . groupId )
&& lastMessage === currentLastMessage . mes ) {
return ;
}
// API is busy
if ( inApiCall ) {
2023-09-18 00:49:00 +02:00
console . debug ( 'Classification API is busy' ) ;
2023-07-20 19:32:15 +02:00
return ;
}
2023-09-10 19:14:57 +02:00
// Throttle classification requests during streaming
2023-10-08 10:22:48 +02:00
if ( ! context . groupId && context . streamingProcessor && ! context . streamingProcessor . isFinished ) {
2023-09-10 19:14:57 +02:00
const now = Date . now ( ) ;
const timeSinceLastServerResponse = now - lastServerResponseTime ;
if ( timeSinceLastServerResponse < STREAMING _UPDATE _INTERVAL ) {
console . log ( 'Streaming in progress: throttling expression update. Next update at ' + new Date ( lastServerResponseTime + STREAMING _UPDATE _INTERVAL ) ) ;
return ;
}
}
2023-07-20 19:32:15 +02:00
try {
inApiCall = true ;
let expression = await getExpressionLabel ( currentLastMessage . mes ) ;
// If we're not already overriding the folder name, account for group chats.
if ( spriteFolderName === currentLastMessage . name && ! context . groupId ) {
spriteFolderName = context . name2 ;
}
const force = ! ! context . groupId ;
// Character won't be angry on you for swiping
if ( currentLastMessage . mes == '...' && expressionsList . includes ( FALLBACK _EXPRESSION ) ) {
expression = FALLBACK _EXPRESSION ;
}
await sendExpressionCall ( spriteFolderName , expression , force , vnMode ) ;
}
catch ( error ) {
console . log ( error ) ;
}
finally {
inApiCall = false ;
lastCharacter = context . groupId || context . characterId ;
lastMessage = currentLastMessage . mes ;
2023-09-10 19:14:57 +02:00
lastServerResponseTime = Date . now ( ) ;
2023-07-20 19:32:15 +02:00
}
}
2023-09-07 16:48:12 +02:00
async function talkingHeadCheck ( ) {
let spriteFolderName = getSpriteFolderName ( ) ;
2023-08-04 07:54:01 +02:00
try {
await validateImages ( spriteFolderName ) ;
2023-08-10 23:52:14 +02:00
let talkingheadObj = spriteCache [ spriteFolderName ] . find ( obj => obj . label === 'talkinghead' ) ;
let talkingheadPath _f = talkingheadObj ? talkingheadObj . path : null ;
2023-08-04 07:54:01 +02:00
2023-08-10 23:55:05 +02:00
if ( talkingheadPath _f != null ) {
2023-08-10 23:52:14 +02:00
//console.log("talkingheadPath_f " + talkingheadPath_f);
2023-08-04 07:54:01 +02:00
return true ;
2023-08-10 11:42:52 +02:00
} else {
2023-08-10 23:55:05 +02:00
//console.log("talkingheadPath_f is null");
2023-08-04 07:54:01 +02:00
unloadLiveChar ( ) ;
return false ;
}
} catch ( err ) {
return err ;
}
}
2023-09-07 16:48:12 +02:00
function getSpriteFolderName ( characterMessage = null , characterName = null ) {
const context = getContext ( ) ;
let spriteFolderName = characterName ? ? context . name2 ;
const message = characterMessage ? ? getLastCharacterMessage ( ) ;
const avatarFileName = getFolderNameByMessage ( message ) ;
const expressionOverride = extension _settings . expressionOverrides . find ( e => e . name == avatarFileName ) ;
if ( expressionOverride && expressionOverride . path ) {
spriteFolderName = expressionOverride . path ;
}
return spriteFolderName ;
}
function setTalkingHeadState ( switch _var ) {
2023-08-10 23:52:14 +02:00
extension _settings . expressions . talkinghead = switch _var ; // Store setting
2023-08-03 12:05:21 +02:00
saveSettingsDebounced ( ) ;
2023-09-11 12:01:45 +02:00
if ( extension _settings . expressions . local ) {
return ;
}
2023-09-07 16:48:12 +02:00
talkingHeadCheck ( ) . then ( result => {
2023-08-04 07:54:01 +02:00
if ( result ) {
2023-08-10 23:52:14 +02:00
//console.log("talkinghead exists!");
2023-08-04 07:54:01 +02:00
2023-08-10 23:55:05 +02:00
if ( extension _settings . expressions . talkinghead ) {
2023-08-10 11:42:52 +02:00
loadLiveChar ( ) ;
} else {
unloadLiveChar ( ) ;
}
2023-09-07 16:48:12 +02:00
handleImageChange ( ) ; // Change image as needed
2023-08-04 07:54:01 +02:00
} else {
2023-08-10 23:52:14 +02:00
//console.log("talkinghead does not exist.");
2023-08-04 07:54:01 +02:00
}
} ) ;
2023-08-03 12:05:21 +02:00
}
2023-09-07 16:48:12 +02:00
function getFolderNameByMessage ( message ) {
2023-07-20 19:32:15 +02:00
const context = getContext ( ) ;
let avatarPath = '' ;
if ( context . groupId ) {
avatarPath = message . original _avatar || context . characters . find ( x => message . force _avatar && message . force _avatar . includes ( encodeURIComponent ( x . avatar ) ) ) ? . avatar ;
}
else if ( context . characterId ) {
avatarPath = getCharaFilename ( ) ;
}
if ( ! avatarPath ) {
return '' ;
}
2023-12-02 19:04:51 +01:00
const folderName = avatarPath . replace ( /\.[^/.]+$/ , '' ) ;
2023-07-20 19:32:15 +02:00
return folderName ;
}
async function sendExpressionCall ( name , expression , force , vnMode ) {
2023-12-30 12:35:10 +01:00
lastExpression [ name . split ( '/' ) [ 0 ] ] = expression ;
2023-07-20 19:32:15 +02:00
if ( ! vnMode ) {
vnMode = isVisualNovelMode ( ) ;
}
if ( vnMode ) {
await updateVisualNovelMode ( name , expression ) ;
} else {
setExpression ( name , expression , force ) ;
}
}
2023-09-14 17:37:13 +02:00
async function setSpriteSetCommand ( _ , folder ) {
if ( ! folder ) {
console . log ( 'Clearing sprite set' ) ;
folder = '' ;
}
2023-09-14 18:12:54 +02:00
if ( folder . startsWith ( '/' ) || folder . startsWith ( '\\' ) ) {
folder = folder . slice ( 1 ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
folder = ` ${ currentLastMessage . name } / ${ folder } ` ;
}
2023-12-02 19:04:51 +01:00
$ ( '#expression_override' ) . val ( folder . trim ( ) ) ;
2023-09-14 17:37:13 +02:00
onClickExpressionOverrideButton ( ) ;
removeExpression ( ) ;
moduleWorker ( ) ;
}
2023-09-07 16:48:12 +02:00
async function setSpriteSlashCommand ( _ , spriteId ) {
if ( ! spriteId ) {
console . log ( 'No sprite id provided' ) ;
return ;
}
spriteId = spriteId . trim ( ) . toLowerCase ( ) ;
2023-09-15 00:17:24 +02:00
const currentLastMessage = getLastCharacterMessage ( ) ;
const spriteFolderName = getSpriteFolderName ( currentLastMessage , currentLastMessage . name ) ;
2023-09-07 16:48:12 +02:00
await validateImages ( spriteFolderName ) ;
// Fuzzy search for sprite
const fuse = new Fuse ( spriteCache [ spriteFolderName ] , { keys : [ 'label' ] } ) ;
const results = fuse . search ( spriteId ) ;
const spriteItem = results [ 0 ] ? . item ;
if ( ! spriteItem ) {
console . log ( 'No sprite found for search term ' + spriteId ) ;
return ;
}
const vnMode = isVisualNovelMode ( ) ;
await sendExpressionCall ( spriteFolderName , spriteItem . label , true , vnMode ) ;
}
2023-09-09 16:31:27 +02:00
/ * *
* Processes the classification text to reduce the amount of text sent to the API .
* Quotes and asterisks are to be removed . If the text is less than 300 characters , it is returned as is .
* If the text is more than 300 characters , the first and last 150 characters are returned .
* The result is trimmed to the end of sentence .
* @ param { string } text The text to process .
* @ returns { string }
* /
function sampleClassifyText ( text ) {
if ( ! text ) {
return text ;
}
// Remove asterisks and quotes
2023-12-02 16:17:31 +01:00
let result = text . replace ( /[*"]/g , '' ) ;
2023-09-09 16:31:27 +02:00
2023-10-23 01:14:29 +02:00
const SAMPLE _THRESHOLD = 500 ;
2023-09-09 16:31:27 +02:00
const HALF _SAMPLE _THRESHOLD = SAMPLE _THRESHOLD / 2 ;
if ( text . length < SAMPLE _THRESHOLD ) {
result = trimToEndSentence ( result ) ;
} else {
result = trimToEndSentence ( result . slice ( 0 , HALF _SAMPLE _THRESHOLD ) ) + ' ' + trimToStartSentence ( result . slice ( - HALF _SAMPLE _THRESHOLD ) ) ;
}
return result . trim ( ) ;
}
2023-07-20 19:32:15 +02:00
async function getExpressionLabel ( text ) {
// Return if text is undefined, saving a costly fetch request
2023-09-09 14:14:16 +02:00
if ( ( ! modules . includes ( 'classify' ) && ! extension _settings . expressions . local ) || ! text ) {
2023-07-20 19:32:15 +02:00
return FALLBACK _EXPRESSION ;
}
2023-09-09 16:31:27 +02:00
text = sampleClassifyText ( text ) ;
2023-09-09 14:14:16 +02:00
try {
if ( extension _settings . expressions . local ) {
// Local transformers pipeline
const apiResult = await fetch ( '/api/extra/classify' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { text : text } ) ,
} ) ;
2023-07-20 19:32:15 +02:00
2023-09-09 14:14:16 +02:00
if ( apiResult . ok ) {
const data = await apiResult . json ( ) ;
return data . classification [ 0 ] . label ;
}
} else {
// Extras
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/classify' ;
const apiResult = await doExtrasFetch ( url , {
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'Bypass-Tunnel-Reminder' : 'bypass' ,
} ,
body : JSON . stringify ( { text : text } ) ,
} ) ;
2023-07-20 19:32:15 +02:00
2023-09-09 14:14:16 +02:00
if ( apiResult . ok ) {
const data = await apiResult . json ( ) ;
return data . classification [ 0 ] . label ;
}
}
} catch ( error ) {
console . log ( error ) ;
return FALLBACK _EXPRESSION ;
2023-07-20 19:32:15 +02:00
}
}
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 , original _avatar : mes . original _avatar , force _avatar : mes . force _avatar } ;
}
return { mes : '' , name : null , original _avatar : null , force _avatar : null } ;
}
function removeExpression ( ) {
lastMessage = null ;
$ ( 'img.expression' ) . off ( 'error' ) ;
$ ( 'img.expression' ) . prop ( 'src' , '' ) ;
$ ( 'img.expression' ) . removeClass ( 'default' ) ;
2023-09-13 14:19:10 +02:00
$ ( '#open_chat_expressions' ) . hide ( ) ;
$ ( '#no_chat_expressions' ) . show ( ) ;
2023-07-20 19:32:15 +02:00
}
async function validateImages ( character , forceRedrawCached ) {
if ( ! character ) {
return ;
}
const labels = await getExpressionsList ( ) ;
if ( spriteCache [ character ] ) {
if ( forceRedrawCached && $ ( '#image_list' ) . data ( 'name' ) !== character ) {
2023-12-02 20:11:06 +01:00
console . debug ( 'force redrawing character sprites list' ) ;
2023-07-20 19:32:15 +02:00
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 = [ ] ;
2023-09-13 14:19:10 +02:00
$ ( '#no_chat_expressions' ) . hide ( ) ;
$ ( '#open_chat_expressions' ) . show ( ) ;
2023-07-20 19:32:15 +02:00
$ ( '#image_list' ) . empty ( ) ;
$ ( '#image_list' ) . data ( 'name' , character ) ;
2023-09-13 14:19:10 +02:00
$ ( '#image_list_header_name' ) . text ( character ) ;
2023-07-20 19:32:15 +02:00
if ( ! Array . isArray ( labels ) ) {
return [ ] ;
}
labels . sort ( ) . forEach ( ( item ) => {
const sprite = sprites . find ( x => x . label == item ) ;
2023-09-14 20:30:02 +02:00
const isCustom = extension _settings . expressions . custom . includes ( item ) ;
2023-07-20 19:32:15 +02:00
if ( sprite ) {
validExpressions . push ( sprite ) ;
2023-09-14 20:30:02 +02:00
$ ( '#image_list' ) . append ( getListItem ( item , sprite . path , 'success' , isCustom ) ) ;
2023-07-20 19:32:15 +02:00
}
else {
2023-09-14 20:30:02 +02:00
$ ( '#image_list' ) . append ( getListItem ( item , '/img/No-Image-Placeholder.svg' , 'failure' , isCustom ) ) ;
2023-07-20 19:32:15 +02:00
}
} ) ;
return validExpressions ;
}
2023-09-14 20:30:02 +02:00
/ * *
* Renders a list item template for the expressions list .
* @ param { string } item Expression name
* @ param { string } imageSrc Path to image
* @ param { 'success' | 'failure' } textClass 'success' or 'failure'
* @ param { boolean } isCustom If expression is added by user
* @ returns { string } Rendered list item template
* /
function getListItem ( item , imageSrc , textClass , isCustom ) {
return renderExtensionTemplate ( MODULE _NAME , 'list-item' , { item , imageSrc , textClass , isCustom } ) ;
2023-07-20 19:32:15 +02:00
}
async function getSpritesList ( name ) {
console . debug ( 'getting sprites list' ) ;
try {
2023-09-16 17:03:31 +02:00
const result = await fetch ( ` /api/sprites/get?name= ${ encodeURIComponent ( name ) } ` ) ;
2023-07-20 19:32:15 +02:00
let sprites = result . ok ? ( await result . json ( ) ) : [ ] ;
return sprites ;
}
catch ( err ) {
console . log ( err ) ;
return [ ] ;
}
}
2023-09-14 20:30:02 +02:00
function renderCustomExpressions ( ) {
if ( ! Array . isArray ( extension _settings . expressions . custom ) ) {
extension _settings . expressions . custom = [ ] ;
2023-07-20 19:32:15 +02:00
}
2023-09-14 20:30:02 +02:00
const customExpressions = extension _settings . expressions . custom . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
$ ( '#expression_custom' ) . empty ( ) ;
for ( const expression of customExpressions ) {
const option = document . createElement ( 'option' ) ;
option . value = expression ;
option . text = expression ;
$ ( '#expression_custom' ) . append ( option ) ;
}
2023-09-14 20:41:30 +02:00
if ( customExpressions . length === 0 ) {
$ ( '#expression_custom' ) . append ( '<option value="" disabled selected>[ No custom expressions ]</option>' ) ;
}
2023-09-14 20:30:02 +02:00
}
async function getExpressionsList ( ) {
// Return cached list if available
2023-07-20 19:32:15 +02:00
if ( Array . isArray ( expressionsList ) ) {
return expressionsList ;
}
2023-09-14 20:30:02 +02:00
/ * *
* Returns the list of expressions from the API or fallback in offline mode .
* @ returns { Promise < string [ ] > }
* /
async function resolveExpressionsList ( ) {
// get something for offline mode (default images)
if ( ! modules . includes ( 'classify' ) && ! extension _settings . expressions . local ) {
return DEFAULT _EXPRESSIONS ;
}
2023-07-20 19:32:15 +02:00
2023-09-14 20:30:02 +02:00
try {
if ( extension _settings . expressions . local ) {
const apiResult = await fetch ( '/api/extra/classify/labels' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
} ) ;
2023-09-09 14:14:16 +02:00
2023-09-14 20:30:02 +02:00
if ( apiResult . ok ) {
const data = await apiResult . json ( ) ;
expressionsList = data . labels ;
return expressionsList ;
}
} else {
const url = new URL ( getApiUrl ( ) ) ;
url . pathname = '/api/classify/labels' ;
2023-09-09 14:14:16 +02:00
2023-09-14 20:30:02 +02:00
const apiResult = await doExtrasFetch ( url , {
method : 'GET' ,
headers : { 'Bypass-Tunnel-Reminder' : 'bypass' } ,
} ) ;
2023-07-20 19:32:15 +02:00
2023-09-14 20:30:02 +02:00
if ( apiResult . ok ) {
2023-07-20 19:32:15 +02:00
2023-09-14 20:30:02 +02:00
const data = await apiResult . json ( ) ;
expressionsList = data . labels ;
return expressionsList ;
}
2023-09-09 14:14:16 +02:00
}
2023-07-20 19:32:15 +02:00
}
2023-09-14 20:30:02 +02:00
catch ( error ) {
console . log ( error ) ;
return [ ] ;
}
2023-07-20 19:32:15 +02:00
}
2023-09-14 20:30:02 +02:00
const result = await resolveExpressionsList ( ) ;
2023-12-21 15:50:30 +01:00
return [ ... result , ... extension _settings . expressions . custom ] ;
2023-07-20 19:32:15 +02:00
}
async function setExpression ( character , expression , force ) {
2023-09-11 12:01:45 +02:00
if ( extension _settings . expressions . local || ! extension _settings . expressions . talkinghead ) {
2023-07-31 19:56:05 +02:00
console . debug ( 'entered setExpressions' ) ;
await validateImages ( character ) ;
const img = $ ( 'img.expression' ) ;
const prevExpressionSrc = img . attr ( 'src' ) ;
2023-12-02 20:11:06 +01:00
const expressionClone = img . clone ( ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
const sprite = ( spriteCache [ character ] && spriteCache [ character ] . find ( x => x . label === expression ) ) ;
console . debug ( 'checking for expression images to show..' ) ;
if ( sprite ) {
console . debug ( 'setting expression from character images folder' ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
if ( force && isVisualNovelMode ( ) ) {
const context = getContext ( ) ;
const group = context . groups . find ( x => x . id === context . groupId ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
for ( const member of group . members ) {
const groupMember = context . characters . find ( x => x . avatar === member ) ;
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
if ( ! groupMember ) {
continue ;
}
2023-07-20 19:32:15 +02:00
2023-07-31 19:56:05 +02:00
if ( groupMember . name == character ) {
await setImage ( $ ( ` .expression-holder[data-avatar=" ${ member } "] img ` ) , sprite . path ) ;
return ;
}
2023-07-20 19:32:15 +02:00
}
}
2023-07-31 19:56:05 +02:00
//only swap expressions when necessary
if ( prevExpressionSrc !== sprite . path
&& ! img . hasClass ( 'expression-animating' ) ) {
//clone expression
2023-12-02 20:11:06 +01:00
expressionClone . addClass ( 'expression-clone' ) ;
2023-07-31 19:56:05 +02:00
//make invisible and remove id to prevent double ids
//must be made invisible to start because they share the same Z-index
expressionClone . attr ( 'id' , '' ) . css ( { opacity : 0 } ) ;
//add new sprite path to clone src
expressionClone . attr ( 'src' , sprite . path ) ;
//add invisible clone to html
2023-12-02 20:11:06 +01:00
expressionClone . appendTo ( $ ( '#expression-holder' ) ) ;
2023-07-31 19:56:05 +02:00
const duration = 200 ;
//add animation flags to both images
//to prevent multiple expression changes happening simultaneously
img . addClass ( 'expression-animating' ) ;
// Set the parent container's min width and height before running the transition
const imgWidth = img . width ( ) ;
const imgHeight = img . height ( ) ;
const expressionHolder = img . parent ( ) ;
expressionHolder . css ( 'min-width' , imgWidth > 100 ? imgWidth : 100 ) ;
expressionHolder . css ( 'min-height' , imgHeight > 100 ? imgHeight : 100 ) ;
//position absolute prevent the original from jumping around during transition
2023-08-25 01:07:59 +02:00
img . css ( 'position' , 'absolute' ) . width ( imgWidth ) . height ( imgHeight ) ;
2023-07-31 19:56:05 +02:00
expressionClone . addClass ( 'expression-animating' ) ;
//fade the clone in
expressionClone . css ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-31 19:56:05 +02:00
} ) . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 1 ,
2023-07-31 19:56:05 +02:00
} , duration )
//when finshed fading in clone, fade out the original
. promise ( ) . done ( function ( ) {
img . animate ( {
2023-12-02 21:06:57 +01:00
opacity : 0 ,
2023-07-31 19:56:05 +02:00
} , duration ) ;
//remove old expression
img . remove ( ) ;
//replace ID so it becomes the new 'original' expression for next change
expressionClone . attr ( 'id' , 'expression-image' ) ;
expressionClone . removeClass ( 'expression-animating' ) ;
// Reset the expression holder min height and width
expressionHolder . css ( 'min-width' , 100 ) ;
expressionHolder . css ( 'min-height' , 100 ) ;
} ) ;
expressionClone . removeClass ( 'expression-clone' ) ;
expressionClone . removeClass ( 'default' ) ;
expressionClone . off ( 'error' ) ;
expressionClone . on ( 'error' , function ( ) {
console . debug ( 'Expression image error' , sprite . path ) ;
$ ( this ) . attr ( 'src' , '' ) ;
$ ( this ) . off ( 'error' ) ;
if ( force && extension _settings . expressions . showDefault ) {
setDefault ( ) ;
}
2023-07-20 19:32:15 +02:00
} ) ;
2023-08-10 11:47:56 +02:00
}
}
else {
if ( extension _settings . expressions . showDefault ) {
setDefault ( ) ;
2023-07-31 07:52:30 +02:00
}
2023-07-20 19:32:15 +02:00
}
2023-07-31 19:56:05 +02:00
function setDefault ( ) {
console . debug ( 'setting default' ) ;
const defImgUrl = ` /img/default-expressions/ ${ expression } .png ` ;
//console.log(defImgUrl);
img . attr ( 'src' , defImgUrl ) ;
img . addClass ( 'default' ) ;
}
2023-12-02 19:04:51 +01:00
document . getElementById ( 'expression-holder' ) . style . display = '' ;
2023-07-31 19:56:05 +02:00
2023-08-03 12:05:21 +02:00
} else {
2023-08-04 07:54:01 +02:00
2023-09-07 16:48:12 +02:00
talkingHeadCheck ( ) . then ( result => {
2023-08-04 07:54:01 +02:00
if ( result ) {
2023-08-10 11:42:52 +02:00
// Find the <img> element with id="expression-image" and class="expression"
const imgElement = document . querySelector ( 'img#expression-image.expression' ) ;
//console.log("searching");
2023-09-07 16:48:12 +02:00
if ( imgElement && imgElement instanceof HTMLImageElement ) {
2023-08-10 11:42:52 +02:00
//console.log("setting value");
2023-08-10 23:55:05 +02:00
imgElement . src = getApiUrl ( ) + '/api/talkinghead/result_feed' ;
2023-08-10 11:42:52 +02:00
}
2023-08-04 07:54:01 +02:00
} else {
//console.log("The fetch failed!");
2023-08-03 13:07:50 +02:00
}
2023-08-04 07:54:01 +02:00
} ) ;
2023-08-25 22:48:59 +02:00
2023-07-31 07:52:30 +02:00
}
2023-07-31 19:56:05 +02:00
}
2023-07-20 19:32:15 +02:00
function onClickExpressionImage ( ) {
const expression = $ ( this ) . attr ( 'id' ) ;
2023-09-07 16:48:12 +02:00
setSpriteSlashCommand ( { } , expression ) ;
2023-07-20 19:32:15 +02:00
}
2023-09-07 16:48:12 +02:00
2023-09-14 20:30:02 +02:00
async function onClickExpressionAddCustom ( ) {
let expressionName = await callPopup ( renderExtensionTemplate ( MODULE _NAME , 'add-custom-expression' ) , 'input' ) ;
if ( ! expressionName ) {
console . debug ( 'No custom expression name provided' ) ;
return ;
}
expressionName = expressionName . trim ( ) . toLowerCase ( ) ;
// a-z, 0-9, dashes and underscores only
if ( ! /^[a-z0-9-_]+$/ . test ( expressionName ) ) {
toastr . info ( 'Invalid custom expression name provided' ) ;
return ;
}
// Check if expression name already exists in default expressions
if ( DEFAULT _EXPRESSIONS . includes ( expressionName ) ) {
toastr . info ( 'Expression name already exists' ) ;
return ;
}
// Check if expression name already exists in custom expressions
if ( extension _settings . expressions . custom . includes ( expressionName ) ) {
toastr . info ( 'Custom expression already exists' ) ;
return ;
}
// Add custom expression into settings
extension _settings . expressions . custom . push ( expressionName ) ;
renderCustomExpressions ( ) ;
saveSettingsDebounced ( ) ;
// Force refresh sprites list
expressionsList = null ;
spriteCache = { } ;
moduleWorker ( ) ;
}
async function onClickExpressionRemoveCustom ( ) {
const selectedExpression = $ ( '#expression_custom' ) . val ( ) ;
if ( ! selectedExpression ) {
console . debug ( 'No custom expression selected' ) ;
return ;
}
const confirmation = await callPopup ( renderExtensionTemplate ( MODULE _NAME , 'remove-custom-expression' , { expression : selectedExpression } ) , 'confirm' ) ;
if ( ! confirmation ) {
console . debug ( 'Custom expression removal cancelled' ) ;
return ;
}
// Remove custom expression from settings
const index = extension _settings . expressions . custom . indexOf ( selectedExpression ) ;
extension _settings . expressions . custom . splice ( index , 1 ) ;
renderCustomExpressions ( ) ;
saveSettingsDebounced ( ) ;
// Force refresh sprites list
expressionsList = null ;
spriteCache = { } ;
moduleWorker ( ) ;
}
2023-07-20 19:32:15 +02:00
async function handleFileUpload ( url , formData ) {
try {
const data = await jQuery . ajax ( {
2023-12-02 19:04:51 +01:00
type : 'POST' ,
2023-07-20 19:32:15 +02:00
url : url ,
data : formData ,
beforeSend : function ( ) { } ,
cache : false ,
contentType : false ,
processData : false ,
} ) ;
// Refresh sprites list
const name = formData . get ( 'name' ) ;
delete spriteCache [ name ] ;
await validateImages ( name ) ;
return data ;
} catch ( error ) {
toastr . error ( 'Failed to upload image' ) ;
}
}
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 ) ;
2023-09-16 16:28:28 +02:00
await handleFileUpload ( '/api/sprites/upload' , formData ) ;
2023-07-20 19:32:15 +02:00
// Reset the input
e . target . form . reset ( ) ;
} ;
$ ( '#expression_upload' )
. off ( 'change' )
. on ( 'change' , handleExpressionUploadChange )
. trigger ( 'click' ) ;
}
async function onClickExpressionOverrideButton ( ) {
const context = getContext ( ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
2023-09-07 16:48:12 +02:00
const avatarFileName = getFolderNameByMessage ( currentLastMessage ) ;
2023-07-20 19:32:15 +02:00
// If the avatar name couldn't be found, abort.
if ( ! avatarFileName ) {
console . debug ( ` Could not find filename for character with name ${ currentLastMessage . name } and ID ${ context . characterId } ` ) ;
return ;
}
2023-12-02 19:04:51 +01:00
const overridePath = String ( $ ( '#expression_override' ) . val ( ) ) ;
2023-07-20 19:32:15 +02:00
const existingOverrideIndex = extension _settings . expressionOverrides . findIndex ( ( e ) =>
2023-12-02 21:06:57 +01:00
e . name == avatarFileName ,
2023-07-20 19:32:15 +02:00
) ;
// If the path is empty, delete the entry from overrides
if ( overridePath === undefined || overridePath . length === 0 ) {
if ( existingOverrideIndex === - 1 ) {
return ;
}
extension _settings . expressionOverrides . splice ( existingOverrideIndex , 1 ) ;
console . debug ( ` Removed existing override for ${ avatarFileName } ` ) ;
} else {
// Properly override objects and clear the sprite cache of the previously set names
const existingOverride = extension _settings . expressionOverrides [ existingOverrideIndex ] ;
if ( existingOverride ) {
Object . assign ( existingOverride , { path : overridePath } ) ;
delete spriteCache [ existingOverride . name ] ;
} else {
const characterOverride = { name : avatarFileName , path : overridePath } ;
extension _settings . expressionOverrides . push ( characterOverride ) ;
delete spriteCache [ currentLastMessage . name ] ;
}
console . debug ( ` Added/edited expression override for character with filename ${ avatarFileName } to folder ${ overridePath } ` ) ;
}
saveSettingsDebounced ( ) ;
// Refresh sprites list. Assume the override path has been properly handled.
try {
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
await validateImages ( overridePath . length === 0 ? currentLastMessage . name : overridePath , true ) ;
const expression = await getExpressionLabel ( currentLastMessage . mes ) ;
await sendExpressionCall ( overridePath . length === 0 ? currentLastMessage . name : overridePath , expression , true ) ;
forceUpdateVisualNovelMode ( ) ;
} catch ( error ) {
console . debug ( ` Setting expression override for ${ avatarFileName } failed with error: ${ error } ` ) ;
}
}
async function onClickExpressionOverrideRemoveAllButton ( ) {
// Remove all the overrided entries from sprite cache
for ( const element of extension _settings . expressionOverrides ) {
delete spriteCache [ element . name ] ;
}
extension _settings . expressionOverrides = [ ] ;
saveSettingsDebounced ( ) ;
2023-12-02 19:04:51 +01:00
console . debug ( 'All expression image overrides have been cleared.' ) ;
2023-07-20 19:32:15 +02:00
// Refresh sprites list to use the default name if applicable
try {
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
const currentLastMessage = getLastCharacterMessage ( ) ;
await validateImages ( currentLastMessage . name , true ) ;
const expression = await getExpressionLabel ( currentLastMessage . mes ) ;
await sendExpressionCall ( currentLastMessage . name , expression , true ) ;
forceUpdateVisualNovelMode ( ) ;
console . debug ( extension _settings . expressionOverrides ) ;
} catch ( error ) {
console . debug ( ` The current expression could not be set because of error: ${ error } ` ) ;
}
}
async function onClickExpressionUploadPackButton ( ) {
const name = $ ( '#image_list' ) . data ( 'name' ) ;
const handleFileUploadChange = async ( e ) => {
const file = e . target . files [ 0 ] ;
if ( ! file ) {
return ;
}
const formData = new FormData ( ) ;
formData . append ( 'name' , name ) ;
formData . append ( 'avatar' , file ) ;
2023-09-16 16:28:28 +02:00
const { count } = await handleFileUpload ( '/api/sprites/upload-zip' , formData ) ;
2023-07-20 19:32:15 +02:00
toastr . success ( ` Uploaded ${ count } image(s) for ${ name } ` ) ;
// Reset the input
e . target . form . reset ( ) ;
} ;
$ ( '#expression_upload_pack' )
. off ( 'change' )
. on ( 'change' , handleFileUploadChange )
. trigger ( 'click' ) ;
}
async function onClickExpressionDelete ( event ) {
// Prevents the expression from being set
event . stopPropagation ( ) ;
2023-12-02 19:04:51 +01:00
const confirmation = await callPopup ( '<h3>Are you sure?</h3>Once deleted, it\'s gone forever!' , 'confirm' ) ;
2023-07-20 19:32:15 +02:00
if ( ! confirmation ) {
return ;
}
const id = $ ( this ) . closest ( '.expression_list_item' ) . attr ( 'id' ) ;
const name = $ ( '#image_list' ) . data ( 'name' ) ;
try {
2023-09-16 16:28:28 +02:00
await fetch ( '/api/sprites/delete' , {
2023-07-20 19:32:15 +02:00
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 setExpressionOverrideHtml ( forceClear = false ) {
const currentLastMessage = getLastCharacterMessage ( ) ;
2023-09-07 16:48:12 +02:00
const avatarFileName = getFolderNameByMessage ( currentLastMessage ) ;
2023-07-20 19:32:15 +02:00
if ( ! avatarFileName ) {
return ;
}
const expressionOverride = extension _settings . expressionOverrides . find ( ( e ) =>
2023-12-02 21:06:57 +01:00
e . name == avatarFileName ,
2023-07-20 19:32:15 +02:00
) ;
if ( expressionOverride && expressionOverride . path ) {
2023-12-02 19:04:51 +01:00
$ ( '#expression_override' ) . val ( expressionOverride . path ) ;
2023-07-20 19:32:15 +02:00
} else if ( expressionOverride ) {
delete extension _settings . expressionOverrides [ expressionOverride . name ] ;
}
if ( forceClear && ! expressionOverride ) {
2023-12-02 19:04:51 +01:00
$ ( '#expression_override' ) . val ( '' ) ;
2023-07-20 19:32:15 +02:00
}
}
( function ( ) {
function addExpressionImage ( ) {
const html = `
< div id = "expression-wrapper" >
2023-08-04 02:27:01 +02:00
< div id = "expression-holder" class = "expression-holder" style = "display:none;" >
2023-07-20 19:32:15 +02:00
< div id = "expression-holderheader" class = "fa-solid fa-grip drag-grabber" > < / d i v >
< img id = "expression-image" class = "expression" >
< / d i v >
< / d i v > ` ;
$ ( 'body' ) . append ( html ) ;
loadMovingUIState ( ) ;
}
function addVisualNovelMode ( ) {
const html = `
< div id = "visual-novel-wrapper" >
2023-12-02 20:11:06 +01:00
< / d i v > ` ;
2023-07-20 19:32:15 +02:00
const element = $ ( html ) ;
element . hide ( ) ;
$ ( 'body' ) . append ( element ) ;
}
function addSettings ( ) {
2023-08-25 22:48:59 +02:00
$ ( '#extensions_settings' ) . append ( renderExtensionTemplate ( MODULE _NAME , 'settings' ) ) ;
2023-07-20 19:32:15 +02:00
$ ( '#expression_override_button' ) . on ( 'click' , onClickExpressionOverrideButton ) ;
$ ( '#expressions_show_default' ) . on ( 'input' , onExpressionsShowDefaultInput ) ;
$ ( '#expression_upload_pack_button' ) . on ( 'click' , onClickExpressionUploadPackButton ) ;
$ ( '#expressions_show_default' ) . prop ( 'checked' , extension _settings . expressions . showDefault ) . trigger ( 'input' ) ;
2023-09-09 14:14:16 +02:00
$ ( '#expression_local' ) . prop ( 'checked' , extension _settings . expressions . local ) . on ( 'input' , function ( ) {
extension _settings . expressions . local = ! ! $ ( this ) . prop ( 'checked' ) ;
2023-09-11 12:01:45 +02:00
moduleWorker ( ) ;
2023-09-09 14:14:16 +02:00
saveSettingsDebounced ( ) ;
} ) ;
2023-07-20 19:32:15 +02:00
$ ( '#expression_override_cleanup_button' ) . on ( 'click' , onClickExpressionOverrideRemoveAllButton ) ;
$ ( document ) . on ( 'dragstart' , '.expression' , ( e ) => {
2023-12-02 20:11:06 +01:00
e . preventDefault ( ) ;
return false ;
} ) ;
2023-07-20 19:32:15 +02:00
$ ( document ) . on ( 'click' , '.expression_list_item' , onClickExpressionImage ) ;
$ ( document ) . on ( 'click' , '.expression_list_upload' , onClickExpressionUpload ) ;
$ ( document ) . on ( 'click' , '.expression_list_delete' , onClickExpressionDelete ) ;
2023-12-02 19:04:51 +01:00
$ ( window ) . on ( 'resize' , updateVisualNovelModeDebounced ) ;
$ ( '#open_chat_expressions' ) . hide ( ) ;
2023-07-31 07:52:30 +02:00
2023-08-03 12:05:21 +02:00
$ ( '#image_type_toggle' ) . on ( 'click' , function ( ) {
2023-09-07 16:48:12 +02:00
if ( this instanceof HTMLInputElement ) {
setTalkingHeadState ( this . checked ) ;
}
2023-07-31 07:52:30 +02:00
} ) ;
2023-09-14 20:30:02 +02:00
renderCustomExpressions ( ) ;
$ ( '#expression_custom_add' ) . on ( 'click' , onClickExpressionAddCustom ) ;
$ ( '#expression_custom_remove' ) . on ( 'click' , onClickExpressionRemoveCustom ) ;
2023-07-20 19:32:15 +02:00
}
addExpressionImage ( ) ;
addVisualNovelMode ( ) ;
addSettings ( ) ;
const wrapper = new ModuleWorkerWrapper ( moduleWorker ) ;
const updateFunction = wrapper . update . bind ( wrapper ) ;
setInterval ( updateFunction , UPDATE _INTERVAL ) ;
moduleWorker ( ) ;
2023-12-02 20:11:06 +01:00
dragElement ( $ ( '#expression-holder' ) ) ;
2023-07-20 19:32:15 +02:00
eventSource . on ( event _types . CHAT _CHANGED , ( ) => {
2023-10-16 08:12:12 +02:00
// character changed
2023-10-29 22:15:40 +01:00
removeExpression ( ) ;
spriteCache = { } ;
2023-12-30 12:35:10 +01:00
lastExpression = { } ;
2023-10-16 08:12:12 +02:00
2023-10-29 22:15:40 +01:00
//clear expression
let imgElement = document . getElementById ( 'expression-image' ) ;
if ( imgElement && imgElement instanceof HTMLImageElement ) {
2023-12-02 19:04:51 +01:00
imgElement . src = '' ;
2023-10-29 22:15:40 +01:00
}
2023-10-16 08:12:12 +02:00
2023-10-29 22:15:40 +01:00
//set checkbox to global var
$ ( '#image_type_toggle' ) . prop ( 'checked' , extension _settings . expressions . talkinghead ) ;
if ( extension _settings . expressions . talkinghead ) {
setTalkingHeadState ( extension _settings . expressions . talkinghead ) ;
2023-10-16 08:12:12 +02:00
}
2023-07-20 19:32:15 +02:00
setExpressionOverrideHtml ( ) ;
if ( isVisualNovelMode ( ) ) {
$ ( '#visual-novel-wrapper' ) . empty ( ) ;
}
2023-10-23 00:56:27 +02:00
updateFunction ( ) ;
2023-07-20 19:32:15 +02:00
} ) ;
eventSource . on ( event _types . MOVABLE _PANELS _RESET , updateVisualNovelModeDebounced ) ;
eventSource . on ( event _types . GROUP _UPDATED , updateVisualNovelModeDebounced ) ;
2023-10-07 18:25:36 +02:00
registerSlashCommand ( 'sprite' , setSpriteSlashCommand , [ 'emote' ] , '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character' , true , true ) ;
registerSlashCommand ( 'spriteoverride' , setSpriteSetCommand , [ 'costume' ] , '<span class="monospace">(optional folder)</span> – 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.' , true , true ) ;
2024-01-01 15:07:21 +01:00
registerSlashCommand ( 'lastsprite' , ( _ , value ) => lastExpression [ value . trim ( ) ] ? ? '' , [ ] , '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.' , true , true ) ;
2023-07-20 19:32:15 +02:00
} ) ( ) ;