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() {
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}
92359236
9236/**9237/**
public/scripts/extensions/quick-reply/html/settings.html+13 -0
@@ -41,6 +41,19 @@
4141
42 <hr>42 <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
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>
public/scripts/extensions/quick-reply/index.js+69 -4
@@ -1,4 +1,4 @@
1import { chat, chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js';1import { chat, chat_metadata, eventSource, event_types, getRequestHeaders, this_chid, characters } from '../../../script.js';
2import { extension_settings } from '../../extensions.js';2import { extension_settings } from '../../extensions.js';
3import { QuickReplyApi } from './api/QuickReplyApi.js';3import { QuickReplyApi } from './api/QuickReplyApi.js';
4import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';4import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
@@ -10,6 +10,7 @@ import { SlashCommandHandler } from './src/SlashCommandHandler.js';
10import { ButtonUi } from './src/ui/ButtonUi.js';10import { ButtonUi } from './src/ui/ButtonUi.js';
11import { SettingsUi } from './src/ui/SettingsUi.js';11import { SettingsUi } from './src/ui/SettingsUi.js';
12import { debounceAsync } from '../../utils.js';12import { debounceAsync } from '../../utils.js';
13import { selected_group } from '../../group-chats.js';
13export { debounceAsync };14export { debounceAsync };
1415
1516
@@ -39,6 +40,8 @@ const defaultSettings = {
39let isReady = false;40let isReady = false;
40/** @type {Function[]}*/41/** @type {Function[]}*/
41let executeQueue = [];42let executeQueue = [];
43/** @type {string}*/
44let lastCharId;
42/** @type {QuickReplySettings}*/45/** @type {QuickReplySettings}*/
43let settings;46let 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};
140145
146const 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
144const init = async () => {175const init = async () => {
145 await loadSets();176 await loadSets();
@@ -154,8 +185,12 @@ const init = async () => {
154 settings.onSave = ()=>buttons.refresh();185 settings.onSave = ()=>buttons.refresh();
155186
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 = [
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)
159 .flat()194 .flat()
160 .find(it=>it.label == name)195 .find(it=>it.label == name)
161 ;196 ;
@@ -199,10 +234,38 @@ const finalizeInit = async () => {
199};234};
200await init();235await init();
201236
237const 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
247const 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
202const onChatChanged = async (chatIdx) => {259const 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};
214eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args));277eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args));
278eventSource.on(event_types.CHARACTER_DELETED, purgeCharacterQuickReplySets);
279eventSource.on(event_types.CHARACTER_RENAMED, updateCharacterQuickReplySets);
215280
216const onUserMessage = async () => {281const onUserMessage = async () => {
217 await autoExec.handleUser();282 await autoExec.handleUser();
public/scripts/extensions/quick-reply/src/AutoExecuteHandler.js+27 -32
@@ -36,58 +36,46 @@ export class AutoExecuteHandler {
36 }36 }
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
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 }
4755
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 }
5660
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 }
6565
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 }
7470
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 }
8375
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 }
9280
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;
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
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 ];
105100
106 await this.performAutoExecute(qrList);101 await this.performAutoExecute(qrList);
public/scripts/extensions/quick-reply/src/QuickReply.js+15 -0
@@ -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 });
11341141
@@ -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 });
11861197
@@ -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'); {
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+1 -1
@@ -4,7 +4,7 @@ import { QuickReplySet } from './QuickReplySet.js';
44
5export class QuickReplyConfig {5export class QuickReplyConfig {
6 /**@type {QuickReplySetLink[]}*/ setList = [];6 /**@type {QuickReplySetLink[]}*/ setList = [];
7 /**@type {Boolean}*/ isGlobal;7 /**@type {'global'|'chat'|'character'}*/ scope;
88
9 /**@type {Function}*/ onUpdate;9 /**@type {Function}*/ onUpdate;
10 /**@type {Function}*/ onRequestEditSet;10 /**@type {Function}*/ onRequestEditSet;
public/scripts/extensions/quick-reply/src/QuickReplySet.js+5 -0
@@ -28,6 +28,7 @@ export class QuickReplySet {
28 }28 }
2929
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} qr256 * @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();
public/scripts/extensions/quick-reply/src/QuickReplySettings.js+25 -0
@@ -5,6 +5,10 @@ import { QuickReplyConfig } from './QuickReplyConfig.js';
5export class QuickReplySettings {5export 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 }
3248
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 }
4360
44 hookConfig(config) {61 hookConfig(config) {
@@ -76,12 +93,20 @@ export class QuickReplySettings {
76 }93 }
7794
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}
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js+2 -2
@@ -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 ;
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+45 -0
@@ -18,6 +18,7 @@ export class SettingsUi {
18 /**@type {HTMLElement}*/ globalSetList;18 /**@type {HTMLElement}*/ globalSetList;
1919
20 /**@type {HTMLElement}*/ chatSetList;20 /**@type {HTMLElement}*/ chatSetList;
21 /**@type {HTMLElement}*/ characterSetList;
2122
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 }
110130
111 prepareQrEditor() {131 prepareQrEditor() {
112 // qr editor132 // 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 }
230257
@@ -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 }
306340
@@ -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();
331370
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();
342382
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 }
498543