| 1 | import { Popup } from '../../../../popup.js'; |
| 2 | import { getSortableDelay } from '../../../../utils.js'; |
| 3 | import { log, warn } from '../../index.js'; |
| 4 | import { QuickReply } from '../QuickReply.js'; |
| 5 | import { QuickReplySet } from '../QuickReplySet.js'; |
| 6 | import { QuickReplySettings } from '../QuickReplySettings.js'; |
| 7 | |
| 8 | export 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 | } |