Gallery: add delete functionality for gallery items

96fb85457f211eb2b1c2d564840c9da57c975d95

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

5 files changed, +102 -6Ignore whitespace
public/script.js+11 -1
@@ -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 || '');
public/scripts/extensions/gallery/index.js+53 -4
@@ -15,10 +15,12 @@ import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/S
15import { DragAndDropHandler } from '../../dragdrop.js';15import { DragAndDropHandler } from '../../dragdrop.js';
16import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';16import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
17import { t, translate } from '../../i18n.js';17import { t, translate } from '../../i18n.js';
18import { Popup } from '../../popup.js';
1819
19const extensionName = 'gallery';20const extensionName = 'gallery';
20const extensionFolderPath = `scripts/extensions/${extensionName}/`;21const extensionFolderPath = `scripts/extensions/${extensionName}/`;
21let firstTime = true;22let firstTime = true;
23let deleteModeActive = false;
2224
23// Exposed defaults for future tweaking25// Exposed defaults for future tweaking
24let thumbnailHeight = 150;26let thumbnailHeight = 150;
@@ -147,6 +149,29 @@ async function getGalleryFolders() {
147}149}
148150
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 */
155async 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 order176 * @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 */
263async function showCharGallery() {288async function showCharGallery(deleteModeState = false) {
264 // Load necessary files if it's the first time calling the function289 // 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 }
277302
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);
431457
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) {
436474
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);
441480
@@ -635,9 +674,19 @@ function sanitizeHTMLId(id) {
635function viewWithDragbox(items) {674function 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/video676 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 extension677 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}
643692
public/scripts/extensions/gallery/style.css+5 -0
@@ -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}
public/style.css+8 -0
@@ -5059,6 +5059,12 @@ a:hover {
5059 cursor: pointer;5059 cursor: pointer;
5060}5060}
50615061
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}
51015107
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,
src/endpoints/images.js+25 -1
@@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer';
5import express from 'express';5import express from 'express';
6import sanitize from 'sanitize-filename';6import sanitize from 'sanitize-filename';
77
8import { clientRelativePath, removeFileExtension, getImages } from '../util.js';8import { clientRelativePath, removeFileExtension, getImages, isPathUnderParent } from '../util.js';
99
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
130router.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});