feat: Add Character Quick Reply Sets (#4215) * charQR * fix * fix_2 * fix_3 * Fix refresh errors and Implement confirmation dialog logic * FIX : The character's quick reply set is executed three times during the initial authorization and import. * Enhance warning dialog content and layout * refactor: Use local linking for Char QR sets instead of embedding * Lint fixes * Clean-up * Emit CHAT_CHANGED on character deletion * Remove isVisible filter, I'm sure it was by design * Revert order of search in executeQuickReplyByName * fix: Address review comments * Add private tag to Char Sets * Add QR rebind on renamed character * Clean-up diff * Don't save empty config lists --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -9231,6 +9231,7 @@ async function removeCharacterFromUI() { | |||
| 9231 | await getCharacters(); | 9231 | await getCharacters(); |
| 9232 | await printMessages(); | 9232 | await printMessages(); |
| 9233 | saveSettingsDebounced(); | 9233 | saveSettingsDebounced(); |
| 9234 | await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); | ||
| 9234 | } | 9235 | } |
| 9235 | 9236 | ||
| 9236 | /** | 9237 | /** |
| @@ -41,6 +41,19 @@ | |||
| 41 | 41 | ||
| 42 | <hr> | 42 | <hr> |
| 43 | 43 | ||
| 44 | <div id="qr--character"> | ||
| 45 | <div class="qr--head"> | ||
| 46 | <div class="qr--title" data-i18n="Character Quick Reply Sets">Character Quick Reply Sets</div> | ||
| 47 | <small data-i18n="(Private)">(Private)</small> | ||
| 48 | <div class="qr--actions"> | ||
| 49 | <div class="qr--setListAdd menu_button menu_button_icon fa-solid fa-plus" id="qr--character-setListAdd" title="Add quick reply set"></div> | ||
| 50 | </div> | ||
| 51 | </div> | ||
| 52 | <div id="qr--character-setList" class="qr--setList"></div> | ||
| 53 | </div> | ||
| 54 | |||
| 55 | <hr> | ||
| 56 | |||
| 44 | <div id="qr--editor"> | 57 | <div id="qr--editor"> |
| 45 | <div class="qr--head"> | 58 | <div class="qr--head"> |
| 46 | <div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div> | 59 | <div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div> |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { chat, chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js'; | 1 | import { chat, chat_metadata, eventSource, event_types, getRequestHeaders, this_chid, characters } from '../../../script.js'; |
| 2 | import { extension_settings } from '../../extensions.js'; | 2 | import { extension_settings } from '../../extensions.js'; |
| 3 | import { QuickReplyApi } from './api/QuickReplyApi.js'; | 3 | import { QuickReplyApi } from './api/QuickReplyApi.js'; |
| 4 | import { AutoExecuteHandler } from './src/AutoExecuteHandler.js'; | 4 | import { AutoExecuteHandler } from './src/AutoExecuteHandler.js'; |
| @@ -10,6 +10,7 @@ import { SlashCommandHandler } from './src/SlashCommandHandler.js'; | |||
| 10 | import { ButtonUi } from './src/ui/ButtonUi.js'; | 10 | import { ButtonUi } from './src/ui/ButtonUi.js'; |
| 11 | import { SettingsUi } from './src/ui/SettingsUi.js'; | 11 | import { SettingsUi } from './src/ui/SettingsUi.js'; |
| 12 | import { debounceAsync } from '../../utils.js'; | 12 | import { debounceAsync } from '../../utils.js'; |
| 13 | import { selected_group } from '../../group-chats.js'; | ||
| 13 | export { debounceAsync }; | 14 | export { debounceAsync }; |
| 14 | 15 | ||
| 15 | 16 | ||
| @@ -39,6 +40,8 @@ const defaultSettings = { | |||
| 39 | let isReady = false; | 40 | let isReady = false; |
| 40 | /** @type {Function[]}*/ | 41 | /** @type {Function[]}*/ |
| 41 | let executeQueue = []; | 42 | let executeQueue = []; |
| 43 | /** @type {string}*/ | ||
| 44 | let lastCharId; | ||
| 42 | /** @type {QuickReplySettings}*/ | 45 | /** @type {QuickReplySettings}*/ |
| 43 | let settings; | 46 | let settings; |
| 44 | /** @type {SettingsUi} */ | 47 | /** @type {SettingsUi} */ |
| @@ -123,6 +126,8 @@ const loadSettings = async () => { | |||
| 123 | } | 126 | } |
| 124 | try { | 127 | try { |
| 125 | settings = QuickReplySettings.from(extension_settings.quickReplyV2); | 128 | settings = QuickReplySettings.from(extension_settings.quickReplyV2); |
| 129 | settings.config.scope = 'global'; | ||
| 130 | settings.config.onUpdate = () => settings.save(); | ||
| 126 | } catch (ex) { | 131 | } catch (ex) { |
| 127 | settings = QuickReplySettings.from(defaultSettings); | 132 | settings = QuickReplySettings.from(defaultSettings); |
| 128 | } | 133 | } |
| @@ -138,8 +143,34 @@ const executeIfReadyElseQueue = async (functionToCall, args) => { | |||
| 138 | } | 143 | } |
| 139 | }; | 144 | }; |
| 140 | 145 | ||
| 146 | const handleCharChange = () => { | ||
| 147 | if (lastCharId === this_chid) return; | ||
| 141 | 148 | ||
| 149 | // Unload the old character's config and update the character ID cache. | ||
| 150 | settings.charConfig = null; | ||
| 151 | lastCharId = this_chid; | ||
| 142 | 152 | ||
| 153 | // If no character is loaded, there's nothing more to do. | ||
| 154 | /** @type {import('../../char-data.js').v1CharData} */ | ||
| 155 | const character = characters[this_chid]; | ||
| 156 | if (!character || selected_group) { | ||
| 157 | return; | ||
| 158 | } | ||
| 159 | |||
| 160 | // Get the character-specific config from the local settings storage. | ||
| 161 | let charConfig = settings.characterConfigs[character.avatar]; | ||
| 162 | |||
| 163 | // If no config exists for this character, create a new one. | ||
| 164 | if (!charConfig) { | ||
| 165 | charConfig = QuickReplyConfig.from({ setList: [] }); | ||
| 166 | settings.characterConfigs[character.avatar] = charConfig; | ||
| 167 | } | ||
| 168 | |||
| 169 | charConfig.scope = 'character'; | ||
| 170 | // The main settings save function will handle persistence. | ||
| 171 | charConfig.onUpdate = () => settings.save(); | ||
| 172 | settings.charConfig = charConfig; | ||
| 173 | }; | ||
| 143 | 174 | ||
| 144 | const init = async () => { | 175 | const init = async () => { |
| 145 | await loadSets(); | 176 | await loadSets(); |
| @@ -154,7 +185,11 @@ const init = async () => { | |||
| 154 | settings.onSave = ()=>buttons.refresh(); | 185 | settings.onSave = ()=>buttons.refresh(); |
| 155 | 186 | ||
| 156 | window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => { | 187 | window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => { |
| 157 | let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])] | 188 | let qr = [ |
| 189 | ...settings.config.setList, | ||
| 190 | ...(settings.chatConfig?.setList ?? []), | ||
| 191 | ...(settings.charConfig?.setList ?? []), | ||
| 192 | ] | ||
| 158 | .map(it => it.set.qrList) | 193 | .map(it => it.set.qrList) |
| 159 | .flat() | 194 | .flat() |
| 160 | .find(it=>it.label == name) | 195 | .find(it=>it.label == name) |
| @@ -199,10 +234,38 @@ const finalizeInit = async () => { | |||
| 199 | }; | 234 | }; |
| 200 | await init(); | 235 | await init(); |
| 201 | 236 | ||
| 237 | const purgeCharacterQuickReplySets = ({ character }) => { | ||
| 238 | // Remove the character's Quick Reply Sets from the settings. | ||
| 239 | const avatar = character?.avatar; | ||
| 240 | if (avatar && avatar in settings.characterConfigs) { | ||
| 241 | log(`Purging Quick Reply Sets for character: ${avatar}`); | ||
| 242 | delete settings.characterConfigs[avatar]; | ||
| 243 | settings.save(); | ||
| 244 | } | ||
| 245 | }; | ||
| 246 | |||
| 247 | const updateCharacterQuickReplySets = (oldAvatar, newAvatar) => { | ||
| 248 | // Update the character's Quick Reply Sets in the settings. | ||
| 249 | if (oldAvatar && newAvatar && oldAvatar !== newAvatar) { | ||
| 250 | log(`Updating Quick Reply Sets for character: ${oldAvatar} -> ${newAvatar}`); | ||
| 251 | if (settings.characterConfigs[oldAvatar]) { | ||
| 252 | settings.characterConfigs[newAvatar] = settings.characterConfigs[oldAvatar]; | ||
| 253 | delete settings.characterConfigs[oldAvatar]; | ||
| 254 | settings.save(); | ||
| 255 | } | ||
| 256 | } | ||
| 257 | }; | ||
| 258 | |||
| 202 | const onChatChanged = async (chatIdx) => { | 259 | const onChatChanged = async (chatIdx) => { |
| 203 | log('CHAT_CHANGED', chatIdx); | 260 | log('CHAT_CHANGED', chatIdx); |
| 261 | |||
| 262 | handleCharChange(); | ||
| 263 | |||
| 204 | if (chatIdx) { | 264 | if (chatIdx) { |
| 205 | settings.chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {}); | 265 | const chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {}); |
| 266 | chatConfig.scope = 'chat'; | ||
| 267 | chatConfig.onUpdate = () => settings.save(); | ||
| 268 | settings.chatConfig = chatConfig; | ||
| 206 | } else { | 269 | } else { |
| 207 | settings.chatConfig = null; | 270 | settings.chatConfig = null; |
| 208 | } | 271 | } |
| @@ -212,6 +275,8 @@ const onChatChanged = async (chatIdx) => { | |||
| 212 | await autoExec.handleChatChanged(); | 275 | await autoExec.handleChatChanged(); |
| 213 | }; | 276 | }; |
| 214 | eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args)); | 277 | eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args)); |
| 278 | eventSource.on(event_types.CHARACTER_DELETED, purgeCharacterQuickReplySets); | ||
| 279 | eventSource.on(event_types.CHARACTER_RENAMED, updateCharacterQuickReplySets); | ||
| 215 | 280 | ||
| 216 | const onUserMessage = async () => { | 281 | const onUserMessage = async () => { |
| 217 | await autoExec.handleUser(); | 282 | await autoExec.handleUser(); |
| @@ -36,58 +36,46 @@ export class AutoExecuteHandler { | |||
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | 38 | ||
| 39 | getCommands(eventName) { | ||
| 40 | const getFromConfig = (config) => { | ||
| 41 | // This safely handles cases where a link exists but the set hasn't been loaded (link.set is null) | ||
| 42 | return config?.setList?.map(link => link.set ? link.set.qrList.filter(qr => qr[eventName]) : [])?.flat() ?? []; | ||
| 43 | }; | ||
| 44 | return [ | ||
| 45 | ...getFromConfig(this.settings.config), | ||
| 46 | ...getFromConfig(this.settings.chatConfig), | ||
| 47 | ...getFromConfig(this.settings.charConfig), | ||
| 48 | ]; | ||
| 49 | } | ||
| 50 | |||
| 39 | async handleStartup() { | 51 | async handleStartup() { |
| 40 | if (!this.checkExecute()) return; | 52 | if (!this.checkExecute()) return; |
| 41 | const qrList = [ | 53 | await this.performAutoExecute(this.getCommands('executeOnStartup')); |
| 42 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(), | ||
| 43 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []), | ||
| 44 | ]; | ||
| 45 | await this.performAutoExecute(qrList); | ||
| 46 | } | 54 | } |
| 47 | 55 | ||
| 48 | async handleUser() { | 56 | async handleUser() { |
| 49 | if (!this.checkExecute()) return; | 57 | if (!this.checkExecute()) return; |
| 50 | const qrList = [ | 58 | await this.performAutoExecute(this.getCommands('executeOnUser')); |
| 51 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(), | ||
| 52 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []), | ||
| 53 | ]; | ||
| 54 | await this.performAutoExecute(qrList); | ||
| 55 | } | 59 | } |
| 56 | 60 | ||
| 57 | async handleAi() { | 61 | async handleAi() { |
| 58 | if (!this.checkExecute()) return; | 62 | if (!this.checkExecute()) return; |
| 59 | const qrList = [ | 63 | await this.performAutoExecute(this.getCommands('executeOnAi')); |
| 60 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(), | ||
| 61 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []), | ||
| 62 | ]; | ||
| 63 | await this.performAutoExecute(qrList); | ||
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | async handleChatChanged() { | 66 | async handleChatChanged() { |
| 67 | if (!this.checkExecute()) return; | 67 | if (!this.checkExecute()) return; |
| 68 | const qrList = [ | 68 | await this.performAutoExecute(this.getCommands('executeOnChatChange')); |
| 69 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(), | ||
| 70 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []), | ||
| 71 | ]; | ||
| 72 | await this.performAutoExecute(qrList); | ||
| 73 | } | 69 | } |
| 74 | 70 | ||
| 75 | async handleGroupMemberDraft() { | 71 | async handleGroupMemberDraft() { |
| 76 | if (!this.checkExecute()) return; | 72 | if (!this.checkExecute()) return; |
| 77 | const qrList = [ | 73 | await this.performAutoExecute(this.getCommands('executeOnGroupMemberDraft')); |
| 78 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft)).flat(), | ||
| 79 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnGroupMemberDraft))?.flat() ?? []), | ||
| 80 | ]; | ||
| 81 | await this.performAutoExecute(qrList); | ||
| 82 | } | 74 | } |
| 83 | 75 | ||
| 84 | async handleNewChat() { | 76 | async handleNewChat() { |
| 85 | if (!this.checkExecute()) return; | 77 | if (!this.checkExecute()) return; |
| 86 | const qrList = [ | 78 | await this.performAutoExecute(this.getCommands('executeOnNewChat')); |
| 87 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat)).flat(), | ||
| 88 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnNewChat))?.flat() ?? []), | ||
| 89 | ]; | ||
| 90 | await this.performAutoExecute(qrList); | ||
| 91 | } | 79 | } |
| 92 | 80 | ||
| 93 | /** | 81 | /** |
| @@ -98,9 +86,16 @@ export class AutoExecuteHandler { | |||
| 98 | const automationIds = entries.map(entry => entry.automationId).filter(Boolean); | 86 | const automationIds = entries.map(entry => entry.automationId).filter(Boolean); |
| 99 | if (automationIds.length === 0) return; | 87 | if (automationIds.length === 0) return; |
| 100 | 88 | ||
| 89 | const getFromConfig = (config) => { | ||
| 90 | return config?.setList | ||
| 91 | ?.map(link => link.set ? link.set.qrList.filter(qr => qr.automationId && automationIds.includes(qr.automationId)) : []) | ||
| 92 | ?.flat() ?? []; | ||
| 93 | }; | ||
| 94 | |||
| 101 | const qrList = [ | 95 | const qrList = [ |
| 102 | ...this.settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId))).flat(), | 96 | ...getFromConfig(this.settings.config), |
| 103 | ...(this.settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.automationId && automationIds.includes(qr.automationId)))?.flat() ?? []), | 97 | ...getFromConfig(this.settings.chatConfig), |
| 98 | ...getFromConfig(this.settings.charConfig), | ||
| 104 | ]; | 99 | ]; |
| 105 | 100 | ||
| 106 | await this.performAutoExecute(qrList); | 101 | await this.performAutoExecute(qrList); |
| @@ -434,6 +434,7 @@ export class QuickReply { | |||
| 434 | this.updateLabel(label.value); | 434 | this.updateLabel(label.value); |
| 435 | }); | 435 | }); |
| 436 | let switcherList; | 436 | let switcherList; |
| 437 | // @ts-ignore | ||
| 437 | dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{ | 438 | dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{ |
| 438 | if (switcherList) { | 439 | if (switcherList) { |
| 439 | switcherList.remove(); | 440 | switcherList.remove(); |
| @@ -618,7 +619,9 @@ export class QuickReply { | |||
| 618 | accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); | 619 | accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); |
| 619 | updateSyntaxEnabled(); | 620 | updateSyntaxEnabled(); |
| 620 | }); | 621 | }); |
| 622 | // @ts-ignore | ||
| 621 | if (navigator.keyboard) { | 623 | if (navigator.keyboard) { |
| 624 | // @ts-ignore | ||
| 622 | navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); | 625 | navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); |
| 623 | } else { | 626 | } else { |
| 624 | dom.querySelector('#qr--modal-commentKey').closest('small').remove(); | 627 | dom.querySelector('#qr--modal-commentKey').closest('small').remove(); |
| @@ -781,6 +784,7 @@ export class QuickReply { | |||
| 781 | message.addEventListener('wheel', (evt)=>{ | 784 | message.addEventListener('wheel', (evt)=>{ |
| 782 | updateScrollDebounced(evt); | 785 | updateScrollDebounced(evt); |
| 783 | }); | 786 | }); |
| 787 | // @ts-ignore | ||
| 784 | message.addEventListener('scroll', (evt)=>{ | 788 | message.addEventListener('scroll', (evt)=>{ |
| 785 | updateScrollDebounced(); | 789 | updateScrollDebounced(); |
| 786 | }); | 790 | }); |
| @@ -1117,6 +1121,7 @@ export class QuickReply { | |||
| 1117 | /**@type {HTMLTextAreaElement} */ | 1121 | /**@type {HTMLTextAreaElement} */ |
| 1118 | const inputOg = document.querySelector('#send_textarea'); | 1122 | const inputOg = document.querySelector('#send_textarea'); |
| 1119 | const inputMirror = dom.querySelector('#qr--modal-send_textarea'); | 1123 | const inputMirror = dom.querySelector('#qr--modal-send_textarea'); |
| 1124 | // @ts-ignore | ||
| 1120 | inputMirror.value = inputOg.value; | 1125 | inputMirror.value = inputOg.value; |
| 1121 | const inputOgMo = new MutationObserver(muts=>{ | 1126 | const inputOgMo = new MutationObserver(muts=>{ |
| 1122 | if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) { | 1127 | if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) { |
| @@ -1125,10 +1130,12 @@ export class QuickReply { | |||
| 1125 | }); | 1130 | }); |
| 1126 | inputOgMo.observe(document.body, { childList:true }); | 1131 | inputOgMo.observe(document.body, { childList:true }); |
| 1127 | const inputOgListener = ()=>{ | 1132 | const inputOgListener = ()=>{ |
| 1133 | // @ts-ignore | ||
| 1128 | inputMirror.value = inputOg.value; | 1134 | inputMirror.value = inputOg.value; |
| 1129 | }; | 1135 | }; |
| 1130 | inputOg.addEventListener('input', inputOgListener); | 1136 | inputOg.addEventListener('input', inputOgListener); |
| 1131 | inputMirror.addEventListener('input', ()=>{ | 1137 | inputMirror.addEventListener('input', ()=>{ |
| 1138 | // @ts-ignore | ||
| 1132 | inputOg.value = inputMirror.value; | 1139 | inputOg.value = inputMirror.value; |
| 1133 | }); | 1140 | }); |
| 1134 | 1141 | ||
| @@ -1172,15 +1179,19 @@ export class QuickReply { | |||
| 1172 | isResizing = true; | 1179 | isResizing = true; |
| 1173 | evt.preventDefault(); | 1180 | evt.preventDefault(); |
| 1174 | resizeStart = evt.x; | 1181 | resizeStart = evt.x; |
| 1182 | // @ts-ignore | ||
| 1175 | wStart = dom.querySelector('#qr--qrOptions').offsetWidth; | 1183 | wStart = dom.querySelector('#qr--qrOptions').offsetWidth; |
| 1176 | const dragListener = debounce((evt)=>{ | 1184 | const dragListener = debounce((evt)=>{ |
| 1177 | const w = wStart + resizeStart - evt.x; | 1185 | const w = wStart + resizeStart - evt.x; |
| 1186 | // @ts-ignore | ||
| 1178 | dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); | 1187 | dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); |
| 1179 | }, 5); | 1188 | }, 5); |
| 1180 | window.addEventListener('pointerup', ()=>{ | 1189 | window.addEventListener('pointerup', ()=>{ |
| 1190 | // @ts-ignore | ||
| 1181 | window.removeEventListener('pointermove', dragListener); | 1191 | window.removeEventListener('pointermove', dragListener); |
| 1182 | isResizing = false; | 1192 | isResizing = false; |
| 1183 | }, { once:true }); | 1193 | }, { once:true }); |
| 1194 | // @ts-ignore | ||
| 1184 | window.addEventListener('pointermove', dragListener); | 1195 | window.addEventListener('pointermove', dragListener); |
| 1185 | }); | 1196 | }); |
| 1186 | 1197 | ||
| @@ -1282,6 +1293,7 @@ export class QuickReply { | |||
| 1282 | syntax.addEventListener('wheel', (evt)=>{ | 1293 | syntax.addEventListener('wheel', (evt)=>{ |
| 1283 | updateScrollDebounced(evt); | 1294 | updateScrollDebounced(evt); |
| 1284 | }); | 1295 | }); |
| 1296 | // @ts-ignore | ||
| 1285 | syntax.addEventListener('scroll', (evt)=>{ | 1297 | syntax.addEventListener('scroll', (evt)=>{ |
| 1286 | updateScrollDebounced(); | 1298 | updateScrollDebounced(); |
| 1287 | }); | 1299 | }); |
| @@ -1414,8 +1426,11 @@ export class QuickReply { | |||
| 1414 | let i = 0; | 1426 | let i = 0; |
| 1415 | let unnamed = this.debugController.unnamedArguments ?? []; | 1427 | let unnamed = this.debugController.unnamedArguments ?? []; |
| 1416 | if (!Array.isArray(unnamed)) unnamed = [unnamed]; | 1428 | if (!Array.isArray(unnamed)) unnamed = [unnamed]; |
| 1429 | // @ts-ignore | ||
| 1417 | while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); | 1430 | while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); |
| 1431 | // @ts-ignore | ||
| 1418 | unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); | 1432 | unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); |
| 1433 | // @ts-ignore | ||
| 1419 | for (const arg of unnamed) { | 1434 | for (const arg of unnamed) { |
| 1420 | i++; | 1435 | i++; |
| 1421 | const item = document.createElement('div'); { | 1436 | const item = document.createElement('div'); { |
| @@ -4,7 +4,7 @@ import { QuickReplySet } from './QuickReplySet.js'; | |||
| 4 | 4 | ||
| 5 | export class QuickReplyConfig { | 5 | export class QuickReplyConfig { |
| 6 | /**@type {QuickReplySetLink[]}*/ setList = []; | 6 | /**@type {QuickReplySetLink[]}*/ setList = []; |
| 7 | /**@type {Boolean}*/ isGlobal; | 7 | /**@type {'global'|'chat'|'character'}*/ scope; |
| 8 | 8 | ||
| 9 | /**@type {Function}*/ onUpdate; | 9 | /**@type {Function}*/ onUpdate; |
| 10 | /**@type {Function}*/ onRequestEditSet; | 10 | /**@type {Function}*/ onRequestEditSet; |
| @@ -28,6 +28,7 @@ export class QuickReplySet { | |||
| 28 | } | 28 | } |
| 29 | 29 | ||
| 30 | /**@type {string}*/ name; | 30 | /**@type {string}*/ name; |
| 31 | /**@type {'global'|'chat'|'character'}*/ scope = 'global'; | ||
| 31 | /**@type {boolean}*/ disableSend = false; | 32 | /**@type {boolean}*/ disableSend = false; |
| 32 | /**@type {boolean}*/ placeBeforeInput = false; | 33 | /**@type {boolean}*/ placeBeforeInput = false; |
| 33 | /**@type {boolean}*/ injectInput = false; | 34 | /**@type {boolean}*/ injectInput = false; |
| @@ -255,6 +256,7 @@ export class QuickReplySet { | |||
| 255 | * @param {QuickReply} qr | 256 | * @param {QuickReply} qr |
| 256 | */ | 257 | */ |
| 257 | hookQuickReply(qr) { | 258 | hookQuickReply(qr) { |
| 259 | // @ts-ignore | ||
| 258 | qr.onDebug = ()=>this.debug(qr); | 260 | qr.onDebug = ()=>this.debug(qr); |
| 259 | qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); | 261 | qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); |
| 260 | qr.onDelete = ()=>this.removeQuickReply(qr); | 262 | qr.onDelete = ()=>this.removeQuickReply(qr); |
| @@ -301,12 +303,14 @@ export class QuickReplySet { | |||
| 301 | } | 303 | } |
| 302 | sel.addEventListener('keyup', (evt)=>{ | 304 | sel.addEventListener('keyup', (evt)=>{ |
| 303 | if (evt.key == 'Shift') { | 305 | if (evt.key == 'Shift') { |
| 306 | // @ts-ignore | ||
| 304 | (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy'); | 307 | (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy'); |
| 305 | return; | 308 | return; |
| 306 | } | 309 | } |
| 307 | }); | 310 | }); |
| 308 | sel.addEventListener('keydown', (evt)=>{ | 311 | sel.addEventListener('keydown', (evt)=>{ |
| 309 | if (evt.key == 'Shift') { | 312 | if (evt.key == 'Shift') { |
| 313 | // @ts-ignore | ||
| 310 | (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy'); | 314 | (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy'); |
| 311 | return; | 315 | return; |
| 312 | } | 316 | } |
| @@ -335,6 +339,7 @@ export class QuickReplySet { | |||
| 335 | isCopy = true; | 339 | isCopy = true; |
| 336 | dlg.completeAffirmative(); | 340 | dlg.completeAffirmative(); |
| 337 | }); | 341 | }); |
| 342 | // @ts-ignore | ||
| 338 | (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn); | 343 | (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn); |
| 339 | } | 344 | } |
| 340 | const prom = dlg.show(); | 345 | const prom = dlg.show(); |
| @@ -5,6 +5,10 @@ import { QuickReplyConfig } from './QuickReplyConfig.js'; | |||
| 5 | export class QuickReplySettings { | 5 | export class QuickReplySettings { |
| 6 | static from(props) { | 6 | static from(props) { |
| 7 | props.config = QuickReplyConfig.from(props.config); | 7 | props.config = QuickReplyConfig.from(props.config); |
| 8 | props.characterConfigs = props.characterConfigs ?? {}; | ||
| 9 | for (const key of Object.keys(props.characterConfigs)) { | ||
| 10 | props.characterConfigs[key] = QuickReplyConfig.from(props.characterConfigs[key]); | ||
| 11 | } | ||
| 8 | const instance = Object.assign(new this(), props); | 12 | const instance = Object.assign(new this(), props); |
| 9 | instance.init(); | 13 | instance.init(); |
| 10 | return instance; | 14 | return instance; |
| @@ -18,7 +22,9 @@ export class QuickReplySettings { | |||
| 18 | /**@type {Boolean}*/ isPopout = false; | 22 | /**@type {Boolean}*/ isPopout = false; |
| 19 | /**@type {Boolean}*/ showPopoutButton = true; | 23 | /**@type {Boolean}*/ showPopoutButton = true; |
| 20 | /**@type {QuickReplyConfig}*/ config; | 24 | /**@type {QuickReplyConfig}*/ config; |
| 25 | /**@type {{[key:string]: QuickReplyConfig}}*/ characterConfigs = {}; | ||
| 21 | /**@type {QuickReplyConfig}*/ _chatConfig; | 26 | /**@type {QuickReplyConfig}*/ _chatConfig; |
| 27 | /**@type {QuickReplyConfig}*/ _charConfig; | ||
| 22 | get chatConfig() { | 28 | get chatConfig() { |
| 23 | return this._chatConfig; | 29 | return this._chatConfig; |
| 24 | } | 30 | } |
| @@ -29,6 +35,16 @@ export class QuickReplySettings { | |||
| 29 | this.hookConfig(this._chatConfig); | 35 | this.hookConfig(this._chatConfig); |
| 30 | } | 36 | } |
| 31 | } | 37 | } |
| 38 | get charConfig() { | ||
| 39 | return this._charConfig; | ||
| 40 | } | ||
| 41 | set charConfig(value) { | ||
| 42 | if (this._charConfig != value) { | ||
| 43 | this.unhookConfig(this._charConfig); | ||
| 44 | this._charConfig = value; | ||
| 45 | this.hookConfig(this._charConfig); | ||
| 46 | } | ||
| 47 | } | ||
| 32 | 48 | ||
| 33 | /**@type {Function}*/ onSave; | 49 | /**@type {Function}*/ onSave; |
| 34 | /**@type {Function}*/ onRequestEditSet; | 50 | /**@type {Function}*/ onRequestEditSet; |
| @@ -39,6 +55,7 @@ export class QuickReplySettings { | |||
| 39 | init() { | 55 | init() { |
| 40 | this.hookConfig(this.config); | 56 | this.hookConfig(this.config); |
| 41 | this.hookConfig(this.chatConfig); | 57 | this.hookConfig(this.chatConfig); |
| 58 | this.hookConfig(this.charConfig); | ||
| 42 | } | 59 | } |
| 43 | 60 | ||
| 44 | hookConfig(config) { | 61 | hookConfig(config) { |
| @@ -76,12 +93,20 @@ export class QuickReplySettings { | |||
| 76 | } | 93 | } |
| 77 | 94 | ||
| 78 | toJSON() { | 95 | toJSON() { |
| 96 | const characterConfigs = {}; | ||
| 97 | for (const key of Object.keys(this.characterConfigs)) { | ||
| 98 | if (this.characterConfigs[key]?.setList?.length === 0) { | ||
| 99 | continue; | ||
| 100 | } | ||
| 101 | characterConfigs[key] = this.characterConfigs[key].toJSON(); | ||
| 102 | } | ||
| 79 | return { | 103 | return { |
| 80 | isEnabled: this.isEnabled, | 104 | isEnabled: this.isEnabled, |
| 81 | isCombined: this.isCombined, | 105 | isCombined: this.isCombined, |
| 82 | isPopout: this.isPopout, | 106 | isPopout: this.isPopout, |
| 83 | showPopoutButton: this.showPopoutButton, | 107 | showPopoutButton: this.showPopoutButton, |
| 84 | config: this.config, | 108 | config: this.config, |
| 109 | characterConfigs, | ||
| 85 | }; | 110 | }; |
| 86 | } | 111 | } |
| 87 | } | 112 | } |
| @@ -90,7 +90,7 @@ export class ButtonUi { | |||
| 90 | root.append(buttons); | 90 | root.append(buttons); |
| 91 | } | 91 | } |
| 92 | } | 92 | } |
| 93 | [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] | 93 | [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])] |
| 94 | .filter(link=>link.isVisible) | 94 | .filter(link=>link.isVisible) |
| 95 | .forEach(link=>buttonHolder.append(link.set.render())) | 95 | .forEach(link=>buttonHolder.append(link.set.render())) |
| 96 | ; | 96 | ; |
| @@ -150,7 +150,7 @@ export class ButtonUi { | |||
| 150 | body.append(buttons); | 150 | body.append(buttons); |
| 151 | } | 151 | } |
| 152 | } | 152 | } |
| 153 | [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])] | 153 | [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])] |
| 154 | .filter(link=>link.isVisible) | 154 | .filter(link=>link.isVisible) |
| 155 | .forEach(link=>buttonHolder.append(link.set.render())) | 155 | .forEach(link=>buttonHolder.append(link.set.render())) |
| 156 | ; | 156 | ; |
| @@ -18,6 +18,7 @@ export class SettingsUi { | |||
| 18 | /**@type {HTMLElement}*/ globalSetList; | 18 | /**@type {HTMLElement}*/ globalSetList; |
| 19 | 19 | ||
| 20 | /**@type {HTMLElement}*/ chatSetList; | 20 | /**@type {HTMLElement}*/ chatSetList; |
| 21 | /**@type {HTMLElement}*/ characterSetList; | ||
| 21 | 22 | ||
| 22 | /**@type {QuickReplySet}*/ currentQrSet; | 23 | /**@type {QuickReplySet}*/ currentQrSet; |
| 23 | /**@type {HTMLInputElement}*/ disableSend; | 24 | /**@type {HTMLInputElement}*/ disableSend; |
| @@ -107,6 +108,25 @@ export class SettingsUi { | |||
| 107 | } | 108 | } |
| 108 | this.dom.querySelector('#qr--chat').replaceWith(clone); | 109 | this.dom.querySelector('#qr--chat').replaceWith(clone); |
| 109 | } | 110 | } |
| 111 | prepareCharacterSetList() { | ||
| 112 | const dom = this.template.querySelector('#qr--character'); | ||
| 113 | const clone = /** @type {HTMLElement} */ (dom.cloneNode(true)); | ||
| 114 | |||
| 115 | if (!this.settings.charConfig) { | ||
| 116 | const setListContainer = /** @type {HTMLElement} */ (clone.querySelector('.qr--setList')); | ||
| 117 | setListContainer.innerHTML = ''; | ||
| 118 | const info = document.createElement('div'); | ||
| 119 | info.textContent = 'No character is currently loaded.'; | ||
| 120 | setListContainer.append(info); | ||
| 121 | } else { | ||
| 122 | // Let the config object handle its own rendering. It will render an empty list if there are no sets, | ||
| 123 | // but the "add" button will always be functional. | ||
| 124 | this.settings.charConfig.renderSettingsInto(clone); | ||
| 125 | } | ||
| 126 | |||
| 127 | // Replace the old DOM element with our newly prepared clone. | ||
| 128 | this.dom.querySelector('#qr--character').replaceWith(clone); | ||
| 129 | } | ||
| 110 | 130 | ||
| 111 | prepareQrEditor() { | 131 | prepareQrEditor() { |
| 112 | // qr editor | 132 | // qr editor |
| @@ -174,21 +194,26 @@ export class SettingsUi { | |||
| 174 | }); | 194 | }); |
| 175 | let initialColorChange = true; | 195 | let initialColorChange = true; |
| 176 | this.color = this.dom.querySelector('#qr--color'); | 196 | this.color = this.dom.querySelector('#qr--color'); |
| 197 | // @ts-ignore | ||
| 177 | this.color.color = this.currentQrSet?.color ?? 'transparent'; | 198 | this.color.color = this.currentQrSet?.color ?? 'transparent'; |
| 178 | this.color.addEventListener('change', (evt)=>{ | 199 | this.color.addEventListener('change', (evt)=>{ |
| 179 | if (!this.dom.closest('body')) return; | 200 | if (!this.dom.closest('body')) return; |
| 180 | const qrs = this.currentQrSet; | 201 | const qrs = this.currentQrSet; |
| 181 | if (initialColorChange) { | 202 | if (initialColorChange) { |
| 182 | initialColorChange = false; | 203 | initialColorChange = false; |
| 204 | // @ts-ignore | ||
| 183 | this.color.color = qrs.color; | 205 | this.color.color = qrs.color; |
| 184 | return; | 206 | return; |
| 185 | } | 207 | } |
| 208 | // @ts-ignore | ||
| 186 | qrs.color = evt.detail.rgb; | 209 | qrs.color = evt.detail.rgb; |
| 187 | qrs.save(); | 210 | qrs.save(); |
| 188 | this.currentQrSet.updateColor(); | 211 | this.currentQrSet.updateColor(); |
| 189 | }); | 212 | }); |
| 213 | // @ts-ignore | ||
| 190 | this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{ | 214 | this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{ |
| 191 | const qrs = this.currentQrSet; | 215 | const qrs = this.currentQrSet; |
| 216 | // @ts-ignore | ||
| 192 | this.color.color = 'transparent'; | 217 | this.color.color = 'transparent'; |
| 193 | qrs.save(); | 218 | qrs.save(); |
| 194 | this.currentQrSet.updateColor(); | 219 | this.currentQrSet.updateColor(); |
| @@ -207,6 +232,7 @@ export class SettingsUi { | |||
| 207 | this.disableSend.checked = this.currentQrSet.disableSend; | 232 | this.disableSend.checked = this.currentQrSet.disableSend; |
| 208 | this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; | 233 | this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; |
| 209 | this.injectInput.checked = this.currentQrSet.injectInput; | 234 | this.injectInput.checked = this.currentQrSet.injectInput; |
| 235 | // @ts-ignore | ||
| 210 | this.color.color = this.currentQrSet.color ?? 'transparent'; | 236 | this.color.color = this.currentQrSet.color ?? 'transparent'; |
| 211 | this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor; | 237 | this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor; |
| 212 | this.qrList.innerHTML = ''; | 238 | this.qrList.innerHTML = ''; |
| @@ -225,6 +251,7 @@ export class SettingsUi { | |||
| 225 | this.prepareGeneralSettings(); | 251 | this.prepareGeneralSettings(); |
| 226 | this.prepareGlobalSetList(); | 252 | this.prepareGlobalSetList(); |
| 227 | this.prepareChatSetList(); | 253 | this.prepareChatSetList(); |
| 254 | this.prepareCharacterSetList(); | ||
| 228 | this.prepareQrEditor(); | 255 | this.prepareQrEditor(); |
| 229 | } | 256 | } |
| 230 | 257 | ||
| @@ -301,6 +328,13 @@ export class SettingsUi { | |||
| 301 | } | 328 | } |
| 302 | } | 329 | } |
| 303 | } | 330 | } |
| 331 | if (this.settings.charConfig) { | ||
| 332 | for (let i = this.settings.charConfig.setList.length - 1; i >= 0; i--) { | ||
| 333 | if (this.settings.charConfig.setList[i].set == qrs) { | ||
| 334 | this.settings.charConfig.setList.splice(i, 1); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | } | ||
| 304 | this.settings.save(); | 338 | this.settings.save(); |
| 305 | } | 339 | } |
| 306 | 340 | ||
| @@ -327,6 +361,11 @@ export class SettingsUi { | |||
| 327 | set.set.name = newName; | 361 | set.set.name = newName; |
| 328 | } | 362 | } |
| 329 | }); | 363 | }); |
| 364 | this.settings.charConfig?.setList.forEach(set => { | ||
| 365 | if (set.set.name === oldName) { | ||
| 366 | set.set.name = newName; | ||
| 367 | } | ||
| 368 | }); | ||
| 330 | this.settings.save(); | 369 | this.settings.save(); |
| 331 | 370 | ||
| 332 | // Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below. | 371 | // Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below. |
| @@ -339,6 +378,7 @@ export class SettingsUi { | |||
| 339 | this.onQrSetChange(); | 378 | this.onQrSetChange(); |
| 340 | this.prepareGlobalSetList(); | 379 | this.prepareGlobalSetList(); |
| 341 | this.prepareChatSetList(); | 380 | this.prepareChatSetList(); |
| 381 | this.prepareCharacterSetList(); | ||
| 342 | 382 | ||
| 343 | console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`); | 383 | console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`); |
| 344 | } | 384 | } |
| @@ -362,6 +402,7 @@ export class SettingsUi { | |||
| 362 | this.onQrSetChange(); | 402 | this.onQrSetChange(); |
| 363 | this.prepareGlobalSetList(); | 403 | this.prepareGlobalSetList(); |
| 364 | this.prepareChatSetList(); | 404 | this.prepareChatSetList(); |
| 405 | this.prepareCharacterSetList(); | ||
| 365 | } | 406 | } |
| 366 | } else { | 407 | } else { |
| 367 | const qrs = new QuickReplySet(); | 408 | const qrs = new QuickReplySet(); |
| @@ -386,6 +427,7 @@ export class SettingsUi { | |||
| 386 | this.onQrSetChange(); | 427 | this.onQrSetChange(); |
| 387 | this.prepareGlobalSetList(); | 428 | this.prepareGlobalSetList(); |
| 388 | this.prepareChatSetList(); | 429 | this.prepareChatSetList(); |
| 430 | this.prepareCharacterSetList(); | ||
| 389 | } | 431 | } |
| 390 | } | 432 | } |
| 391 | } | 433 | } |
| @@ -421,6 +463,7 @@ export class SettingsUi { | |||
| 421 | this.onQrSetChange(); | 463 | this.onQrSetChange(); |
| 422 | this.prepareGlobalSetList(); | 464 | this.prepareGlobalSetList(); |
| 423 | this.prepareChatSetList(); | 465 | this.prepareChatSetList(); |
| 466 | this.prepareCharacterSetList(); | ||
| 424 | } | 467 | } |
| 425 | } else { | 468 | } else { |
| 426 | const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1); | 469 | const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1); |
| @@ -443,6 +486,7 @@ export class SettingsUi { | |||
| 443 | this.onQrSetChange(); | 486 | this.onQrSetChange(); |
| 444 | this.prepareGlobalSetList(); | 487 | this.prepareGlobalSetList(); |
| 445 | this.prepareChatSetList(); | 488 | this.prepareChatSetList(); |
| 489 | this.prepareCharacterSetList(); | ||
| 446 | } | 490 | } |
| 447 | } | 491 | } |
| 448 | } catch (ex) { | 492 | } catch (ex) { |
| @@ -493,6 +537,7 @@ export class SettingsUi { | |||
| 493 | this.onQrSetChange(); | 537 | this.onQrSetChange(); |
| 494 | this.prepareGlobalSetList(); | 538 | this.prepareGlobalSetList(); |
| 495 | this.prepareChatSetList(); | 539 | this.prepareChatSetList(); |
| 540 | this.prepareCharacterSetList(); | ||
| 496 | } | 541 | } |
| 497 | } | 542 | } |
| 498 | 543 | ||