Refactor: Replace SD image generation indicator with ActionLoader system (#5472)
* refactor: replace custom generation indicator with action-loader Remove custom generation tracking (activeGenerations counter, generationToast) and replace with action-loader's non-blocking stoppable toast. Import loader from action-loader.js and use loader.show() with onStop callback in generatePicture() and generateMediaSwipe(). Remove updateGenerationIndicator(), startGenerationTracking(), and endGenerationTracking() functions. Remove manual stop button show/hide logic and generation counter updates * Remove legacy stop button, add sentinel handler value * fix: reassign loaderHandle in generateMediaSwipe * feat: add data attributes to action-loader toast for easier selection Add loaderId, title, and blocking data attributes to toast content div to enable programmatic identification and filtering of active loader toasts * Revert "feat: add data attributes to action-loader toast for easier selection" This reverts commit e8da27b4c94b389d5970a6bea6c7b1c94459b460. --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
@@ -73,6 +73,15 @@ function hasBlockingLoaders() {
|
||||
* Manages its own toast, stop handler, and lifecycle.
|
||||
*/
|
||||
export class ActionLoaderHandle {
|
||||
/**
|
||||
* A special empty handle that is already disposed. Useful as a default value to avoid null checks.
|
||||
* Does not generate any id, toast, or overlay, and all its methods are no-ops.
|
||||
* @type {ActionLoaderHandle}
|
||||
*/
|
||||
static get EMPTY() {
|
||||
return new ActionLoaderHandle({ predisposed: true });
|
||||
}
|
||||
|
||||
/** @type {string} Unique identifier for this handle */
|
||||
id;
|
||||
|
||||
@@ -99,6 +108,7 @@ export class ActionLoaderHandle {
|
||||
* @param {string} [options.message='Generating...'] - Message to display in the toast
|
||||
* @param {string} [options.title] - Title for the toast notification
|
||||
* @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button
|
||||
* @param {boolean} [options.predisposed=false] - Whether this handle is already disposed (for special use)
|
||||
* @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner)
|
||||
* @param {(() => void)|null} [options.onStop] - Custom stop handler
|
||||
* @param {(() => void)|null} [options.onHide] - Custom hide handler
|
||||
@@ -112,7 +122,13 @@ export class ActionLoaderHandle {
|
||||
overlayContent = null,
|
||||
onStop = null,
|
||||
onHide = null,
|
||||
predisposed = false,
|
||||
} = {}) {
|
||||
if (predisposed) {
|
||||
this.#disposed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = generateLoaderId();
|
||||
this.#blocking = blocking;
|
||||
this.#onStop = onStop;
|
||||
|
||||
@@ -2,7 +2,3 @@
|
||||
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion"></div>
|
||||
<span data-i18n="Generate Image">Generate Image</span>
|
||||
</div>
|
||||
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task" data-i18n="[title]Abort current image generation task"></div>
|
||||
<span data-i18n="Stop Image Generation">Stop Image Generation</span>
|
||||
</div>
|
||||
|
||||
@@ -62,18 +62,13 @@ import { t, translate } from '../../i18n.js';
|
||||
import { oai_settings } from '../../openai.js';
|
||||
import { power_user } from '/scripts/power-user.js';
|
||||
import { MacrosParser } from '/scripts/macros.js';
|
||||
import { ActionLoaderHandle, loader } from '/scripts/action-loader.js';
|
||||
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'sd';
|
||||
// This is a 1x1 transparent PNG
|
||||
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
const CUSTOM_STOP_EVENT = 'sd_stop_generation';
|
||||
|
||||
// Generation tracking for status indicator
|
||||
let activeGenerations = 0;
|
||||
/** @type {JQuery<HTMLElement>|null} */
|
||||
let generationToast = null;
|
||||
|
||||
const sources = {
|
||||
extras: 'extras',
|
||||
@@ -2980,62 +2975,6 @@ function ensureSelectionExists(setting, selector) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the generation status indicator based on active generation count.
|
||||
* Shows/hides various UI indicators to inform user of background image generation.
|
||||
*/
|
||||
function updateGenerationIndicator() {
|
||||
if (activeGenerations > 0) {
|
||||
const countText = activeGenerations > 1 ? ` (${activeGenerations})` : '';
|
||||
const toastText = `<i class="fa-solid fa-spinner fa-spin"></i> ${t`Generating an image`}${countText}...`;
|
||||
|
||||
// Show persistent toast if not already showing
|
||||
if (!generationToast) {
|
||||
generationToast = toastr.info(
|
||||
toastText,
|
||||
'Image Generation',
|
||||
{
|
||||
timeOut: 0,
|
||||
extendedTimeOut: 0,
|
||||
tapToDismiss: true,
|
||||
escapeHtml: false,
|
||||
onHidden: () => {
|
||||
generationToast = null;
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (activeGenerations > 1) {
|
||||
// Update count in existing toast
|
||||
const toastMessage = $(generationToast).find('.toast-message');
|
||||
if (toastMessage.length) {
|
||||
toastMessage.html(toastText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Hide toast when done
|
||||
if (generationToast) {
|
||||
toastr.clear(generationToast);
|
||||
generationToast = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the active generation counter and updates indicators.
|
||||
*/
|
||||
function startGenerationTracking() {
|
||||
activeGenerations++;
|
||||
updateGenerationIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the active generation counter and updates indicators.
|
||||
*/
|
||||
function endGenerationTracking() {
|
||||
activeGenerations = Math.max(0, activeGenerations - 1);
|
||||
updateGenerationIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image based on the given trigger word.
|
||||
* @param {string} initiator The initiator of the image generation
|
||||
@@ -3096,12 +3035,13 @@ async function generatePicture(initiator, args, trigger, message, callback) {
|
||||
|
||||
const dimensions = setTypeSpecificDimensions(generationType);
|
||||
const abortController = new AbortController();
|
||||
const stopButton = document.getElementById('sd_stop_gen');
|
||||
let negativePromptPrefix = args?.negative || '';
|
||||
let imagePath = '';
|
||||
|
||||
const stopListener = () => abortController.abort('Aborted by user');
|
||||
|
||||
let loaderHandle = ActionLoaderHandle.EMPTY;
|
||||
|
||||
try {
|
||||
const combineNegatives = (prefix) => { negativePromptPrefix = combinePrefixes(negativePromptPrefix, prefix); };
|
||||
|
||||
@@ -3114,16 +3054,18 @@ async function generatePicture(initiator, args, trigger, message, callback) {
|
||||
await eventSource.emit(event_types.SD_PROMPT_PROCESSING, eventData);
|
||||
prompt = eventData.prompt; // Allow extensions to modify the prompt
|
||||
|
||||
// Track this generation for status indicator
|
||||
startGenerationTracking();
|
||||
// Show stop button after prompt is ready (prompt generation uses separate abort mechanism)
|
||||
$(stopButton).show();
|
||||
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
|
||||
|
||||
if (typeof args?._abortController?.addEventListener === 'function') {
|
||||
args._abortController.addEventListener('abort', stopListener);
|
||||
}
|
||||
|
||||
// Show non-blocking stoppable toast for this generation
|
||||
loaderHandle = loader.show({
|
||||
blocking: false,
|
||||
title: t`Image Generation`,
|
||||
message: t`Generating an image...`,
|
||||
onStop: stopListener,
|
||||
});
|
||||
|
||||
// generate the image
|
||||
imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);
|
||||
} catch (err) {
|
||||
@@ -3142,10 +3084,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
|
||||
toastr.error(errorText, 'Image Generation');
|
||||
throw new Error(errorText);
|
||||
} finally {
|
||||
$(stopButton).hide();
|
||||
restoreOriginalDimensions(dimensions);
|
||||
eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
|
||||
endGenerationTracking();
|
||||
await loaderHandle.hide();
|
||||
}
|
||||
|
||||
return imagePath;
|
||||
@@ -5128,10 +5068,6 @@ async function addSDGenButtons() {
|
||||
generatePicture(initiators.wand, {}, param);
|
||||
}
|
||||
});
|
||||
|
||||
const stopGenButton = $('#sd_stop_gen');
|
||||
stopGenButton.hide();
|
||||
stopGenButton.on('click', () => eventSource.emit(CUSTOM_STOP_EVENT));
|
||||
}
|
||||
|
||||
function isValidState() {
|
||||
@@ -5216,10 +5152,6 @@ async function sdMessageButton($icon, { animate } = {}) {
|
||||
$icon.toggleClass(classes.idle, !isBusy);
|
||||
$icon.toggleClass(classes.busy, isBusy);
|
||||
$media.toggleClass(classes.animation, isBusy);
|
||||
|
||||
// Update generation counter toast
|
||||
const trackingFunction = isBusy ? startGenerationTracking : endGenerationTracking;
|
||||
trackingFunction();
|
||||
}
|
||||
|
||||
let $media = jQuery();
|
||||
@@ -5334,7 +5266,6 @@ async function writePromptFields(characterId) {
|
||||
* @returns {Promise<MediaAttachment|null>} - A promise that resolves to the newly generated media attachment, or null if generation failed or was aborted.
|
||||
*/
|
||||
async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, abortController = new AbortController()) {
|
||||
const stopButton = document.getElementById('sd_stop_gen');
|
||||
const stopListener = () => abortController.abort('Aborted by user');
|
||||
const generationType = mediaAttachment.generation_type ?? message?.extra?.generationType ?? generationMode.FREE;
|
||||
let dimensions = { width: extension_settings.sd.width, height: extension_settings.sd.height };
|
||||
@@ -5348,9 +5279,9 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
|
||||
source: MEDIA_SOURCE.GENERATED,
|
||||
};
|
||||
|
||||
let loaderHandle = ActionLoaderHandle.EMPTY;
|
||||
|
||||
try {
|
||||
$(stopButton).show();
|
||||
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
|
||||
const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; };
|
||||
const savedPrompt = mediaAttachment.title ?? message.extra.title ?? '';
|
||||
const savedNegative = mediaAttachment.negative ?? message.extra.negative ?? '';
|
||||
@@ -5366,6 +5297,14 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
|
||||
? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
|
||||
: context.characters[context.characterId]?.name;
|
||||
|
||||
// Show non-blocking stoppable toast for this generation
|
||||
loaderHandle = loader.show({
|
||||
blocking: false,
|
||||
title: t`Image Generation`,
|
||||
message: t`Generating an image...`,
|
||||
onStop: stopListener,
|
||||
});
|
||||
|
||||
onStart();
|
||||
result.url = await sendGenerationRequest(generationType, prompt, refineArgs.negative, characterName, callback, initiators.swipe, abortController.signal);
|
||||
result.generation_type = generationType;
|
||||
@@ -5377,11 +5316,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
|
||||
}
|
||||
} finally {
|
||||
onComplete();
|
||||
$(stopButton).hide();
|
||||
eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
|
||||
restoreOriginalDimensions(dimensions);
|
||||
extension_settings.sd.seed = extension_settings.sd.original_seed;
|
||||
delete extension_settings.sd.original_seed;
|
||||
await loaderHandle.hide();
|
||||
}
|
||||
|
||||
if (!result.url) {
|
||||
|
||||
Reference in New Issue
Block a user