Spaces:
Paused
Paused
| import { | |
| eventSource, | |
| this_chid, | |
| characters, | |
| getRequestHeaders, | |
| event_types, | |
| animation_duration, | |
| animation_easing, | |
| } from '../../../script.js'; | |
| import { groups, selected_group } from '../../group-chats.js'; | |
| import { loadFileToDocument, delay, getBase64Async, getSanitizedFilename } from '../../utils.js'; | |
| import { loadMovingUIState } from '../../power-user.js'; | |
| import { dragElement } from '../../RossAscends-mods.js'; | |
| import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | |
| import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | |
| import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | |
| 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; | |
| let paginationVisiblePages = 10; | |
| let paginationMaxLinesPerPage = 2; | |
| let galleryMaxRows = 3; | |
| // Remove all draggables associated with the gallery | |
| $('#movingDivs').on('click', '.dragClose', function () { | |
| const relatedId = $(this).data('related-id'); | |
| if (!relatedId) return; | |
| const relatedElement = $(`#movingDivs > .draggable[id="${relatedId}"]`); | |
| relatedElement.transition({ | |
| opacity: 0, | |
| duration: animation_duration, | |
| easing: animation_easing, | |
| complete: () => { | |
| relatedElement.remove(); | |
| }, | |
| }); | |
| }); | |
| const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved'; | |
| const mutationObserver = new MutationObserver((mutations) => { | |
| mutations.forEach((mutation) => { | |
| mutation.removedNodes.forEach((node) => { | |
| if (node instanceof HTMLElement && node.tagName === 'DIV' && node.id === 'gallery') { | |
| eventSource.emit(CUSTOM_GALLERY_REMOVED_EVENT); | |
| } | |
| }); | |
| }); | |
| }); | |
| mutationObserver.observe(document.body, { | |
| childList: true, | |
| subtree: false, | |
| }); | |
| const SORT = Object.freeze({ | |
| NAME_ASC: { value: 'nameAsc', field: 'name', order: 'asc', label: t`Sort By: Name (A-Z)` }, | |
| NAME_DESC: { value: 'nameDesc', field: 'name', order: 'desc', label: t`Sort By: Name (Z-A)` }, | |
| DATE_ASC: { value: 'dateAsc', field: 'date', order: 'asc', label: t`Sort By: Date (Oldest First)` }, | |
| DATE_DESC: { value: 'dateDesc', field: 'date', order: 'desc', label: t`Sort By: Date (Newest First)` }, | |
| }); | |
| const defaultSettings = Object.freeze({ | |
| folders: {}, | |
| sort: SORT.DATE_ASC.value, | |
| }); | |
| /** | |
| * Initializes the settings for the gallery extension. | |
| */ | |
| function initSettings() { | |
| let shouldSave = false; | |
| const context = SillyTavern.getContext(); | |
| if (!context.extensionSettings.gallery) { | |
| context.extensionSettings.gallery = structuredClone(defaultSettings); | |
| shouldSave = true; | |
| } | |
| for (const key of Object.keys(defaultSettings)) { | |
| if (!Object.hasOwn(context.extensionSettings.gallery, key)) { | |
| context.extensionSettings.gallery[key] = structuredClone(defaultSettings[key]); | |
| shouldSave = true; | |
| } | |
| } | |
| if (shouldSave) { | |
| context.saveSettingsDebounced(); | |
| } | |
| } | |
| /** | |
| * Retrieves the gallery folder for a given character. | |
| * @param {import('../../char-data.js').v1CharData} char Character data | |
| * @returns {string} The gallery folder for the character | |
| */ | |
| function getGalleryFolder(char) { | |
| return SillyTavern.getContext().extensionSettings.gallery.folders[char?.avatar] ?? char?.name; | |
| } | |
| /** | |
| * Retrieves a list of gallery items based on a given URL. This function calls an API endpoint | |
| * to get the filenames and then constructs the item list. | |
| * | |
| * @param {string} url - The base URL to retrieve the list of images. | |
| * @returns {Promise<Array>} - Resolves with an array of gallery item objects, rejects on error. | |
| */ | |
| async function getGalleryItems(url) { | |
| const sortValue = getSortOrder(); | |
| const sortObj = Object.values(SORT).find(it => it.value === sortValue) ?? SORT.DATE_ASC; | |
| const response = await fetch('/api/images/list', { | |
| method: 'POST', | |
| headers: getRequestHeaders(), | |
| body: JSON.stringify({ | |
| folder: url, | |
| sortField: sortObj.field, | |
| sortOrder: sortObj.order, | |
| }), | |
| }); | |
| url = await getSanitizedFilename(url); | |
| const data = await response.json(); | |
| const items = data.map((file) => ({ | |
| src: `user/images/${url}/${file}`, | |
| srct: `user/images/${url}/${file}`, | |
| title: '', // Optional title for each item | |
| })); | |
| return items; | |
| } | |
| /** | |
| * Retrieves a list of gallery folders. This function calls an API endpoint | |
| * @returns {Promise<string[]>} - Resolves with an array of gallery folders. | |
| */ | |
| async function getGalleryFolders() { | |
| try { | |
| const response = await fetch('/api/images/folders', { | |
| method: 'POST', | |
| headers: getRequestHeaders(), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error. Status: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return data; | |
| } catch (error) { | |
| console.error('Failed to fetch gallery folders:', error); | |
| return []; | |
| } | |
| } | |
| /** | |
| * 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 | |
| */ | |
| function setSortOrder(order) { | |
| const context = SillyTavern.getContext(); | |
| context.extensionSettings.gallery.sort = order; | |
| context.saveSettingsDebounced(); | |
| } | |
| /** | |
| * Retrieves the current sort order for the gallery. | |
| * @returns {string} The current sort order for the gallery. | |
| */ | |
| function getSortOrder() { | |
| return SillyTavern.getContext().extensionSettings.gallery.sort ?? defaultSettings.sort; | |
| } | |
| /** | |
| * Initializes a gallery using the provided items and sets up the drag-and-drop functionality. | |
| * It uses the nanogallery2 library to display the items and also initializes | |
| * event listeners to handle drag-and-drop of files onto the gallery. | |
| * | |
| * @param {Array<Object>} items - An array of objects representing the items to display in the gallery. | |
| * @param {string} url - The URL to use when a file is dropped onto the gallery for uploading. | |
| * @returns {Promise<void>} - Promise representing the completion of the gallery initialization. | |
| */ | |
| async function initGallery(items, url) { | |
| const nonce = `nonce-${Math.random().toString(36).substring(2, 15)}`; | |
| const gallery = $('#dragGallery'); | |
| gallery.addClass(nonce); | |
| gallery.nanogallery2({ | |
| 'items': items, | |
| thumbnailWidth: 'auto', | |
| thumbnailHeight: thumbnailHeight, | |
| paginationVisiblePages: paginationVisiblePages, | |
| paginationMaxLinesPerPage: paginationMaxLinesPerPage, | |
| galleryMaxRows: galleryMaxRows, | |
| galleryPaginationTopButtons: false, | |
| galleryNavigationOverlayButtons: true, | |
| galleryTheme: { | |
| navigationBar: { background: 'none', borderTop: '', borderBottom: '', borderRight: '', borderLeft: '' }, | |
| navigationBreadcrumb: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, | |
| navigationFilter: { color: '#ddd', background: '#111', colorSelected: '#fff', backgroundSelected: '#111', borderRadius: '4px' }, | |
| navigationPagination: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' }, | |
| thumbnail: { background: '#444', backgroundImage: 'linear-gradient(315deg, #111 0%, #445 90%)', borderColor: '#000', borderRadius: '0px', labelOpacity: 1, labelBackground: 'rgba(34, 34, 34, 0)', titleColor: '#fff', titleBgColor: 'transparent', titleShadow: '', descriptionColor: '#ccc', descriptionBgColor: 'transparent', descriptionShadow: '', stackBackground: '#aaa' }, | |
| thumbnailIcon: { padding: '5px', color: '#fff', shadow: '' }, | |
| pagination: { background: '#181818', backgroundSelected: '#666', color: '#fff', borderRadius: '2px', shapeBorder: '3px solid var(--SmartThemeQuoteColor)', shapeColor: '#444', shapeSelectedColor: '#aaa' }, | |
| }, | |
| galleryDisplayMode: 'pagination', | |
| fnThumbnailOpen: viewWithDragbox, | |
| fnThumbnailInit: function (/** @type {JQuery<HTMLElement>} */ $thumbnail, /** @type {{src: string}} */ item) { | |
| if (!item?.src) return; | |
| $thumbnail.attr('title', String(item.src).split('/').pop()); | |
| }, | |
| }); | |
| const dragDropHandler = new DragAndDropHandler(`#dragGallery.${nonce}`, async (files) => { | |
| if (!Array.isArray(files) || files.length === 0) { | |
| return; | |
| } | |
| // Upload each file | |
| for (const file of files) { | |
| await uploadFile(file, url); | |
| } | |
| // Refresh the gallery | |
| const newItems = await getGalleryItems(url); | |
| $('#dragGallery').closest('#gallery').remove(); | |
| await makeMovable(url); | |
| await delay(100); | |
| await initGallery(newItems, url); | |
| }); | |
| const resizeHandler = function () { | |
| gallery.nanogallery2('resize'); | |
| }; | |
| eventSource.on('resizeUI', resizeHandler); | |
| eventSource.once(event_types.CHAT_CHANGED, function () { | |
| gallery.closest('#gallery').remove(); | |
| }); | |
| eventSource.once(CUSTOM_GALLERY_REMOVED_EVENT, function () { | |
| gallery.nanogallery2('destroy'); | |
| dragDropHandler.destroy(); | |
| eventSource.removeListener('resizeUI', resizeHandler); | |
| }); | |
| // Set dropzone height to be the same as the parent | |
| gallery.css('height', gallery.parent().css('height')); | |
| //let images populate first | |
| await delay(100); | |
| //unset the height (which must be getting set by the gallery library at some point) | |
| gallery.css('height', 'unset'); | |
| //force a resize to make images display correctly | |
| gallery.nanogallery2('resize'); | |
| } | |
| /** | |
| * Displays a character gallery using the nanogallery2 library. | |
| * | |
| * This function takes care of: | |
| * - Loading necessary resources for the gallery on the first invocation. | |
| * - Preparing gallery items based on the character or group selection. | |
| * - Handling the drag-and-drop functionality for image upload. | |
| * - Displaying the gallery in a popup. | |
| * - Cleaning up resources when the gallery popup is closed. | |
| * | |
| * @returns {Promise<void>} - Promise representing the completion of the gallery display process. | |
| */ | |
| async function showCharGallery(deleteModeState = false) { | |
| // Load necessary files if it's the first time calling the function | |
| if (firstTime) { | |
| await loadFileToDocument( | |
| `${extensionFolderPath}nanogallery2.woff.min.css`, | |
| 'css', | |
| ); | |
| await loadFileToDocument( | |
| `${extensionFolderPath}jquery.nanogallery2.min.js`, | |
| 'js', | |
| ); | |
| firstTime = false; | |
| toastr.info('Images can also be found in the folder `user/images`', 'Drag and drop images onto the gallery to upload them', { timeOut: 6000 }); | |
| } | |
| try { | |
| deleteModeActive = deleteModeState; | |
| let url = selected_group || this_chid; | |
| if (!selected_group && this_chid !== undefined) { | |
| url = getGalleryFolder(characters[this_chid]); | |
| } | |
| const items = await getGalleryItems(url); | |
| // if there already is a gallery, destroy it and place this one in its place | |
| $('#dragGallery').closest('#gallery').remove(); | |
| await makeMovable(url); | |
| await delay(100); | |
| await initGallery(items, url); | |
| } catch (err) { | |
| console.trace(); | |
| console.error(err); | |
| } | |
| } | |
| /** | |
| * Uploads a given file to a specified URL. | |
| * Once the file is uploaded, it provides a success message using toastr, | |
| * destroys the existing gallery, fetches the latest items, and reinitializes the gallery. | |
| * | |
| * @param {File} file - The file object to be uploaded. | |
| * @param {string} url - The URL indicating where the file should be uploaded. | |
| * @returns {Promise<void>} - Promise representing the completion of the file upload and gallery refresh. | |
| */ | |
| async function uploadFile(file, url) { | |
| try { | |
| // Convert the file to a base64 string | |
| const base64Data = await getBase64Async(file); | |
| // Create the payload | |
| const payload = { | |
| image: base64Data, | |
| ch_name: url, | |
| }; | |
| const response = await fetch('/api/images/upload', { | |
| method: 'POST', | |
| headers: getRequestHeaders(), | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! Status: ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| toastr.success(t`File uploaded successfully. Saved at: ${result.path}`); | |
| } catch (error) { | |
| console.error('There was an issue uploading the file:', error); | |
| // Replacing alert with toastr error notification | |
| toastr.error(t`Failed to upload the file.`); | |
| } | |
| } | |
| /** | |
| * Creates a new draggable container based on a template. | |
| * This function takes a template with the ID 'generic_draggable_template' and clones it. | |
| * The cloned element has its attributes set, a new child div appended, and is made visible on the body. | |
| * Additionally, it sets up the element to prevent dragging on its images. | |
| * @param {string} url - The URL of the image source. | |
| * @returns {Promise<void>} - Promise representing the completion of the draggable container creation. | |
| */ | |
| async function makeMovable(url) { | |
| console.debug('making new container from template'); | |
| const id = 'gallery'; | |
| const template = $('#generic_draggable_template').html(); | |
| const newElement = $(template); | |
| newElement.css({ 'background-color': 'var(--SmartThemeBlurTintColor)', 'opacity': 0 }); | |
| newElement.attr('forChar', id); | |
| newElement.attr('id', id); | |
| newElement.find('.drag-grabber').attr('id', `${id}header`); | |
| const dragTitle = newElement.find('.dragTitle'); | |
| dragTitle.addClass('flex-container justifySpaceBetween alignItemsBaseline'); | |
| const titleText = document.createElement('span'); | |
| titleText.textContent = t`Image Gallery`; | |
| dragTitle.append(titleText); | |
| const sortSelect = document.createElement('select'); | |
| sortSelect.classList.add('gallery-sort-select'); | |
| for (const sort of Object.values(SORT)) { | |
| const option = document.createElement('option'); | |
| option.value = sort.value; | |
| option.textContent = sort.label; | |
| sortSelect.appendChild(option); | |
| } | |
| sortSelect.addEventListener('change', async () => { | |
| const selectedOption = sortSelect.options[sortSelect.selectedIndex].value; | |
| setSortOrder(selectedOption); | |
| closeButton.trigger('click'); | |
| await showCharGallery(); | |
| }); | |
| sortSelect.value = getSortOrder(); | |
| dragTitle.append(sortSelect); | |
| // add no-scrollbar class to this element | |
| newElement.addClass('no-scrollbar'); | |
| // get the close button and set its id and data-related-id | |
| const closeButton = newElement.find('.dragClose'); | |
| closeButton.attr('id', `${id}close`); | |
| closeButton.attr('data-related-id', `${id}`); | |
| const topBarElement = document.createElement('div'); | |
| topBarElement.classList.add('flex-container', 'alignItemsCenter'); | |
| const onChangeFolder = async (/** @type {Event} */ e) => { | |
| if (e instanceof KeyboardEvent && e.key !== 'Enter') { | |
| return; | |
| } | |
| try { | |
| const newUrl = await getSanitizedFilename(galleryFolderInput.value); | |
| updateGalleryFolder(newUrl); | |
| closeButton.trigger('click'); | |
| await showCharGallery(); | |
| toastr.info(t`Gallery folder changed to ${newUrl}`); | |
| galleryFolderInput.value = newUrl; | |
| } catch (error) { | |
| console.error('Failed to change gallery folder:', error); | |
| toastr.error(error?.message || t`Unknown error`, t`Failed to change gallery folder`); | |
| } | |
| }; | |
| const onRestoreFolder = async () => { | |
| try { | |
| restoreGalleryFolder(); | |
| closeButton.trigger('click'); | |
| await showCharGallery(); | |
| } catch (error) { | |
| console.error('Failed to restore gallery folder:', error); | |
| toastr.error(error?.message || t`Unknown error`, t`Failed to restore gallery folder`); | |
| } | |
| }; | |
| const galleryFolderInput = document.createElement('input'); | |
| galleryFolderInput.type = 'text'; | |
| galleryFolderInput.placeholder = t`Folder Name`; | |
| galleryFolderInput.title = t`Enter a folder name to change the gallery folder`; | |
| galleryFolderInput.value = url; | |
| galleryFolderInput.classList.add('text_pole', 'gallery-folder-input', 'flex1'); | |
| galleryFolderInput.addEventListener('keyup', onChangeFolder); | |
| const galleryFolderAccept = document.createElement('div'); | |
| galleryFolderAccept.classList.add('right_menu_button', 'fa-solid', 'fa-check', 'fa-fw'); | |
| 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`; | |
| galleryFolderRestore.addEventListener('click', onRestoreFolder); | |
| topBarElement.appendChild(galleryFolderInput); | |
| topBarElement.appendChild(galleryFolderAccept); | |
| topBarElement.appendChild(galleryDeleteMode); | |
| topBarElement.appendChild(galleryFolderRestore); | |
| newElement.append(topBarElement); | |
| // Populate the gallery folder input with a list of available folders | |
| const folders = await getGalleryFolders(); | |
| $(galleryFolderInput) | |
| .autocomplete({ | |
| source: (i, o) => { | |
| const term = i.term.toLowerCase(); | |
| const filtered = folders.filter(f => f.toLowerCase().includes(term)); | |
| o(filtered); | |
| }, | |
| select: (e, u) => { | |
| galleryFolderInput.value = u.item.value; | |
| onChangeFolder(e); | |
| }, | |
| minLength: 0, | |
| }) | |
| .on('focus', () => $(galleryFolderInput).autocomplete('search', '')); | |
| //add a div for the gallery | |
| newElement.append('<div id="dragGallery"></div>'); | |
| $('#dragGallery').css('display', 'block'); | |
| $('#movingDivs').append(newElement); | |
| loadMovingUIState(); | |
| $(`.draggable[forChar="${id}"]`).css('display', 'block'); | |
| dragElement(newElement); | |
| newElement.transition({ | |
| opacity: 1, | |
| duration: animation_duration, | |
| easing: animation_easing, | |
| }); | |
| $(`.draggable[forChar="${id}"] img`).on('dragstart', (e) => { | |
| console.log('saw drag on avatar!'); | |
| e.preventDefault(); | |
| return false; | |
| }); | |
| } | |
| /** | |
| * Sets the gallery folder to a new URL. | |
| * @param {string} newUrl - The new URL to set for the gallery folder. | |
| */ | |
| function updateGalleryFolder(newUrl) { | |
| if (!newUrl) { | |
| throw new Error('Folder name cannot be empty'); | |
| } | |
| const context = SillyTavern.getContext(); | |
| if (context.groupId) { | |
| throw new Error('Cannot change gallery folder in group chat'); | |
| } | |
| if (context.characterId === undefined) { | |
| throw new Error('Character is not selected'); | |
| } | |
| const avatar = context.characters[context.characterId]?.avatar; | |
| const name = context.characters[context.characterId]?.name; | |
| if (!avatar) { | |
| throw new Error('Character PNG ID is not found'); | |
| } | |
| if (newUrl === name) { | |
| // Default folder name is picked, remove the override | |
| delete context.extensionSettings.gallery.folders[avatar]; | |
| } else { | |
| // Custom folder name is provided, set the override | |
| context.extensionSettings.gallery.folders[avatar] = newUrl; | |
| } | |
| context.saveSettingsDebounced(); | |
| } | |
| /** | |
| * Restores the gallery folder to the default value. | |
| */ | |
| function restoreGalleryFolder() { | |
| const context = SillyTavern.getContext(); | |
| if (context.groupId) { | |
| throw new Error('Cannot change gallery folder in group chat'); | |
| } | |
| if (context.characterId === undefined) { | |
| throw new Error('Character is not selected'); | |
| } | |
| const avatar = context.characters[context.characterId]?.avatar; | |
| if (!avatar) { | |
| throw new Error('Character PNG ID is not found'); | |
| } | |
| const existingOverride = context.extensionSettings.gallery.folders[avatar]; | |
| if (!existingOverride) { | |
| throw new Error('No folder override found'); | |
| } | |
| delete context.extensionSettings.gallery.folders[avatar]; | |
| context.saveSettingsDebounced(); | |
| } | |
| /** | |
| * Creates a new draggable image based on a template. | |
| * | |
| * This function clones a provided template with the ID 'generic_draggable_template', | |
| * appends the given image URL, ensures the element has a unique ID, | |
| * and attaches the element to the body. After appending, it also prevents | |
| * dragging on the appended image. | |
| * | |
| * @param {string} id - A base identifier for the new draggable element. | |
| * @param {string} url - The URL of the image to be added to the draggable element. | |
| */ | |
| function makeDragImg(id, url) { | |
| // Step 1: Clone the template content | |
| const template = document.getElementById('generic_draggable_template'); | |
| if (!(template instanceof HTMLTemplateElement)) { | |
| console.error('The element is not a <template> tag'); | |
| return; | |
| } | |
| const newElement = document.importNode(template.content, true); | |
| // Step 2: Append the given image | |
| const imgElem = document.createElement('img'); | |
| imgElem.src = url; | |
| let uniqueId = `draggable_${id}`; | |
| const draggableElem = /** @type {HTMLElement} */ (newElement.querySelector('.draggable')); | |
| if (draggableElem) { | |
| draggableElem.appendChild(imgElem); | |
| // Find a unique id for the draggable element | |
| let counter = 1; | |
| while (document.getElementById(uniqueId)) { | |
| uniqueId = `draggable_${id}_${counter}`; | |
| counter++; | |
| } | |
| draggableElem.id = uniqueId; | |
| // Ensure that the newly added element is displayed as block | |
| draggableElem.style.display = 'block'; | |
| //and has no padding unlike other non-zoomed-avatar draggables | |
| draggableElem.style.padding = '0'; | |
| // Add an id to the close button | |
| // If the close button exists, set related-id | |
| const closeButton = /** @type {HTMLElement} */ (draggableElem.querySelector('.dragClose')); | |
| if (closeButton) { | |
| closeButton.id = `${uniqueId}close`; | |
| closeButton.dataset.relatedId = uniqueId; | |
| } | |
| // Find the .drag-grabber and set its matching unique ID | |
| const dragGrabber = draggableElem.querySelector('.drag-grabber'); | |
| if (dragGrabber) { | |
| dragGrabber.id = `${uniqueId}header`; // appending _header to make it match the parent's unique ID | |
| } | |
| } | |
| // Step 3: Attach it to the movingDivs container | |
| document.getElementById('movingDivs').appendChild(newElement); | |
| // Step 4: Call dragElement and loadMovingUIState | |
| const appendedElement = document.getElementById(uniqueId); | |
| if (appendedElement) { | |
| var elmntName = $(appendedElement); | |
| loadMovingUIState(); | |
| dragElement(elmntName); | |
| // Prevent dragging the image | |
| $(`#${uniqueId} img`).on('dragstart', (e) => { | |
| console.log('saw drag on avatar!'); | |
| e.preventDefault(); | |
| return false; | |
| }); | |
| } else { | |
| console.error('Failed to append the template content or retrieve the appended content.'); | |
| } | |
| } | |
| /** | |
| * Sanitizes a given ID to ensure it can be used as an HTML ID. | |
| * This function replaces spaces and non-word characters with dashes. | |
| * It also removes any non-ASCII characters. | |
| * @param {string} id - The ID to be sanitized. | |
| * @returns {string} - The sanitized ID. | |
| */ | |
| function sanitizeHTMLId(id) { | |
| // Replace spaces and non-word characters | |
| id = id.replace(/\s+/g, '-') | |
| .replace(/[^\x00-\x7F]/g, '-') | |
| .replace(/\W/g, ''); | |
| return id; | |
| } | |
| /** | |
| * Processes a list of items (containing URLs) and creates a draggable box for the first item. | |
| * | |
| * If the provided list of items is non-empty, it takes the URL of the first item, | |
| * derives an ID from the URL, and uses the makeDragImg function to create | |
| * a draggable image element based on that ID and URL. | |
| * | |
| * @param {Array} items - A list of items where each item has a responsiveURL method that returns a URL. | |
| */ | |
| function viewWithDragbox(items) { | |
| if (items && items.length > 0) { | |
| const url = items[0].responsiveURL(); // Get the URL of the clicked image/video | |
| 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); | |
| } | |
| } | |
| } | |
| // Registers a simple command for opening the char gallery. | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'show-gallery', | |
| aliases: ['sg'], | |
| callback: () => { | |
| showCharGallery(); | |
| return ''; | |
| }, | |
| helpString: 'Shows the gallery.', | |
| })); | |
| SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| name: 'list-gallery', | |
| aliases: ['lg'], | |
| callback: listGalleryCommand, | |
| returns: 'list of images', | |
| namedArgumentList: [ | |
| SlashCommandNamedArgument.fromProps({ | |
| name: 'char', | |
| description: 'character name', | |
| typeList: [ARGUMENT_TYPE.STRING], | |
| enumProvider: commonEnumProviders.characters('character'), | |
| }), | |
| SlashCommandNamedArgument.fromProps({ | |
| name: 'group', | |
| description: 'group name', | |
| typeList: [ARGUMENT_TYPE.STRING], | |
| enumProvider: commonEnumProviders.characters('group'), | |
| }), | |
| ], | |
| helpString: 'List images in the gallery of the current char / group or a specified char / group.', | |
| })); | |
| async function listGalleryCommand(args) { | |
| try { | |
| let url = args.char ?? (args.group ? groups.find(it => it.name == args.group)?.id : null) ?? (selected_group || this_chid); | |
| if (!args.char && !args.group && !selected_group && this_chid !== undefined) { | |
| url = getGalleryFolder(characters[this_chid]); | |
| } | |
| const items = await getGalleryItems(url); | |
| return JSON.stringify(items.map(it => it.src)); | |
| } catch (err) { | |
| console.trace(); | |
| console.error(err); | |
| } | |
| return JSON.stringify([]); | |
| } | |
| // On extension load, ensure the settings are initialized | |
| (function () { | |
| initSettings(); | |
| eventSource.on(event_types.CHARACTER_RENAMED, (oldAvatar, newAvatar) => { | |
| const context = SillyTavern.getContext(); | |
| const galleryFolder = context.extensionSettings.gallery.folders[oldAvatar]; | |
| if (galleryFolder) { | |
| context.extensionSettings.gallery.folders[newAvatar] = galleryFolder; | |
| delete context.extensionSettings.gallery.folders[oldAvatar]; | |
| context.saveSettingsDebounced(); | |
| } | |
| }); | |
| eventSource.on(event_types.CHARACTER_DELETED, (data) => { | |
| const avatar = data?.character?.avatar; | |
| if (!avatar) return; | |
| const context = SillyTavern.getContext(); | |
| delete context.extensionSettings.gallery.folders[avatar]; | |
| context.saveSettingsDebounced(); | |
| }); | |
| eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (selectedOptionId) => { | |
| if (selectedOptionId === 'show_char_gallery') { | |
| showCharGallery(); | |
| } | |
| }); | |
| // Add an option to the dropdown | |
| $('#char-management-dropdown').append( | |
| $('<option>', { | |
| id: 'show_char_gallery', | |
| text: translate('Show Gallery'), | |
| }), | |
| ); | |
| })(); | |