Gallery: add delete functionality for gallery items
| @@ -2439,7 +2439,7 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | |||
| 2439 | const image = messageElement.find('.mes_img'); | 2439 | const image = messageElement.find('.mes_img'); |
| 2440 | const text = messageElement.find('.mes_text'); | 2440 | const text = messageElement.find('.mes_text'); |
| 2441 | const isInline = !!mes.extra?.inline_image; | 2441 | const isInline = !!mes.extra?.inline_image; |
| 2442 | image.off('load').on('load', function () { | 2442 | const doAdjustScroll = () => { |
| 2443 | if (!adjustScroll) { | 2443 | if (!adjustScroll) { |
| 2444 | return; | 2444 | return; |
| 2445 | } | 2445 | } |
| @@ -2447,6 +2447,16 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | |||
| 2447 | const newChatHeight = $('#chat').prop('scrollHeight'); | 2447 | const newChatHeight = $('#chat').prop('scrollHeight'); |
| 2448 | const diff = newChatHeight - chatHeight; | 2448 | const diff = newChatHeight - chatHeight; |
| 2449 | $('#chat').scrollTop(scrollPosition + diff); | 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 | image.attr('src', mes.extra?.image); | 2461 | image.attr('src', mes.extra?.image); |
| 2452 | image.attr('title', mes.extra?.title || mes.title || ''); | 2462 | image.attr('title', mes.extra?.title || mes.title || ''); |
| @@ -15,10 +15,12 @@ import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/S | |||
| 15 | import { DragAndDropHandler } from '../../dragdrop.js'; | 15 | import { DragAndDropHandler } from '../../dragdrop.js'; |
| 16 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 16 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 17 | import { t, translate } from '../../i18n.js'; | 17 | import { t, translate } from '../../i18n.js'; |
| 18 | import { Popup } from '../../popup.js'; | ||
| 18 | 19 | ||
| 19 | const extensionName = 'gallery'; | 20 | const extensionName = 'gallery'; |
| 20 | const extensionFolderPath = `scripts/extensions/${extensionName}/`; | 21 | const extensionFolderPath = `scripts/extensions/${extensionName}/`; |
| 21 | let firstTime = true; | 22 | let firstTime = true; |
| 23 | let deleteModeActive = false; | ||
| 22 | 24 | ||
| 23 | // Exposed defaults for future tweaking | 25 | // Exposed defaults for future tweaking |
| 24 | let thumbnailHeight = 150; | 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 | * Sets the sort order for the gallery. | 175 | * Sets the sort order for the gallery. |
| 151 | * @param {string} order Sort order | 176 | * @param {string} order Sort order |
| 152 | */ | 177 | */ |
| @@ -260,7 +285,7 @@ async function initGallery(items, url) { | |||
| 260 | * | 285 | * |
| 261 | * @returns {Promise<void>} - Promise representing the completion of the gallery display process. | 286 | * @returns {Promise<void>} - Promise representing the completion of the gallery display process. |
| 262 | */ | 287 | */ |
| 263 | async function showCharGallery() { | 288 | async function showCharGallery(deleteModeState = false) { |
| 264 | // Load necessary files if it's the first time calling the function | 289 | // Load necessary files if it's the first time calling the function |
| 265 | if (firstTime) { | 290 | if (firstTime) { |
| 266 | await loadFileToDocument( | 291 | await loadFileToDocument( |
| @@ -276,6 +301,7 @@ async function showCharGallery() { | |||
| 276 | } | 301 | } |
| 277 | 302 | ||
| 278 | try { | 303 | try { |
| 304 | deleteModeActive = deleteModeState; | ||
| 279 | let url = selected_group || this_chid; | 305 | let url = selected_group || this_chid; |
| 280 | if (!selected_group && this_chid !== undefined) { | 306 | if (!selected_group && this_chid !== undefined) { |
| 281 | url = getGalleryFolder(characters[this_chid]); | 307 | url = getGalleryFolder(characters[this_chid]); |
| @@ -429,6 +455,18 @@ async function makeMovable(url) { | |||
| 429 | galleryFolderAccept.title = t`Change gallery folder`; | 455 | galleryFolderAccept.title = t`Change gallery folder`; |
| 430 | galleryFolderAccept.addEventListener('click', onChangeFolder); | 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 | const galleryFolderRestore = document.createElement('div'); | 470 | const galleryFolderRestore = document.createElement('div'); |
| 433 | galleryFolderRestore.classList.add('right_menu_button', 'fa-solid', 'fa-recycle', 'fa-fw'); | 471 | galleryFolderRestore.classList.add('right_menu_button', 'fa-solid', 'fa-recycle', 'fa-fw'); |
| 434 | galleryFolderRestore.title = t`Restore gallery folder`; | 472 | galleryFolderRestore.title = t`Restore gallery folder`; |
| @@ -436,6 +474,7 @@ async function makeMovable(url) { | |||
| 436 | 474 | ||
| 437 | topBarElement.appendChild(galleryFolderInput); | 475 | topBarElement.appendChild(galleryFolderInput); |
| 438 | topBarElement.appendChild(galleryFolderAccept); | 476 | topBarElement.appendChild(galleryFolderAccept); |
| 477 | topBarElement.appendChild(galleryDeleteMode); | ||
| 439 | topBarElement.appendChild(galleryFolderRestore); | 478 | topBarElement.appendChild(galleryFolderRestore); |
| 440 | newElement.append(topBarElement); | 479 | newElement.append(topBarElement); |
| 441 | 480 | ||
| @@ -635,9 +674,19 @@ function sanitizeHTMLId(id) { | |||
| 635 | function viewWithDragbox(items) { | 674 | function viewWithDragbox(items) { |
| 636 | if (items && items.length > 0) { | 675 | if (items && items.length > 0) { |
| 637 | const url = items[0].responsiveURL(); // Get the URL of the clicked image/video | 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 | #dragGallery { | 43 | #dragGallery { |
| 44 | min-height: 25dvh; | 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 | cursor: pointer; | 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 | .mes_img_swipes, | 5068 | .mes_img_swipes, |
| 5063 | .mes_img_controls { | 5069 | .mes_img_controls { |
| 5064 | position: absolute; | 5070 | position: absolute; |
| @@ -5099,6 +5105,8 @@ a:hover { | |||
| 5099 | filter: brightness(150%); | 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 | .mes_img_container:hover .mes_img_swipes, | 5110 | .mes_img_container:hover .mes_img_swipes, |
| 5103 | .mes_img_container:focus-within .mes_img_swipes, | 5111 | .mes_img_container:focus-within .mes_img_swipes, |
| 5104 | .mes_img_container:hover .mes_img_controls, | 5112 | .mes_img_container:hover .mes_img_controls, |
| @@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'; | |||
| 5 | import express from 'express'; | 5 | import express from 'express'; |
| 6 | import sanitize from 'sanitize-filename'; | 6 | import sanitize from 'sanitize-filename'; |
| 7 | 7 | ||
| 8 | import { clientRelativePath, removeFileExtension, getImages } from '../util.js'; | 8 | import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js'; |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| 11 | * Ensure the directory for the provided file path exists. | 11 | * Ensure the directory for the provided file path exists. |
| @@ -126,3 +126,27 @@ router.post('/folders', (request, response) => { | |||
| 126 | return response.status(500).send({ error: 'Unable to retrieve folders' }); | 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 | }); | ||