diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 8ed195520..034a5194c 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -94,14 +94,27 @@

Testing

- diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index d7a8bc251..c70aa374e 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -1,5 +1,6 @@ import { POPUP_TYPE, Popup } from '../../../popup.js'; import { setSlashCommandAutoComplete } from '../../../slash-commands.js'; +import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js'; import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js'; import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js'; import { getSortableDelay } from '../../../utils.js'; @@ -50,9 +51,14 @@ export class QuickReply { /**@type {Popup}*/ editorPopup; /**@type {HTMLElement}*/ editorExecuteBtn; + /**@type {HTMLElement}*/ editorExecuteBtnPause; + /**@type {HTMLElement}*/ editorExecuteBtnStop; + /**@type {HTMLElement}*/ editorExecuteProgress; /**@type {HTMLElement}*/ editorExecuteErrors; + /**@type {HTMLElement}*/ editorExecuteResult; /**@type {HTMLInputElement}*/ editorExecuteHide; /**@type {Promise}*/ editorExecutePromise; + /**@type {SlashCommandAbortController}*/ abortController; get hasContext() { @@ -426,9 +432,15 @@ export class QuickReply { this.updateContext(); }); + /**@type {HTMLElement}*/ + const executeProgress = dom.querySelector('#qr--modal-executeProgress'); + this.editorExecuteProgress = executeProgress; /**@type {HTMLElement}*/ const executeErrors = dom.querySelector('#qr--modal-executeErrors'); this.editorExecuteErrors = executeErrors; + /**@type {HTMLElement}*/ + const executeResult = dom.querySelector('#qr--modal-executeResult'); + this.editorExecuteResult = executeResult; /**@type {HTMLInputElement}*/ const executeHide = dom.querySelector('#qr--modal-executeHide'); this.editorExecuteHide = executeHide; @@ -438,6 +450,26 @@ export class QuickReply { executeBtn.addEventListener('click', async()=>{ await this.executeFromEditor(); }); + /**@type {HTMLElement}*/ + const executeBtnPause = dom.querySelector('#qr--modal-pause'); + this.editorExecuteBtnPause = executeBtnPause; + executeBtnPause.addEventListener('click', async()=>{ + if (this.abortController) { + if (this.abortController.signal.paused) { + this.abortController.continue('Continue button clicked'); + this.editorExecuteProgress.classList.remove('qr--paused'); + } else { + this.abortController.pause('Pause button clicked'); + this.editorExecuteProgress.classList.add('qr--paused'); + } + } + }); + /**@type {HTMLElement}*/ + const executeBtnStop = dom.querySelector('#qr--modal-stop'); + this.editorExecuteBtnStop = executeBtnStop; + executeBtnStop.addEventListener('click', async()=>{ + this.abortController?.abort('Stop button clicked'); + }); await popupResult; } else { @@ -448,17 +480,33 @@ export class QuickReply { async executeFromEditor() { if (this.editorExecutePromise) return; this.editorExecuteBtn.classList.add('qr--busy'); + this.editorExecuteProgress.style.setProperty('--prog', '0'); + this.editorExecuteErrors.classList.remove('qr--hasErrors'); + this.editorExecuteResult.classList.remove('qr--hasResult'); + this.editorExecuteProgress.classList.remove('qr--error'); + this.editorExecuteProgress.classList.remove('qr--success'); + this.editorExecuteProgress.classList.remove('qr--paused'); + this.editorExecuteProgress.classList.remove('qr--aborted'); this.editorExecuteErrors.innerHTML = ''; + this.editorExecuteResult.innerHTML = ''; if (this.editorExecuteHide.checked) { this.editorPopup.dom.classList.add('qr--hide'); } try { this.editorExecutePromise = this.execute({}, true); - await this.editorExecutePromise; - this.editorExecuteErrors.classList.remove('qr--hasErrors'); - this.editorExecuteErrors.innerHTML = ''; + const result = await this.editorExecutePromise; + if (this.abortController?.signal?.aborted) { + this.editorExecuteProgress.classList.add('qr--aborted'); + } else { + this.editorExecuteResult.textContent = result?.toString(); + this.editorExecuteResult.classList.add('qr--hasResult'); + this.editorExecuteProgress.classList.add('qr--success'); + } + this.editorExecuteProgress.classList.remove('qr--paused'); } catch (ex) { this.editorExecuteErrors.classList.add('qr--hasErrors'); + this.editorExecuteProgress.classList.add('qr--error'); + this.editorExecuteProgress.classList.remove('qr--paused'); if (ex instanceof SlashCommandParserError) { this.editorExecuteErrors.innerHTML = `
${ex.message}
@@ -476,6 +524,10 @@ export class QuickReply { this.editorPopup.dom.classList.remove('qr--hide'); } + updateEditorProgress(done, total) { + this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`); + } + @@ -557,6 +609,9 @@ export class QuickReply { for (const key of Object.keys(args)) { scope.setMacro(`arg::${key}`, args[key]); } + if (isEditor) { + this.abortController = new SlashCommandAbortController(); + } return await this.onExecute(this, { message:this.message, isAutoExecute: args.isAutoExecute ?? false, diff --git a/public/scripts/extensions/quick-reply/src/QuickReplySet.js b/public/scripts/extensions/quick-reply/src/QuickReplySet.js index 8b147fa60..106084081 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplySet.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplySet.js @@ -144,6 +144,8 @@ export class QuickReplySet { result = await executeSlashCommandsWithOptions(input, { handleParserErrors: false, scope: options.scope, + abortController: qr.abortController, + onProgress: (done, total) => qr.updateEditorProgress(done, total), }); } else { result = await executeSlashCommandsOnChatInput(input, { diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index dbb0e6d18..e911d403a 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -286,14 +286,87 @@ .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message { flex: 1 1 auto; } -.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute { +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons { + display: flex; + gap: 1em; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton { + border-width: 2px; + border-style: solid; display: flex; flex-direction: row; gap: 0.5em; + padding: 0.5em 0.75em; } -.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy { - opacity: 0.5; +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon { + display: flex; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute { + transition: 200ms; + filter: grayscale(0); +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy { cursor: wait; + opacity: 0.5; + filter: grayscale(1); +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute { + border-color: #51a351; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause, +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { + cursor: default; + opacity: 0.5; + filter: grayscale(1); + pointer-events: none; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause, +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop { + cursor: pointer; + opacity: 1; + filter: grayscale(0); + pointer-events: all; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause { + border-color: #92befc; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop { + border-color: #d78872; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress { + --prog: 0; + --progColor: #92befc; + --progFlashColor: #d78872; + --progSuccessColor: #51a351; + --progErrorColor: #bd362f; + --progAbortedColor: #d78872; + height: 0.5em; + background-color: var(--black50a); + position: relative; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress:after { + content: ''; + background-color: var(--progColor); + position: absolute; + inset: 0; + right: calc(100% - var(--prog) * 1%); + transition: 200ms; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after { + animation-name: qr--progressPulse; + animation-duration: 1500ms; + animation-timing-function: ease-in-out; + animation-delay: 0s; + animation-iteration-count: infinite; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after { + background-color: var(--progAbortedColor); +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--success:after { + background-color: var(--progSuccessColor); +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--error:after { + background-color: var(--progErrorColor); } .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors { display: none; @@ -309,6 +382,32 @@ .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors { display: block; } +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult { + display: none; + text-align: left; + font-size: smaller; + background-color: #51a351; + color: white; + padding: 0.5em; + overflow: auto; + min-width: 100%; + width: 0; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult.qr--hasResult { + display: block; +} +.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult:before { + content: 'Result: '; +} +@keyframes qr--progressPulse { + 0%, + 100% { + background-color: var(--progColor); + } + 50% { + background-color: var(--progFlashColor); + } +} .shadow_popup.qr--hide { opacity: 0 !important; } diff --git a/public/scripts/extensions/quick-reply/style.less b/public/scripts/extensions/quick-reply/style.less index 0774e9e09..0c9da0af9 100644 --- a/public/scripts/extensions/quick-reply/style.less +++ b/public/scripts/extensions/quick-reply/style.less @@ -313,13 +313,86 @@ } } - #qr--modal-execute { + #qr--modal-executeButtons { display: flex; - flex-direction: row; - gap: 0.5em; - &.qr--busy { + gap: 1em; + .qr--modal-executeButton { + border-width: 2px; + border-style: solid; + display: flex; + flex-direction: row; + gap: 0.5em; + padding: 0.5em 0.75em; + .qr--modal-executeComboIcon { + display: flex; + } + } + #qr--modal-execute { + transition: 200ms; + filter: grayscale(0); + &.qr--busy { + cursor: wait; + opacity: 0.5; + filter: grayscale(1); + } + } + #qr--modal-execute { + border-color: rgb(81, 163, 81); + } + #qr--modal-pause, #qr--modal-stop { + cursor: default; opacity: 0.5; - cursor: wait; + filter: grayscale(1); + pointer-events: none; + } + .qr--busy { + ~ #qr--modal-pause, ~ #qr--modal-stop { + cursor: pointer; + opacity: 1; + filter: grayscale(0); + pointer-events: all; + } + } + #qr--modal-pause { + border-color: rgb(146, 190, 252); + } + #qr--modal-stop { + border-color: rgb(215, 136, 114); + } + } + #qr--modal-executeProgress { + --prog: 0; + --progColor: rgb(146, 190, 252); + --progFlashColor: rgb(215, 136, 114); + --progSuccessColor: rgb(81, 163, 81); + --progErrorColor: rgb(189, 54, 47); + --progAbortedColor: rgb(215, 136, 114); + height: 0.5em; + background-color: var(--black50a); + position: relative; + &:after { + content: ''; + background-color: var(--progColor); + position: absolute; + inset: 0; + right: calc(100% - var(--prog) * 1%); + transition: 200ms; + } + &.qr--paused:after { + animation-name: qr--progressPulse; + animation-duration: 1500ms; + animation-timing-function: ease-in-out; + animation-delay: 0s; + animation-iteration-count: infinite; + } + &.qr--aborted:after { + background-color: var(--progAbortedColor); + } + &.qr--success:after { + background-color: var(--progSuccessColor); + } + &.qr--error:after { + background-color: var(--progErrorColor); } } #qr--modal-executeErrors { @@ -336,9 +409,32 @@ min-width: 100%; width: 0; } + #qr--modal-executeResult { + display: none; + &.qr--hasResult { + display: block; + } + &:before { content: 'Result: '; } + text-align: left; + font-size: smaller; + background-color: rgb(81, 163, 81); + color: white; + padding: 0.5em; + overflow: auto; + min-width: 100%; + width: 0; + } } } } +@keyframes qr--progressPulse { + 0%, 100% { + background-color: var(--progColor); + } + 50% { + background-color: var(--progFlashColor); + } +} .shadow_popup.qr--hide { opacity: 0 !important;