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 | 9231 | await getCharacters(); |
| 9232 | 9232 | await printMessages(); |
| 9233 | 9233 | saveSettingsDebounced(); |
| 9234 | + await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); | |
| 9234 | 9235 | } |
| 9235 | 9236 | |
| 9236 | 9237 | /** |
| @@ -41,6 +41,19 @@ | ||
| 41 | 41 | |
| 42 | 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 | 57 | <div id="qr--editor"> |
| 45 | 58 | <div class="qr--head"> |
| 46 | 59 | <div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div> |
| @@ -1,4 +1,4 @@ | ||
| 1 | 1 | import { chat, chat_metadata, eventSource, event_types, getRequestHeaders, this_chid, characters } from '../../../script.js'; |
| 2 | 2 | import { extension_settings } from '../../extensions.js'; |
| 3 | 3 | import { QuickReplyApi } from './api/QuickReplyApi.js'; |
| 4 | 4 | import { AutoExecuteHandler } from './src/AutoExecuteHandler.js'; |
| @@ -10,6 +10,7 @@ import { SlashCommandHandler } from './src/SlashCommandHandler.js'; | ||
| 10 | 10 | import { ButtonUi } from './src/ui/ButtonUi.js'; |
| 11 | 11 | import { SettingsUi } from './src/ui/SettingsUi.js'; |
| 12 | 12 | import { debounceAsync } from '../../utils.js'; |
| 13 | +import { selected_group } from '../../group-chats.js'; | |
| 13 | 14 | export { debounceAsync }; |
| 14 | 15 | |
| 15 | 16 | |
| @@ -39,6 +40,8 @@ const defaultSettings = { | ||
| 39 | 40 | let isReady = false; |
| 40 | 41 | /** @type {Function[]}*/ |
| 41 | 42 | let executeQueue = []; |
| 43 | +/** @type {string}*/ | |
| 44 | +let lastCharId; | |
| 42 | 45 | /** @type {QuickReplySettings}*/ |
| 43 | 46 | let settings; |
| 44 | 47 | /** @type {SettingsUi} */ |
| @@ -123,6 +126,8 @@ const loadSettings = async () => { | ||
| 123 | 126 | } |
| 124 | 127 | try { |
| 125 | 128 | settings = QuickReplySettings.from(extension_settings.quickReplyV2); |
| 129 | + settings.config.scope = 'global'; | |
| 130 | + settings.config.onUpdate = () => settings.save(); | |
| 126 | 131 | } catch (ex) { |
| 127 | 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 | 175 | const init = async () => { |
| 145 | 176 | await loadSets(); |
| @@ -154,7 +185,11 @@ const init = async () => { | ||
| 154 | 185 | settings.onSave = ()=>buttons.refresh(); |
| 155 | 186 | |
| 156 | 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 | 193 | .map(it => it.set.qrList) |
| 159 | 194 | .flat() |
| 160 | 195 | .find(it=>it.label == name) |
| @@ -199,10 +234,38 @@ const finalizeInit = async () => { | ||
| 199 | 234 | }; |
| 200 | 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 | 259 | const onChatChanged = async (chatIdx) => { |
| 203 | 260 | log('CHAT_CHANGED', chatIdx); |
| 261 | + | |
| 262 | + handleCharChange(); | |
| 263 | + | |
| 204 | 264 | if (chatIdx) { |
| 205 | 265 | settings.const chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {}); |
| 266 | + chatConfig.scope = 'chat'; | |
| 267 | + chatConfig.onUpdate = () => settings.save(); | |
| 268 | + settings.chatConfig = chatConfig; | |
| 206 | 269 | } else { |
| 207 | 270 | settings.chatConfig = null; |
| 208 | 271 | } |
| @@ -212,6 +275,8 @@ const onChatChanged = async (chatIdx) => { | ||
| 212 | 275 | await autoExec.handleChatChanged(); |
| 213 | 276 | }; |
| 214 | 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 | 281 | const onUserMessage = async () => { |
| 217 | 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 | 51 | async handleStartup() { |
| 40 | 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 | 56 | async handleUser() { |
| 49 | 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 | 61 | async handleAi() { |
| 58 | 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 | 66 | async handleChatChanged() { |
| 67 | 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 | 71 | async handleGroupMemberDraft() { |
| 76 | 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 | 76 | async handleNewChat() { |
| 85 | 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 | 86 | const automationIds = entries.map(entry => entry.automationId).filter(Boolean); |
| 99 | 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 | 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 | 101 | await this.performAutoExecute(qrList); |
| @@ -434,6 +434,7 @@ export class QuickReply { | ||
| 434 | 434 | this.updateLabel(label.value); |
| 435 | 435 | }); |
| 436 | 436 | let switcherList; |
| 437 | + // @ts-ignore | |
| 437 | 438 | dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{ |
| 438 | 439 | if (switcherList) { |
| 439 | 440 | switcherList.remove(); |
| @@ -618,7 +619,9 @@ export class QuickReply { | ||
| 618 | 619 | accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked)); |
| 619 | 620 | updateSyntaxEnabled(); |
| 620 | 621 | }); |
| 622 | + // @ts-ignore | |
| 621 | 623 | if (navigator.keyboard) { |
| 624 | + // @ts-ignore | |
| 622 | 625 | navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash')); |
| 623 | 626 | } else { |
| 624 | 627 | dom.querySelector('#qr--modal-commentKey').closest('small').remove(); |
| @@ -781,6 +784,7 @@ export class QuickReply { | ||
| 781 | 784 | message.addEventListener('wheel', (evt)=>{ |
| 782 | 785 | updateScrollDebounced(evt); |
| 783 | 786 | }); |
| 787 | + // @ts-ignore | |
| 784 | 788 | message.addEventListener('scroll', (evt)=>{ |
| 785 | 789 | updateScrollDebounced(); |
| 786 | 790 | }); |
| @@ -1117,6 +1121,7 @@ export class QuickReply { | ||
| 1117 | 1121 | /**@type {HTMLTextAreaElement} */ |
| 1118 | 1122 | const inputOg = document.querySelector('#send_textarea'); |
| 1119 | 1123 | const inputMirror = dom.querySelector('#qr--modal-send_textarea'); |
| 1124 | + // @ts-ignore | |
| 1120 | 1125 | inputMirror.value = inputOg.value; |
| 1121 | 1126 | const inputOgMo = new MutationObserver(muts=>{ |
| 1122 | 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 | 1131 | inputOgMo.observe(document.body, { childList:true }); |
| 1127 | 1132 | const inputOgListener = ()=>{ |
| 1133 | + // @ts-ignore | |
| 1128 | 1134 | inputMirror.value = inputOg.value; |
| 1129 | 1135 | }; |
| 1130 | 1136 | inputOg.addEventListener('input', inputOgListener); |
| 1131 | 1137 | inputMirror.addEventListener('input', ()=>{ |
| 1138 | + // @ts-ignore | |
| 1132 | 1139 | inputOg.value = inputMirror.value; |
| 1133 | 1140 | }); |
| 1134 | 1141 | |
| @@ -1172,15 +1179,19 @@ export class QuickReply { | ||
| 1172 | 1179 | isResizing = true; |
| 1173 | 1180 | evt.preventDefault(); |
| 1174 | 1181 | resizeStart = evt.x; |
| 1182 | + // @ts-ignore | |
| 1175 | 1183 | wStart = dom.querySelector('#qr--qrOptions').offsetWidth; |
| 1176 | 1184 | const dragListener = debounce((evt)=>{ |
| 1177 | 1185 | const w = wStart + resizeStart - evt.x; |
| 1186 | + // @ts-ignore | |
| 1178 | 1187 | dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`); |
| 1179 | 1188 | }, 5); |
| 1180 | 1189 | window.addEventListener('pointerup', ()=>{ |
| 1190 | + // @ts-ignore | |
| 1181 | 1191 | window.removeEventListener('pointermove', dragListener); |
| 1182 | 1192 | isResizing = false; |
| 1183 | 1193 | }, { once:true }); |
| 1194 | + // @ts-ignore | |
| 1184 | 1195 | window.addEventListener('pointermove', dragListener); |
| 1185 | 1196 | }); |
| 1186 | 1197 | |
| @@ -1282,6 +1293,7 @@ export class QuickReply { | ||
| 1282 | 1293 | syntax.addEventListener('wheel', (evt)=>{ |
| 1283 | 1294 | updateScrollDebounced(evt); |
| 1284 | 1295 | }); |
| 1296 | + // @ts-ignore | |
| 1285 | 1297 | syntax.addEventListener('scroll', (evt)=>{ |
| 1286 | 1298 | updateScrollDebounced(); |
| 1287 | 1299 | }); |
| @@ -1414,8 +1426,11 @@ export class QuickReply { | ||
| 1414 | 1426 | let i = 0; |
| 1415 | 1427 | let unnamed = this.debugController.unnamedArguments ?? []; |
| 1416 | 1428 | if (!Array.isArray(unnamed)) unnamed = [unnamed]; |
| 1429 | + // @ts-ignore | |
| 1417 | 1430 | while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined); |
| 1431 | + // @ts-ignore | |
| 1418 | 1432 | unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]); |
| 1433 | + // @ts-ignore | |
| 1419 | 1434 | for (const arg of unnamed) { |
| 1420 | 1435 | i++; |
| 1421 | 1436 | const item = document.createElement('div'); { |
| @@ -4,7 +4,7 @@ import { QuickReplySet } from './QuickReplySet.js'; | ||
| 4 | 4 | |
| 5 | 5 | export class QuickReplyConfig { |
| 6 | 6 | /**@type {QuickReplySetLink[]}*/ setList = []; |
| 7 | 7 | /**@type {Boolean'global'|'chat'|'character'}*/ isGlobalscope; |
| 8 | 8 | |
| 9 | 9 | /**@type {Function}*/ onUpdate; |
| 10 | 10 | /**@type {Function}*/ onRequestEditSet; |
| @@ -28,6 +28,7 @@ export class QuickReplySet { | ||
| 28 | 28 | } |
| 29 | 29 | |
| 30 | 30 | /**@type {string}*/ name; |
| 31 | + /**@type {'global'|'chat'|'character'}*/ scope = 'global'; | |
| 31 | 32 | /**@type {boolean}*/ disableSend = false; |
| 32 | 33 | /**@type {boolean}*/ placeBeforeInput = false; |
| 33 | 34 | /**@type {boolean}*/ injectInput = false; |
| @@ -255,6 +256,7 @@ export class QuickReplySet { | ||
| 255 | 256 | * @param {QuickReply} qr |
| 256 | 257 | */ |
| 257 | 258 | hookQuickReply(qr) { |
| 259 | + // @ts-ignore | |
| 258 | 260 | qr.onDebug = ()=>this.debug(qr); |
| 259 | 261 | qr.onExecute = (_, options)=>this.executeWithOptions(qr, options); |
| 260 | 262 | qr.onDelete = ()=>this.removeQuickReply(qr); |
| @@ -301,12 +303,14 @@ export class QuickReplySet { | ||
| 301 | 303 | } |
| 302 | 304 | sel.addEventListener('keyup', (evt)=>{ |
| 303 | 305 | if (evt.key == 'Shift') { |
| 306 | + // @ts-ignore | |
| 304 | 307 | (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy'); |
| 305 | 308 | return; |
| 306 | 309 | } |
| 307 | 310 | }); |
| 308 | 311 | sel.addEventListener('keydown', (evt)=>{ |
| 309 | 312 | if (evt.key == 'Shift') { |
| 313 | + // @ts-ignore | |
| 310 | 314 | (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy'); |
| 311 | 315 | return; |
| 312 | 316 | } |
| @@ -335,6 +339,7 @@ export class QuickReplySet { | ||
| 335 | 339 | isCopy = true; |
| 336 | 340 | dlg.completeAffirmative(); |
| 337 | 341 | }); |
| 342 | + // @ts-ignore | |
| 338 | 343 | (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn); |
| 339 | 344 | } |
| 340 | 345 | const prom = dlg.show(); |
| @@ -5,6 +5,10 @@ import { QuickReplyConfig } from './QuickReplyConfig.js'; | ||
| 5 | 5 | export class QuickReplySettings { |
| 6 | 6 | static from(props) { |
| 7 | 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 | 12 | const instance = Object.assign(new this(), props); |
| 9 | 13 | instance.init(); |
| 10 | 14 | return instance; |
| @@ -18,7 +22,9 @@ export class QuickReplySettings { | ||
| 18 | 22 | /**@type {Boolean}*/ isPopout = false; |
| 19 | 23 | /**@type {Boolean}*/ showPopoutButton = true; |
| 20 | 24 | /**@type {QuickReplyConfig}*/ config; |
| 25 | + /**@type {{[key:string]: QuickReplyConfig}}*/ characterConfigs = {}; | |
| 21 | 26 | /**@type {QuickReplyConfig}*/ _chatConfig; |
| 27 | + /**@type {QuickReplyConfig}*/ _charConfig; | |
| 22 | 28 | get chatConfig() { |
| 23 | 29 | return this._chatConfig; |
| 24 | 30 | } |
| @@ -29,6 +35,16 @@ export class QuickReplySettings { | ||
| 29 | 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 | 49 | /**@type {Function}*/ onSave; |
| 34 | 50 | /**@type {Function}*/ onRequestEditSet; |
| @@ -39,6 +55,7 @@ export class QuickReplySettings { | ||
| 39 | 55 | init() { |
| 40 | 56 | this.hookConfig(this.config); |
| 41 | 57 | this.hookConfig(this.chatConfig); |
| 58 | + this.hookConfig(this.charConfig); | |
| 42 | 59 | } |
| 43 | 60 | |
| 44 | 61 | hookConfig(config) { |
| @@ -76,12 +93,20 @@ export class QuickReplySettings { | ||
| 76 | 93 | } |
| 77 | 94 | |
| 78 | 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 | 103 | return { |
| 80 | 104 | isEnabled: this.isEnabled, |
| 81 | 105 | isCombined: this.isCombined, |
| 82 | 106 | isPopout: this.isPopout, |
| 83 | 107 | showPopoutButton: this.showPopoutButton, |
| 84 | 108 | config: this.config, |
| 109 | + characterConfigs, | |
| 85 | 110 | }; |
| 86 | 111 | } |
| 87 | 112 | } |
| @@ -90,7 +90,7 @@ export class ButtonUi { | ||
| 90 | 90 | root.append(buttons); |
| 91 | 91 | } |
| 92 | 92 | } |
| 93 | 93 | [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])] |
| 94 | 94 | .filter(link=>link.isVisible) |
| 95 | 95 | .forEach(link=>buttonHolder.append(link.set.render())) |
| 96 | 96 | ; |
| @@ -150,7 +150,7 @@ export class ButtonUi { | ||
| 150 | 150 | body.append(buttons); |
| 151 | 151 | } |
| 152 | 152 | } |
| 153 | 153 | [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])] |
| 154 | 154 | .filter(link=>link.isVisible) |
| 155 | 155 | .forEach(link=>buttonHolder.append(link.set.render())) |
| 156 | 156 | ; |
| @@ -18,6 +18,7 @@ export class SettingsUi { | ||
| 18 | 18 | /**@type {HTMLElement}*/ globalSetList; |
| 19 | 19 | |
| 20 | 20 | /**@type {HTMLElement}*/ chatSetList; |
| 21 | + /**@type {HTMLElement}*/ characterSetList; | |
| 21 | 22 | |
| 22 | 23 | /**@type {QuickReplySet}*/ currentQrSet; |
| 23 | 24 | /**@type {HTMLInputElement}*/ disableSend; |
| @@ -107,6 +108,25 @@ export class SettingsUi { | ||
| 107 | 108 | } |
| 108 | 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 | 131 | prepareQrEditor() { |
| 112 | 132 | // qr editor |
| @@ -174,21 +194,26 @@ export class SettingsUi { | ||
| 174 | 194 | }); |
| 175 | 195 | let initialColorChange = true; |
| 176 | 196 | this.color = this.dom.querySelector('#qr--color'); |
| 197 | + // @ts-ignore | |
| 177 | 198 | this.color.color = this.currentQrSet?.color ?? 'transparent'; |
| 178 | 199 | this.color.addEventListener('change', (evt)=>{ |
| 179 | 200 | if (!this.dom.closest('body')) return; |
| 180 | 201 | const qrs = this.currentQrSet; |
| 181 | 202 | if (initialColorChange) { |
| 182 | 203 | initialColorChange = false; |
| 204 | + // @ts-ignore | |
| 183 | 205 | this.color.color = qrs.color; |
| 184 | 206 | return; |
| 185 | 207 | } |
| 208 | + // @ts-ignore | |
| 186 | 209 | qrs.color = evt.detail.rgb; |
| 187 | 210 | qrs.save(); |
| 188 | 211 | this.currentQrSet.updateColor(); |
| 189 | 212 | }); |
| 213 | + // @ts-ignore | |
| 190 | 214 | this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{ |
| 191 | 215 | const qrs = this.currentQrSet; |
| 216 | + // @ts-ignore | |
| 192 | 217 | this.color.color = 'transparent'; |
| 193 | 218 | qrs.save(); |
| 194 | 219 | this.currentQrSet.updateColor(); |
| @@ -207,6 +232,7 @@ export class SettingsUi { | ||
| 207 | 232 | this.disableSend.checked = this.currentQrSet.disableSend; |
| 208 | 233 | this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput; |
| 209 | 234 | this.injectInput.checked = this.currentQrSet.injectInput; |
| 235 | + // @ts-ignore | |
| 210 | 236 | this.color.color = this.currentQrSet.color ?? 'transparent'; |
| 211 | 237 | this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor; |
| 212 | 238 | this.qrList.innerHTML = ''; |
| @@ -225,6 +251,7 @@ export class SettingsUi { | ||
| 225 | 251 | this.prepareGeneralSettings(); |
| 226 | 252 | this.prepareGlobalSetList(); |
| 227 | 253 | this.prepareChatSetList(); |
| 254 | + this.prepareCharacterSetList(); | |
| 228 | 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 | 338 | this.settings.save(); |
| 305 | 339 | } |
| 306 | 340 | |
| @@ -327,6 +361,11 @@ export class SettingsUi { | ||
| 327 | 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 | 369 | this.settings.save(); |
| 331 | 370 | |
| 332 | 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 | 378 | this.onQrSetChange(); |
| 340 | 379 | this.prepareGlobalSetList(); |
| 341 | 380 | this.prepareChatSetList(); |
| 381 | + this.prepareCharacterSetList(); | |
| 342 | 382 | |
| 343 | 383 | console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`); |
| 344 | 384 | } |
| @@ -362,6 +402,7 @@ export class SettingsUi { | ||
| 362 | 402 | this.onQrSetChange(); |
| 363 | 403 | this.prepareGlobalSetList(); |
| 364 | 404 | this.prepareChatSetList(); |
| 405 | + this.prepareCharacterSetList(); | |
| 365 | 406 | } |
| 366 | 407 | } else { |
| 367 | 408 | const qrs = new QuickReplySet(); |
| @@ -386,6 +427,7 @@ export class SettingsUi { | ||
| 386 | 427 | this.onQrSetChange(); |
| 387 | 428 | this.prepareGlobalSetList(); |
| 388 | 429 | this.prepareChatSetList(); |
| 430 | + this.prepareCharacterSetList(); | |
| 389 | 431 | } |
| 390 | 432 | } |
| 391 | 433 | } |
| @@ -421,6 +463,7 @@ export class SettingsUi { | ||
| 421 | 463 | this.onQrSetChange(); |
| 422 | 464 | this.prepareGlobalSetList(); |
| 423 | 465 | this.prepareChatSetList(); |
| 466 | + this.prepareCharacterSetList(); | |
| 424 | 467 | } |
| 425 | 468 | } else { |
| 426 | 469 | const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1); |
| @@ -443,6 +486,7 @@ export class SettingsUi { | ||
| 443 | 486 | this.onQrSetChange(); |
| 444 | 487 | this.prepareGlobalSetList(); |
| 445 | 488 | this.prepareChatSetList(); |
| 489 | + this.prepareCharacterSetList(); | |
| 446 | 490 | } |
| 447 | 491 | } |
| 448 | 492 | } catch (ex) { |
| @@ -493,6 +537,7 @@ export class SettingsUi { | ||
| 493 | 537 | this.onQrSetChange(); |
| 494 | 538 | this.prepareGlobalSetList(); |
| 495 | 539 | this.prepareChatSetList(); |
| 540 | + this.prepareCharacterSetList(); | |
| 496 | 541 | } |
| 497 | 542 | } |
| 498 | 543 | |