PixAI Utilities Mod

Preloads images; download prompt filename; auto open slideshow; negative prompt persist option; keeps selection highlighted.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            PixAI Utilities Mod
// @namespace       Violentmonkey Scripts
// @match           https://pixai.art/*
// @version         1.3.1
// @author          brunon
// @description     Preloads images; download prompt filename; auto open slideshow; negative prompt persist option; keeps selection highlighted.
// @grant           GM_addStyle
// @grant           GM_download
// @grant           GM_info
// @grant           GM_setClipboard
// @grant           GM.getValue
// @grant           GM_getValue
// @grant           GM.setValue
// @grant           GM_setValue
// @require         https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// @icon            https://www.google.com/s2/favicons?sz=64&domain=pixai.art
// ==/UserScript==


(async () => {




function saveValue(key, array) {
    const saveFunc = (typeof GM !== 'undefined' && GM.setValue) || GM_setValue;
    if (!saveFunc) return; // console.log("Oh no, no save method available");

    const result = saveFunc(key, array);
    if (result instanceof Promise) {
        result.then(() => { /* console.log("Array saved successfully"); */ })
              .catch(e => console.error("Error:", e));
    } else {
        // console.log("Array saved successfully");
    }
}


  function retrieveValueFromStorage(key) {
    if (typeof GM_getValue === "function") {
      return GM_getValue(key, false);
    }

    if (typeof GM === "object" && typeof GM.getValue === "function") {
      return GM.getValue(key, false).then(value => value);
    }

    console.error("Unsupported userscript manager.");
    return undefined;
  }







    if (!VM.shortcut) {
        console.error('VM.shortcut is not available!');
        return;
    }

    const shortcuts = new VM.shortcut.KeyboardService();
    shortcuts.enable();

    shortcuts.setContext('slideshowOpen', false);
    shortcuts.setContext('isInput', false);

    shortcuts.register(
        'd',
        () => {
            let downloadBtn = document.querySelector('#custom-download');
            if (!!downloadBtn) downloadBtn.click();
        },
        {
            condition: 'slideshowOpen',
        }
    );

    const shortcutMapping = {
      up: -3,
      down: 3,
      right: 1,
      left: -1
    };

    Object.entries(shortcutMapping).forEach(([shortcut, direction]) => {
      shortcuts.register(
        shortcut,
        () => arrowNavigation(direction),
        {
          condition: '!slideshowOpen && !isInput',
        }
      );
    });



    let regenerateBtnText = 'Regenerate All';


    let imgPreviewSelector = 'main img[src^="https://images-ng.pixai.art/images/thumb/"]';
    let imgOriginalSelector = 'main img[src^="https://images-ng.pixai.art/images/orig/"]';
    let imgThumbsSelector = '[data-test-id="virtuoso-item-list"] .contents>button';
    let promptTextareaSelector = 'textarea.w-full';
    let scrollListSelector = '[data-test-id="virtuoso-scroller"]';
    let selectedListThumbSelector = `${scrollListSelector} button.outline`;

    let thumbIcons = [];
    let openedPreviewCache = new Map();
    let lastCacheUpdate = 0;
    let slideshowPresent = false;
    let dbNameAntiban = `${GM_info.script.name}-${GM_info.uuid}`.replace(/\s+/g, '-');

    let generateButtonListener;
    let pauseThumbListener = false;
    let latestClickedElement;
    let currentImgObserver;
    let latestEventListener;
    let shouldEnforceNegative = !!localStorage.getItem('enforceNegativeNegativePrompt');
    let previewListCheckListener;
    let latestClickedID;
    let stopGenerationLoop = false;
    let latestToastMessage = "";
    let scrollingToElement = false;

    performDatabaseOperation(1, 'previews', (store, id) => {});


  async function waitForFocus() {
    return new Promise(resolve => {
        function onFocus() {
            window.removeEventListener('focus', onFocus);
            resolve();
        }
        if (document.hasFocus()) {
            resolve();
        } else {
            window.addEventListener('focus', onFocus);
        }
    });
}



    function findButtonByInnerText(innerText, extraSelector = '') {
      return [...document.querySelectorAll('button'+extraSelector)].find(button => button.textContent.includes(innerText)) || null;
    }



    await waitForElement(imgThumbsSelector);
    console.log("Running")

    window.print = function () { };


    function preloadImages(imageUrls) {
        imageUrls.forEach(url => {
            const img = new Image();
            img.src = url;
        });
    }

    async function waitForElements(selector) {
        const startTime = Date.now();
        const waitTime = 10000;

        return new Promise(resolve => {
            const checkInterval = setInterval(() => {
                const elements = document.querySelectorAll(selector);
                if (elements.length >= 4 || Date.now() - startTime > waitTime) {
                    clearInterval(checkInterval);
                    resolve(elements);
                }
            }, 150);
        });
    }

    async function updateThumbs(refresh = false) {
        if (refresh) {
            thumbIcons = [];
            await updatePreviewCache();
        }

        let newThumbs = await waitForElements(imgThumbsSelector);

        newThumbs.forEach(newThumb => {
            let id = extractPreviewId(newThumb);
            displayOpenedState(id, newThumb);

            if (!thumbIcons.includes(newThumb)) thumbIcons.push(newThumb);
        });

        updateListeners();
    }



    async function getImagePreviews() {
        return await waitForElements(imgPreviewSelector);
    }



    async function preloadFullImages() {
        const imagePreviews = await getImagePreviews();
        preloadImages(Array.from(imagePreviews).map(img => {
            return img.src.replace("thumb", "orig");
        }));
    }


async function waitForElement(selector, timeout = 10) {
    return new Promise((resolve) => {

        const timeoutId = setTimeout(() => resolve(null), timeout * 1000);

        const observer = new MutationObserver(() => {
            const element = document.querySelector(selector);
            if (!!element) {
                clearTimeout(timeoutId);
                observer.disconnect();
                resolve(element);
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    });
}






    async function waitForClass(element, className) {
        if (!element) {
            return Promise.reject('Element is null');
        }

        return new Promise(resolve => {
            const checkInterval = setInterval(() => {
                if (element.classList.contains(className)) {
                    clearInterval(checkInterval);
                    resolve();
                }
            }, 100); // Check every 100 milliseconds
        });
    }


    async function highlightSelected(target) {
        await waitForClass(target, 'ring-theme-primary');

        thumbIcons.forEach(icon => {
            icon.classList.remove('selected-thumb');
        });

        target.classList.add('selected-thumb');
    }

    function eventToElement(event) {
        return event.currentTarget;
    }



    async function updatePreviewCache() {
        if (Date.now() - lastCacheUpdate < 100) return Promise.resolve();
        lastCacheUpdate = Date.now();

        try {
            const request = openDatabase(); // Get the database request
            const db = await new Promise((resolve, reject) => {
                request.onsuccess = ({ target }) => resolve(target.result); // Resolve with the database object
                request.onerror = () => reject('IndexedDB error');
            });

            const store = db.transaction('previews', 'readonly').objectStore('previews');
            const allValues = [];

            return new Promise((resolve, reject) => {
                const cursorRequest = store.openCursor();
                cursorRequest.onsuccess = ({ target }) => {
                    const cursor = target.result;
                    if (!cursor) {
                        allValues.forEach(item => openedPreviewCache.set(item.id, item));
                        resolve();
                    } else {
                        allValues.push(cursor.value);
                        cursor.continue();
                    }
                };
                cursorRequest.onerror = () => reject('Error retrieving cache values');
            });
        } catch (error) {
            // console.error("Error accessing the database:", error);
            throw new Error("Error accessing the database:", error);
        }
    }
    async function measureUpdatePreviewCacheTime() {
        const startTime = performance.now(); // Start timing

        await updatePreviewCache();

        const endTime = performance.now(); // End timing
        console.log(`updatePreviewCache execution time: ${endTime - startTime} ms`);
    }

    measureUpdatePreviewCacheTime();


    function extractSrcId(src) {
        try {
            return src.split('/').pop();
        } catch (error) {
            console.error('Error occurred with src:', src);
        }
    }



    function extractPreviewId(element) {
        const img = element.querySelector('div img');
        const src = img?.getAttribute('src');
        if (!src) return null;

        return (extractSrcId(src))
    }

    function openDatabase() {
        return indexedDB.open(dbNameAntiban, 3);
    }



function performDatabaseOperation(id, storeName, operation) {
    const request = openDatabase();

    request.onupgradeneeded = function (event) {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(storeName)) {
            db.createObjectStore(storeName, { keyPath: 'id' });
        }
    };

    request.onsuccess = function (event) {
        const db = event.target.result;
        const transaction = db.transaction(storeName, 'readwrite');
        const store = transaction.objectStore(storeName);
        operation(store, id);
        transaction.oncomplete = () => null;
        transaction.onerror = () => console.error(`Transaction ${id} error: ${event.target.error}`);
    };

    request.onerror = function (event) {
        console.error('IndexedDB error:', event.target.error);
    };
}







    function upsertDatabase(id) {
        if (!id) {
            console.error('Invalid ID provided for upsert operation');
            return;
        }

        let infoID = { id, timestamp: new Date().toISOString() };

        performDatabaseOperation(id, 'previews', (store, id) => {
            store.put(infoID);
        });

        openedPreviewCache.set(id, infoID);

    }










    async function isIdPresentInDatabase(id, storeName) {
        return new Promise((resolve) => {
            performDatabaseOperation(id, storeName, (store, id) => {
                const request = store.get(id);
                request.onsuccess = () => resolve(request.result !== undefined);
                request.onerror = () => resolve(false);
            });
        });
    }





    async function getValueById(id, storeName) {
        const request = openDatabase();
        return new Promise((resolve, reject) => {
            request.onsuccess = ({ target }) => {
                const store = target.result.transaction(storeName, 'readonly').objectStore(storeName);
                store.get(id).onsuccess = e => resolve(e.target.result || null);
                store.get(id).onerror = () => reject('Error retrieving value');
            };
            request.onerror = () => reject('IndexedDB error');
        });
    }

    async function wasAlreadyOpenedCheck(id, forceRefresh = false) {

        if (forceRefresh) {
            let storedValue = await getValueById(id, 'previews');
            if (storedValue) openedPreviewCache.set(id, storedValue);
            return !!storedValue;
        }

        return openedPreviewCache.has(id);
    }



    function displayOpenedState(id, previewButton, forceRecheckDB = false) {
        let existingSpan = previewButton.querySelector('span[data-label="check"]');

        wasAlreadyOpenedCheck(id, forceRecheckDB).then(isPresent => {

            if (!isPresent && !!existingSpan) {
                existingSpan.remove();
                return;
            }

            if (isPresent && !existingSpan) {
                let span = document.createElement('span');
                span.setAttribute('data-label', 'check');
                span.textContent = '✔️';
                span.style.opacity = '0';
                previewButton.appendChild(span);
                setTimeout(() => span.style.opacity = '1', 5)
            }

        });
    }


    function selectThumbFromId(id) {
        let img = document.querySelector(`[data-test-id="virtuoso-item-list"] .contents>button img[src$="${id}"]`);
        if (!img) return;

        thumbIcons.forEach(icon => {
            icon.classList.remove('selected-thumb');
        });
        let elementToSelect = img.parentElement.parentElement;
        elementToSelect.classList.add('selected-thumb');
        // console.log("Selecting",elementToSelect,'because of ID',id);
    }


    function latestClickSrc() {
        if (!latestClickedElement) return {
            src: null,
            img: null
        };

        let latestClickImg = latestClickedElement.querySelector("img");
        if (!latestClickImg) return;

        return {
            src: latestClickImg.src,
            img: latestClickImg
        };
    }

    async function updateListenersOnNewGeneration() {

      /**
       * Removes currentImgObserver whis is only used inside this function
       *
       * */


        let srcRestore = latestClickSrc();
      console.log("srcRestore:", srcRestore)
        if (!srcRestore) return;


        if (currentImgObserver) currentImgObserver.disconnect();

        currentImgObserver = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (!(mutation.type === 'attributes' && mutation.attributeName === 'src')) return;



                let srcId = extractSrcId(srcRestore.src);

                console.log("Generate click detected, runnign selectThumbFromId on",srcId, "from updateListenersOnNewGeneration()");

                selectThumbFromId(srcId);
                updateThumbs(true);
                currentImgObserver.disconnect();
            });
        });


        currentImgObserver.observe(srcRestore.img, { attributes: true, attributeFilter: ['src'] });
    }





    async function addGenerateButtonListener() {
      /*
       * Replaces the previous listener to updateListenersOnNewGeneration()
       *
       * */


        while (!findButtonByInnerText('Generate', ".outline-2")) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }

        let generateBtn = findButtonByInnerText('Generate', ".outline-2");
        if (!generateBtn) return;

        console.log("We have generateBtn", generateBtn)

        if (generateButtonListener) generateBtn.removeEventListener('click', generateButtonListener);

        generateButtonListener = () => {
            console.log("Generate btn clicked")
            updateListenersOnNewGeneration();
        }

        generateBtn.addEventListener('click', generateButtonListener);
        console.log("We added generateButtonListener()", generateButtonListener)
    }

    async function openFirstImage() {
        let observer = new MutationObserver(() => {
            if (!document.body.innerText.match(/completed/i)) {

                let firstPreview = document.querySelector(imgOriginalSelector);
                if (!!firstPreview) {
                    // await new Promise(resolve => setTimeout(resolve, 100));
                    firstPreview.click();
                    observer.disconnect();
                } else {
                    console.error("Couldn't locate", firstPreview, "with", imgPreviewSelector)
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }


    async function thumbListener(event) {
        if(isTabActive('favorites')) {
          console.log("Returning, because we're inside favourites");
          return;
        }

        let clickedElementTarget = eventToElement(event);

        let latestSrc = latestClickSrc();

        let id = extractPreviewId(clickedElementTarget);

        if(!id) {
          console.log("Element has no ID. Returning");
          return;
        }

        // console.log("ID:",id);

        // console.log("Clicked:", clickedElementTarget, event)


        latestClickedElement = clickedElementTarget;


        let alreadyInsideDB = await isIdPresentInDatabase(id, 'previews');

        // console.log(alreadyInsideDB ? "Was already inside DB" : "Wasn't previously inside DB");


        await highlightSelected(clickedElementTarget);
        console.log(clickedElementTarget, "has been highlighted");

        // upsertDatabase(id);
        latestClickedID = id;
        saveValue("latestClickedID", {id: latestClickedID})

        preloadFullImages();
        updateThumbs();


        displayOpenedState(id, clickedElementTarget, true);
        addGenerateButtonListener();


        // createEnforceNegativeCheckbox();

        if (!alreadyInsideDB) openFirstImage()

    }


    function manageEnforceNegative(isChecked) {
        const textarea = document.querySelector('textarea[placeholder="Enter negative prompt here"]');
        let storedValue = localStorage.getItem('enforceNegativeNegativePrompt');

        if (!isChecked && !storedValue) {
            localStorage.removeItem('enforceNegativeNegativePrompt');
            shouldEnforceNegative = false;
            return;
        }
        localStorage.setItem('enforceNegativeNegativePrompt', textarea.value);
        shouldEnforceNegative = true;
    }

    let toggleCheckbox = (selector, isChecked) => {
        let checkboxLabel = document.querySelector(selector);
        if (!checkboxLabel) return;
        let checkedPath = checkboxLabel.querySelector('.checked-path');

        checkedPath.style.display = !isChecked ? 'none' : 'block';

        let checkbox = checkboxLabel.querySelector('input[type="checkbox"]');
        checkbox.checked = isChecked;

        // console.log("setting",checkbox, "to", isChecked)
    };

    function syncNegativePrompt() {
        toggleCheckbox('#enforce-negative', shouldEnforceNegative);
        if (!shouldEnforceNegative) {
            // console.log("if (!shouldEnforceNegative)")

            return;
        }

        let textarea = document.querySelector('textarea[placeholder="Enter negative prompt here"]');
        if (document.activeElement === textarea) {
            localStorage.removeItem('enforceNegativeNegativePrompt');
            shouldEnforceNegative = false;
            // console.log("Active element, skipping")
            return;
        }

        let storedValue = localStorage.getItem('enforceNegativeNegativePrompt');
        if (!storedValue) {
            // console.log("f (!storedValue) {")

            return;
        }

        if (textarea.value.trim() === storedValue.trim()) {
            // console.log("alrready changed");
            return;
        }



        textarea.value = ''; // Clear the textarea
        textarea.value = storedValue; // Set the new value
        textarea.dispatchEvent(new Event('input', { bubbles: true })); // Trigger input event
        console.log("set to", storedValue);


        awakeTextarea(textarea);

    }


    setInterval(syncNegativePrompt, 1 * 1000);

    async function slideShowDowloadButtonManager() {
        const observer = new MutationObserver(() => {
            const nextButton = document.querySelector('.pswp__button--arrow--next');
            if (!nextButton || !!document.querySelector('#custom-download')) return;

            const button = document.createElement('button');
            button.id = 'custom-download';
            button.title = 'Download with prompt as file name';
            button.innerHTML = `<svg aria-hidden="true" viewBox="0 0 32 32" width="32" height="32"><use class="pswp__icn-shadow" xlink:href="#pswp__icn-download"></use><path d="M20.5 14.3 17.1 18V10h-2.2v7.9l-3.4-3.6L10 16l6 6.1 6-6.1ZM23 23H9v2h14Z" id="pswp__icn-download"></path></svg>`;

            button.onclick = async () => {
                nextButton.style.cursor = 'pointer';
                await saveImage();
            };

            nextButton.insertAdjacentElement('beforebegin', button);
            setTimeout(() => button.classList.add('show'), 10);
            addHideSlideshowListeners(button);
        });

        observer.observe(document.body, { childList: true, subtree: true });
        window.addEventListener('beforeunload', () => {
            observer.disconnect();
        });
    }


    function sanitizeFilename(filename) {
        const maxLength = 125;
        const dotIndex = filename.lastIndexOf('.');
        const extension = dotIndex !== -1 ? filename.substring(dotIndex) : ''; // Get the file extension
        const baseFilename = dotIndex !== -1 ? filename.substring(0, dotIndex) : filename; // Get the base filename
        const sanitizedBase = baseFilename
            .replace(/[^a-zA-Z0-9-_\. ]/g, '_') // Replace invalid characters with underscores
            .replace(/\s+/g, '_') // Replace spaces with underscores
            .replace(/_+/g, '_') // Remove duplicate underscores
            .substring(0, maxLength - extension.length); // Truncate to max length minus extension

        return sanitizedBase + extension; // Combine sanitized base with extension
    }


    async function saveImage() {
        const textarea = document.querySelector(promptTextareaSelector);
        const imgSrc = document.querySelector('#pswp__items .pswp__item[aria-hidden="false"] img.pswp__img')?.src;
        if (!textarea || !imgSrc) return;
        let filename = sanitizeFilename(`${textarea.value.trim()}.png`);
        await GM_download({
            url: imgSrc,
            name: filename,
            saveAs: false
        });
    }


    function highlightOpenThumbnail() {
        let firstPreviewSrc = document.querySelector(imgPreviewSelector);
        if (!firstPreviewSrc) return console.warn(`Element not found for selector: ${imgPreviewSelector}`);

        let currentId = extractSrcId(firstPreviewSrc.src);
        if (!currentId) return console.warn('Current ID could not be extracted from the image source.');
        console.log("highlightOpenThumbnail(): Selection id", currentId, "from", firstPreviewSrc.src, 'of', firstPreviewSrc)
        selectThumbFromId(currentId);
    }

    function createCheckbox(id, labelText, onChangeFunction) {
        const newLabel = document.createElement('label');
        newLabel.style.userSelect = "none";
        newLabel.id = id;
        newLabel.innerHTML = `
    ${labelText}
    <input type="checkbox" style="display:none">
    <svg class="sc-eDvSVe cSfylm MuiSvgIcon-root MuiSvgIcon-fontSizeMedium" focusable="false" aria-hidden="true" viewBox="0 0 24 24">
      <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"></path>
      <path class="checked-path" d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path>
    </svg>`;

        const checkbox = newLabel.querySelector('input[type="checkbox"]');
        const checkedPath = newLabel.querySelector('.checked-path');
        checkbox.checked = shouldEnforceNegative;

        checkbox.addEventListener('change', function () {
            checkedPath.style.display = !this.checked ? 'none' : 'block';
            if (typeof onChangeFunction === 'function') onChangeFunction(this.checked);
        });

        return newLabel;
    }

    function createEnforceNegativeCheckbox() {
        const negativeLabel = Array.from(document.querySelectorAll('label')).find(label => label.textContent === 'Negative');
        if (!negativeLabel || document.getElementById('enforce-negative')) return;

        negativeLabel.parentElement.insertBefore(createCheckbox('enforce-negative', 'Enforce negative for every task', manageEnforceNegative), negativeLabel);
    }



    async function slideShowLifetimeMonitor() {
        while (true) {

            while (!document.querySelector('#pswp__items')) await new Promise(resolve => setTimeout(resolve, 100));
            slideshowPresent = true;
            shortcuts.setContext('slideshowOpen', true);

            if (latestClickedID) {
                console.log("Adding", latestClickedID, "(latestClickedID) to DB");
                upsertDatabase(latestClickedID);
            }




            while (!!document.querySelector('#pswp__items')) await new Promise(resolve => setTimeout(resolve, 100));
            slideshowPresent = false;
            shortcuts.setContext('slideshowOpen', false);



            await new Promise(resolve => setTimeout(resolve, 100));

            highlightOpenThumbnail();
        }
    }


    function checkOpenedImageOpacity() {
        let openedImage = document.querySelector('div.pswp__item[aria-hidden="false"] > div.pswp__zoom-wrap > img');

        // console.log("opened",openedImage,"opacity:",parseFloat(openedImage.style.opacity));

        return !!openedImage && (openedImage.complete && openedImage.naturalWidth !== 0) && (!openedImage.style.opacity || parseFloat(openedImage.style.opacity) >= 1);


    }


    async function addHideSlideshowListeners(customDownload) {
        if (!customDownload) return;

        let firstImageShowed = false;

        const elements = document.querySelectorAll('.pswp__scroll-wrap, .pswp__button--close');
        const bg = document.querySelector('.pswp__bg');
        bg.classList.add("black-bg");

        if (!firstImageShowed) {
            firstImageShowed = checkOpenedImageOpacity();
        }



        const hideDownload = async (e) => {
            const initialOpacity = parseFloat(bg.style.opacity) || 1;
            let opacityChanged = false;


            if (!firstImageShowed) {
                firstImageShowed = checkOpenedImageOpacity();
            }


            const observer = new MutationObserver(() => {



                if (firstImageShowed && !bg.classList.contains("black-bg") && !opacityChanged) bg.classList.add("black-bg");

                if (parseFloat(bg.style.opacity) !== initialOpacity) {
                    customDownload.classList.add('hide');
                    opacityChanged = true;
                    bg.classList.remove("black-bg");
                    observer.disconnect();
                }
            });

            observer.observe(bg, { attributes: true });
            document.addEventListener('visibilitychange', () => {
                if (document.visibilityState === 'visible' && !opacityChanged) observer.disconnect();
            });

            bg.classList.remove("black-bg");

            await new Promise(resolve => setTimeout(resolve, 2000));
            if (opacityChanged) {
                customDownload.classList.add('hide');
            }
            observer.disconnect();
        };

        elements.forEach(el => el.addEventListener('click', hideDownload));
        document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideDownload(e); });
    }





function isTabActive(tab = "favorites") {
 /*

   tasks / favorites

  */
  let tabFavourites = document.querySelector(`#radix-\\:r9\\:-trigger-${tab}[data-state="active"]`);

  return !!tabFavourites && tabFavourites.getAttribute('data-state') === 'active';
}



  function createActionButton(id = "newBtn", text = "Go!", action = () => {}, remove = false) {
    let generateButton = findButtonByInnerText('Generate', '.outline-2');
    if (!generateButton) return;


    let button = document.querySelector(`#${id}`);

    if (remove && !!button){
        button.remove();
        return;
    }

    if (!!button || remove) return;

    button = document.createElement('button');
    button.id = id;
    button.textContent = text;
    button.classList.add('mr-3', 'grid-cols-[auto_1fr_auto]', 'focus:outline', 'outline-2', 'outline-offset-1', 'duration-75', 'disabled:text-black/50', 'disabled:bg-black/15', 'dark:disabled:text-white/50', 'dark:disabled:bg-white/15', 'text-white', 'bg-purple-600', 'hover:bg-purple-500', 'outline-purple-600', 'dark:outline-purple-500', 'text-sm', '[--ui-size:theme(spacing.8)]', 'w-full', 'mlg:w-auto', 'h-[--height]', 'flex', 'items-center', 'transition', 'font-semibold', 'dense:text-sm', 'px-4', 'mlg:py-2', 'dense:px-3', 'dense:py-0.5', 'rounded-xl', 'mlg:rounded-lg', 'dense:rounded-md');
    button.classList.add('bg-gradient-member');
    generateButton.parentNode.insertBefore(button, generateButton);
    button.addEventListener('click', action);
  }

createActionButton("Copy", "Copy!", ()=>{

    const textarea = document.querySelector(promptTextareaSelector);
  if (!textarea) return;


GM_setClipboard(textarea.value.trim(),'text/plain');

});






  function pasteButton(remove = false) {
    let generateButton = findButtonByInnerText('Generate', '.outline-2');
    if (!generateButton) return;


    let pasteTbn = document.querySelector('#pasteButton');

    if (remove && !!pasteTbn){
        pasteTbn.remove();
        return;
    }

    if (!!pasteTbn || remove) return;

    pasteTbn = document.createElement('button');
    pasteTbn.id = 'pasteButton';
    pasteTbn.textContent = "Paste! (demo)";
    pasteTbn.classList.add('mr-3', 'grid-cols-[auto_1fr_auto]', 'focus:outline', 'outline-2', 'outline-offset-1', 'duration-75', 'disabled:text-black/50', 'disabled:bg-black/15', 'dark:disabled:text-white/50', 'dark:disabled:bg-white/15', 'text-white', 'bg-purple-600', 'hover:bg-purple-500', 'outline-purple-600', 'dark:outline-purple-500', 'text-sm', '[--ui-size:theme(spacing.8)]', 'w-full', 'mlg:w-auto', 'h-[--height]', 'flex', 'items-center', 'transition', 'font-semibold', 'dense:text-sm', 'px-4', 'mlg:py-2', 'dense:px-3', 'dense:py-0.5', 'rounded-xl', 'mlg:rounded-lg', 'dense:rounded-md');
    pasteTbn.classList.add('bg-gradient-member');
    generateButton.parentNode.insertBefore(pasteTbn, generateButton);
    pasteTbn.addEventListener('click', pasteIntoTextarea);

    const textarea = document.querySelector(promptTextareaSelector);
    console.log(textarea)
  }

  // pasteButton();


function pasteIntoTextarea() {
    const textarea = document.querySelector(promptTextareaSelector);
    if (textarea) {
        const currentValue = 'Your text here';

        // Focus on the textarea
        textarea.focus();
        textarea.dispatchEvent(new Event('focus', { bubbles: true }));

        // Simulate paste event
        const clipboardData = new DataTransfer();
        clipboardData.setData('text/plain', currentValue);

        const pasteEvent = new ClipboardEvent('paste', {
            bubbles: true,
            cancelable: true
        });

        Object.defineProperty(pasteEvent, 'clipboardData', {
            get: () => clipboardData
        });

        textarea.dispatchEvent(pasteEvent);

        // Directly trigger React’s onChange handler
        const changeEvent = new Event('change', { bubbles: true });
        textarea.value = currentValue;
        textarea.dispatchEvent(changeEvent);

        // Manually trigger input and change events to ensure React updates state
        textarea.dispatchEvent(new Event('input', { bubbles: true }));
        textarea.dispatchEvent(new Event('change', { bubbles: true }));

        // Optionally simulate submit action
        const submitButton = document.querySelector('button[type="submit"]');
        if (submitButton) submitButton.click();
    }
}







function favouritesRegenerateButton(remove = false) {
  let generateButton = findButtonByInnerText('Generate', '.outline-2');
  if (!generateButton) return;


  let regenerateBtn = document.querySelector('#favouriteRegenerateAutomation');

  if (remove && !!regenerateBtn){
      regenerateBtn.remove();
      return;
  }

  if (!!regenerateBtn || remove) return;

  // console.log("Adding because remove =",remove)
  regenerateBtn = document.createElement('button');
  regenerateBtn.id = 'favouriteRegenerateAutomation';
  regenerateBtn.textContent = regenerateBtnText;
  regenerateBtn.classList.add('mr-3', 'grid-cols-[auto_1fr_auto]', 'focus:outline', 'outline-2', 'outline-offset-1', 'duration-75', 'disabled:text-black/50', 'disabled:bg-black/15', 'dark:disabled:text-white/50', 'dark:disabled:bg-white/15', 'text-white', 'bg-purple-600', 'hover:bg-purple-500', 'outline-purple-600', 'dark:outline-purple-500', 'text-sm', '[--ui-size:theme(spacing.8)]', 'w-full', 'mlg:w-auto', 'h-[--height]', 'flex', 'items-center', 'transition', 'font-semibold', 'dense:text-sm', 'px-4', 'mlg:py-2', 'dense:px-3', 'dense:py-0.5', 'rounded-xl', 'mlg:rounded-lg', 'dense:rounded-md');
  regenerateBtn.classList.add('bg-gradient-member');
  generateButton.parentNode.insertBefore(regenerateBtn, generateButton);
  regenerateBtn.addEventListener('click', favouriteRegenerateAutomation);

  let tasksTab = findButtonByInnerText('Tasks', '[role="tab"]');
  tasksTab.addEventListener('click', () => favouritesRegenerateButton(true));

}


async function favouriteRegenerateAutomation(){



    let regenerateBtn = document.querySelector('#favouriteRegenerateAutomation');

    if (!!regenerateBtn && regenerateBtn.getAttribute('data-running') === 'true') {
        console.log("Stopping")
        stopGenerationLoop = true;
        stoppedFavouriteLoopButtonDOM(regenerateBtn);
        return;
    }

    let generateButton = findButtonByInnerText('Generate', '.outline-2');

    if (!generateButton || !regenerateBtn) throw new Error('Generate button or regenerate button not found. What the actual..? Impossible!');

    let currentlySelectedItem = document.querySelector(selectedListThumbSelector);
    if(!currentlySelectedItem) {
      alert("Please select the oldest item to start")
      return;
    }

    regenerateBtn.textContent = 'Stop!';
    regenerateBtn.classList.remove('bg-gradient-member');
    regenerateBtn.setAttribute('data-running', 'true');


    stopGenerationLoop = false;

    let successCount = 0;

    while (true) {
        if(successCount > 0) await new Promise(resolve => setTimeout(resolve, 1000));

        if(stopGenerationLoop) break;

        let selectedButton = document.querySelector('button.outline');

        console.info("Start wait for Generate Button")
         while (!findButtonByInnerText('Generate', ".outline-2")) {
            await new Promise(resolve => setTimeout(resolve, 100));
        }
        console.info("End wait for Generate Button")

        generateButton.click();

        console.log("Waiting for toast")
        const toast = await waitForToastify();
        console.log("Toast found");

        let toastText = toast.textContent;


        let closeBtn = toast.querySelector('.Toastify__close-button');
        if(!!closeBtn) {
          closeBtn.click();
          console.log("Closed toast");
        }

        if(stopGenerationLoop) break;

        if (toastText.includes('submitted')) {
            console.log('Success for',document.querySelector(promptTextareaSelector).value);
            successCount++;
        } else if (toastText.includes('error')) {
            console.log('Error, retrying...');
            continue;
        } else if(document.body.innerText.includes("Too many tasks in queue")) {
            console.log("Too many tasks in queue");
            findButtonByInnerText('OK').click();
            continue;
        }

        let newestElement = document.querySelector('[data-state="active"][role="tabpanel"] button');
        // console.log("Newest element:", newestElement);

        if(selectedButton === newestElement){
          console.log("End reached");
          break;
        }

        if (!arrowNavigation(-1, selectedButton)) break;

    }

    console.log("Success count:",successCount)

    stoppedFavouriteLoopButtonDOM(regenerateBtn);

}

  function stoppedFavouriteLoopButtonDOM(regenerateBtn){
    regenerateBtn.textContent = regenerateBtnText;
    regenerateBtn.classList.add('bg-gradient-member');
    regenerateBtn.removeAttribute('data-running');
  }


async function waitForToastify() {
    return new Promise(resolve => {
        const checkToast = setInterval(() => {
            if (stopGenerationLoop) {
                clearInterval(checkToast);
                return;
            }
            const toastElement = document.querySelector('.Toastify');
            let messageText = toastElement.textContent;
            if (!!toastElement && (messageText.includes('submitted') || messageText.includes('fail'))) {
                latestToastMessage = messageText;
                clearInterval(checkToast);
                resolve(toastElement);
            }
        }, 100);
    });
}





function arrowNavigation(direction = -1, givenButton = null) {
  let selectedButton = givenButton || document.querySelector('button.outline');
  if (!selectedButton) return;

  let tileHeight = selectedButton.offsetHeight;

  let scrollContainer = document.querySelector(scrollListSelector);
  if (!scrollContainer) return console.error("There is no scrollContainer");

  let positionFromTop = selectedButton.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top;

  if (positionFromTop <= tileHeight) {
      scrollContainer.scrollTop -= tileHeight * 1.5;
  }

  let buttons = Array.from(document.querySelectorAll('button'));
  let selectedIndex = buttons.indexOf(selectedButton);

  if (selectedIndex <= 0) return;

  let previousButton = buttons[selectedIndex + direction];
  if (!previousButton) {
    console.log("No more buttons")
    return false;
 }

  previousButton.click();
  return true;
}






    function updateListeners() {
        if(isTabActive('favorites')){
          thumbIcons = [];
          favouritesRegenerateButton();
          console.log("Returning because we are inside Favourites");
          return;
        }

        // console.info("Running checks for listeners inside updateListeners()");
        thumbIcons.forEach(icon => {
            if (!icon.thumbClickListenerAdded) {
                icon.addEventListener('click', thumbListener);
                icon.thumbClickListenerAdded = true;
            }
        });

        favouritesRegenerateButton(true); // remove

    }

    async function detectScroll() {
        let scroller = await waitForElements(scrollListSelector);

        scroller[0].addEventListener('scroll', () => {
            requestAnimationFrame(() => updateThumbs(true));
        });
    }

    const scale = (x) => {
        if (x >= 2) return 1.5;
        if (x < 1) return x;
        return 0.5 * (x - 1) + 1;
    };



    function awakeTextarea(textarea, input = null) {
        textarea.focus();
        if (!input) {
            textarea.value += ' ';
        } else {
            textarea.value = input;
        }
        textarea.dispatchEvent(new InputEvent('input', { bubbles: true }));
        setTimeout(() => {
            textarea.value = textarea.value.slice(0, -1);
            textarea.dispatchEvent(new InputEvent('input', { bubbles: true }));
            textarea.dispatchEvent(new Event('change', { bubbles: true }));
            textarea.blur();
        }, 500);
    };



const textareaPasteFix = async (promptTextareaSelector) => {
    const textarea = await waitForElement(promptTextareaSelector);
    textarea.addEventListener('paste', (event) => {
        const clipboardData = event.clipboardData.getData('text/plain');
        const modifiedText = clipboardData
            .replace(/(\d+) year old/g, '$1yo')
            .replace(/(\d+) years old/g, '$1yo')
            .replace(/(\d+) years/g, '$1yo')
            .replace(/(\d+) years-old/g, '$1yo')
            .replace(/(\d+) year-old/g, '$1yo')
            .replace(/(\d+)-year-old/g, '$1yo')
            .replace(/(\d+)-years-old/g, '$1yo')
            .replace(/thx/g, 'thanks')
            .replace('suckling', 'sucking')
            .replace(/\(\(/g, '(')
            .replace(/\)\)/g, ')')
            .replace(/:(\d+(\.\d+)?)/g, (match, num) => {
                const scaledNum = scale(parseFloat(num));
                return `:${Math.round(scaledNum * 10) / 10}`;
            })
            .replace(/<[^>]*>/g, '');  // Remove content between '<' and '>'


        if (clipboardData !== modifiedText) {
            event.preventDefault();
            const { selectionStart: start, selectionEnd: end } = textarea;
            const textBefore = textarea.value.slice(0, start);
            const textAfter = textarea.value.slice(end);
            textarea.value = textBefore + modifiedText + textAfter;
            textarea.selectionStart = textarea.selectionEnd = start + modifiedText.length;
            awakeTextarea(textarea);
        }
    });
};


function createScrollToBottomButton() {
  const list = document.querySelector(scrollListSelector);
  const button = document.createElement('div');
  Object.assign(button.style, {
    position: 'fixed',
    bottom: '10px',
    left: '14px',
    width: '50px',
    height: '50px',
    backgroundColor: 'rgba(0,0,0,0.6)',
    borderRadius: '10px',
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    cursor: 'pointer',
    color: 'white',
    fontSize: '24px',
    zIndex: '5'
  });
  button.innerHTML = '&#8595;';
  button.id = 'scrollDownBtn';
  button.onclick = () => {


    scrollingToElement = !scrollingToElement;
    button.innerHTML = scrollingToElement ? '&#9634;' : '&#8595;';
    if(scrollingToElement) scrollUntilElement();
  };
  list.appendChild(button);
}


function scrollUntilElement(id = null) {
  const list = document.querySelector(scrollListSelector);
  if (!list) return;

  if (id === null) {
    id = retrieveValueFromStorage('latestClickedID')?.['id'];
    console.log("Finding ID", id);
  }

  const interval = setInterval(() => {

    if(!scrollingToElement) {
      clearInterval(interval);
      return;
    }

    const target = id !== undefined
      ? Array.from(list.querySelectorAll('img')).find(img => img.src.includes(id))
      : list.querySelector('span[data-label="check"]');

    if (target) {
      clearInterval(interval);
      const offset = window.innerHeight * 0.7;
      const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
      window.scrollTo({ top: targetPosition, behavior: 'smooth' });

      let scrollBtn = document.querySelector('#scrollDownBtn');
      if (!!scrollBtn) scrollBtn.innerHTML = '&#8595;';
      updateThumbs();

    } else {
      list.scrollBy(0, 100);
    }
  }, 100);
}





  await waitForFocus();

  const textarea = await waitForElement(promptTextareaSelector, 60*60);

  textarea.addEventListener('focus', () => {
    shortcuts.setContext('isInput', true);
    // console.log("isInput", true)
  });

  textarea.addEventListener('blur', () => {
    shortcuts.setContext('isInput', false);
    // console.log("isInput", false)
  });





window.addEventListener('blur', () => {
    if(!previewListCheckListener) return;
    previewListCheckListener.disconnect();
});

window.addEventListener('focus', () => {
    startScrollListListener();
});


    function startScrollListListener() {
        const targetNode = document.querySelector(scrollListSelector);
        if (!targetNode) {
          console.log("Returning because there is no scrollListSelector inside startScrollListListener()");
          return;
        }

        previewListCheckListener = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType !== 1 || !node.matches('[data-label="check"]')) return;

                    const parent = node.parentElement;
                    const id = extractPreviewId(parent);
                    if (!openedPreviewCache.get(id)) {
                        node.remove();
                        console.log("Removed", node, "because", id, "not present")
                    }
                });
            });
        });

        previewListCheckListener.observe(targetNode, { childList: true, subtree: true });
    }

    startScrollListListener();




    textareaPasteFix(promptTextareaSelector)


    upsertDatabase(1)
    detectScroll();
    slideShowLifetimeMonitor();

    slideShowDowloadButtonManager();
    createEnforceNegativeCheckbox();
    updateThumbs(true);


    window.addEventListener('focus', () => {
        updateThumbs(true);
        highlightOpenThumbnail();
    });


    let scrollList = await waitForElement(scrollListSelector);

  createScrollToBottomButton()


    // let scrollList = document.querySelector(scrollListSelector);

    scrollList.addEventListener('mouseenter', () => updateThumbs(true));
    scrollList.addEventListener('mouseleave', () => updateThumbs(true));
    scrollList.addEventListener('mousemove', () => updateThumbs());



    GM_addStyle(`
#favouriteRegenerateAutomation:hover{
  filter: brightness(1.1) contrast(1.05);
}
.black-bg{
  opacity: 1 !important;
}

.pswp__bg{
  transition: opacity .5s cubic-bezier(0.25,0.1,0.25,1);
}

.selected-thumb{
  outline: 2px solid hsla(0, 12%, 85.3%, 0.77);
  transition: outline 100ms;
}
#app .ring-2{
  box-shadow: none;
}
[data-test-id="virtuoso-item-list"] .contents>button img{
  cursor:pointer;
  transition: filter .1s ease;
}
[data-test-id="virtuoso-item-list"] .contents>button img:hover{
filter: brightness(1.05);
}
[data-label="check"] {
	position: absolute;
	left: 0;
	bottom: 0;
	background: #ffffff2b;
	border-top-right-radius: 5px;
	backdrop-filter: blur(10px);
	filter: brightness(1.3);
  transition: opacity .5s ease;
  opacity: 0;
}
#custom-download{
  width: 75px;
  height: 100px;
  margin-top: -50px;
  position: absolute;
  top: 50%;
  right: calc(75px + .5rem);
  display: flex; justify-content: center; align-items: center;
  opacity:0;
  will-change: opacity;
  transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
}
#custom-download.show{
  opacity:1;
}
#custom-download.hide{
  opacity:0;
}
#custom-download > svg{
fill: var(--pswp-icon-color);
/*   color: var(--pswp-icon-color-secondary); */
  width: 60px;
  height: 60px;
}
#custom-download > svg > .pswp__icn-shadow {
	stroke-width: 1px;
}
button[aria-label="Download"][type="button"]{
  display:none
}
`);
})();