Blame Raw
Cohee · e3f41666 · · 540 lines (22.3 KB)
1 contributor
1import { Popup } from '../../../../popup.js';
2import { getSortableDelay } from '../../../../utils.js';
3import { log, warn } from '../../index.js';
4import { QuickReply } from '../QuickReply.js';
5import { QuickReplySet } from '../QuickReplySet.js';
6import { QuickReplySettings } from '../QuickReplySettings.js';
7
8export class SettingsUi {
9 /** @type {QuickReplySettings} */ settings;
10
11 /** @type {HTMLElement} */ template;
12 /** @type {HTMLElement} */ dom;
13
14 /**@type {HTMLInputElement}*/ isEnabled;
15 /**@type {HTMLInputElement}*/ isCombined;
16 /**@type {HTMLInputElement}*/ showPopoutButton;
17
18 /**@type {HTMLElement}*/ globalSetList;
19
20 /**@type {HTMLElement}*/ chatSetList;
21 /**@type {HTMLElement}*/ characterSetList;
22
23 /**@type {QuickReplySet}*/ currentQrSet;
24 /**@type {HTMLInputElement}*/ disableSend;
25 /**@type {HTMLInputElement}*/ placeBeforeInput;
26 /**@type {HTMLInputElement}*/ injectInput;
27 /**@type {HTMLInputElement}*/ color;
28 /**@type {HTMLInputElement}*/ onlyBorderColor;
29 /**@type {HTMLSelectElement}*/ currentSet;
30
31
32 constructor(/**@type {QuickReplySettings}*/settings) {
33 this.settings = settings;
34 settings.onRequestEditSet = (qrs) => this.selectQrSet(qrs);
35 }
36
37
38 rerender() {
39 if (!this.dom) return;
40 const content = this.dom.querySelector('.inline-drawer-content');
41 content.innerHTML = '';
42 // @ts-ignore
43 Array.from(this.template.querySelector('.inline-drawer-content').cloneNode(true).children).forEach(el => {
44 content.append(el);
45 });
46 this.prepareDom();
47 }
48 unrender() {
49 this.dom?.remove();
50 this.dom = null;
51 }
52 async render() {
53 if (!this.dom) {
54 const response = await fetch('/scripts/extensions/quick-reply/html/settings.html', { cache: 'no-store' });
55 if (response.ok) {
56 this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--settings');
57 // @ts-ignore
58 this.dom = this.template.cloneNode(true);
59 this.prepareDom();
60 } else {
61 warn('failed to fetch settings template');
62 }
63 }
64 return this.dom;
65 }
66
67
68 prepareGeneralSettings() {
69 // general settings
70 this.isEnabled = this.dom.querySelector('#qr--isEnabled');
71 this.isEnabled.checked = this.settings.isEnabled;
72 this.isEnabled.addEventListener('click', () => this.onIsEnabled());
73
74 this.isCombined = this.dom.querySelector('#qr--isCombined');
75 this.isCombined.checked = this.settings.isCombined;
76 this.isCombined.addEventListener('click', () => this.onIsCombined());
77
78 this.showPopoutButton = this.dom.querySelector('#qr--showPopoutButton');
79 this.showPopoutButton.checked = this.settings.showPopoutButton;
80 this.showPopoutButton.addEventListener('click', () => this.onShowPopoutButton());
81 }
82
83 prepareGlobalSetList() {
84 const dom = this.template.querySelector('#qr--global');
85 const clone = dom.cloneNode(true);
86 // @ts-ignore
87 this.settings.config.renderSettingsInto(clone);
88 this.dom.querySelector('#qr--global').replaceWith(clone);
89 }
90 prepareChatSetList() {
91 const dom = this.template.querySelector('#qr--chat');
92 const clone = dom.cloneNode(true);
93 if (this.settings.chatConfig) {
94 // @ts-ignore
95 this.settings.chatConfig.renderSettingsInto(clone);
96 } else {
97 const info = document.createElement('div'); {
98 info.textContent = 'No active chat.';
99 // @ts-ignore
100 clone.append(info);
101 }
102 }
103 this.dom.querySelector('#qr--chat').replaceWith(clone);
104 }
105 prepareCharacterSetList() {
106 const dom = this.template.querySelector('#qr--character');
107 const clone = /** @type {HTMLElement} */ (dom.cloneNode(true));
108
109 if (!this.settings.charConfig) {
110 const setListContainer = /** @type {HTMLElement} */ (clone.querySelector('.qr--setList'));
111 setListContainer.innerHTML = '';
112 const info = document.createElement('div');
113 info.textContent = 'No character is currently loaded.';
114 setListContainer.append(info);
115 } else {
116 // Let the config object handle its own rendering. It will render an empty list if there are no sets,
117 // but the "add" button will always be functional.
118 this.settings.charConfig.renderSettingsInto(clone);
119 }
120
121 // Replace the old DOM element with our newly prepared clone.
122 this.dom.querySelector('#qr--character').replaceWith(clone);
123 }
124
125 prepareQrEditor() {
126 // qr editor
127 this.dom.querySelector('#qr--set-rename').addEventListener('click', async () => this.renameQrSet());
128 this.dom.querySelector('#qr--set-new').addEventListener('click', async () => this.addQrSet());
129 /**@type {HTMLInputElement}*/
130 const importFile = this.dom.querySelector('#qr--set-importFile');
131 importFile.addEventListener('change', async () => {
132 await this.importQrSet(importFile.files);
133 importFile.value = null;
134 });
135 this.dom.querySelector('#qr--set-import').addEventListener('click', () => importFile.click());
136 this.dom.querySelector('#qr--set-export').addEventListener('click', async () => this.exportQrSet());
137 this.dom.querySelector('#qr--set-duplicate').addEventListener('click', async () => this.duplicateQrSet());
138 this.dom.querySelector('#qr--set-delete').addEventListener('click', async () => this.deleteQrSet());
139 this.dom.querySelector('#qr--set-add').addEventListener('click', async () => {
140 this.currentQrSet.addQuickReply();
141 });
142 this.dom.querySelector('#qr--set-paste').addEventListener('click', async () => {
143 const text = await navigator.clipboard.readText();
144 this.currentQrSet.addQuickReplyFromText(text);
145 });
146 this.dom.querySelector('#qr--set-importQr').addEventListener('click', async () => {
147 const inp = document.createElement('input'); {
148 inp.type = 'file';
149 inp.accept = '.json';
150 inp.addEventListener('change', async () => {
151 if (inp.files.length > 0) {
152 for (const file of inp.files) {
153 const text = await file.text();
154 this.currentQrSet.addQuickReply(JSON.parse(text));
155 }
156 }
157 });
158 inp.click();
159 }
160 });
161 this.qrList = this.dom.querySelector('#qr--set-qrList');
162 this.currentSet = this.dom.querySelector('#qr--set');
163 this.currentSet.addEventListener('change', () => this.onQrSetChange());
164 QuickReplySet.list.toSorted((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs => {
165 const opt = document.createElement('option'); {
166 opt.value = qrs.name;
167 opt.textContent = qrs.name;
168 this.currentSet.append(opt);
169 }
170 });
171 this.disableSend = this.dom.querySelector('#qr--disableSend');
172 this.disableSend.addEventListener('click', () => {
173 const qrs = this.currentQrSet;
174 qrs.disableSend = this.disableSend.checked;
175 qrs.save();
176 });
177 this.placeBeforeInput = this.dom.querySelector('#qr--placeBeforeInput');
178 this.placeBeforeInput.addEventListener('click', () => {
179 const qrs = this.currentQrSet;
180 qrs.placeBeforeInput = this.placeBeforeInput.checked;
181 qrs.save();
182 });
183 this.injectInput = this.dom.querySelector('#qr--injectInput');
184 this.injectInput.addEventListener('click', () => {
185 const qrs = this.currentQrSet;
186 qrs.injectInput = this.injectInput.checked;
187 qrs.save();
188 });
189 let initialColorChange = true;
190 this.color = this.dom.querySelector('#qr--color');
191 // @ts-ignore
192 this.color.color = this.currentQrSet?.color ?? 'transparent';
193 this.color.addEventListener('change', (evt) => {
194 if (!this.dom.closest('body')) return;
195 const qrs = this.currentQrSet;
196 if (initialColorChange) {
197 initialColorChange = false;
198 // @ts-ignore
199 this.color.color = qrs.color;
200 return;
201 }
202 // @ts-ignore
203 qrs.color = evt.detail.rgb;
204 qrs.save();
205 this.currentQrSet.updateColor();
206 });
207 // @ts-ignore
208 this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt) => {
209 const qrs = this.currentQrSet;
210 // @ts-ignore
211 this.color.color = 'transparent';
212 qrs.save();
213 this.currentQrSet.updateColor();
214 });
215 this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
216 this.onlyBorderColor.addEventListener('click', () => {
217 const qrs = this.currentQrSet;
218 qrs.onlyBorderColor = this.onlyBorderColor.checked;
219 qrs.save();
220 this.currentQrSet.updateColor();
221 });
222 this.onQrSetChange();
223 }
224 onQrSetChange() {
225 this.currentQrSet = QuickReplySet.get(this.currentSet.value) ?? new QuickReplySet();
226 this.disableSend.checked = this.currentQrSet.disableSend;
227 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
228 this.injectInput.checked = this.currentQrSet.injectInput;
229 // @ts-ignore
230 this.color.color = this.currentQrSet.color ?? 'transparent';
231 this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
232 this.qrList.innerHTML = '';
233 const qrsDom = this.currentQrSet.renderSettings();
234 this.qrList.append(qrsDom);
235 // @ts-ignore
236 $(qrsDom).sortable({
237 delay: getSortableDelay(),
238 handle: '.drag-handle',
239 stop: () => this.onQrListSort(),
240 });
241 }
242
243
244 prepareDom() {
245 this.prepareGeneralSettings();
246 this.prepareGlobalSetList();
247 this.prepareChatSetList();
248 this.prepareCharacterSetList();
249 this.prepareQrEditor();
250 }
251
252
253 async onIsEnabled() {
254 this.settings.isEnabled = this.isEnabled.checked;
255 this.settings.save();
256 }
257
258 async onIsCombined() {
259 this.settings.isCombined = this.isCombined.checked;
260 this.settings.save();
261 }
262
263 async onShowPopoutButton() {
264 this.settings.showPopoutButton = this.showPopoutButton.checked;
265 this.settings.save();
266 }
267
268 async onGlobalSetListSort() {
269 this.settings.config.setList = Array.from(this.globalSetList.children).map((it, idx) => {
270 const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
271 it.setAttribute('data-order', String(idx));
272 return set;
273 });
274 this.settings.save();
275 }
276
277 async onChatSetListSort() {
278 this.settings.chatConfig.setList = Array.from(this.chatSetList.children).map((it, idx) => {
279 const set = this.settings.chatConfig.setList[Number(it.getAttribute('data-order'))];
280 it.setAttribute('data-order', String(idx));
281 return set;
282 });
283 this.settings.save();
284 }
285
286 updateOrder(list) {
287 Array.from(list.children).forEach((it, idx) => {
288 it.setAttribute('data-order', idx);
289 });
290 }
291
292 async onQrListSort() {
293 this.currentQrSet.qrList = Array.from(this.qrList.querySelectorAll('.qr--set-item')).map((it, idx) => {
294 const qr = this.currentQrSet.qrList.find(qr => qr.id == Number(it.getAttribute('data-id')));
295 it.setAttribute('data-order', String(idx));
296 return qr;
297 });
298 this.currentQrSet.save();
299 }
300
301 async deleteQrSet() {
302 const confirmed = await Popup.show.confirm('Delete Quick Reply Set', `Are you sure you want to delete the Quick Reply Set "${this.currentQrSet.name}"?<br>This cannot be undone.`);
303 if (confirmed) {
304 await this.doDeleteQrSet(this.currentQrSet);
305 this.rerender();
306 }
307 }
308 async doDeleteQrSet(qrs) {
309 await qrs.delete();
310 //TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners
311 for (let i = this.settings.config.setList.length - 1; i >= 0; i--) {
312 if (this.settings.config.setList[i].set == qrs) {
313 this.settings.config.setList.splice(i, 1);
314 }
315 }
316 if (this.settings.chatConfig) {
317 for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) {
318 if (this.settings.chatConfig.setList[i].set == qrs) {
319 this.settings.chatConfig.setList.splice(i, 1);
320 }
321 }
322 }
323 if (this.settings.charConfig) {
324 for (let i = this.settings.charConfig.setList.length - 1; i >= 0; i--) {
325 if (this.settings.charConfig.setList[i].set == qrs) {
326 this.settings.charConfig.setList.splice(i, 1);
327 }
328 }
329 }
330 this.settings.save();
331 }
332
333 async renameQrSet() {
334 const newName = await Popup.show.input('Rename Quick Reply Set', 'Enter a new name:', this.currentQrSet.name);
335 if (newName && newName.length > 0) {
336 const existingSet = QuickReplySet.get(newName);
337 if (existingSet) {
338 toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
339 return;
340 }
341 const oldName = this.currentQrSet.name;
342 this.currentQrSet.name = newName;
343 await this.currentQrSet.save();
344
345 // Update it in both set lists
346 this.settings.config.setList.forEach(set => {
347 if (set.set.name === oldName) {
348 set.set.name = newName;
349 }
350 });
351 this.settings.chatConfig?.setList.forEach(set => {
352 if (set.set.name === oldName) {
353 set.set.name = newName;
354 }
355 });
356 this.settings.charConfig?.setList.forEach(set => {
357 if (set.set.name === oldName) {
358 set.set.name = newName;
359 }
360 });
361 this.settings.save();
362
363 // Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below.
364 /** @type {HTMLOptionElement} */
365 const option = this.currentSet.querySelector(`#qr--set option[value="${oldName}"]`);
366 option.value = newName;
367 option.textContent = newName;
368
369 this.currentSet.value = newName;
370 this.onQrSetChange();
371 this.prepareGlobalSetList();
372 this.prepareChatSetList();
373 this.prepareCharacterSetList();
374
375 console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`);
376 }
377 }
378
379 async addQrSet() {
380 const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
381 if (name && name.length > 0) {
382 const oldQrs = QuickReplySet.get(name);
383 if (oldQrs) {
384 const replace = Popup.show.confirm('Replace existing World Info', `A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`);
385 if (replace) {
386 const idx = QuickReplySet.list.indexOf(oldQrs);
387 await this.doDeleteQrSet(oldQrs);
388 const qrs = new QuickReplySet();
389 qrs.name = name;
390 qrs.addQuickReply();
391 QuickReplySet.list.splice(idx, 0, qrs);
392 this.rerender();
393 this.currentSet.value = name;
394 this.onQrSetChange();
395 this.prepareGlobalSetList();
396 this.prepareChatSetList();
397 this.prepareCharacterSetList();
398 }
399 } else {
400 const qrs = new QuickReplySet();
401 qrs.name = name;
402 qrs.addQuickReply();
403 const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
404 if (idx > -1) {
405 QuickReplySet.list.splice(idx, 0, qrs);
406 } else {
407 QuickReplySet.list.push(qrs);
408 }
409 const opt = document.createElement('option'); {
410 opt.value = qrs.name;
411 opt.textContent = qrs.name;
412 if (idx > -1) {
413 this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
414 } else {
415 this.currentSet.append(opt);
416 }
417 }
418 this.currentSet.value = name;
419 this.onQrSetChange();
420 this.prepareGlobalSetList();
421 this.prepareChatSetList();
422 this.prepareCharacterSetList();
423 }
424 }
425 }
426
427 async importQrSet(/**@type {FileList}*/files) {
428 for (let i = 0; i < files.length; i++) {
429 await this.importSingleQrSet(files.item(i));
430 }
431 }
432 async importSingleQrSet(/**@type {File}*/file) {
433 log('FILE', file);
434 try {
435 const text = await file.text();
436 const props = JSON.parse(text);
437 if (!Number.isInteger(props.version) || typeof props.name != 'string') {
438 toastr.error(`The file "${file.name}" does not appear to be a valid quick reply set.`);
439 warn(`The file "${file.name}" does not appear to be a valid quick reply set.`);
440 } else {
441 /**@type {QuickReplySet}*/
442 const qrs = QuickReplySet.from(JSON.parse(JSON.stringify(props)));
443 qrs.qrList = props.qrList.map(it => QuickReply.from(it));
444 qrs.init();
445 const oldQrs = QuickReplySet.get(props.name);
446 if (oldQrs) {
447 const replace = Popup.show.confirm('Replace existing World Info', `A Quick Reply Set named "${name}" already exists.<br>Do you want to overwrite the existing Quick Reply Set?<br>The existing set will be deleted. This cannot be undone.`);
448 if (replace) {
449 const idx = QuickReplySet.list.indexOf(oldQrs);
450 await this.doDeleteQrSet(oldQrs);
451 QuickReplySet.list.splice(idx, 0, qrs);
452 await qrs.save();
453 this.rerender();
454 this.currentSet.value = qrs.name;
455 this.onQrSetChange();
456 this.prepareGlobalSetList();
457 this.prepareChatSetList();
458 this.prepareCharacterSetList();
459 }
460 } else {
461 const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
462 if (idx > -1) {
463 QuickReplySet.list.splice(idx, 0, qrs);
464 } else {
465 QuickReplySet.list.push(qrs);
466 }
467 await qrs.save();
468 const opt = document.createElement('option'); {
469 opt.value = qrs.name;
470 opt.textContent = qrs.name;
471 if (idx > -1) {
472 this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
473 } else {
474 this.currentSet.append(opt);
475 }
476 }
477 this.currentSet.value = qrs.name;
478 this.onQrSetChange();
479 this.prepareGlobalSetList();
480 this.prepareChatSetList();
481 this.prepareCharacterSetList();
482 }
483 }
484 } catch (ex) {
485 warn(ex);
486 toastr.error(`Failed to import "${file.name}":\n\n${ex.message}`);
487 }
488 }
489
490 exportQrSet() {
491 const blob = new Blob([JSON.stringify(this.currentQrSet)], { type: 'application/json' });
492 const url = URL.createObjectURL(blob);
493 const a = document.createElement('a'); {
494 a.href = url;
495 a.download = `${this.currentQrSet.name}.json`;
496 a.click();
497 }
498 URL.revokeObjectURL(url);
499 }
500
501 async duplicateQrSet() {
502 const newName = await Popup.show.input('Duplicate Quick Reply Set', 'Enter a name for the new Quick Reply Set:', `${this.currentQrSet.name} (Copy)`);
503 if (newName && newName.length > 0) {
504 const existingSet = QuickReplySet.get(newName);
505 if (existingSet) {
506 toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
507 return;
508 }
509 const newQrSet = QuickReplySet.from(this.currentQrSet.toJSON());
510 newQrSet.name = newName;
511 newQrSet.qrList = this.currentQrSet.qrList.map(qr => QuickReply.from(qr.toJSON()));
512 newQrSet.init();
513 const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(newName.toLowerCase()) == 1);
514 if (idx > -1) {
515 QuickReplySet.list.splice(idx, 0, newQrSet);
516 } else {
517 QuickReplySet.list.push(newQrSet);
518 }
519 const opt = document.createElement('option'); {
520 opt.value = newQrSet.name;
521 opt.textContent = newQrSet.name;
522 if (idx > -1) {
523 this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
524 } else {
525 this.currentSet.append(opt);
526 }
527 }
528 this.currentSet.value = newName;
529 this.onQrSetChange();
530 this.prepareGlobalSetList();
531 this.prepareChatSetList();
532 this.prepareCharacterSetList();
533 }
534 }
535
536 selectQrSet(qrs) {
537 this.currentSet.value = qrs.name;
538 this.onQrSetChange();
539 }
540}