From 96fb85457f211eb2b1c2d564840c9da57c975d95 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 4 Jun 2025 23:53:18 +0300 Subject: [PATCH] Gallery: add delete functionality for gallery items --- public/script.js | 12 ++++- public/scripts/extensions/gallery/index.js | 57 +++++++++++++++++++-- public/scripts/extensions/gallery/style.css | 5 ++ public/style.css | 8 +++ src/endpoints/images.js | 26 +++++++++- 5 files changed, 102 insertions(+), 6 deletions(-) diff --git a/public/script.js b/public/script.js index 184379da0..0d0689da8 100644 --- a/public/script.js +++ b/public/script.js @@ -2439,7 +2439,7 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { const image = messageElement.find('.mes_img'); const text = messageElement.find('.mes_text'); const isInline = !!mes.extra?.inline_image; - image.off('load').on('load', function () { + const doAdjustScroll = () => { if (!adjustScroll) { return; } @@ -2447,6 +2447,16 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { const newChatHeight = $('#chat').prop('scrollHeight'); const diff = newChatHeight - chatHeight; $('#chat').scrollTop(scrollPosition + diff); + }; + image.off('load').on('load', function () { + image.removeAttr('alt'); + image.removeClass('error'); + doAdjustScroll(); + }); + image.off('error').on('error', function () { + image.attr('alt', ''); + image.addClass('error'); + doAdjustScroll(); }); image.attr('src', mes.extra?.image); image.attr('title', mes.extra?.title || mes.title || ''); diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js index 5ad8044da..fd41ac60a 100644 --- a/public/scripts/extensions/gallery/index.js +++ b/public/scripts/extensions/gallery/index.js @@ -15,10 +15,12 @@ import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/S import { DragAndDropHandler } from '../../dragdrop.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { t, translate } from '../../i18n.js'; +import { Popup } from '../../popup.js'; const extensionName = 'gallery'; const extensionFolderPath = `scripts/extensions/${extensionName}/`; let firstTime = true; +let deleteModeActive = false; // Exposed defaults for future tweaking let thumbnailHeight = 150; @@ -146,6 +148,29 @@ async function getGalleryFolders() { } } +/** + * Deletes a gallery item based on the provided URL. + * @param {string} url - The URL of the image to be deleted. + */ +async function deleteGalleryItem(url) { + try { + const response = await fetch('/api/images/delete', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ path: url }), + }); + + if (!response.ok) { + throw new Error(`HTTP error. Status: ${response.status}`); + } + + toastr.success(t`Image deleted successfully.`); + } catch (error) { + console.error('Failed to delete the image:', error); + toastr.error(t`Failed to delete the image. Check the console for details.`); + } +} + /** * Sets the sort order for the gallery. * @param {string} order Sort order @@ -260,7 +285,7 @@ async function initGallery(items, url) { * * @returns {Promise} - Promise representing the completion of the gallery display process. */ -async function showCharGallery() { +async function showCharGallery(deleteModeState = false) { // Load necessary files if it's the first time calling the function if (firstTime) { await loadFileToDocument( @@ -276,6 +301,7 @@ async function showCharGallery() { } try { + deleteModeActive = deleteModeState; let url = selected_group || this_chid; if (!selected_group && this_chid !== undefined) { url = getGalleryFolder(characters[this_chid]); @@ -429,6 +455,18 @@ async function makeMovable(url) { galleryFolderAccept.title = t`Change gallery folder`; galleryFolderAccept.addEventListener('click', onChangeFolder); + const galleryDeleteMode = document.createElement('div'); + galleryDeleteMode.classList.add('right_menu_button', 'fa-solid', 'fa-trash', 'fa-fw'); + galleryDeleteMode.classList.toggle('warning', deleteModeActive); + galleryDeleteMode.title = t`Delete mode`; + galleryDeleteMode.addEventListener('click', () => { + deleteModeActive = !deleteModeActive; + galleryDeleteMode.classList.toggle('warning', deleteModeActive); + if (deleteModeActive) { + toastr.info(t`Delete mode is ON. Click on images you want to delete.`); + } + }); + const galleryFolderRestore = document.createElement('div'); galleryFolderRestore.classList.add('right_menu_button', 'fa-solid', 'fa-recycle', 'fa-fw'); galleryFolderRestore.title = t`Restore gallery folder`; @@ -436,6 +474,7 @@ async function makeMovable(url) { topBarElement.appendChild(galleryFolderInput); topBarElement.appendChild(galleryFolderAccept); + topBarElement.appendChild(galleryDeleteMode); topBarElement.appendChild(galleryFolderRestore); newElement.append(topBarElement); @@ -635,9 +674,19 @@ function sanitizeHTMLId(id) { function viewWithDragbox(items) { if (items && items.length > 0) { const url = items[0].responsiveURL(); // Get the URL of the clicked image/video - // ID should just be the last part of the URL, removing the extension - const id = sanitizeHTMLId(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'))); - makeDragImg(id, url); + if (deleteModeActive) { + Popup.show.confirm(t`Are you sure you want to delete this image?`, url) + .then(async (confirmed) => { + if (!confirmed) { + return; + } + deleteGalleryItem(url).then(() => showCharGallery(deleteModeActive)); + }); + } else { + // ID should just be the last part of the URL, removing the extension + const id = sanitizeHTMLId(url.substring(url.lastIndexOf('/') + 1, url.lastIndexOf('.'))); + makeDragImg(id, url); + } } } diff --git a/public/scripts/extensions/gallery/style.css b/public/scripts/extensions/gallery/style.css index 9f63a7d58..50d19c033 100644 --- a/public/scripts/extensions/gallery/style.css +++ b/public/scripts/extensions/gallery/style.css @@ -43,3 +43,8 @@ #dragGallery { min-height: 25dvh; } + +#gallery .right_menu_button.warning { + opacity: 1; + filter: unset; +} diff --git a/public/style.css b/public/style.css index 7a118dc58..03a04f079 100644 --- a/public/style.css +++ b/public/style.css @@ -5059,6 +5059,12 @@ a:hover { cursor: pointer; } +.mes_img.error { + visibility: hidden; + min-height: 100px; + min-width: 120px; +} + .mes_img_swipes, .mes_img_controls { position: absolute; @@ -5099,6 +5105,8 @@ a:hover { filter: brightness(150%); } +.mes_img_container:has(.mes_img.error) .mes_img_swipes, +.mes_img_container:has(.mes_img.error) .mes_img_controls, .mes_img_container:hover .mes_img_swipes, .mes_img_container:focus-within .mes_img_swipes, .mes_img_container:hover .mes_img_controls, diff --git a/src/endpoints/images.js b/src/endpoints/images.js index 9bc60a2b4..69e825b4c 100644 --- a/src/endpoints/images.js +++ b/src/endpoints/images.js @@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'; import express from 'express'; import sanitize from 'sanitize-filename'; -import { clientRelativePath, removeFileExtension, getImages } from '../util.js'; +import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js'; /** * Ensure the directory for the provided file path exists. @@ -126,3 +126,27 @@ router.post('/folders', (request, response) => { return response.status(500).send({ error: 'Unable to retrieve folders' }); } }); + +router.post('/delete', async (request, response) => { + try { + if (!request.body.path) { + return response.status(400).send('No path specified'); + } + + const pathToDelete = path.join(request.user.directories.root, request.body.path); + if (!isPathUnderParent(request.user.directories.userImages, pathToDelete)) { + return response.status(400).send('Invalid path'); + } + + if (!fs.existsSync(pathToDelete)) { + return response.status(404).send('File not found'); + } + + fs.unlinkSync(pathToDelete); + console.info(`Deleted image: ${request.body.path} from ${request.user.profile.handle}`); + return response.sendStatus(200); + } catch (error) { + console.error(error); + return response.sendStatus(500); + } +});