// ==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 = '↓';
button.id = 'scrollDownBtn';
button.onclick = () => {
scrollingToElement = !scrollingToElement;
button.innerHTML = scrollingToElement ? '▢' : '↓';
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 = '↓';
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
}
`);
})();