diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index d77e59eac..05113c5f1 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -347,6 +347,7 @@ body.movingUI #sheld, body.movingUI .drawer-content, body.movingUI #expression-holder, body.movingUI .zoomed_avatar, +body.movingUI .draggable, body.movingUI #floatingPrompt, body.movingUI #groupMemberListPopout { resize: both; diff --git a/public/index.html b/public/index.html index a1edce383..2c3e26f20 100644 --- a/public/index.html +++ b/public/index.html @@ -4387,6 +4387,16 @@ + + +
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index acc26e9c4..f4073fa8a 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -17,6 +17,7 @@ import { getEntitiesList, getThumbnailUrl, selectCharacterById, + eventSource, } from "../script.js"; import { @@ -34,6 +35,7 @@ import { debounce, delay, getStringHash, waitUntilCondition } from "./utils.js"; import { chat_completion_sources, oai_settings } from "./openai.js"; import { getTokenCount } from "./tokenizers.js"; + var RPanelPin = document.getElementById("rm_button_panel_pin"); var LPanelPin = document.getElementById("lm_button_panel_pin"); var WIPanelPin = document.getElementById("WI_panel_pin"); @@ -448,8 +450,9 @@ export function dragElement(elmnt) { topbar, topbarWidth, topBarFirstX, topBarLastX, topBarLastY, sheldWidth; var elmntName = elmnt.attr('id'); - + console.log(`dragElement called for ${elmntName}`); const elmntNameEscaped = $.escapeSelector(elmntName); + console.log(`dragElement escaped name: ${elmntNameEscaped}`); const elmntHeader = $(`#${elmntNameEscaped}header`); if (elmntHeader.length) { @@ -556,6 +559,7 @@ export function dragElement(elmnt) { console.debug(`Saving ${elmntName} Height/Width`) power_user.movingUIState[elmntName].width = width; power_user.movingUIState[elmntName].height = height; + eventSource.emit('resizeUI', elmntName); saveSettingsDebounced(); }) } @@ -950,8 +954,15 @@ export function initRossMods() { CheckLocal(); } + // Helper function to check if nanogallery2's lightbox is active + function isNanogallery2LightboxActive() { + // Check if the body has the 'nGY2On' class, adjust this based on actual behavior + return $('body').hasClass('nGY2_body_scrollbar'); + } + if (event.key == "ArrowLeft") { //swipes left if ( + !isNanogallery2LightboxActive() && // Check if lightbox is NOT active $(".swipe_left:last").css('display') === 'flex' && $("#send_textarea").val() === '' && $("#character_popup").css("display") === "none" && @@ -963,6 +974,7 @@ export function initRossMods() { } if (event.key == "ArrowRight") { //swipes right if ( + !isNanogallery2LightboxActive() && // Check if lightbox is NOT active $(".swipe_right:last").css('display') === 'flex' && $("#send_textarea").val() === '' && $("#character_popup").css("display") === "none" && @@ -973,6 +985,7 @@ export function initRossMods() { } } + if (event.ctrlKey && event.key == "ArrowUp") { //edits last USER message if chatbar is empty and focused if ( $("#send_textarea").val() === '' && @@ -1074,6 +1087,13 @@ export function initRossMods() { } } + if ($(".draggable").is(":visible")) { + // Remove the first matched element + $('.draggable:first').remove(); + return; + } + + if (event.ctrlKey && /^[1-9]$/.test(event.key)) { // This will eventually be to trigger quick replies event.preventDefault(); diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js new file mode 100644 index 000000000..39fc982ef --- /dev/null +++ b/public/scripts/extensions/gallery/index.js @@ -0,0 +1,398 @@ +import { + eventSource, + this_chid, + characters, + getRequestHeaders, +} from "../../../script.js"; +import { selected_group } from "../../group-chats.js"; +import { loadFileToDocument } from "../../utils.js"; +import { loadMovingUIState } from '../../power-user.js'; +import { dragElement } from '../../RossAscends-mods.js' + +const extensionName = "gallery"; +const extensionFolderPath = `scripts/extensions/${extensionName}/`; +let firstTime = true; + +// Exposed defaults for future tweaking +let thumbnailHeight = 150; +let paginationVisiblePages = 10; +let paginationMaxLinesPerPage = 2; +let galleryMaxRows = 3; + + +/** + * 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} - Resolves with an array of gallery item objects, rejects on error. + */ +async function getGalleryItems(url) { + const response = await fetch(`/listimgfiles/${url}`, { + method: 'POST', + headers: getRequestHeaders(), + }); + + 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; +} + +/** + * 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} 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} - Promise representing the completion of the gallery initialization. + */ +async function initGallery(items, url) { + $("#dragGallery").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, + }); + + $(document).ready(function () { + $('.nGY2GThumbnailImage').on('click', function () { + let imageUrl = $(this).find('.nGY2GThumbnailImg').attr('src'); + // Do what you want with the imageUrl, for example: + // Display it in a full-size view or replace the gallery grid content with this image + console.log(imageUrl); + }); + }); + + eventSource.on('resizeUI', function (elmntName) { + console.log('resizeUI saw', elmntName); + // Your logic here + + // If you want to resize the nanogallery2 instance when this event is triggered: + jQuery("#dragGallery").nanogallery2('resize'); + }); + + const dropZone = $('#dragGallery'); + //remove any existing handlers + dropZone.off('dragover'); + dropZone.off('dragleave'); + dropZone.off('drop'); + + + // Initialize dropzone handlers + dropZone.on('dragover', function (e) { + e.stopPropagation(); // Ensure this event doesn't propagate + e.preventDefault(); + $(this).addClass('dragging'); // Add a CSS class to change appearance during drag-over + }); + + dropZone.on('dragleave', function (e) { + e.stopPropagation(); // Ensure this event doesn't propagate + $(this).removeClass('dragging'); + }); + + dropZone.on('drop', function (e) { + e.stopPropagation(); // Ensure this event doesn't propagate + e.preventDefault(); + $(this).removeClass('dragging'); + let file = e.originalEvent.dataTransfer.files[0]; + uploadFile(file, url); // Added url parameter to know where to upload + }); +} + +/** + * 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} - Promise representing the completion of the gallery display process. + */ +async function showCharGallery() { + // 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 { + let url = selected_group || this_chid; + if (!selected_group && this_chid) { + const char = characters[this_chid]; + url = char.avatar.replace(".png", ""); + } + + const items = await getGalleryItems(url); + // if there already is a gallery, destroy it and place this one in its place + if ($(`#dragGallery`).length) { + $(`#dragGallery`).nanogallery2("destroy"); + initGallery(items, url); + } else { + makeMovable(); + setTimeout(async () => { + await initGallery(items, url); + }, 100); + } + + } 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} - Promise representing the completion of the file upload and gallery refresh. + */ +async function uploadFile(file, url) { + // Convert the file to a base64 string + const reader = new FileReader(); + reader.onloadend = async function () { + const base64Data = reader.result; + + // Create the payload + const payload = { + image: base64Data + }; + + // Add the ch_name from the provided URL (assuming it's the character name) + payload.ch_name = url; + + try { + const headers = await getRequestHeaders(); + + // Merge headers with content-type for JSON + Object.assign(headers, { + 'Content-Type': 'application/json' + }); + + const response = await fetch('/uploadimage', { + method: 'POST', + headers: headers, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const result = await response.json(); + + toastr.success('File uploaded successfully. Saved at: ' + result.path); + + // Refresh the gallery + $("#dragGallery").nanogallery2("destroy"); // Destroy old gallery + const newItems = await getGalleryItems(url); // Fetch the latest items + initGallery(newItems, url); // Reinitialize the gallery with new items and pass 'url' + + + } catch (error) { + console.error("There was an issue uploading the file:", error); + + // Replacing alert with toastr error notification + toastr.error('Failed to upload the file.'); + } + } + reader.readAsDataURL(file); +} + +$(document).ready(function () { + // Register an event listener + eventSource.on("charManagementDropdown", (selectedOptionId) => { + if (selectedOptionId === "show_char_gallery") { + showCharGallery(); + } + }); + + // Add an option to the dropdown + $("#char-management-dropdown").append( + $("