Image swipe is back 🗣️🔥

73cedc115dbd82dff891acb877deb5494f8e653d

Cohee <18619528+Cohee1207@users.noreply.github.com>

5 files changed, +86 -26Ignore whitespace
public/index.html+9 -0
@@ -5086,6 +5086,15 @@
50865086 <option value="1" data-i18n="Enabled">Enabled</option>
50875087 </select>
50885088 </div>
5089+ <div class="flex-container alignItemsCenter">
5090+ <small data-i18n="Image Swipe Behavior:">
5091+ Image Swipe Behavior:
5092+ </small>
5093+ <select id="image_overswipe" class="widthNatural flex1 margin0">
5094+ <option value="generate" data-i18n="Generate new">Generate new</option>
5095+ <option value="rollover" data-i18n="Roll over">Roll over</option>
5096+ </select>
5097+ </div>
50895098 <label class="checkbox_label" for="continue_on_send">
50905099 <input id="continue_on_send" type="checkbox" />
50915100 <small data-i18n="Press Send to continue">
public/scripts/chats.js+7 -19
@@ -2080,24 +2080,6 @@ async function onImageSwiped(messageId, element, direction) {
20802080 return;
20812081 }
20822082
2083- // Show a message that swipe right no longer automatically generates an image
2084- if (direction === SWIPE_DIRECTION.RIGHT && message.extra.media_index === media.length - 1) {
2085- const key = 'imageSwipeNoticeShown';
2086- const hasSeenNotice = accountStorage.getItem(key);
2087- if (!hasSeenNotice) {
2088- await Popup.show.text(
2089- t`Image swiping no longer automatically generates new images.`,
2090- t`Use the 'Generate Image' (paintbrush) button in the message actions menu to generate more images. This message will not be shown again.`,
2091- );
2092- accountStorage.setItem(key, 'true');
2093- }
2094- }
2095-
2096- if (media.length === 1) {
2097- console.warn('Only one media item in the message, swiping is not applicable');
2098- return;
2099- }
2100-
21012083 const currentIndex = getMediaIndex(message);
21022084 const mediaDisplay = getMediaDisplay(message);
21032085
@@ -2106,6 +2088,13 @@ async function onImageSwiped(messageId, element, direction) {
21062088 return;
21072089 }
21082090
2091+ await eventSource.emit(event_types.IMAGE_SWIPED, { message, element, direction });
2092+
2093+ if (media.length === 1) {
2094+ console.warn('Only one media item in the message, swiping is not applicable');
2095+ return;
2096+ }
2097+
21092098 // Switch to previous image or wrap around if at the beginning
21102099 if (direction === SWIPE_DIRECTION.LEFT) {
21112100 const newIndex = currentIndex === 0 ? media.length - 1 : currentIndex - 1;
@@ -2119,7 +2108,6 @@ async function onImageSwiped(messageId, element, direction) {
21192108 }
21202109
21212110 await saveChatConditional();
2122- await eventSource.emit(event_types.IMAGE_SWIPED, { message, element, direction });
21232111 appendMediaToMessage(message, element);
21242112}
21252113
public/scripts/constants.js+9 -0
@@ -88,6 +88,15 @@ export const MEDIA_DISPLAY = {
8888};
8989
9090/**
91+ * @enum {string}
92+ * @readonly
93+ */
94+export const IMAGE_OVERSWIPE = {
95+ GENERATE: 'generate',
96+ ROLLOVER: 'rollover',
97+};
98+
99+/**
91100 * @readonly
92101 */
93102export const MEDIA_TYPE = {
public/scripts/extensions/stable-diffusion/index.js+53 -6
@@ -52,7 +52,7 @@ import {
5252 SlashCommandArgument,
5353 SlashCommandNamedArgument,
5454} from '../../slash-commands/SlashCommandArgument.js';
5555import { debounce_timeout, IMAGE_OVERSWIPE, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION, VIDEO_EXTENSIONS } from '../../constants.js';
5656import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
5757import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
5858import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -4238,7 +4238,7 @@ async function addSDGenButtons() {
42384238 placement: 'top',
42394239 });
42404240
42414241 $(document).on('click', '.sd_message_gen', (e) => sdMessageButton($(e.currentTarget), { animate: false }));
42424242
42434243 $(document).on('click touchend', function (e) {
42444244 const target = $(e.target);
@@ -4326,10 +4326,12 @@ let buttonAbortController = null;
43264326
43274327/**
43284328 * "Paintbrush" button handler to generate a new image for a message.
43294329 * @param {JQuery.ClickEvent<HTMLElement>} e$icon The click event objecttarget.
4330+ * @param {Object} [options] Additional options for image generation.
4331+ * @param {boolean} [options.animate] Whether to animate the media during generation.
43304332 * @returns {Promise<void>} A promise that resolves when the image generation process is complete.
43314333 */
43324334async function sdMessageButton(e$icon, { animate } = {}) {
43334335 /**
43344336 * Sets the icon to indicate busy or idle state.
43354337 * @param {boolean} isBusy Whether the icon should indicate a busy state.
@@ -4337,11 +4339,13 @@ async function sdMessageButton(e) {
43374339 function setBusyIcon(isBusy) {
43384340 $icon.toggleClass(classes.idle, !isBusy);
43394341 $icon.toggleClass(classes.busy, isBusy);
4342+ $media.toggleClass(classes.animation, isBusy);
43404343 }
43414344
4342- const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush' };
4345+ let $media = jQuery();
4346+
4347+ const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush', animation: 'fa-fade' };
43434348 const context = getContext();
4344- const $icon = $(e.currentTarget);
43454349
43464350 if ($icon.hasClass(classes.busy)) {
43474351 buttonAbortController?.abort('Aborted by user');
@@ -4373,6 +4377,11 @@ async function sdMessageButton(e) {
43734377 ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1])
43744378 : { url: '', title: message.mes, type: MEDIA_TYPE.IMAGE, generation_type: generationMode.FREE };
43754379
4380+ if (animate && message.extra.media.length > 0) {
4381+ const index = message.extra.media.indexOf(selectedMedia);
4382+ $media = messageElement.find(`.mes_media_container[data-index="${index}"]`).find('.mes_img, .mes_video');
4383+ }
4384+
43764385 buttonAbortController = new AbortController();
43774386 const newMediaAttachment = await generateMediaSwipe(
43784387 selectedMedia,
@@ -4482,6 +4491,43 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
44824491}
44834492
44844493/**
4494+ * Handles the image swipe event to potentially generate a new image.
4495+ * @param {object} param Parameters object
4496+ * @param {ChatMessage} param.message Message object
4497+ * @param {JQuery<HTMLElement>} param.element Message element
4498+ * @param {string} param.direction Swipe direction
4499+ */
4500+async function onImageSwiped({ message, element, direction }) {
4501+ const { powerUserSettings, accountStorage } = getContext();
4502+
4503+ if (!message || direction !== SWIPE_DIRECTION.RIGHT || powerUserSettings.image_overswipe !== IMAGE_OVERSWIPE.GENERATE) {
4504+ return;
4505+ }
4506+
4507+ const media = message?.extra?.media;
4508+ if (!Array.isArray(media) || media.length === 0) {
4509+ return;
4510+ }
4511+
4512+ const shouldGenerate = message?.extra?.media_index === media.length - 1;
4513+ if (!shouldGenerate) {
4514+ return;
4515+ }
4516+
4517+ const key = 'imageSwipeNoticeShown';
4518+ const hasSeenNotice = accountStorage.getItem(key);
4519+ if (!hasSeenNotice) {
4520+ await Popup.show.text(
4521+ t`Image Generation Notice`,
4522+ t`To disable generation on image swipes, change the "Image Swipe Behavior" setting in the User Settings panel. This message will not be shown again.`,
4523+ );
4524+ accountStorage.setItem(key, 'true');
4525+ }
4526+
4527+ await sdMessageButton(element.find('.sd_message_gen'), { animate: true });
4528+}
4529+
4530+/**
44854531 * Applies the command arguments to the extension settings.
44864532 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments
44874533 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture
@@ -4972,6 +5018,7 @@ jQuery(async () => {
49725018 });
49735019
49745020 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
5021+ eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped);
49755022
49765023 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
49775024 eventSource.on(event, async (/** @type {string} */ key) => {
public/scripts/power-user.js+8 -1
@@ -63,7 +63,7 @@ import { fuzzySearchCategories } from './filters.js';
6363import { accountStorage } from './util/AccountStorage.js';
6464import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
6565import { bindModelTemplates } from './chat-templates.js';
6666import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js';
6767import { t } from './i18n.js';
6868
6969export const toastPositionClasses = [
@@ -341,6 +341,7 @@ export const power_user = {
341341 pin_styles: true,
342342 click_to_edit: false,
343343 media_display: MEDIA_DISPLAY.LIST,
344+ image_overswipe: IMAGE_OVERSWIPE.GENERATE,
344345};
345346
346347let themes = [];
@@ -1774,6 +1775,7 @@ export async function loadPowerUserSettings(settings, data) {
17741775 $('#pin_styles').prop('checked', power_user.pin_styles);
17751776 $('#click_to_edit').prop('checked', power_user.click_to_edit);
17761777 $('#media_display').val(power_user.media_display);
1778+ $('#image_overswipe').val(power_user.image_overswipe);
17771779
17781780 for (const theme of themes) {
17791781 const option = document.createElement('option');
@@ -4190,6 +4192,11 @@ jQuery(() => {
41904192 }
41914193 });
41924194
4195+ $('#image_overswipe').on('input', function () {
4196+ power_user.image_overswipe = $(this).val().toString();
4197+ saveSettingsDebounced();
4198+ });
4199+
41934200 $(document).on('click', '#debug_table [data-debug-function]', function () {
41944201 const functionId = $(this).data('debug-function');
41954202 const functionRecord = debug_functions.find(f => f.functionId === functionId);