Image swipe is back 🗣️🔥
| @@ -5086,6 +5086,15 @@ | ||
| 5086 | 5086 | <option value="1" data-i18n="Enabled">Enabled</option> |
| 5087 | 5087 | </select> |
| 5088 | 5088 | </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> | |
| 5089 | 5098 | <label class="checkbox_label" for="continue_on_send"> |
| 5090 | 5099 | <input id="continue_on_send" type="checkbox" /> |
| 5091 | 5100 | <small data-i18n="Press Send to continue"> |
| @@ -2080,24 +2080,6 @@ async function onImageSwiped(messageId, element, direction) { | ||
| 2080 | 2080 | return; |
| 2081 | 2081 | } |
| 2082 | 2082 | |
| 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 | - | |
| 2101 | 2083 | const currentIndex = getMediaIndex(message); |
| 2102 | 2084 | const mediaDisplay = getMediaDisplay(message); |
| 2103 | 2085 | |
| @@ -2106,6 +2088,13 @@ async function onImageSwiped(messageId, element, direction) { | ||
| 2106 | 2088 | return; |
| 2107 | 2089 | } |
| 2108 | 2090 | |
| 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 | + | |
| 2109 | 2098 | // Switch to previous image or wrap around if at the beginning |
| 2110 | 2099 | if (direction === SWIPE_DIRECTION.LEFT) { |
| 2111 | 2100 | const newIndex = currentIndex === 0 ? media.length - 1 : currentIndex - 1; |
| @@ -2119,7 +2108,6 @@ async function onImageSwiped(messageId, element, direction) { | ||
| 2119 | 2108 | } |
| 2120 | 2109 | |
| 2121 | 2110 | await saveChatConditional(); |
| 2122 | - await eventSource.emit(event_types.IMAGE_SWIPED, { message, element, direction }); | |
| 2123 | 2111 | appendMediaToMessage(message, element); |
| 2124 | 2112 | } |
| 2125 | 2113 | |
| @@ -88,6 +88,15 @@ export const MEDIA_DISPLAY = { | ||
| 88 | 88 | }; |
| 89 | 89 | |
| 90 | 90 | /** |
| 91 | + * @enum {string} | |
| 92 | + * @readonly | |
| 93 | + */ | |
| 94 | +export const IMAGE_OVERSWIPE = { | |
| 95 | + GENERATE: 'generate', | |
| 96 | + ROLLOVER: 'rollover', | |
| 97 | +}; | |
| 98 | + | |
| 99 | +/** | |
| 91 | 100 | * @readonly |
| 92 | 101 | */ |
| 93 | 102 | export const MEDIA_TYPE = { |
| @@ -52,7 +52,7 @@ import { | ||
| 52 | 52 | SlashCommandArgument, |
| 53 | 53 | SlashCommandNamedArgument, |
| 54 | 54 | } from '../../slash-commands/SlashCommandArgument.js'; |
| 55 | 55 | import { debounce_timeout, IMAGE_OVERSWIPE, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION, VIDEO_EXTENSIONS } from '../../constants.js'; |
| 56 | 56 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 57 | 57 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 58 | 58 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -4238,7 +4238,7 @@ async function addSDGenButtons() { | ||
| 4238 | 4238 | placement: 'top', |
| 4239 | 4239 | }); |
| 4240 | 4240 | |
| 4241 | 4241 | $(document).on('click', '.sd_message_gen', (e) => sdMessageButton($(e.currentTarget), { animate: false })); |
| 4242 | 4242 | |
| 4243 | 4243 | $(document).on('click touchend', function (e) { |
| 4244 | 4244 | const target = $(e.target); |
| @@ -4326,10 +4326,12 @@ let buttonAbortController = null; | ||
| 4326 | 4326 | |
| 4327 | 4327 | /** |
| 4328 | 4328 | * "Paintbrush" button handler to generate a new image for a message. |
| 4329 | 4329 | * @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. | |
| 4330 | 4332 | * @returns {Promise<void>} A promise that resolves when the image generation process is complete. |
| 4331 | 4333 | */ |
| 4332 | 4334 | async function sdMessageButton(e$icon, { animate } = {}) { |
| 4333 | 4335 | /** |
| 4334 | 4336 | * Sets the icon to indicate busy or idle state. |
| 4335 | 4337 | * @param {boolean} isBusy Whether the icon should indicate a busy state. |
| @@ -4337,11 +4339,13 @@ async function sdMessageButton(e) { | ||
| 4337 | 4339 | function setBusyIcon(isBusy) { |
| 4338 | 4340 | $icon.toggleClass(classes.idle, !isBusy); |
| 4339 | 4341 | $icon.toggleClass(classes.busy, isBusy); |
| 4342 | + $media.toggleClass(classes.animation, isBusy); | |
| 4340 | 4343 | } |
| 4341 | 4344 | |
| 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' }; | |
| 4343 | 4348 | const context = getContext(); |
| 4344 | - const $icon = $(e.currentTarget); | |
| 4345 | 4349 | |
| 4346 | 4350 | if ($icon.hasClass(classes.busy)) { |
| 4347 | 4351 | buttonAbortController?.abort('Aborted by user'); |
| @@ -4373,6 +4377,11 @@ async function sdMessageButton(e) { | ||
| 4373 | 4377 | ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1]) |
| 4374 | 4378 | : { url: '', title: message.mes, type: MEDIA_TYPE.IMAGE, generation_type: generationMode.FREE }; |
| 4375 | 4379 | |
| 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 | + | |
| 4376 | 4385 | buttonAbortController = new AbortController(); |
| 4377 | 4386 | const newMediaAttachment = await generateMediaSwipe( |
| 4378 | 4387 | selectedMedia, |
| @@ -4482,6 +4491,43 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, | ||
| 4482 | 4491 | } |
| 4483 | 4492 | |
| 4484 | 4493 | /** |
| 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 | +/** | |
| 4485 | 4531 | * Applies the command arguments to the extension settings. |
| 4486 | 4532 | * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments |
| 4487 | 4533 | * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture |
| @@ -4972,6 +5018,7 @@ jQuery(async () => { | ||
| 4972 | 5018 | }); |
| 4973 | 5019 | |
| 4974 | 5020 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 5021 | + eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped); | |
| 4975 | 5022 | |
| 4976 | 5023 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { |
| 4977 | 5024 | eventSource.on(event, async (/** @type {string} */ key) => { |
| @@ -63,7 +63,7 @@ import { fuzzySearchCategories } from './filters.js'; | ||
| 63 | 63 | import { accountStorage } from './util/AccountStorage.js'; |
| 64 | 64 | import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js'; |
| 65 | 65 | import { bindModelTemplates } from './chat-templates.js'; |
| 66 | 66 | import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js'; |
| 67 | 67 | import { t } from './i18n.js'; |
| 68 | 68 | |
| 69 | 69 | export const toastPositionClasses = [ |
| @@ -341,6 +341,7 @@ export const power_user = { | ||
| 341 | 341 | pin_styles: true, |
| 342 | 342 | click_to_edit: false, |
| 343 | 343 | media_display: MEDIA_DISPLAY.LIST, |
| 344 | + image_overswipe: IMAGE_OVERSWIPE.GENERATE, | |
| 344 | 345 | }; |
| 345 | 346 | |
| 346 | 347 | let themes = []; |
| @@ -1774,6 +1775,7 @@ export async function loadPowerUserSettings(settings, data) { | ||
| 1774 | 1775 | $('#pin_styles').prop('checked', power_user.pin_styles); |
| 1775 | 1776 | $('#click_to_edit').prop('checked', power_user.click_to_edit); |
| 1776 | 1777 | $('#media_display').val(power_user.media_display); |
| 1778 | + $('#image_overswipe').val(power_user.image_overswipe); | |
| 1777 | 1779 | |
| 1778 | 1780 | for (const theme of themes) { |
| 1779 | 1781 | const option = document.createElement('option'); |
| @@ -4190,6 +4192,11 @@ jQuery(() => { | ||
| 4190 | 4192 | } |
| 4191 | 4193 | }); |
| 4192 | 4194 | |
| 4195 | + $('#image_overswipe').on('input', function () { | |
| 4196 | + power_user.image_overswipe = $(this).val().toString(); | |
| 4197 | + saveSettingsDebounced(); | |
| 4198 | + }); | |
| 4199 | + | |
| 4193 | 4200 | $(document).on('click', '#debug_table [data-debug-function]', function () { |
| 4194 | 4201 | const functionId = $(this).data('debug-function'); |
| 4195 | 4202 | const functionRecord = debug_functions.find(f => f.functionId === functionId); |