From 9ef5b166632b39282f7eda5a095d82b19e0a8da8 Mon Sep 17 00:00:00 2001 From: Tremendoussly Date: Sun, 15 Mar 2026 17:38:39 +0100 Subject: [PATCH] Break expression sync cycle and guard portrait lookup --- index.js | 12 ++- src/systems/integration/expressionSync.js | 86 ++++---------------- src/systems/ui/alternatePresentCharacters.js | 2 +- src/utils/expressionPortraits.js | 73 +++++++++++++++++ src/utils/presentCharacters.js | 2 +- 5 files changed, 101 insertions(+), 74 deletions(-) create mode 100644 src/utils/expressionPortraits.js diff --git a/index.js b/index.js index 5131fc2..ceaeefb 100644 --- a/index.js +++ b/index.js @@ -134,7 +134,10 @@ import { removeDesktopTabs, updateStripWidgets } from './src/systems/ui/desktop.js'; -import { removeAlternatePresentCharactersPanel } from './src/systems/ui/alternatePresentCharacters.js'; +import { + removeAlternatePresentCharactersPanel, + renderAlternatePresentCharacters +} from './src/systems/ui/alternatePresentCharacters.js'; import { initExpressionSync, queueExpressionCaptureForSpeaker, @@ -142,7 +145,8 @@ import { onAlternatePresentCharactersVisibilityChanged, onHideDefaultExpressionDisplaySettingChanged, clearExpressionSyncCache, - onExpressionSyncChatChanged + onExpressionSyncChatChanged, + setExpressionSyncRefreshHandler } from './src/systems/integration/expressionSync.js'; // Feature modules @@ -173,6 +177,10 @@ import { // Old state variable declarations removed - now imported from core modules // (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js) +setExpressionSyncRefreshHandler(() => { + renderAlternatePresentCharacters({ useCommittedFallback: true }); +}); + // Utility functions removed - now imported from src/utils/avatars.js // (getSafeThumbnailUrl) diff --git a/src/systems/integration/expressionSync.js b/src/systems/integration/expressionSync.js index da25730..95ef26e 100644 --- a/src/systems/integration/expressionSync.js +++ b/src/systems/integration/expressionSync.js @@ -11,12 +11,11 @@ import { extensionSettings, syncedExpressionPortraits, setSyncedExpressionPortrait, - getSyncedExpressionPortrait, removeSyncedExpressionPortrait } from '../../core/state.js'; import { saveChatData } from '../../core/persistence.js'; -import { isSafeImageSrc, normalizeImageSrc, resolveImageUrl } from '../../utils/imageUrls.js'; -import { renderAlternatePresentCharacters } from '../ui/alternatePresentCharacters.js'; +import { normalizeImageSrc } from '../../utils/imageUrls.js'; +import { isUsableExpressionSrc } from '../../utils/expressionPortraits.js'; let expressionContainerObserver = null; let expressionImageObserver = null; @@ -28,53 +27,26 @@ let lastCapturedExpressionSrc = null; let scheduledCaptureTimers = []; let hiddenExpressionStyleElement = null; let pendingCaptureRequestId = 0; +let refreshExpressionConsumersHandler = null; function normalizeName(name) { return String(name || '').trim().toLowerCase(); } +function namesMatch(a, b) { + const left = normalizeName(a); + const right = normalizeName(b); + if (!left || !right) { + return false; + } + + return left === right || left.startsWith(right + ' ') || right.startsWith(left + ' '); +} + function normalizeExpressionSrc(src) { return normalizeImageSrc(src); } -function resolveExpressionUrl(src) { - return resolveImageUrl(src); -} - -function isDocumentLikeUrl(src) { - const candidate = resolveExpressionUrl(src); - if (!candidate) { - return false; - } - - const current = new URL(window.location.href); - return candidate.origin === current.origin - && candidate.pathname === current.pathname - && candidate.search === current.search; -} - -function isUsableExpressionSrc(src) { - const normalized = normalizeExpressionSrc(src); - if (!normalized) { - return false; - } - - const lower = normalized.toLowerCase(); - if (lower.includes('/img/default-expressions/') || lower.includes('/default-expressions/')) { - return false; - } - - if (isDocumentLikeUrl(normalized)) { - return false; - } - - if (!isSafeImageSrc(normalized)) { - return false; - } - - return true; -} - function purgeInvalidSyncedExpressionPortraits() { let changed = false; @@ -92,34 +64,8 @@ function purgeInvalidSyncedExpressionPortraits() { return changed; } -function namesMatch(a, b) { - const left = normalizeName(a); - const right = normalizeName(b); - if (!left || !right) { - return false; - } - - return left === right || left.startsWith(right + ' ') || right.startsWith(left + ' '); -} - -export function getExpressionPortraitForCharacter(characterName) { - const target = normalizeName(characterName); - if (!target) { - return null; - } - - const exact = getSyncedExpressionPortrait(target); - if (isUsableExpressionSrc(exact)) { - return exact; - } - - for (const [storedName, src] of Object.entries(syncedExpressionPortraits)) { - if (namesMatch(storedName, target) && isUsableExpressionSrc(src)) { - return src; - } - } - - return null; +export function setExpressionSyncRefreshHandler(handler) { + refreshExpressionConsumersHandler = typeof handler === 'function' ? handler : null; } function getLatestAssistantSpeakerName() { @@ -325,7 +271,7 @@ function findExpressionImageElement(speakerName = null) { } function refreshExpressionConsumers() { - renderAlternatePresentCharacters({ useCommittedFallback: true }); + refreshExpressionConsumersHandler?.(); } function getHideStyleCss() { diff --git a/src/systems/ui/alternatePresentCharacters.js b/src/systems/ui/alternatePresentCharacters.js index e0a28d0..4c3e7e6 100644 --- a/src/systems/ui/alternatePresentCharacters.js +++ b/src/systems/ui/alternatePresentCharacters.js @@ -1,7 +1,7 @@ import { extensionSettings } from '../../core/state.js'; import { i18n } from '../../core/i18n.js'; +import { getExpressionPortraitForCharacter } from '../../utils/expressionPortraits.js'; import { getSafeImageSrc } from '../../utils/imageUrls.js'; -import { getExpressionPortraitForCharacter } from '../integration/expressionSync.js'; import { getPresentCharactersTrackerData, parsePresentCharacters, diff --git a/src/utils/expressionPortraits.js b/src/utils/expressionPortraits.js new file mode 100644 index 0000000..2672c99 --- /dev/null +++ b/src/utils/expressionPortraits.js @@ -0,0 +1,73 @@ +import { + syncedExpressionPortraits, + getSyncedExpressionPortrait +} from '../core/state.js'; +import { + isSafeImageSrc, + normalizeImageSrc, + resolveImageUrl +} from './imageUrls.js'; + +function normalizeName(name) { + return String(name || '').trim().toLowerCase(); +} + +function namesMatch(a, b) { + const left = normalizeName(a); + const right = normalizeName(b); + if (!left || !right) { + return false; + } + + return left === right || left.startsWith(right + ' ') || right.startsWith(left + ' '); +} + +function isDocumentLikeUrl(src) { + const candidate = resolveImageUrl(src); + if (!candidate) { + return false; + } + + const current = new URL(window.location.href); + return candidate.origin === current.origin + && candidate.pathname === current.pathname + && candidate.search === current.search; +} + +export function isUsableExpressionSrc(src) { + const normalized = normalizeImageSrc(src); + if (!normalized) { + return false; + } + + const lower = normalized.toLowerCase(); + if (lower.includes('/img/default-expressions/') || lower.includes('/default-expressions/')) { + return false; + } + + if (isDocumentLikeUrl(normalized)) { + return false; + } + + return isSafeImageSrc(normalized); +} + +export function getExpressionPortraitForCharacter(characterName) { + const target = normalizeName(characterName); + if (!target) { + return null; + } + + const exact = getSyncedExpressionPortrait(target); + if (isUsableExpressionSrc(exact)) { + return exact; + } + + for (const [storedName, src] of Object.entries(syncedExpressionPortraits)) { + if (namesMatch(storedName, target) && isUsableExpressionSrc(src)) { + return src; + } + } + + return null; +} diff --git a/src/utils/presentCharacters.js b/src/utils/presentCharacters.js index 7a9ad2d..68cb6ef 100644 --- a/src/utils/presentCharacters.js +++ b/src/utils/presentCharacters.js @@ -226,7 +226,7 @@ export function resolvePresentCharacterPortrait(name) { } } - if (this_chid !== undefined && characters[this_chid]?.name && namesMatch(characters[this_chid].name, name)) { + if (this_chid !== undefined && characters?.[this_chid]?.name && namesMatch(characters[this_chid].name, name)) { const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar); if (thumbnailUrl) { return thumbnailUrl;