您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Preloads images; download prompt filename; auto open slideshow; negative prompt persist option; keeps selection highlighted.
当前为
// ==UserScript== // @name PixAI Utilities Mod // @namespace Violentmonkey Scripts // @match https://pixai.art/* // @version 0.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 imgThumbsSelector = '[data-test-id="virtuoso-item-list"] .contents>button'; 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 = false; 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'); 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 (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 checkCompleted() { let observer = new MutationObserver(() => { if (!document.body.innerText.match(/completed/i)) { let firstPreview = document.querySelector(imgPreviewSelector); if(firstPreview){ firstPreview.click(); observer.disconnect(); }else{ let srcRestore = latestClickSrc(); selectThumbFromId(extractSrcId(srcRestore.src)); } } }); observer.observe(document.body, { childList: true, subtree: true }); } async function thumbListener(event) { let clickedElementTarget = eventToElement(event); let latestSrc = latestClickSrc(); 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(); checkCompleted() } function manageEnforceNegative(isChecked) { const textarea = document.querySelector('textarea[placeholder="Enter negative prompt here"]'); if (!isChecked) { 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) return; let textarea = document.querySelector('textarea[placeholder="Enter negative prompt here"]'); if (document.activeElement === textarea) { localStorage.removeItem('enforceNegativeNegativePrompt'); shouldEnforceNegative = false; return; } let storedValue = localStorage.getItem('enforceNegativeNegativePrompt'); if (storedValue) textarea.value = storedValue; } setInterval(syncNegativePrompt, 500); 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('textarea.w-full'); 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)); }); } 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 } `); })();