// ==UserScript==
// @name PixAI Utilities Mod
// @namespace Violentmonkey Scripts
// @match https://pixai.art/*
// @version 1.0.0
// @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
// ==/UserScript==
(async () => {
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 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 currentImgSrcObserver;
let latestEventListener;
let shouldEnforceNegative = !!localStorage.getItem('enforceNegativeNegativePrompt');
async function runCode() {
await waitForElement(imgThumbsSelector);
console.log("Running")
}
await runCode();
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) {
let scroller = document.querySelector('[data-test-id="virtuoso-scroller"]');
let loader = document.createElement('progress');
loader.style.width = "100%";
// scroller.prepend(loader);
if (refresh) {
thumbIcons = [];
await updatePreviewCache();
}
const newThumbs = await waitForElements(imgThumbsSelector);
newThumbs.forEach(newThumb => {
let id = extractPreviewId(newThumb);
displayOpenedState(id, newThumb);
if (!thumbIcons.includes(newThumb)) thumbIcons.push(newThumb);
});
updateListeners();
loader.remove();
}
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) {
return new Promise((resolve) => {
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
}
async function waitForClass(element, className) {
return new Promise(resolve => {
const checkInterval = setInterval(() => {
if (element.classList.contains(className)) {
clearInterval(checkInterval);
resolve();
}
}, 100); // Check every 100 milliseconds
});
}
async function highlightSelected(event) {
const target = event.currentTarget;
await waitForClass(target, 'ring-offset-background-light');
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('IndexedDB error');
}
}
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, 1);
}
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); // Perform the operation
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) {
// console.log('Upsert ID:', id); // Log the ID being passed
if (!id) {
console.error('Invalid ID provided for upsert operation');
return; // Exit early if ID is invalid
}
performDatabaseOperation(id, 'previews', (store, id) => {
store.put({ id }); // Store the object with the id property
});
}
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 alreadyOpened(id, force = false) {
if (force) {
const value = await getValueById(id, 'previews');
if (value) openedPreviewCache.set(id, value);
return !!value;
}
return openedPreviewCache.has(id);
}
function displayOpenedState(id, element, force = false) {
let existingSpan = element.querySelector('span[data-label="check"]');
alreadyOpened(id, force).then(isPresent => {
if (!isPresent && existingSpan) {
existingSpan.remove(); // Remove the span if ID is not present
return;
}
if (isPresent && !existingSpan) {
let span = document.createElement('span');
span.setAttribute('data-label', 'check');
span.textContent = '✔️';
element.appendChild(span);
}
});
}
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() {
let srcRestore = latestClickSrc();
if(!srcRestore) return;
if (currentImgSrcObserver) currentImgSrcObserver.disconnect();
currentImgSrcObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
selectThumbFromId(extractSrcId(srcRestore.src));
updateThumbs(true);
currentImgSrcObserver.disconnect();
}
});
});
currentImgSrcObserver.observe(srcRestore.img, { attributes: true, attributeFilter: ['src'] });
}
async function addGenerateButtonListener() {
const button = await waitForElement('[data-tutorial-target="generate-button"]');
if (!button) return;
if (generateButtonListener) button.removeEventListener('click', generateButtonListener);
generateButtonListener = () => {
updateListenersOnNewGeneration();
}
button.addEventListener('click', generateButtonListener);
}
async function openFirstImage() {
let observer = new MutationObserver(() => {
if (!document.body.innerText.match(/completed/i)) {
let firstPreview = document.querySelector(imgOriginalSelector);
if(firstPreview){
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) {
let clickedElementTarget = eventToElement(event);
let latestSrc = latestClickSrc();
if(!latestSrc) return;
if (latestSrc.src && extractSrcId(clickedElementTarget.src) === extractSrcId(latestSrc.src)) return;
console.log("Clicked:", clickedElementTarget)
let id = extractPreviewId(clickedElementTarget);
latestClickedElement = clickedElementTarget;
await highlightSelected(event);
console.log("Was highlight selected!")
upsertDatabase(id);
preloadFullImages();
updateThumbs();
displayOpenedState(id, clickedElementTarget, true);
addGenerateButtonListener();
createEnforceNegativeCheckbox();
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() {
slideShowLifetimeMonitor();
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.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();
});
}
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;
await GM_download({
url: imgSrc,
name: `${textarea.value.trim()}.png`,
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;
while (!!document.querySelector('#pswp__items')) await new Promise(resolve => setTimeout(resolve, 100));
slideshowPresent = false;
await new Promise(resolve => setTimeout(resolve, 100));
highlightOpenThumbnail();
}
}
function logBgStyleChanges() {
const bg = document.querySelector('.pswp__bg');
if (!bg) return;
const observer = new MutationObserver(() => {
console.debug(bg.style.cssText);
});
observer.observe(bg, { attributes: true });
}
async function addHideSlideshowListeners(customDownload) {
if (!customDownload) return;
const elements = document.querySelectorAll('.pswp__scroll-wrap, .pswp__button--close');
const bg = document.querySelector('.pswp__bg');
const hideDownload = async (e) => {
const initialOpacity = parseFloat(bg.style.opacity) || 1;
let opacityChanged = false;
const observer = new MutationObserver(() => {
if (parseFloat(bg.style.opacity) !== initialOpacity) {
// console.log(parseFloat(bg.style.opacity))
customDownload.classList.add('hide');
opacityChanged = true;
observer.disconnect();
}
});
observer.observe(bg, { attributes: true });
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && !opacityChanged) observer.disconnect();
});
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 updateListeners() {
thumbIcons.forEach(icon => {
if (!icon.thumbClickListenerAdded) {
icon.addEventListener('click', thumbListener);
icon.thumbClickListenerAdded = true;
}
});
}
async function detectScroll() {
let scroller = await waitForElements('[data-test-id="virtuoso-scroller"]');
// if (!scroller.length) return;
scroller[0].addEventListener('scroll', () => {
// console.log("scrolling")
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();
}, 200);
};
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-old/g, '$1yo')
.replace(/thx/g, 'thanks')
.replace(/\(\(/g, '(')
.replace(/\)\)/g, ')')
.replace(/:(\d+(\.\d+)?)/g, (match, num) => {
const scaledNum = scale(parseFloat(num));
return `:${Math.round(scaledNum * 10) / 10}`;
});
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);
}
});
};
textareaPasteFix(promptTextareaSelector)
upsertDatabase(1)
detectScroll();
slideShowDowloadButtonManager();
createEnforceNegativeCheckbox();
updateThumbs(true);
window.addEventListener('focus', () => {
updateThumbs(true);
highlightOpenThumbnail();
});
GM_addStyle(`
.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;
}
#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
}
`);
})();