mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-20 14:10:39 +01:00
implement QR basics
This commit is contained in:
parent
e19bf1afdd
commit
69d6b9379a
53
public/scripts/extensions/quick-reply/html/qrOptions.html
Normal file
53
public/scripts/extensions/quick-reply/html/qrOptions.html
Normal file
@ -0,0 +1,53 @@
|
||||
<div id="qr--qrOptions">
|
||||
<h3>Context Menu</h3>
|
||||
<div id="qr--ctxEditor">
|
||||
<template id="qr--ctxItem">
|
||||
<div class="qr--ctxItem" data-order="0">
|
||||
<div class="drag-handle ui-sortable-handle">☰</div>
|
||||
<select class="qr--set"></select>
|
||||
<label class="qr--isChainedLabel checkbox_label" title="When enabled, the current Quick Reply will be sent together with (before) the clicked QR from the context menu.">
|
||||
Chaining:
|
||||
<input type="checkbox" class="qr--isChained">
|
||||
</label>
|
||||
<div class="qr--delete menu_button menu_button_icon fa-solid fa-trash-can" title="Remove entry"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="qr--ctxEditorActions">
|
||||
<span id="qr--ctxAdd" class="menu_button menu_button_icon fa-solid fa-plus" title="Add quick reply set to context menu"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<h3>Auto-Execute</h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--isHidden" >
|
||||
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnStartup" >
|
||||
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnUser" >
|
||||
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnAi" >
|
||||
<span><i class="fa-solid fa-fw fa-robot"></i> Execute on AI message</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" id="qr--executeOnChatChange" >
|
||||
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<h3>UI Options</h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label>
|
||||
Title (tooltip, leave empty to show the message or /command)
|
||||
<input type="text" class="text_pole" id="qr--title">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
61
public/scripts/extensions/quick-reply/html/settings.html
Normal file
61
public/scripts/extensions/quick-reply/html/settings.html
Normal file
@ -0,0 +1,61 @@
|
||||
<div id="qr--settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<strong>Quick Reply</strong>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--isEnabled"> Enable Quick Replies
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--isCombined"> Combine buttons from all active sets
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="qr--head">
|
||||
<div class="qr--title">Global Quick Reply Sets</div>
|
||||
<div class="qr--actions">
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--global-setListAdd" title="Add quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--global-setList" class="qr--setList"></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="qr--head">
|
||||
<div class="qr--title">Chat Quick Reply Sets</div>
|
||||
<div class="qr--actions">
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--chat-setListAdd" title="Add quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--chat-setList" class="qr--setList"></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="qr--head">
|
||||
<div class="qr--title">Edit Quick Replies</div>
|
||||
<div class="qr--actions">
|
||||
<select id="qr--set" class="text_pole"></select>
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-new" title="Create new quick reply set"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="qr--set-settings">
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--disableSend"> <span>Disable send (insert into input field)</span>
|
||||
</label>
|
||||
<label class="flex-container">
|
||||
<input type="checkbox" id="qr--placeBeforeInput"> <span>Place quick reply before input</span>
|
||||
</label>
|
||||
<label class="flex-container" id="qr--injectInputContainer">
|
||||
<input type="checkbox" id="qr--injectInput"> <span>Inject user input automatically <small>(if disabled, use <code>{{input}}</code> macro for manual injection)</small></span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="qr--set-qrList" class="qr--qrList"></div>
|
||||
<div class="qr--set-qrListActions">
|
||||
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
202
public/scripts/extensions/quick-reply/index.js
Normal file
202
public/scripts/extensions/quick-reply/index.js
Normal file
@ -0,0 +1,202 @@
|
||||
import { chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { QuickReply } from './src/QuickReply.js';
|
||||
import { QuickReplyConfig } from './src/QuickReplyConfig.js';
|
||||
import { QuickReplyContextLink } from './src/QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from './src/QuickReplySet.js';
|
||||
import { QuickReplySettings } from './src/QuickReplySettings.js';
|
||||
import { ButtonUi } from './src/ui/ButtonUi.js';
|
||||
import { SettingsUi } from './src/ui/SettingsUi.js';
|
||||
|
||||
|
||||
|
||||
|
||||
//TODO popout QR button bar (allow separate popouts for each QR set?)
|
||||
//TODO context menus
|
||||
//TODO move advanced QR options into own UI class
|
||||
//TODO slash commands
|
||||
//TODO create new QR set
|
||||
//TODO delete QR set
|
||||
//TODO easy way to CRUD QRs and sets
|
||||
//TODO easy way to set global and chat sets
|
||||
|
||||
|
||||
|
||||
|
||||
const _VERBOSE = true;
|
||||
export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
|
||||
export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
|
||||
|
||||
|
||||
const defaultConfig = {
|
||||
setList: [{
|
||||
set: 'Default',
|
||||
isVisible: true,
|
||||
}],
|
||||
};
|
||||
|
||||
const defaultSettings = {
|
||||
isEnabled: true,
|
||||
isCombined: false,
|
||||
config: defaultConfig,
|
||||
};
|
||||
|
||||
|
||||
/** @type {QuickReplySettings}*/
|
||||
let settings;
|
||||
/** @type {SettingsUi} */
|
||||
let manager;
|
||||
/** @type {ButtonUi} */
|
||||
let buttons;
|
||||
|
||||
|
||||
|
||||
|
||||
const loadSets = async () => {
|
||||
const response = await fetch('/api/settings/get', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const setList = (await response.json()).quickReplyPresets ?? [];
|
||||
for (const set of setList) {
|
||||
if (set.version == 2) {
|
||||
QuickReplySet.list.push(QuickReplySet.from(set));
|
||||
} else {
|
||||
const qrs = new QuickReplySet();
|
||||
qrs.name = set.name;
|
||||
qrs.disableSend = set.quickActionEnabled ?? false;
|
||||
qrs.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
|
||||
qrs.injectInput = set.AutoInputInject ?? false;
|
||||
qrs.qrList = set.quickReplySlots.map((slot,idx)=>{
|
||||
const qr = new QuickReply();
|
||||
qr.id = idx + 1;
|
||||
qr.label = slot.label;
|
||||
qr.title = slot.title;
|
||||
qr.message = slot.mes;
|
||||
qr.isHidden = slot.hidden ?? false;
|
||||
qr.executeOnStartup = slot.autoExecute_appStartup ?? false;
|
||||
qr.executeOnUser = slot.autoExecute_userMessage ?? false;
|
||||
qr.executeOnAi = slot.autoExecute_botMessage ?? false;
|
||||
qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
|
||||
qr.contextList = (slot.contextMenu ?? []).map(it=>(QuickReplyContextLink.from({
|
||||
set: it.preset,
|
||||
isChained: it.chain,
|
||||
})));
|
||||
return qr;
|
||||
});
|
||||
QuickReplySet.list.push(qrs);
|
||||
await qrs.save();
|
||||
}
|
||||
}
|
||||
log('sets: ', QuickReplySet.list);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSettings = async () => {
|
||||
//TODO migrate old settings
|
||||
if (!extension_settings.quickReplyV2) {
|
||||
extension_settings.quickReplyV2 = defaultSettings;
|
||||
}
|
||||
try {
|
||||
settings = QuickReplySettings.from(extension_settings.quickReplyV2);
|
||||
} catch {
|
||||
settings = QuickReplySettings.from(defaultSettings);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
const init = async () => {
|
||||
await loadSets();
|
||||
await loadSettings();
|
||||
log('settings: ', settings);
|
||||
|
||||
manager = new SettingsUi(settings);
|
||||
document.querySelector('#extensions_settings2').append(await manager.render());
|
||||
|
||||
buttons = new ButtonUi(settings);
|
||||
buttons.show();
|
||||
settings.onSave = ()=>buttons.refresh();
|
||||
|
||||
window['executeQuickReplyByName'] = async(name) => {
|
||||
let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
|
||||
.map(it=>it.set.qrList)
|
||||
.flat()
|
||||
.find(it=>it.label == name)
|
||||
;
|
||||
if (!qr) {
|
||||
let [setName, ...qrName] = name.split('.');
|
||||
name = qrName.join('.');
|
||||
let qrs = QuickReplySet.get(setName);
|
||||
if (qrs) {
|
||||
qr = qrs.qrList.find(it=>it.label == name);
|
||||
}
|
||||
}
|
||||
if (qr && qr.onExecute) {
|
||||
return await qr.onExecute();
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.isEnabled) {
|
||||
const qrList = [
|
||||
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup)).flat(),
|
||||
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnStartup))?.flat() ?? []),
|
||||
];
|
||||
for (const qr of qrList) {
|
||||
await qr.onExecute();
|
||||
}
|
||||
}
|
||||
};
|
||||
eventSource.on(event_types.APP_READY, init);
|
||||
|
||||
const onChatChanged = async (chatIdx) => {
|
||||
log('CHAT_CHANGED', chatIdx);
|
||||
if (chatIdx) {
|
||||
settings.chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
|
||||
} else {
|
||||
settings.chatConfig = null;
|
||||
}
|
||||
manager.rerender();
|
||||
buttons.refresh();
|
||||
|
||||
if (settings.isEnabled) {
|
||||
const qrList = [
|
||||
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange)).flat(),
|
||||
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnChatChange))?.flat() ?? []),
|
||||
];
|
||||
for (const qr of qrList) {
|
||||
await qr.onExecute();
|
||||
}
|
||||
}
|
||||
};
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
|
||||
const onUserMessage = async () => {
|
||||
if (settings.isEnabled) {
|
||||
const qrList = [
|
||||
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser)).flat(),
|
||||
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnUser))?.flat() ?? []),
|
||||
];
|
||||
for (const qr of qrList) {
|
||||
await qr.onExecute();
|
||||
}
|
||||
}
|
||||
};
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, onUserMessage);
|
||||
|
||||
const onAiMessage = async () => {
|
||||
if (settings.isEnabled) {
|
||||
const qrList = [
|
||||
...settings.config.setList.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi)).flat(),
|
||||
...(settings.chatConfig?.setList?.map(link=>link.set.qrList.filter(qr=>qr.executeOnAi))?.flat() ?? []),
|
||||
];
|
||||
for (const qr of qrList) {
|
||||
await qr.onExecute();
|
||||
}
|
||||
}
|
||||
};
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onAiMessage);
|
338
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
338
public/scripts/extensions/quick-reply/src/QuickReply.js
Normal file
@ -0,0 +1,338 @@
|
||||
import { callPopup } from '../../../../script.js';
|
||||
import { getSortableDelay } from '../../../utils.js';
|
||||
import { log, warn } from '../index.js';
|
||||
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReply {
|
||||
/**
|
||||
* @param {{ id?: number; contextList?: any; }} props
|
||||
*/
|
||||
static from(props) {
|
||||
props.contextList = (props.contextList ?? []).map((/** @type {any} */ it)=>QuickReplyContextLink.from(it));
|
||||
return Object.assign(new this(), props);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {Number}*/ id;
|
||||
/**@type {String}*/ label = '';
|
||||
/**@type {String}*/ title = '';
|
||||
/**@type {String}*/ message = '';
|
||||
|
||||
/**@type {QuickReplyContextLink[]}*/ contextList;
|
||||
|
||||
/**@type {Boolean}*/ isHidden = false;
|
||||
/**@type {Boolean}*/ executeOnStartup = false;
|
||||
/**@type {Boolean}*/ executeOnUser = false;
|
||||
/**@type {Boolean}*/ executeOnAi = false;
|
||||
/**@type {Boolean}*/ executeOnChatChange = false;
|
||||
|
||||
/**@type {Function}*/ onExecute;
|
||||
/**@type {Function}*/ onDelete;
|
||||
/**@type {Function}*/ onUpdate;
|
||||
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ settingsDom;
|
||||
|
||||
|
||||
|
||||
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
updateRender() {
|
||||
if (!this.dom) return;
|
||||
this.dom.title = this.title || this.message;
|
||||
this.dom.textContent = this.label;
|
||||
}
|
||||
render() {
|
||||
this.unrender();
|
||||
if (!this.dom) {
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
root.classList.add('qr--button');
|
||||
root.title = this.title || this.message;
|
||||
root.textContent = this.label;
|
||||
root.addEventListener('click', ()=>{
|
||||
if (this.message?.length > 0 && this.onExecute) {
|
||||
this.onExecute(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {any} idx
|
||||
*/
|
||||
renderSettings(idx) {
|
||||
if (!this.settingsDom) {
|
||||
const item = document.createElement('div'); {
|
||||
this.settingsDom = item;
|
||||
item.classList.add('qr--set-item');
|
||||
item.setAttribute('data-order', String(idx));
|
||||
item.setAttribute('data-id', String(this.id));
|
||||
const drag = document.createElement('div'); {
|
||||
drag.classList.add('drag-handle');
|
||||
drag.classList.add('ui-sortable-handle');
|
||||
drag.textContent = '☰';
|
||||
item.append(drag);
|
||||
}
|
||||
const lblContainer = document.createElement('div'); {
|
||||
lblContainer.classList.add('qr--set-itemLabelContainer');
|
||||
const lbl = document.createElement('input'); {
|
||||
lbl.classList.add('qr--set-itemLabel');
|
||||
lbl.classList.add('text_pole');
|
||||
lbl.value = this.label;
|
||||
lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
|
||||
lblContainer.append(lbl);
|
||||
}
|
||||
item.append(lblContainer);
|
||||
}
|
||||
const optContainer = document.createElement('div'); {
|
||||
optContainer.classList.add('qr--set-optionsContainer');
|
||||
const opt = document.createElement('div'); {
|
||||
opt.classList.add('qr--action');
|
||||
opt.classList.add('menu_button');
|
||||
opt.classList.add('fa-solid');
|
||||
opt.textContent = '⁝';
|
||||
opt.title = 'Additional options:\n - context menu\n - auto-execution\n - tooltip';
|
||||
opt.addEventListener('click', ()=>this.showOptions());
|
||||
optContainer.append(opt);
|
||||
}
|
||||
item.append(optContainer);
|
||||
}
|
||||
const expandContainer = document.createElement('div'); {
|
||||
expandContainer.classList.add('qr--set-optionsContainer');
|
||||
const expand = document.createElement('div'); {
|
||||
expand.classList.add('qr--expand');
|
||||
expand.classList.add('menu_button');
|
||||
expand.classList.add('menu_button_icon');
|
||||
expand.classList.add('editor_maximize');
|
||||
expand.classList.add('fa-solid');
|
||||
expand.classList.add('fa-maximize');
|
||||
expand.title = 'Expand the editor';
|
||||
expand.setAttribute('data-for', `qr--set--item${this.id}`);
|
||||
expand.setAttribute('data-tab', 'true');
|
||||
expandContainer.append(expand);
|
||||
}
|
||||
item.append(expandContainer);
|
||||
}
|
||||
const mes = document.createElement('textarea'); {
|
||||
mes.id = `qr--set--item${this.id}`;
|
||||
mes.classList.add('qr--set-itemMessage');
|
||||
mes.value = this.message;
|
||||
//HACK need to use jQuery to catch the triggered event from the expanded editor
|
||||
$(mes).on('input', ()=>this.updateMessage(mes.value));
|
||||
item.append(mes);
|
||||
}
|
||||
const actions = document.createElement('div'); {
|
||||
actions.classList.add('qr--actions');
|
||||
const del = document.createElement('div'); {
|
||||
del.classList.add('qr--action');
|
||||
del.classList.add('menu_button');
|
||||
del.classList.add('menu_button_icon');
|
||||
del.classList.add('fa-solid');
|
||||
del.classList.add('fa-trash-can');
|
||||
del.title = 'Remove quick reply';
|
||||
del.addEventListener('click', ()=>this.delete());
|
||||
actions.append(del);
|
||||
}
|
||||
item.append(actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.settingsDom;
|
||||
}
|
||||
unrenderSettings() {
|
||||
this.settingsDom?.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
delete() {
|
||||
if (this.onDelete) {
|
||||
this.unrender();
|
||||
this.unrenderSettings();
|
||||
this.onDelete(this);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
updateMessage(value) {
|
||||
if (this.onUpdate) {
|
||||
this.message = value;
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {string} value
|
||||
*/
|
||||
updateLabel(value) {
|
||||
if (this.onUpdate) {
|
||||
this.label = value;
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
updateContext() {
|
||||
if (this.onUpdate) {
|
||||
this.updateRender();
|
||||
this.onUpdate(this);
|
||||
}
|
||||
}
|
||||
|
||||
async showOptions() {
|
||||
const response = await fetch('/scripts/extensions/quick-reply/html/qrOptions.html', { cache: 'no-store' });
|
||||
if (response.ok) {
|
||||
this.template = document.createRange().createContextualFragment(await response.text()).querySelector('#qr--qrOptions');
|
||||
/**@type {HTMLElement} */
|
||||
// @ts-ignore
|
||||
const dom = this.template.cloneNode(true);
|
||||
const popupResult = callPopup(dom, 'text', undefined, { okButton: 'OK', wide: false, large: false, rows: 1 });
|
||||
/**@type {HTMLTemplateElement}*/
|
||||
const tpl = dom.querySelector('#qr--ctxItem');
|
||||
const linkList = dom.querySelector('#qr--ctxEditor');
|
||||
const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
|
||||
[{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => {
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
opt.selected = qrs.name == link.set?.name;
|
||||
select.append(opt);
|
||||
}
|
||||
});
|
||||
};
|
||||
const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => {
|
||||
/**@type {HTMLElement} */
|
||||
// @ts-ignore
|
||||
const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
|
||||
itemDom.setAttribute('data-order', String(idx));
|
||||
|
||||
/**@type {HTMLSelectElement} */
|
||||
const select = itemDom.querySelector('.qr--set');
|
||||
fillQrSetSelect(select, link);
|
||||
select.addEventListener('change', () => {
|
||||
link.set = QuickReplySet.get(select.value);
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
/**@type {HTMLInputElement} */
|
||||
const chain = itemDom.querySelector('.qr--isChained');
|
||||
chain.checked = link.isChained;
|
||||
chain.addEventListener('click', () => {
|
||||
link.isChained = chain.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
itemDom.querySelector('.qr--delete').addEventListener('click', () => {
|
||||
itemDom.remove();
|
||||
this.contextList.splice(this.contextList.indexOf(link), 1);
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
linkList.append(itemDom);
|
||||
}
|
||||
};
|
||||
[...this.contextList].forEach((link, idx) => addCtxItem(link, idx));
|
||||
dom.querySelector('#qr--ctxAdd').addEventListener('click', () => {
|
||||
const link = new QuickReplyContextLink();
|
||||
this.contextList.push(link);
|
||||
addCtxItem(link, this.contextList.length - 1);
|
||||
});
|
||||
const onContextSort = () => {
|
||||
this.contextList = Array.from(linkList.querySelectorAll('.qr--ctxItem')).map((it,idx) => {
|
||||
const link = this.contextList[Number(it.getAttribute('data-order'))];
|
||||
it.setAttribute('data-order', String(idx));
|
||||
return link;
|
||||
});
|
||||
this.updateContext();
|
||||
};
|
||||
// @ts-ignore
|
||||
$(linkList).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: () => onContextSort(),
|
||||
});
|
||||
|
||||
// auto-exec
|
||||
/**@type {HTMLInputElement}*/
|
||||
const isHidden = dom.querySelector('#qr--isHidden');
|
||||
isHidden.checked = this.isHidden;
|
||||
isHidden.addEventListener('click', ()=>{
|
||||
this.isHidden = isHidden.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnStartup = dom.querySelector('#qr--executeOnStartup');
|
||||
executeOnStartup.checked = this.executeOnStartup;
|
||||
executeOnStartup.addEventListener('click', ()=>{
|
||||
this.executeOnStartup = executeOnStartup.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnUser = dom.querySelector('#qr--executeOnUser');
|
||||
executeOnUser.checked = this.executeOnUser;
|
||||
executeOnUser.addEventListener('click', ()=>{
|
||||
this.executeOnUser = executeOnUser.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnAi = dom.querySelector('#qr--executeOnAi');
|
||||
executeOnAi.checked = this.executeOnAi;
|
||||
executeOnAi.addEventListener('click', ()=>{
|
||||
this.executeOnAi = executeOnAi.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
/**@type {HTMLInputElement}*/
|
||||
const executeOnChatChange = dom.querySelector('#qr--executeOnChatChange');
|
||||
executeOnChatChange.checked = this.executeOnChatChange;
|
||||
executeOnChatChange.addEventListener('click', ()=>{
|
||||
this.executeOnChatChange = executeOnChatChange.checked;
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
// UI options
|
||||
/**@type {HTMLInputElement}*/
|
||||
const title = dom.querySelector('#qr--title');
|
||||
title.value = this.title;
|
||||
title.addEventListener('input', () => {
|
||||
this.title = title.value.trim();
|
||||
this.updateContext();
|
||||
});
|
||||
|
||||
await popupResult;
|
||||
} else {
|
||||
warn('failed to fetch qrOptions template');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
label: this.label,
|
||||
title: this.title,
|
||||
message: this.message,
|
||||
contextList: this.contextList,
|
||||
isHidden: this.isHidden,
|
||||
executeOnStartup: this.executeOnStartup,
|
||||
executeOnUser: this.executeOnUser,
|
||||
executeOnAi: this.executeOnAi,
|
||||
executeOnChatChange: this.executeOnChatChange,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
import { QuickReplyLink } from './QuickReplyLink.js';
|
||||
|
||||
export class QuickReplyConfig {
|
||||
/**@type {QuickReplyLink[]}*/ setList = [];
|
||||
|
||||
|
||||
|
||||
|
||||
static from(props) {
|
||||
props.setList = props.setList?.map(it=>QuickReplyLink.from(it)) ?? [];
|
||||
return Object.assign(new this(), props);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
setList: this.setList,
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplyContextLink {
|
||||
static from(props) {
|
||||
props.set = QuickReplySet.get(props.set);
|
||||
const x = Object.assign(new this(), props);
|
||||
return x;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {QuickReplySet}*/ set;
|
||||
/**@type {Boolean}*/ isChained = false;
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
set: this.set?.name,
|
||||
isChained: this.isChained,
|
||||
};
|
||||
}
|
||||
}
|
24
public/scripts/extensions/quick-reply/src/QuickReplyLink.js
Normal file
24
public/scripts/extensions/quick-reply/src/QuickReplyLink.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { QuickReplySet } from './QuickReplySet.js';
|
||||
|
||||
export class QuickReplyLink {
|
||||
/**@type {QuickReplySet}*/ set;
|
||||
/**@type {Boolean}*/ isVisible = true;
|
||||
|
||||
|
||||
|
||||
|
||||
static from(props) {
|
||||
props.set = QuickReplySet.get(props.set);
|
||||
return Object.assign(new this(), props);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
set: this.set.name,
|
||||
isVisible: this.isVisible,
|
||||
};
|
||||
}
|
||||
}
|
192
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
192
public/scripts/extensions/quick-reply/src/QuickReplySet.js
Normal file
@ -0,0 +1,192 @@
|
||||
import { getRequestHeaders } from '../../../../script.js';
|
||||
import { executeSlashCommands } from '../../../slash-commands.js';
|
||||
import { debounce } from '../../../utils.js';
|
||||
import { warn } from '../index.js';
|
||||
import { QuickReply } from './QuickReply.js';
|
||||
|
||||
export class QuickReplySet {
|
||||
/**@type {QuickReplySet[]}*/ static list = [];
|
||||
|
||||
|
||||
static from(props) {
|
||||
props.qrList = props.qrList?.map(it=>QuickReply.from(it));
|
||||
const instance = Object.assign(new this(), props);
|
||||
instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} name - name of the QuickReplySet
|
||||
*/
|
||||
static get(name) {
|
||||
return this.list.find(it=>it.name == name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {String}*/ name;
|
||||
|
||||
/**@type {Boolean}*/ disableSend = false;
|
||||
/**@type {Boolean}*/ placeBeforeInput = false;
|
||||
/**@type {Boolean}*/ injectInput = false;
|
||||
|
||||
/**@type {QuickReply[]}*/ qrList = [];
|
||||
|
||||
/**@type {Number}*/ idIndex = 0;
|
||||
|
||||
/**@type {Function}*/ save;
|
||||
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
/**@type {HTMLElement}*/ settingsDom;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor() {
|
||||
this.save = debounce(()=>this.performSave(), 200);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.qrList.forEach(qr=>this.hookQuickReply(qr));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
render() {
|
||||
this.unrender();
|
||||
if (!this.dom) {
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
root.classList.add('qr--buttons');
|
||||
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
|
||||
root.append(qr.render());
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
rerender() {
|
||||
if (!this.dom) return;
|
||||
this.dom.innerHTML = '';
|
||||
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
|
||||
this.dom.append(qr.render());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
renderSettings() {
|
||||
if (!this.settingsDom) {
|
||||
this.settingsDom = document.createElement('div'); {
|
||||
this.settingsDom.classList.add('qr--set-qrListContents');
|
||||
this.qrList.forEach((qr,idx)=>{
|
||||
this.renderSettingsItem(qr, idx);
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.settingsDom;
|
||||
}
|
||||
renderSettingsItem(qr, idx) {
|
||||
this.settingsDom.append(qr.renderSettings(idx));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {QuickReply} qr
|
||||
*/
|
||||
async execute(qr) {
|
||||
/**@type {HTMLTextAreaElement}*/
|
||||
const ta = document.querySelector('#send_textarea');
|
||||
let input = ta.value;
|
||||
if (this.injectInput && input.length > 0) {
|
||||
if (this.placeBeforeInput) {
|
||||
input = `${qr.message} ${input}`;
|
||||
} else {
|
||||
input = `${input} ${qr.message}`;
|
||||
}
|
||||
} else {
|
||||
input = `${qr.message} `;
|
||||
}
|
||||
|
||||
if (input[0] == '/' && !this.disableSend) {
|
||||
const result = await executeSlashCommands(input);
|
||||
return typeof result === 'object' ? result?.pipe : '';
|
||||
}
|
||||
|
||||
ta.value = input;
|
||||
ta.focus();
|
||||
|
||||
if (!this.disableSend) {
|
||||
// @ts-ignore
|
||||
document.querySelector('#send_but').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
addQuickReply() {
|
||||
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
|
||||
this.idIndex = id + 1;
|
||||
const qr = QuickReply.from({ id });
|
||||
this.qrList.push(qr);
|
||||
this.hookQuickReply(qr);
|
||||
if (this.settingsDom) {
|
||||
this.renderSettingsItem(qr, this.qrList.length - 1);
|
||||
}
|
||||
if (this.dom) {
|
||||
this.dom.append(qr.render());
|
||||
}
|
||||
this.save();
|
||||
return qr;
|
||||
}
|
||||
|
||||
hookQuickReply(qr) {
|
||||
qr.onExecute = ()=>this.execute(qr);
|
||||
qr.onDelete = ()=>this.removeQuickReply(qr);
|
||||
qr.onUpdate = ()=>this.save();
|
||||
}
|
||||
|
||||
removeQuickReply(qr) {
|
||||
this.qrList.splice(this.qrList.indexOf(qr), 1);
|
||||
this.save();
|
||||
}
|
||||
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
version: 2,
|
||||
name: this.name,
|
||||
disableSend: this.disableSend,
|
||||
placeBeforeInput: this.placeBeforeInput,
|
||||
injectInput: this.injectInput,
|
||||
qrList: this.qrList,
|
||||
idIndex: this.idIndex,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async performSave() {
|
||||
const response = await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(this),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.rerender();
|
||||
} else {
|
||||
warn(`Failed to save Quick Reply Set: ${this.name}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { chat_metadata, saveChatDebounced, saveSettingsDebounced } from '../../../../script.js';
|
||||
import { extension_settings } from '../../../extensions.js';
|
||||
import { QuickReplyConfig } from './QuickReplyConfig.js';
|
||||
|
||||
export class QuickReplySettings {
|
||||
static from(props) {
|
||||
props.config = QuickReplyConfig.from(props.config);
|
||||
return Object.assign(new this(), props);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**@type {Boolean}*/ isEnabled = false;
|
||||
/**@type {Boolean}*/ isCombined = false;
|
||||
/**@type {QuickReplyConfig}*/ config;
|
||||
/**@type {QuickReplyConfig}*/ chatConfig;
|
||||
|
||||
/**@type {Function}*/ onSave;
|
||||
|
||||
|
||||
|
||||
|
||||
save() {
|
||||
extension_settings.quickReplyV2 = this.toJSON();
|
||||
saveSettingsDebounced();
|
||||
if (this.chatConfig) {
|
||||
chat_metadata.quickReply = this.chatConfig.toJSON();
|
||||
saveChatDebounced();
|
||||
}
|
||||
if (this.onSave) {
|
||||
this.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
isEnabled: this.isEnabled,
|
||||
config: this.config,
|
||||
};
|
||||
}
|
||||
}
|
63
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
63
public/scripts/extensions/quick-reply/src/ui/ButtonUi.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { QuickReplySettings } from '../QuickReplySettings.js';
|
||||
|
||||
export class ButtonUi {
|
||||
/**@type {QuickReplySettings}*/ settings;
|
||||
|
||||
/**@type {HTMLElement}*/ dom;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
if (!this.dom) {
|
||||
let buttonHolder;
|
||||
const root = document.createElement('div'); {
|
||||
this.dom = root;
|
||||
buttonHolder = root;
|
||||
root.id = 'qr--bar';
|
||||
root.classList.add('flex-container');
|
||||
root.classList.add('flexGap5');
|
||||
if (this.settings.isCombined) {
|
||||
const buttons = document.createElement('div'); {
|
||||
buttonHolder = buttons;
|
||||
buttons.classList.add('qr--buttons');
|
||||
root.append(buttons);
|
||||
}
|
||||
}
|
||||
[...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
|
||||
.filter(link=>link.isVisible)
|
||||
.forEach(link=>buttonHolder.append(link.set.render()))
|
||||
;
|
||||
}
|
||||
}
|
||||
return this.dom;
|
||||
}
|
||||
unrender() {
|
||||
this.dom?.remove();
|
||||
this.dom = null;
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.settings.isEnabled) return;
|
||||
const sendForm = document.querySelector('#send_form');
|
||||
if (sendForm.children.length > 0) {
|
||||
sendForm.children[0].insertAdjacentElement('beforebegin', this.render());
|
||||
} else {
|
||||
sendForm.append(this.render());
|
||||
}
|
||||
}
|
||||
hide() {
|
||||
this.unrender();
|
||||
}
|
||||
refresh() {
|
||||
this.hide();
|
||||
this.show();
|
||||
}
|
||||
}
|
310
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
310
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js
Normal file
@ -0,0 +1,310 @@
|
||||
import { getSortableDelay } from '../../../../utils.js';
|
||||
import { warn } from '../../index.js';
|
||||
import { QuickReplyLink } from '../QuickReplyLink.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 {HTMLElement}*/ globalSetList;
|
||||
|
||||
/**@type {HTMLElement}*/ chatSetList;
|
||||
|
||||
/**@type {QuickReplySet}*/ currentQrSet;
|
||||
/**@type {HTMLInputElement}*/ disableSend;
|
||||
/**@type {HTMLInputElement}*/ placeBeforeInput;
|
||||
/**@type {HTMLInputElement}*/ injectInput;
|
||||
/**@type {HTMLSelectElement}*/ currentSet;
|
||||
|
||||
|
||||
|
||||
|
||||
constructor(/**@type {QuickReplySettings}*/settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QuickReplyLink} qrl
|
||||
* @param {Number} idx
|
||||
*/
|
||||
renderQrLinkItem(qrl, idx, isGlobal) {
|
||||
const item = document.createElement('div'); {
|
||||
item.classList.add('qr--item');
|
||||
item.setAttribute('data-order', String(idx));
|
||||
const drag = document.createElement('div'); {
|
||||
drag.classList.add('drag-handle');
|
||||
drag.classList.add('ui-sortable-handle');
|
||||
drag.textContent = '☰';
|
||||
item.append(drag);
|
||||
}
|
||||
const set = document.createElement('select'); {
|
||||
set.addEventListener('change', ()=>{
|
||||
qrl.set = QuickReplySet.get(set.value);
|
||||
this.settings.save();
|
||||
});
|
||||
QuickReplySet.list.forEach(qrs=>{
|
||||
const opt = document.createElement('option'); {
|
||||
opt.value = qrs.name;
|
||||
opt.textContent = qrs.name;
|
||||
opt.selected = qrs == qrl.set;
|
||||
set.append(opt);
|
||||
}
|
||||
});
|
||||
item.append(set);
|
||||
}
|
||||
const visible = document.createElement('label'); {
|
||||
visible.classList.add('qr--visible');
|
||||
const cb = document.createElement('input'); {
|
||||
cb.type = 'checkbox';
|
||||
cb.checked = qrl.isVisible;
|
||||
cb.addEventListener('click', ()=>{
|
||||
qrl.isVisible = cb.checked;
|
||||
this.settings.save();
|
||||
});
|
||||
visible.append(cb);
|
||||
}
|
||||
visible.append('Show buttons');
|
||||
item.append(visible);
|
||||
}
|
||||
const edit = document.createElement('div'); {
|
||||
edit.classList.add('menu_button');
|
||||
edit.classList.add('menu_button_icon');
|
||||
edit.classList.add('fa-solid');
|
||||
edit.classList.add('fa-pencil');
|
||||
edit.title = 'Edit quick reply set';
|
||||
edit.addEventListener('click', ()=>{
|
||||
this.currentSet.value = qrl.set.name;
|
||||
this.onQrSetChange();
|
||||
});
|
||||
item.append(edit);
|
||||
}
|
||||
const del = document.createElement('div'); {
|
||||
del.classList.add('menu_button');
|
||||
del.classList.add('menu_button_icon');
|
||||
del.classList.add('fa-solid');
|
||||
del.classList.add('fa-trash-can');
|
||||
del.title = 'Remove quick reply set';
|
||||
del.addEventListener('click', ()=>{
|
||||
item.remove();
|
||||
if (isGlobal) {
|
||||
this.settings.config.setList.splice(this.settings.config.setList.indexOf(qrl), 1);
|
||||
this.updateOrder(this.globalSetList);
|
||||
} else {
|
||||
this.settings.chatConfig.setList.splice(this.settings.chatConfig.setList.indexOf(qrl), 1);
|
||||
this.updateOrder(this.chatSetList);
|
||||
}
|
||||
this.settings.save();
|
||||
});
|
||||
item.append(del);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
prepareGlobalSetList() {
|
||||
// global set list
|
||||
this.dom.querySelector('#qr--global-setListAdd').addEventListener('click', ()=>{
|
||||
const qrl = new QuickReplyLink();
|
||||
qrl.set = QuickReplySet.list[0];
|
||||
this.settings.config.setList.push(qrl);
|
||||
this.globalSetList.append(this.renderQrLinkItem(qrl, this.settings.config.setList.length - 1, true));
|
||||
this.settings.save();
|
||||
});
|
||||
this.globalSetList = this.dom.querySelector('#qr--global-setList');
|
||||
// @ts-ignore
|
||||
$(this.globalSetList).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: ()=>this.onGlobalSetListSort(),
|
||||
});
|
||||
this.settings.config.setList.forEach((qrl,idx)=>{
|
||||
this.globalSetList.append(this.renderQrLinkItem(qrl, idx, true));
|
||||
});
|
||||
}
|
||||
prepareChatSetList() {
|
||||
// chat set list
|
||||
this.dom.querySelector('#qr--chat-setListAdd').addEventListener('click', ()=>{
|
||||
if (!this.settings.chatConfig) {
|
||||
toastr.warning('No active chat.');
|
||||
return;
|
||||
}
|
||||
const qrl = new QuickReplyLink();
|
||||
qrl.set = QuickReplySet.list[0];
|
||||
this.settings.chatConfig.setList.push(qrl);
|
||||
this.chatSetList.append(this.renderQrLinkItem(qrl, this.settings.chatConfig.setList.length - 1, false));
|
||||
this.settings.save();
|
||||
});
|
||||
|
||||
this.chatSetList = this.dom.querySelector('#qr--chat-setList');
|
||||
if (!this.settings.chatConfig) {
|
||||
const info = document.createElement('small'); {
|
||||
info.textContent = 'No active chat.';
|
||||
this.chatSetList.append(info);
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
$(this.chatSetList).sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: ()=>this.onChatSetListSort(),
|
||||
});
|
||||
this.settings.chatConfig?.setList?.forEach((qrl,idx)=>{
|
||||
this.chatSetList.append(this.renderQrLinkItem(qrl, idx, false));
|
||||
});
|
||||
}
|
||||
|
||||
prepareQrEditor() {
|
||||
// qr editor
|
||||
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
|
||||
this.currentQrSet.addQuickReply();
|
||||
});
|
||||
this.qrList = this.dom.querySelector('#qr--set-qrList');
|
||||
this.currentSet = this.dom.querySelector('#qr--set');
|
||||
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
|
||||
QuickReplySet.list.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();
|
||||
});
|
||||
this.onQrSetChange();
|
||||
}
|
||||
onQrSetChange() {
|
||||
this.currentQrSet = QuickReplySet.get(this.currentSet.value);
|
||||
this.disableSend.checked = this.currentQrSet.disableSend;
|
||||
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
|
||||
this.injectInput.checked = this.currentQrSet.injectInput;
|
||||
this.qrList.innerHTML = '';
|
||||
const qrsDom = this.currentQrSet.renderSettings();
|
||||
this.qrList.append(qrsDom);
|
||||
// @ts-ignore
|
||||
$(qrsDom).sortable({
|
||||
delay: getSortableDelay(),
|
||||
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 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();
|
||||
}
|
||||
}
|
114
public/scripts/extensions/quick-reply/style.css
Normal file
114
public/scripts/extensions/quick-reply/style.css
Normal file
@ -0,0 +1,114 @@
|
||||
#qr--bar {
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
order: 1;
|
||||
position: relative;
|
||||
}
|
||||
#qr--bar > .qr--buttons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
#qr--bar > .qr--buttons > .qr--buttons {
|
||||
display: contents;
|
||||
}
|
||||
#qr--bar > .qr--buttons .qr--button {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
margin: 3px 0;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
#qr--bar > .qr--buttons .qr--button:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
#qr--settings .qr--head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1em;
|
||||
}
|
||||
#qr--settings .qr--head > .qr--title {
|
||||
font-weight: bold;
|
||||
}
|
||||
#qr--settings .qr--head > .qr--actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
}
|
||||
#qr--settings .qr--setList > .qr--item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#qr--settings .qr--setList > .qr--item > .qr--visible {
|
||||
flex: 1 1 200px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
#qr--settings #qr--set-settings #qr--injectInputContainer {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {
|
||||
flex: 1 1 25%;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {
|
||||
flex: 1 1 75%;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(6) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
|
||||
margin: 0;
|
||||
}
|
||||
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {
|
||||
font-size: smaller;
|
||||
}
|
||||
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
124
public/scripts/extensions/quick-reply/style.less
Normal file
124
public/scripts/extensions/quick-reply/style.less
Normal file
@ -0,0 +1,124 @@
|
||||
#qr--bar {
|
||||
outline: none;
|
||||
margin: 0;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
order: 1;
|
||||
position: relative;
|
||||
|
||||
> .qr--buttons {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
|
||||
> .qr--buttons {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.qr--button {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
border: 1px solid var(--SmartThemeBorderColor);
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
margin: 3px 0;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#qr--settings {
|
||||
.qr--head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1em;
|
||||
> .qr--title {
|
||||
font-weight: bold;
|
||||
}
|
||||
> .qr--actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
||||
.qr--setList {
|
||||
> .qr--item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0 0.5em;
|
||||
> .qr--visible {
|
||||
flex: 1 1 200px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
#qr--set-settings {
|
||||
#qr--injectInputContainer {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
#qr--set-qrList {
|
||||
.qr--set-qrListContents > {
|
||||
padding: 0 0.5em;
|
||||
> .qr--set-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
padding: 0.25em 0;
|
||||
> :nth-child(1) { flex: 0 0 auto; }
|
||||
> :nth-child(2) { flex: 1 1 25%; }
|
||||
> :nth-child(3) { flex: 0 0 auto; }
|
||||
> :nth-child(4) { flex: 0 0 auto; }
|
||||
> :nth-child(5) { flex: 1 1 75%; }
|
||||
> :nth-child(6) { flex: 0 0 auto; }
|
||||
.qr--set-itemLabel, .qr--action {
|
||||
margin: 0;
|
||||
}
|
||||
.qr--set-itemMessage {
|
||||
font-size: smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#qr--qrOptions {
|
||||
> #qr--ctxEditor {
|
||||
.qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user