mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-13 02:20:14 +01:00
505 lines
20 KiB
JavaScript
505 lines
20 KiB
JavaScript
import { Popup } from '../../../../popup.js';
|
|
import { getSortableDelay } from '../../../../utils.js';
|
|
import { log, warn } from '../../index.js';
|
|
import { QuickReply } from '../QuickReply.js';
|
|
import { QuickReplySet } from '../QuickReplySet.js';
|
|
// eslint-disable-next-line no-unused-vars
|
|
import { QuickReplySettings } from '../QuickReplySettings.js';
|
|
|
|
export class SettingsUi {
|
|
/**@type {QuickReplySettings}*/ settings;
|
|
|
|
/**@type {HTMLElement}*/ template;
|
|
/**@type {HTMLElement}*/ dom;
|
|
|
|
/**@type {HTMLInputElement}*/ isEnabled;
|
|
/**@type {HTMLInputElement}*/ isCombined;
|
|
/**@type {HTMLInputElement}*/ showPopoutButton;
|
|
|
|
/**@type {HTMLElement}*/ globalSetList;
|
|
|
|
/**@type {HTMLElement}*/ chatSetList;
|
|
|
|
/**@type {QuickReplySet}*/ currentQrSet;
|
|
/**@type {HTMLInputElement}*/ disableSend;
|
|
/**@type {HTMLInputElement}*/ placeBeforeInput;
|
|
/**@type {HTMLInputElement}*/ injectInput;
|
|
/**@type {HTMLInputElement}*/ color;
|
|
/**@type {HTMLInputElement}*/ onlyBorderColor;
|
|
/**@type {HTMLSelectElement}*/ currentSet;
|
|
|
|
|
|
|
|
|
|
constructor(/**@type {QuickReplySettings}*/settings) {
|
|
this.settings = settings;
|
|
settings.onRequestEditSet = (qrs) => this.selectQrSet(qrs);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
rerender() {
|
|
if (!this.dom) return;
|
|
const content = this.dom.querySelector('.inline-drawer-content');
|
|
content.innerHTML = '';
|
|
// @ts-ignore
|
|
Array.from(this.template.querySelector('.inline-drawer-content').cloneNode(true).children).forEach(el=>{
|
|
content.append(el);
|
|
});
|
|
this.prepareDom();
|
|
}
|
|
unrender() {
|
|
this.dom?.remove();
|
|
this.dom = null;
|
|
}
|
|
async render() {
|
|
if (!this.dom) {
|
|
const response = await fetch('/scripts/extensions/quick-reply/html/settings.html', { cache: 'no-store' });
|
|
if (response.ok) {
|
|
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--settings');
|
|
// @ts-ignore
|
|
this.dom = this.template.cloneNode(true);
|
|
this.prepareDom();
|
|
} else {
|
|
warn('failed to fetch settings template');
|
|
}
|
|
}
|
|
return this.dom;
|
|
}
|
|
|
|
|
|
prepareGeneralSettings() {
|
|
// general settings
|
|
this.isEnabled = this.dom.querySelector('#qr--isEnabled');
|
|
this.isEnabled.checked = this.settings.isEnabled;
|
|
this.isEnabled.addEventListener('click', ()=>this.onIsEnabled());
|
|
|
|
this.isCombined = this.dom.querySelector('#qr--isCombined');
|
|
this.isCombined.checked = this.settings.isCombined;
|
|
this.isCombined.addEventListener('click', ()=>this.onIsCombined());
|
|
|
|
this.showPopoutButton = this.dom.querySelector('#qr--showPopoutButton');
|
|
this.showPopoutButton.checked = this.settings.showPopoutButton;
|
|
this.showPopoutButton.addEventListener('click', ()=>this.onShowPopoutButton());
|
|
}
|
|
|
|
prepareGlobalSetList() {
|
|
const dom = this.template.querySelector('#qr--global');
|
|
const clone = dom.cloneNode(true);
|
|
// @ts-ignore
|
|
this.settings.config.renderSettingsInto(clone);
|
|
this.dom.querySelector('#qr--global').replaceWith(clone);
|
|
}
|
|
prepareChatSetList() {
|
|
const dom = this.template.querySelector('#qr--chat');
|
|
const clone = dom.cloneNode(true);
|
|
if (this.settings.chatConfig) {
|
|
// @ts-ignore
|
|
this.settings.chatConfig.renderSettingsInto(clone);
|
|
} else {
|
|
const info = document.createElement('div'); {
|
|
info.textContent = 'No active chat.';
|
|
// @ts-ignore
|
|
clone.append(info);
|
|
}
|
|
}
|
|
this.dom.querySelector('#qr--chat').replaceWith(clone);
|
|
}
|
|
|
|
prepareQrEditor() {
|
|
// qr editor
|
|
this.dom.querySelector('#qr--set-rename').addEventListener('click', async () => this.renameQrSet());
|
|
this.dom.querySelector('#qr--set-new').addEventListener('click', async()=>this.addQrSet());
|
|
/**@type {HTMLInputElement}*/
|
|
const importFile = this.dom.querySelector('#qr--set-importFile');
|
|
importFile.addEventListener('change', async()=>{
|
|
await this.importQrSet(importFile.files);
|
|
importFile.value = null;
|
|
});
|
|
this.dom.querySelector('#qr--set-import').addEventListener('click', ()=>importFile.click());
|
|
this.dom.querySelector('#qr--set-export').addEventListener('click', async () => this.exportQrSet());
|
|
this.dom.querySelector('#qr--set-duplicate').addEventListener('click', async () => this.duplicateQrSet());
|
|
this.dom.querySelector('#qr--set-delete').addEventListener('click', async()=>this.deleteQrSet());
|
|
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
|
|
this.currentQrSet.addQuickReply();
|
|
});
|
|
this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
|
|
const text = await navigator.clipboard.readText();
|
|
this.currentQrSet.addQuickReplyFromText(text);
|
|
});
|
|
this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{
|
|
const inp = document.createElement('input'); {
|
|
inp.type = 'file';
|
|
inp.accept = '.json';
|
|
inp.addEventListener('change', async()=>{
|
|
if (inp.files.length > 0) {
|
|
for (const file of inp.files) {
|
|
const text = await file.text();
|
|
this.currentQrSet.addQuickReply(JSON.parse(text));
|
|
}
|
|
}
|
|
});
|
|
inp.click();
|
|
}
|
|
});
|
|
this.qrList = this.dom.querySelector('#qr--set-qrList');
|
|
this.currentSet = this.dom.querySelector('#qr--set');
|
|
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
|
|
QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
|
|
const opt = document.createElement('option'); {
|
|
opt.value = qrs.name;
|
|
opt.textContent = qrs.name;
|
|
this.currentSet.append(opt);
|
|
}
|
|
});
|
|
this.disableSend = this.dom.querySelector('#qr--disableSend');
|
|
this.disableSend.addEventListener('click', ()=>{
|
|
const qrs = this.currentQrSet;
|
|
qrs.disableSend = this.disableSend.checked;
|
|
qrs.save();
|
|
});
|
|
this.placeBeforeInput = this.dom.querySelector('#qr--placeBeforeInput');
|
|
this.placeBeforeInput.addEventListener('click', ()=>{
|
|
const qrs = this.currentQrSet;
|
|
qrs.placeBeforeInput = this.placeBeforeInput.checked;
|
|
qrs.save();
|
|
});
|
|
this.injectInput = this.dom.querySelector('#qr--injectInput');
|
|
this.injectInput.addEventListener('click', ()=>{
|
|
const qrs = this.currentQrSet;
|
|
qrs.injectInput = this.injectInput.checked;
|
|
qrs.save();
|
|
});
|
|
let initialColorChange = true;
|
|
this.color = this.dom.querySelector('#qr--color');
|
|
this.color.color = this.currentQrSet?.color ?? 'transparent';
|
|
this.color.addEventListener('change', (evt)=>{
|
|
if (!this.dom.closest('body')) return;
|
|
const qrs = this.currentQrSet;
|
|
if (initialColorChange) {
|
|
initialColorChange = false;
|
|
this.color.color = qrs.color;
|
|
return;
|
|
}
|
|
qrs.color = evt.detail.rgb;
|
|
qrs.save();
|
|
this.currentQrSet.updateColor();
|
|
});
|
|
this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
|
|
const qrs = this.currentQrSet;
|
|
this.color.color = 'transparent';
|
|
qrs.save();
|
|
this.currentQrSet.updateColor();
|
|
});
|
|
this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
|
|
this.onlyBorderColor.addEventListener('click', ()=>{
|
|
const qrs = this.currentQrSet;
|
|
qrs.onlyBorderColor = this.onlyBorderColor.checked;
|
|
qrs.save();
|
|
this.currentQrSet.updateColor();
|
|
});
|
|
this.onQrSetChange();
|
|
}
|
|
onQrSetChange() {
|
|
this.currentQrSet = QuickReplySet.get(this.currentSet.value) ?? new QuickReplySet();
|
|
this.disableSend.checked = this.currentQrSet.disableSend;
|
|
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
|
|
this.injectInput.checked = this.currentQrSet.injectInput;
|
|
this.color.color = this.currentQrSet.color ?? 'transparent';
|
|
this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
|
|
this.qrList.innerHTML = '';
|
|
const qrsDom = this.currentQrSet.renderSettings();
|
|
this.qrList.append(qrsDom);
|
|
// @ts-ignore
|
|
$(qrsDom).sortable({
|
|
delay: getSortableDelay(),
|
|
handle: '.drag-handle',
|
|
stop: ()=>this.onQrListSort(),
|
|
});
|
|
}
|
|
|
|
|
|
prepareDom() {
|
|
this.prepareGeneralSettings();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
this.prepareQrEditor();
|
|
}
|
|
|
|
|
|
|
|
|
|
async onIsEnabled() {
|
|
this.settings.isEnabled = this.isEnabled.checked;
|
|
this.settings.save();
|
|
}
|
|
|
|
async onIsCombined() {
|
|
this.settings.isCombined = this.isCombined.checked;
|
|
this.settings.save();
|
|
}
|
|
|
|
async onShowPopoutButton() {
|
|
this.settings.showPopoutButton = this.showPopoutButton.checked;
|
|
this.settings.save();
|
|
}
|
|
|
|
async onGlobalSetListSort() {
|
|
this.settings.config.setList = Array.from(this.globalSetList.children).map((it,idx)=>{
|
|
const set = this.settings.config.setList[Number(it.getAttribute('data-order'))];
|
|
it.setAttribute('data-order', String(idx));
|
|
return set;
|
|
});
|
|
this.settings.save();
|
|
}
|
|
|
|
async onChatSetListSort() {
|
|
this.settings.chatConfig.setList = Array.from(this.chatSetList.children).map((it,idx)=>{
|
|
const set = this.settings.chatConfig.setList[Number(it.getAttribute('data-order'))];
|
|
it.setAttribute('data-order', String(idx));
|
|
return set;
|
|
});
|
|
this.settings.save();
|
|
}
|
|
|
|
updateOrder(list) {
|
|
Array.from(list.children).forEach((it,idx)=>{
|
|
it.setAttribute('data-order', idx);
|
|
});
|
|
}
|
|
|
|
async onQrListSort() {
|
|
this.currentQrSet.qrList = Array.from(this.qrList.querySelectorAll('.qr--set-item')).map((it,idx)=>{
|
|
const qr = this.currentQrSet.qrList.find(qr=>qr.id == Number(it.getAttribute('data-id')));
|
|
it.setAttribute('data-order', String(idx));
|
|
return qr;
|
|
});
|
|
this.currentQrSet.save();
|
|
}
|
|
|
|
async deleteQrSet() {
|
|
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.`);
|
|
if (confirmed) {
|
|
await this.doDeleteQrSet(this.currentQrSet);
|
|
this.rerender();
|
|
}
|
|
}
|
|
async doDeleteQrSet(qrs) {
|
|
await qrs.delete();
|
|
//TODO (HACK) should just bubble up from QuickReplySet.delete() but that would require proper or at least more comples onDelete listeners
|
|
for (let i = this.settings.config.setList.length - 1; i >= 0; i--) {
|
|
if (this.settings.config.setList[i].set == qrs) {
|
|
this.settings.config.setList.splice(i, 1);
|
|
}
|
|
}
|
|
if (this.settings.chatConfig) {
|
|
for (let i = this.settings.chatConfig.setList.length - 1; i >= 0; i--) {
|
|
if (this.settings.chatConfig.setList[i].set == qrs) {
|
|
this.settings.chatConfig.setList.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
this.settings.save();
|
|
}
|
|
|
|
async renameQrSet() {
|
|
const newName = await Popup.show.input('Rename Quick Reply Set', 'Enter a new name:', this.currentQrSet.name);
|
|
if (newName && newName.length > 0) {
|
|
const existingSet = QuickReplySet.get(newName);
|
|
if (existingSet) {
|
|
toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
|
|
return;
|
|
}
|
|
const oldName = this.currentQrSet.name;
|
|
this.currentQrSet.name = newName;
|
|
await this.currentQrSet.save();
|
|
|
|
// Update it in both set lists
|
|
this.settings.config.setList.forEach(set => {
|
|
if (set.set.name === oldName) {
|
|
set.set.name = newName;
|
|
}
|
|
});
|
|
this.settings.chatConfig?.setList.forEach(set => {
|
|
if (set.set.name === oldName) {
|
|
set.set.name = newName;
|
|
}
|
|
});
|
|
this.settings.save();
|
|
|
|
// Update the option in the current selected QR dropdown. All others will be refreshed via the prepare calls below.
|
|
/** @type {HTMLOptionElement} */
|
|
const option = this.currentSet.querySelector(`#qr--set option[value="${oldName}"]`);
|
|
option.value = newName;
|
|
option.textContent = newName;
|
|
|
|
this.currentSet.value = newName;
|
|
this.onQrSetChange();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
|
|
console.info(`Quick Reply Set renamed from ""${oldName}" to "${newName}".`);
|
|
}
|
|
}
|
|
|
|
async addQrSet() {
|
|
const name = await Popup.show.input('Create a new Quick Reply Set', 'Enter a name for the new Quick Reply Set:');
|
|
if (name && name.length > 0) {
|
|
const oldQrs = QuickReplySet.get(name);
|
|
if (oldQrs) {
|
|
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.`);
|
|
if (replace) {
|
|
const idx = QuickReplySet.list.indexOf(oldQrs);
|
|
await this.doDeleteQrSet(oldQrs);
|
|
const qrs = new QuickReplySet();
|
|
qrs.name = name;
|
|
qrs.addQuickReply();
|
|
QuickReplySet.list.splice(idx, 0, qrs);
|
|
this.rerender();
|
|
this.currentSet.value = name;
|
|
this.onQrSetChange();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
}
|
|
} else {
|
|
const qrs = new QuickReplySet();
|
|
qrs.name = name;
|
|
qrs.addQuickReply();
|
|
const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
|
|
if (idx > -1) {
|
|
QuickReplySet.list.splice(idx, 0, qrs);
|
|
} else {
|
|
QuickReplySet.list.push(qrs);
|
|
}
|
|
const opt = document.createElement('option'); {
|
|
opt.value = qrs.name;
|
|
opt.textContent = qrs.name;
|
|
if (idx > -1) {
|
|
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
|
} else {
|
|
this.currentSet.append(opt);
|
|
}
|
|
}
|
|
this.currentSet.value = name;
|
|
this.onQrSetChange();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
}
|
|
}
|
|
}
|
|
|
|
async importQrSet(/**@type {FileList}*/files) {
|
|
for (let i = 0; i < files.length; i++) {
|
|
await this.importSingleQrSet(files.item(i));
|
|
}
|
|
}
|
|
async importSingleQrSet(/**@type {File}*/file) {
|
|
log('FILE', file);
|
|
try {
|
|
const text = await file.text();
|
|
const props = JSON.parse(text);
|
|
if (!Number.isInteger(props.version) || typeof props.name != 'string') {
|
|
toastr.error(`The file "${file.name}" does not appear to be a valid quick reply set.`);
|
|
warn(`The file "${file.name}" does not appear to be a valid quick reply set.`);
|
|
} else {
|
|
/**@type {QuickReplySet}*/
|
|
const qrs = QuickReplySet.from(JSON.parse(JSON.stringify(props)));
|
|
qrs.qrList = props.qrList.map(it=>QuickReply.from(it));
|
|
qrs.init();
|
|
const oldQrs = QuickReplySet.get(props.name);
|
|
if (oldQrs) {
|
|
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.`);
|
|
if (replace) {
|
|
const idx = QuickReplySet.list.indexOf(oldQrs);
|
|
await this.doDeleteQrSet(oldQrs);
|
|
QuickReplySet.list.splice(idx, 0, qrs);
|
|
await qrs.save();
|
|
this.rerender();
|
|
this.currentSet.value = qrs.name;
|
|
this.onQrSetChange();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
}
|
|
} else {
|
|
const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
|
|
if (idx > -1) {
|
|
QuickReplySet.list.splice(idx, 0, qrs);
|
|
} else {
|
|
QuickReplySet.list.push(qrs);
|
|
}
|
|
await qrs.save();
|
|
const opt = document.createElement('option'); {
|
|
opt.value = qrs.name;
|
|
opt.textContent = qrs.name;
|
|
if (idx > -1) {
|
|
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
|
} else {
|
|
this.currentSet.append(opt);
|
|
}
|
|
}
|
|
this.currentSet.value = qrs.name;
|
|
this.onQrSetChange();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
warn(ex);
|
|
toastr.error(`Failed to import "${file.name}":\n\n${ex.message}`);
|
|
}
|
|
}
|
|
|
|
exportQrSet() {
|
|
const blob = new Blob([JSON.stringify(this.currentQrSet)], { type:'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a'); {
|
|
a.href = url;
|
|
a.download = `${this.currentQrSet.name}.json`;
|
|
a.click();
|
|
}
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
async duplicateQrSet() {
|
|
const newName = await Popup.show.input('Duplicate Quick Reply Set', 'Enter a name for the new Quick Reply Set:', `${this.currentQrSet.name} (Copy)`);
|
|
if (newName && newName.length > 0) {
|
|
const existingSet = QuickReplySet.get(newName);
|
|
if (existingSet) {
|
|
toastr.error(`A Quick Reply Set named "${newName}" already exists.`);
|
|
return;
|
|
}
|
|
const newQrSet = QuickReplySet.from(JSON.parse(JSON.stringify(this.currentQrSet)));
|
|
newQrSet.name = newName;
|
|
newQrSet.qrList = this.currentQrSet.qrList.map(qr => QuickReply.from(JSON.parse(JSON.stringify(qr))));
|
|
newQrSet.addQuickReply();
|
|
const idx = QuickReplySet.list.findIndex(it => it.name.toLowerCase().localeCompare(newName.toLowerCase()) == 1);
|
|
if (idx > -1) {
|
|
QuickReplySet.list.splice(idx, 0, newQrSet);
|
|
} else {
|
|
QuickReplySet.list.push(newQrSet);
|
|
}
|
|
const opt = document.createElement('option'); {
|
|
opt.value = newQrSet.name;
|
|
opt.textContent = newQrSet.name;
|
|
if (idx > -1) {
|
|
this.currentSet.children[idx].insertAdjacentElement('beforebegin', opt);
|
|
} else {
|
|
this.currentSet.append(opt);
|
|
}
|
|
}
|
|
this.currentSet.value = newName;
|
|
this.onQrSetChange();
|
|
this.prepareGlobalSetList();
|
|
this.prepareChatSetList();
|
|
}
|
|
}
|
|
|
|
selectQrSet(qrs) {
|
|
this.currentSet.value = qrs.name;
|
|
this.onQrSetChange();
|
|
}
|
|
}
|