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>

f107df8ca6196470005497187959e85aeb4eb292

MAX-TAB <60381043+MAX-TAB@users.noreply.github.com>

Signed
10 files changed, +203 -39Ignore whitespace
public/script.js+1 -0
@@ -9231,6 +9231,7 @@ async function removeCharacterFromUI() {
92319231 await getCharacters();
92329232 await printMessages();
92339233 saveSettingsDebounced();
9234+ await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
92349235}
92359236
92369237/**
public/scripts/extensions/quick-reply/html/settings.html+13 -0
@@ -41,6 +41,19 @@
4141
4242 <hr>
4343
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+
4457 <div id="qr--editor">
4558 <div class="qr--head">
4659 <div class="qr--title" data-i18n="Edit Quick Replies">Edit Quick Replies</div>
public/scripts/extensions/quick-reply/index.js+69 -4
@@ -1,4 +1,4 @@
11import { chat, chat_metadata, eventSource, event_types, getRequestHeaders, this_chid, characters } from '../../../script.js';
22import { extension_settings } from '../../extensions.js';
33import { QuickReplyApi } from './api/QuickReplyApi.js';
44import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
@@ -10,6 +10,7 @@ import { SlashCommandHandler } from './src/SlashCommandHandler.js';
1010import { ButtonUi } from './src/ui/ButtonUi.js';
1111import { SettingsUi } from './src/ui/SettingsUi.js';
1212import { debounceAsync } from '../../utils.js';
13+import { selected_group } from '../../group-chats.js';
1314export { debounceAsync };
1415
1516
@@ -39,6 +40,8 @@ const defaultSettings = {
3940let isReady = false;
4041/** @type {Function[]}*/
4142let executeQueue = [];
43+/** @type {string}*/
44+let lastCharId;
4245/** @type {QuickReplySettings}*/
4346let settings;
4447/** @type {SettingsUi} */
@@ -123,6 +126,8 @@ const loadSettings = async () => {
123126 }
124127 try {
125128 settings = QuickReplySettings.from(extension_settings.quickReplyV2);
129+ settings.config.scope = 'global';
130+ settings.config.onUpdate = () => settings.save();
126131 } catch (ex) {
127132 settings = QuickReplySettings.from(defaultSettings);
128133 }
@@ -138,8 +143,34 @@ const executeIfReadyElseQueue = async (functionToCall, args) => {
138143 }
139144};
140145
146+const handleCharChange = () => {
147+ if (lastCharId === this_chid) return;
141148
149+ // Unload the old character's config and update the character ID cache.
150+ settings.charConfig = null;
151+ lastCharId = this_chid;
142152
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+};
143174
144175const init = async () => {
145176 await loadSets();
@@ -154,8 +185,12 @@ const init = async () => {
154185 settings.onSave = ()=>buttons.refresh();
155186
156187 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
157- let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
188+ let qr = [
158- .map(it=>it.set.qrList)
189+ ...settings.config.setList,
190+ ...(settings.chatConfig?.setList ?? []),
191+ ...(settings.charConfig?.setList ?? []),
192+ ]
193+ .map(it => it.set.qrList)
159194 .flat()
160195 .find(it=>it.label == name)
161196 ;
@@ -199,10 +234,38 @@ const finalizeInit = async () => {
199234};
200235await init();
201236
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+
202259const onChatChanged = async (chatIdx) => {
203260 log('CHAT_CHANGED', chatIdx);
261+
262+ handleCharChange();
263+
204264 if (chatIdx) {
205265 settings.const chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
266+ chatConfig.scope = 'chat';
267+ chatConfig.onUpdate = () => settings.save();
268+ settings.chatConfig = chatConfig;
206269 } else {
207270 settings.chatConfig = null;
208271 }
@@ -212,6 +275,8 @@ const onChatChanged = async (chatIdx) => {
212275 await autoExec.handleChatChanged();
213276};
214277eventSource.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);
215280
216281const onUserMessage = async () => {
217282 await autoExec.handleUser();
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+27 -32
@@ -36,58 +36,46 @@ export class AutoExecuteHandler {
3636 }
3737
3838
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+
3951 async handleStartup() {
4052 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);
4654 }
4755
4856 async handleUser() {
4957 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);
5559 }
5660
5761 async handleAi() {
5862 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);
6464 }
6565
6666 async handleChatChanged() {
6767 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);
7369 }
7470
7571 async handleGroupMemberDraft() {
7672 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);
8274 }
8375
8476 async handleNewChat() {
8577 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);
9179 }
9280
9381 /**
@@ -98,9 +86,16 @@ export class AutoExecuteHandler {
9886 const automationIds = entries.map(entry => entry.automationId).filter(Boolean);
9987 if (automationIds.length === 0) return;
10088
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+
10195 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),
10499 ];
105100
106101 await this.performAutoExecute(qrList);
public/scripts/extensions/quick-reply/src/QuickReply.js+15 -0
@@ -434,6 +434,7 @@ export class QuickReply {
434434 this.updateLabel(label.value);
435435 });
436436 let switcherList;
437+ // @ts-ignore
437438 dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{
438439 if (switcherList) {
439440 switcherList.remove();
@@ -618,7 +619,9 @@ export class QuickReply {
618619 accountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
619620 updateSyntaxEnabled();
620621 });
622+ // @ts-ignore
621623 if (navigator.keyboard) {
624+ // @ts-ignore
622625 navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
623626 } else {
624627 dom.querySelector('#qr--modal-commentKey').closest('small').remove();
@@ -781,6 +784,7 @@ export class QuickReply {
781784 message.addEventListener('wheel', (evt)=>{
782785 updateScrollDebounced(evt);
783786 });
787+ // @ts-ignore
784788 message.addEventListener('scroll', (evt)=>{
785789 updateScrollDebounced();
786790 });
@@ -1117,6 +1121,7 @@ export class QuickReply {
11171121 /**@type {HTMLTextAreaElement} */
11181122 const inputOg = document.querySelector('#send_textarea');
11191123 const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1124+ // @ts-ignore
11201125 inputMirror.value = inputOg.value;
11211126 const inputOgMo = new MutationObserver(muts=>{
11221127 if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) {
@@ -1125,10 +1130,12 @@ export class QuickReply {
11251130 });
11261131 inputOgMo.observe(document.body, { childList:true });
11271132 const inputOgListener = ()=>{
1133+ // @ts-ignore
11281134 inputMirror.value = inputOg.value;
11291135 };
11301136 inputOg.addEventListener('input', inputOgListener);
11311137 inputMirror.addEventListener('input', ()=>{
1138+ // @ts-ignore
11321139 inputOg.value = inputMirror.value;
11331140 });
11341141
@@ -1172,15 +1179,19 @@ export class QuickReply {
11721179 isResizing = true;
11731180 evt.preventDefault();
11741181 resizeStart = evt.x;
1182+ // @ts-ignore
11751183 wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
11761184 const dragListener = debounce((evt)=>{
11771185 const w = wStart + resizeStart - evt.x;
1186+ // @ts-ignore
11781187 dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
11791188 }, 5);
11801189 window.addEventListener('pointerup', ()=>{
1190+ // @ts-ignore
11811191 window.removeEventListener('pointermove', dragListener);
11821192 isResizing = false;
11831193 }, { once:true });
1194+ // @ts-ignore
11841195 window.addEventListener('pointermove', dragListener);
11851196 });
11861197
@@ -1282,6 +1293,7 @@ export class QuickReply {
12821293 syntax.addEventListener('wheel', (evt)=>{
12831294 updateScrollDebounced(evt);
12841295 });
1296+ // @ts-ignore
12851297 syntax.addEventListener('scroll', (evt)=>{
12861298 updateScrollDebounced();
12871299 });
@@ -1414,8 +1426,11 @@ export class QuickReply {
14141426 let i = 0;
14151427 let unnamed = this.debugController.unnamedArguments ?? [];
14161428 if (!Array.isArray(unnamed)) unnamed = [unnamed];
1429+ // @ts-ignore
14171430 while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1431+ // @ts-ignore
14181432 unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]);
1433+ // @ts-ignore
14191434 for (const arg of unnamed) {
14201435 i++;
14211436 const item = document.createElement('div'); {
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+1 -1
@@ -4,7 +4,7 @@ import { QuickReplySet } from './QuickReplySet.js';
44
55export class QuickReplyConfig {
66 /**@type {QuickReplySetLink[]}*/ setList = [];
77 /**@type {Boolean'global'|'chat'|'character'}*/ isGlobalscope;
88
99 /**@type {Function}*/ onUpdate;
1010 /**@type {Function}*/ onRequestEditSet;
public/scripts/extensions/quick-reply/src/QuickReplySet.js+5 -0
@@ -28,6 +28,7 @@ export class QuickReplySet {
2828 }
2929
3030 /**@type {string}*/ name;
31+ /**@type {'global'|'chat'|'character'}*/ scope = 'global';
3132 /**@type {boolean}*/ disableSend = false;
3233 /**@type {boolean}*/ placeBeforeInput = false;
3334 /**@type {boolean}*/ injectInput = false;
@@ -255,6 +256,7 @@ export class QuickReplySet {
255256 * @param {QuickReply} qr
256257 */
257258 hookQuickReply(qr) {
259+ // @ts-ignore
258260 qr.onDebug = ()=>this.debug(qr);
259261 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
260262 qr.onDelete = ()=>this.removeQuickReply(qr);
@@ -301,12 +303,14 @@ export class QuickReplySet {
301303 }
302304 sel.addEventListener('keyup', (evt)=>{
303305 if (evt.key == 'Shift') {
306+ // @ts-ignore
304307 (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
305308 return;
306309 }
307310 });
308311 sel.addEventListener('keydown', (evt)=>{
309312 if (evt.key == 'Shift') {
313+ // @ts-ignore
310314 (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
311315 return;
312316 }
@@ -335,6 +339,7 @@ export class QuickReplySet {
335339 isCopy = true;
336340 dlg.completeAffirmative();
337341 });
342+ // @ts-ignore
338343 (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
339344 }
340345 const prom = dlg.show();
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+25 -0
@@ -5,6 +5,10 @@ import { QuickReplyConfig } from './QuickReplyConfig.js';
55export class QuickReplySettings {
66 static from(props) {
77 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+ }
812 const instance = Object.assign(new this(), props);
913 instance.init();
1014 return instance;
@@ -18,7 +22,9 @@ export class QuickReplySettings {
1822 /**@type {Boolean}*/ isPopout = false;
1923 /**@type {Boolean}*/ showPopoutButton = true;
2024 /**@type {QuickReplyConfig}*/ config;
25+ /**@type {{[key:string]: QuickReplyConfig}}*/ characterConfigs = {};
2126 /**@type {QuickReplyConfig}*/ _chatConfig;
27+ /**@type {QuickReplyConfig}*/ _charConfig;
2228 get chatConfig() {
2329 return this._chatConfig;
2430 }
@@ -29,6 +35,16 @@ export class QuickReplySettings {
2935 this.hookConfig(this._chatConfig);
3036 }
3137 }
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+ }
3248
3349 /**@type {Function}*/ onSave;
3450 /**@type {Function}*/ onRequestEditSet;
@@ -39,6 +55,7 @@ export class QuickReplySettings {
3955 init() {
4056 this.hookConfig(this.config);
4157 this.hookConfig(this.chatConfig);
58+ this.hookConfig(this.charConfig);
4259 }
4360
4461 hookConfig(config) {
@@ -76,12 +93,20 @@ export class QuickReplySettings {
7693 }
7794
7895 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+ }
79103 return {
80104 isEnabled: this.isEnabled,
81105 isCombined: this.isCombined,
82106 isPopout: this.isPopout,
83107 showPopoutButton: this.showPopoutButton,
84108 config: this.config,
109+ characterConfigs,
85110 };
86111 }
87112}
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+2 -2
@@ -90,7 +90,7 @@ export class ButtonUi {
9090 root.append(buttons);
9191 }
9292 }
9393 [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])]
9494 .filter(link=>link.isVisible)
9595 .forEach(link=>buttonHolder.append(link.set.render()))
9696 ;
@@ -150,7 +150,7 @@ export class ButtonUi {
150150 body.append(buttons);
151151 }
152152 }
153153 [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? []), ...(this.settings.charConfig?.setList ?? [])]
154154 .filter(link=>link.isVisible)
155155 .forEach(link=>buttonHolder.append(link.set.render()))
156156 ;
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+45 -0
@@ -18,6 +18,7 @@ export class SettingsUi {
1818 /**@type {HTMLElement}*/ globalSetList;
1919
2020 /**@type {HTMLElement}*/ chatSetList;
21+ /**@type {HTMLElement}*/ characterSetList;
2122
2223 /**@type {QuickReplySet}*/ currentQrSet;
2324 /**@type {HTMLInputElement}*/ disableSend;
@@ -107,6 +108,25 @@ export class SettingsUi {
107108 }
108109 this.dom.querySelector('#qr--chat').replaceWith(clone);
109110 }
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+ }
110130
111131 prepareQrEditor() {
112132 // qr editor
@@ -174,21 +194,26 @@ export class SettingsUi {
174194 });
175195 let initialColorChange = true;
176196 this.color = this.dom.querySelector('#qr--color');
197+ // @ts-ignore
177198 this.color.color = this.currentQrSet?.color ?? 'transparent';
178199 this.color.addEventListener('change', (evt)=>{
179200 if (!this.dom.closest('body')) return;
180201 const qrs = this.currentQrSet;
181202 if (initialColorChange) {
182203 initialColorChange = false;
204+ // @ts-ignore
183205 this.color.color = qrs.color;
184206 return;
185207 }
208+ // @ts-ignore
186209 qrs.color = evt.detail.rgb;
187210 qrs.save();
188211 this.currentQrSet.updateColor();
189212 });
213+ // @ts-ignore
190214 this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
191215 const qrs = this.currentQrSet;
216+ // @ts-ignore
192217 this.color.color = 'transparent';
193218 qrs.save();
194219 this.currentQrSet.updateColor();
@@ -207,6 +232,7 @@ export class SettingsUi {
207232 this.disableSend.checked = this.currentQrSet.disableSend;
208233 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
209234 this.injectInput.checked = this.currentQrSet.injectInput;
235+ // @ts-ignore
210236 this.color.color = this.currentQrSet.color ?? 'transparent';
211237 this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
212238 this.qrList.innerHTML = '';
@@ -225,6 +251,7 @@ export class SettingsUi {
225251 this.prepareGeneralSettings();
226252 this.prepareGlobalSetList();
227253 this.prepareChatSetList();
254+ this.prepareCharacterSetList();
228255 this.prepareQrEditor();
229256 }
230257
@@ -301,6 +328,13 @@ export class SettingsUi {
301328 }
302329 }
303330 }
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+ }
304338 this.settings.save();
305339 }
306340
@@ -327,6 +361,11 @@ export class SettingsUi {
327361 set.set.name = newName;
328362 }
329363 });
364+ this.settings.charConfig?.setList.forEach(set => {
365+ if (set.set.name === oldName) {
366+ set.set.name = newName;
367+ }
368+ });
330369 this.settings.save();
331370
332371 // 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 {
339378 this.onQrSetChange();
340379 this.prepareGlobalSetList();
341380 this.prepareChatSetList();
381+ this.prepareCharacterSetList();
342382
343383 console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`);
344384 }
@@ -362,6 +402,7 @@ export class SettingsUi {
362402 this.onQrSetChange();
363403 this.prepareGlobalSetList();
364404 this.prepareChatSetList();
405+ this.prepareCharacterSetList();
365406 }
366407 } else {
367408 const qrs = new QuickReplySet();
@@ -386,6 +427,7 @@ export class SettingsUi {
386427 this.onQrSetChange();
387428 this.prepareGlobalSetList();
388429 this.prepareChatSetList();
430+ this.prepareCharacterSetList();
389431 }
390432 }
391433 }
@@ -421,6 +463,7 @@ export class SettingsUi {
421463 this.onQrSetChange();
422464 this.prepareGlobalSetList();
423465 this.prepareChatSetList();
466+ this.prepareCharacterSetList();
424467 }
425468 } else {
426469 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
@@ -443,6 +486,7 @@ export class SettingsUi {
443486 this.onQrSetChange();
444487 this.prepareGlobalSetList();
445488 this.prepareChatSetList();
489+ this.prepareCharacterSetList();
446490 }
447491 }
448492 } catch (ex) {
@@ -493,6 +537,7 @@ export class SettingsUi {
493537 this.onQrSetChange();
494538 this.prepareGlobalSetList();
495539 this.prepareChatSetList();
540+ this.prepareCharacterSetList();
496541 }
497542 }
498543