Gallery: add delete functionality for gallery items
| @@ -2439,7 +2439,7 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | ||
| 2439 | 2439 | const image = messageElement.find('.mes_img'); |
| 2440 | 2440 | const text = messageElement.find('.mes_text'); |
| 2441 | 2441 | const isInline = !!mes.extra?.inline_image; |
| 2442 | - image.off('load').on('load', function () { | |
| 2442 | + const doAdjustScroll = () => { | |
| 2443 | 2443 | if (!adjustScroll) { |
| 2444 | 2444 | return; |
| 2445 | 2445 | } |
| @@ -2447,6 +2447,16 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | ||
| 2447 | 2447 | const newChatHeight = $('#chat').prop('scrollHeight'); |
| 2448 | 2448 | const diff = newChatHeight - chatHeight; |
| 2449 | 2449 | $('#chat').scrollTop(scrollPosition + diff); |
| 2450 | + }; | |
| 2451 | + image.off('load').on('load', function () { | |
| 2452 | + image.removeAttr('alt'); | |
| 2453 | + image.removeClass('error'); | |
| 2454 | + doAdjustScroll(); | |
| 2455 | + }); | |
| 2456 | + image.off('error').on('error', function () { | |
| 2457 | + image.attr('alt', ''); | |
| 2458 | + image.addClass('error'); | |
| 2459 | + doAdjustScroll(); | |
| 2450 | 2460 | }); |
| 2451 | 2461 | image.attr('src', mes.extra?.image); |
| 2452 | 2462 | image.attr('title', mes.extra?.title || mes.title || ''); |
| @@ -15,10 +15,12 @@ import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/S | ||
| 15 | 15 | import { DragAndDropHandler } from '../../dragdrop.js'; |
| 16 | 16 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 17 | 17 | import { t, translate } from '../../i18n.js'; |
| 18 | +import { Popup } from '../../popup.js'; | |
| 18 | 19 | |
| 19 | 20 | const extensionName = 'gallery'; |
| 20 | 21 | const extensionFolderPath = `scripts/extensions/${extensionName}/`; |
| 21 | 22 | let firstTime = true; |
| 23 | +let deleteModeActive = false; | |
| 22 | 24 | |
| 23 | 25 | // Exposed defaults for future tweaking |
| 24 | 26 | let thumbnailHeight = 150; |
| @@ -147,6 +149,29 @@ async function getGalleryFolders() { | ||
| 147 | 149 | } |
| 148 | 150 | |
| 149 | 151 | /** |
| 152 | + * Deletes a gallery item based on the provided URL. | |
| 153 | + * @param {string} url - The URL of the image to be deleted. | |
| 154 | + */ | |
| 155 | +async function deleteGalleryItem(url) { | |
| 156 | + try { | |
| 157 | + const response = await fetch('/api/images/delete', { | |
| 158 | + method: 'POST', | |
| 159 | + headers: getRequestHeaders(), | |
| 160 | + body: JSON.stringify({ path: url }), | |
| 161 | + }); | |
| 162 | + | |
| 163 | + if (!response.ok) { | |
| 164 | + throw new Error(`HTTP error. Status: ${response.status}`); | |
| 165 | + } | |
| 166 | + | |
| 167 | + toastr.success(t`Image deleted successfully.`); | |
| 168 | + } catch (error) { | |
| 169 | + console.error('Failed to delete the image:', error); | |
| 170 | + toastr.error(t`Failed to delete the image. Check the console for details.`); | |
| 171 | + } | |
| 172 | +} | |
| 173 | + | |
| 174 | +/** | |
| 150 | 175 | * Sets the sort order for the gallery. |
| 151 | 176 | * @param {string} order Sort order |
| 152 | 177 | */ |
| @@ -260,7 +285,7 @@ async function initGallery(items, url) { | ||
| 260 | 285 | * |
| 261 | 286 | * @returns {Promise<void>} - Promise representing the completion of the gallery display process. |
| 262 | 287 | */ |
| 263 | 288 | async function showCharGallery(deleteModeState = false) { |
| 264 | 289 | // Load necessary files if it's the first time calling the function |
| 265 | 290 | if (firstTime) { |
| 266 | 291 | await loadFileToDocument( |
| @@ -276,6 +301,7 @@ async function showCharGallery() { | ||
| 276 | 301 | } |
| 277 | 302 | |
| 278 | 303 | try { |
| 304 | + deleteModeActive = deleteModeState; | |
| 279 | 305 | let url = selected_group || this_chid; |
| 280 | 306 | if (!selected_group && this_chid !== undefined) { |
| 281 | 307 | url = getGalleryFolder(characters[this_chid]); |
| @@ -429,6 +455,18 @@ async function makeMovable(url) { | ||
| 429 | 455 | galleryFolderAccept.title = t`Change gallery folder`; |
| 430 | 456 | galleryFolderAccept.addEventListener('click', onChangeFolder); |
| 431 | 457 | |
| 458 | + const galleryDeleteMode = document.createElement('div'); | |
| 459 | + galleryDeleteMode.classList.add('right_menu_button', 'fa-solid', 'fa-trash', 'fa-fw'); | |
| 460 | + galleryDeleteMode.classList.toggle('warning', deleteModeActive); | |
| 461 | + galleryDeleteMode.title = t`Delete mode`; | |
| 462 | + galleryDeleteMode.addEventListener('click', () => { | |
| 463 | + deleteModeActive = !deleteModeActive; | |
| 464 | + galleryDeleteMode.classList.toggle('warning', deleteModeActive); | |
| 465 | + if (deleteModeActive) { | |
| 466 | + toastr.info(t`Delete mode is ON. Click on images you want to delete.`); | |
| 467 | + } | |
| 468 | + }); | |
| 469 | + | |
| 432 | 470 | const galleryFolderRestore = document.createElement('div'); |
| 433 | 471 | galleryFolderRestore.classList.add('right_menu_button', 'fa-solid', 'fa-recycle', 'fa-fw'); |
| 434 | 472 | galleryFolderRestore.title = t`Restore gallery folder`; |
| @@ -436,6 +474,7 @@ async function makeMovable(url) { | ||
| 436 | 474 | |
| 437 | 475 | topBarElement.appendChild(galleryFolderInput); |
| 438 | 476 | topBarElement.appendChild(galleryFolderAccept); |
| 477 | + topBarElement.appendChild(galleryDeleteMode); | |
| 439 | 478 | topBarElement.appendChild(galleryFolderRestore); |
| 440 | 479 | newElement.append(topBarElement); |
| 441 | 480 | |
| @@ -635,9 +674,19 @@ function sanitizeHTMLId(id) { | ||
| 635 | 674 | function viewWithDragbox(items) { |
| 636 | 675 | if (items && items.length > 0) { |
| 637 | 676 | const url = items[0].responsiveURL(); // Get the URL of the clicked image/video |
| 638 | - // ID should just be the last part of the URL, removing the extension | |
| 677 | + if (deleteModeActive) { | |
| 639 | - const id = sanitizeHTMLId(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'))); | |
| 678 | + Popup.show.confirm(t`Are you sure you want to delete this image?`, url) | |
| 640 | - makeDragImg(id, url); | |
| 679 | + .then(async (confirmed) => { | |
| 680 | + if (!confirmed) { | |
| 681 | + return; | |
| 682 | + } | |
| 683 | + deleteGalleryItem(url).then(() => showCharGallery(deleteModeActive)); | |
| 684 | + }); | |
| 685 | + } else { | |
| 686 | + // ID should just be the last part of the URL, removing the extension | |
| 687 | + const id = sanitizeHTMLId(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'))); | |
| 688 | + makeDragImg(id, url); | |
| 689 | + } | |
| 641 | 690 | } |
| 642 | 691 | } |
| 643 | 692 | |
| @@ -43,3 +43,8 @@ | ||
| 43 | 43 | #dragGallery { |
| 44 | 44 | min-height: 25dvh; |
| 45 | 45 | } |
| 46 | + | |
| 47 | +#gallery .right_menu_button.warning { | |
| 48 | + opacity: 1; | |
| 49 | + filter: unset; | |
| 50 | +} | |
| @@ -5059,6 +5059,12 @@ a:hover { | ||
| 5059 | 5059 | cursor: pointer; |
| 5060 | 5060 | } |
| 5061 | 5061 | |
| 5062 | +.mes_img.error { | |
| 5063 | + visibility: hidden; | |
| 5064 | + min-height: 100px; | |
| 5065 | + min-width: 120px; | |
| 5066 | +} | |
| 5067 | + | |
| 5062 | 5068 | .mes_img_swipes, |
| 5063 | 5069 | .mes_img_controls { |
| 5064 | 5070 | position: absolute; |
| @@ -5099,6 +5105,8 @@ a:hover { | ||
| 5099 | 5105 | filter: brightness(150%); |
| 5100 | 5106 | } |
| 5101 | 5107 | |
| 5108 | +.mes_img_container:has(.mes_img.error) .mes_img_swipes, | |
| 5109 | +.mes_img_container:has(.mes_img.error) .mes_img_controls, | |
| 5102 | 5110 | .mes_img_container:hover .mes_img_swipes, |
| 5103 | 5111 | .mes_img_container:focus-within .mes_img_swipes, |
| 5104 | 5112 | .mes_img_container:hover .mes_img_controls, |
| @@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'; | ||
| 5 | 5 | import express from 'express'; |
| 6 | 6 | import sanitize from 'sanitize-filename'; |
| 7 | 7 | |
| 8 | 8 | import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js'; |
| 9 | 9 | |
| 10 | 10 | /** |
| 11 | 11 | * Ensure the directory for the provided file path exists. |
| @@ -126,3 +126,27 @@ router.post('/folders', (request, response) => { | ||
| 126 | 126 | return response.status(500).send({ error: 'Unable to retrieve folders' }); |
| 127 | 127 | } |
| 128 | 128 | }); |
| 129 | + | |
| 130 | +router.post('/delete', async (request, response) => { | |
| 131 | + try { | |
| 132 | + if (!request.body.path) { | |
| 133 | + return response.status(400).send('No path specified'); | |
| 134 | + } | |
| 135 | + | |
| 136 | + const pathToDelete = path.join(request.user.directories.root, request.body.path); | |
| 137 | + if (!isPathUnderParent(request.user.directories.userImages, pathToDelete)) { | |
| 138 | + return response.status(400).send('Invalid path'); | |
| 139 | + } | |
| 140 | + | |
| 141 | + if (!fs.existsSync(pathToDelete)) { | |
| 142 | + return response.status(404).send('File not found'); | |
| 143 | + } | |
| 144 | + | |
| 145 | + fs.unlinkSync(pathToDelete); | |
| 146 | + console.info(`Deleted image: ${request.body.path} from ${request.user.profile.handle}`); | |
| 147 | + return response.sendStatus(200); | |
| 148 | + } catch (error) { | |
| 149 | + console.error(error); | |
| 150 | + return response.sendStatus(500); | |
| 151 | + } | |
| 152 | +}); | |