PixAI Utilities Mod

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

  1. // ==UserScript==
  2. // @name PixAI Utilities Mod
  3. // @namespace Violentmonkey Scripts
  4. // @match https://pixai.art/*
  5. // @version 1.3.1
  6. // @author brunon
  7. // @description Preloads images; download prompt filename; auto open slideshow; negative prompt persist option; keeps selection highlighted.
  8. // @grant GM_addStyle
  9. // @grant GM_download
  10. // @grant GM_info
  11. // @grant GM_setClipboard
  12. // @grant GM.getValue
  13. // @grant GM_getValue
  14. // @grant GM.setValue
  15. // @grant GM_setValue
  16. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
  17. // @icon https://www.google.com/s2/favicons?sz=64&domain=pixai.art
  18. // ==/UserScript==
  19.  
  20.  
  21. (async () => {
  22.  
  23.  
  24.  
  25.  
  26. function saveValue(key, array) {
  27. const saveFunc = (typeof GM !== 'undefined' && GM.setValue) || GM_setValue;
  28. if (!saveFunc) return; // console.log("Oh no, no save method available");
  29.  
  30. const result = saveFunc(key, array);
  31. if (result instanceof Promise) {
  32. result.then(() => { /* console.log("Array saved successfully"); */ })
  33. .catch(e => console.error("Error:", e));
  34. } else {
  35. // console.log("Array saved successfully");
  36. }
  37. }
  38.  
  39.  
  40. function retrieveValueFromStorage(key) {
  41. if (typeof GM_getValue === "function") {
  42. return GM_getValue(key, false);
  43. }
  44.  
  45. if (typeof GM === "object" && typeof GM.getValue === "function") {
  46. return GM.getValue(key, false).then(value => value);
  47. }
  48.  
  49. console.error("Unsupported userscript manager.");
  50. return undefined;
  51. }
  52.  
  53.  
  54.  
  55.  
  56.  
  57.  
  58.  
  59. if (!VM.shortcut) {
  60. console.error('VM.shortcut is not available!');
  61. return;
  62. }
  63.  
  64. const shortcuts = new VM.shortcut.KeyboardService();
  65. shortcuts.enable();
  66.  
  67. shortcuts.setContext('slideshowOpen', false);
  68. shortcuts.setContext('isInput', false);
  69.  
  70. shortcuts.register(
  71. 'd',
  72. () => {
  73. let downloadBtn = document.querySelector('#custom-download');
  74. if (!!downloadBtn) downloadBtn.click();
  75. },
  76. {
  77. condition: 'slideshowOpen',
  78. }
  79. );
  80.  
  81. const shortcutMapping = {
  82. up: -3,
  83. down: 3,
  84. right: 1,
  85. left: -1
  86. };
  87.  
  88. Object.entries(shortcutMapping).forEach(([shortcut, direction]) => {
  89. shortcuts.register(
  90. shortcut,
  91. () => arrowNavigation(direction),
  92. {
  93. condition: '!slideshowOpen && !isInput',
  94. }
  95. );
  96. });
  97.  
  98.  
  99.  
  100. let regenerateBtnText = 'Regenerate All';
  101.  
  102.  
  103. let imgPreviewSelector = 'main img[src^="https://images-ng.pixai.art/images/thumb/"]';
  104. let imgOriginalSelector = 'main img[src^="https://images-ng.pixai.art/images/orig/"]';
  105. let imgThumbsSelector = '[data-test-id="virtuoso-item-list"] .contents>button';
  106. let promptTextareaSelector = 'textarea.w-full';
  107. let scrollListSelector = '[data-test-id="virtuoso-scroller"]';
  108. let selectedListThumbSelector = `${scrollListSelector} button.outline`;
  109.  
  110. let thumbIcons = [];
  111. let openedPreviewCache = new Map();
  112. let lastCacheUpdate = 0;
  113. let slideshowPresent = false;
  114. let dbNameAntiban = `${GM_info.script.name}-${GM_info.uuid}`.replace(/\s+/g, '-');
  115.  
  116. let generateButtonListener;
  117. let pauseThumbListener = false;
  118. let latestClickedElement;
  119. let currentImgObserver;
  120. let latestEventListener;
  121. let shouldEnforceNegative = !!localStorage.getItem('enforceNegativeNegativePrompt');
  122. let previewListCheckListener;
  123. let latestClickedID;
  124. let stopGenerationLoop = false;
  125. let latestToastMessage = "";
  126. let scrollingToElement = false;
  127.  
  128. performDatabaseOperation(1, 'previews', (store, id) => {});
  129.  
  130.  
  131. async function waitForFocus() {
  132. return new Promise(resolve => {
  133. function onFocus() {
  134. window.removeEventListener('focus', onFocus);
  135. resolve();
  136. }
  137. if (document.hasFocus()) {
  138. resolve();
  139. } else {
  140. window.addEventListener('focus', onFocus);
  141. }
  142. });
  143. }
  144.  
  145.  
  146.  
  147. function findButtonByInnerText(innerText, extraSelector = '') {
  148. return [...document.querySelectorAll('button'+extraSelector)].find(button => button.textContent.includes(innerText)) || null;
  149. }
  150.  
  151.  
  152.  
  153. await waitForElement(imgThumbsSelector);
  154. console.log("Running")
  155.  
  156. window.print = function () { };
  157.  
  158.  
  159. function preloadImages(imageUrls) {
  160. imageUrls.forEach(url => {
  161. const img = new Image();
  162. img.src = url;
  163. });
  164. }
  165.  
  166. async function waitForElements(selector) {
  167. const startTime = Date.now();
  168. const waitTime = 10000;
  169.  
  170. return new Promise(resolve => {
  171. const checkInterval = setInterval(() => {
  172. const elements = document.querySelectorAll(selector);
  173. if (elements.length >= 4 || Date.now() - startTime > waitTime) {
  174. clearInterval(checkInterval);
  175. resolve(elements);
  176. }
  177. }, 150);
  178. });
  179. }
  180.  
  181. async function updateThumbs(refresh = false) {
  182. if (refresh) {
  183. thumbIcons = [];
  184. await updatePreviewCache();
  185. }
  186.  
  187. let newThumbs = await waitForElements(imgThumbsSelector);
  188.  
  189. newThumbs.forEach(newThumb => {
  190. let id = extractPreviewId(newThumb);
  191. displayOpenedState(id, newThumb);
  192.  
  193. if (!thumbIcons.includes(newThumb)) thumbIcons.push(newThumb);
  194. });
  195.  
  196. updateListeners();
  197. }
  198.  
  199.  
  200.  
  201. async function getImagePreviews() {
  202. return await waitForElements(imgPreviewSelector);
  203. }
  204.  
  205.  
  206.  
  207. async function preloadFullImages() {
  208. const imagePreviews = await getImagePreviews();
  209. preloadImages(Array.from(imagePreviews).map(img => {
  210. return img.src.replace("thumb", "orig");
  211. }));
  212. }
  213.  
  214.  
  215. async function waitForElement(selector, timeout = 10) {
  216. return new Promise((resolve) => {
  217.  
  218. const timeoutId = setTimeout(() => resolve(null), timeout * 1000);
  219.  
  220. const observer = new MutationObserver(() => {
  221. const element = document.querySelector(selector);
  222. if (!!element) {
  223. clearTimeout(timeoutId);
  224. observer.disconnect();
  225. resolve(element);
  226. }
  227. });
  228. observer.observe(document.body, { childList: true, subtree: true });
  229. });
  230. }
  231.  
  232.  
  233.  
  234.  
  235.  
  236.  
  237. async function waitForClass(element, className) {
  238. if (!element) {
  239. return Promise.reject('Element is null');
  240. }
  241.  
  242. return new Promise(resolve => {
  243. const checkInterval = setInterval(() => {
  244. if (element.classList.contains(className)) {
  245. clearInterval(checkInterval);
  246. resolve();
  247. }
  248. }, 100); // Check every 100 milliseconds
  249. });
  250. }
  251.  
  252.  
  253. async function highlightSelected(target) {
  254. await waitForClass(target, 'ring-theme-primary');
  255.  
  256. thumbIcons.forEach(icon => {
  257. icon.classList.remove('selected-thumb');
  258. });
  259.  
  260. target.classList.add('selected-thumb');
  261. }
  262.  
  263. function eventToElement(event) {
  264. return event.currentTarget;
  265. }
  266.  
  267.  
  268.  
  269. async function updatePreviewCache() {
  270. if (Date.now() - lastCacheUpdate < 100) return Promise.resolve();
  271. lastCacheUpdate = Date.now();
  272.  
  273. try {
  274. const request = openDatabase(); // Get the database request
  275. const db = await new Promise((resolve, reject) => {
  276. request.onsuccess = ({ target }) => resolve(target.result); // Resolve with the database object
  277. request.onerror = () => reject('IndexedDB error');
  278. });
  279.  
  280. const store = db.transaction('previews', 'readonly').objectStore('previews');
  281. const allValues = [];
  282.  
  283. return new Promise((resolve, reject) => {
  284. const cursorRequest = store.openCursor();
  285. cursorRequest.onsuccess = ({ target }) => {
  286. const cursor = target.result;
  287. if (!cursor) {
  288. allValues.forEach(item => openedPreviewCache.set(item.id, item));
  289. resolve();
  290. } else {
  291. allValues.push(cursor.value);
  292. cursor.continue();
  293. }
  294. };
  295. cursorRequest.onerror = () => reject('Error retrieving cache values');
  296. });
  297. } catch (error) {
  298. // console.error("Error accessing the database:", error);
  299. throw new Error("Error accessing the database:", error);
  300. }
  301. }
  302. async function measureUpdatePreviewCacheTime() {
  303. const startTime = performance.now(); // Start timing
  304.  
  305. await updatePreviewCache();
  306.  
  307. const endTime = performance.now(); // End timing
  308. console.log(`updatePreviewCache execution time: ${endTime - startTime} ms`);
  309. }
  310.  
  311. measureUpdatePreviewCacheTime();
  312.  
  313.  
  314. function extractSrcId(src) {
  315. try {
  316. return src.split('/').pop();
  317. } catch (error) {
  318. console.error('Error occurred with src:', src);
  319. }
  320. }
  321.  
  322.  
  323.  
  324. function extractPreviewId(element) {
  325. const img = element.querySelector('div img');
  326. const src = img?.getAttribute('src');
  327. if (!src) return null;
  328.  
  329. return (extractSrcId(src))
  330. }
  331.  
  332. function openDatabase() {
  333. return indexedDB.open(dbNameAntiban, 3);
  334. }
  335.  
  336.  
  337.  
  338. function performDatabaseOperation(id, storeName, operation) {
  339. const request = openDatabase();
  340.  
  341. request.onupgradeneeded = function (event) {
  342. const db = event.target.result;
  343. if (!db.objectStoreNames.contains(storeName)) {
  344. db.createObjectStore(storeName, { keyPath: 'id' });
  345. }
  346. };
  347.  
  348. request.onsuccess = function (event) {
  349. const db = event.target.result;
  350. const transaction = db.transaction(storeName, 'readwrite');
  351. const store = transaction.objectStore(storeName);
  352. operation(store, id);
  353. transaction.oncomplete = () => null;
  354. transaction.onerror = () => console.error(`Transaction ${id} error: ${event.target.error}`);
  355. };
  356.  
  357. request.onerror = function (event) {
  358. console.error('IndexedDB error:', event.target.error);
  359. };
  360. }
  361.  
  362.  
  363.  
  364.  
  365.  
  366.  
  367.  
  368. function upsertDatabase(id) {
  369. if (!id) {
  370. console.error('Invalid ID provided for upsert operation');
  371. return;
  372. }
  373.  
  374. let infoID = { id, timestamp: new Date().toISOString() };
  375.  
  376. performDatabaseOperation(id, 'previews', (store, id) => {
  377. store.put(infoID);
  378. });
  379.  
  380. openedPreviewCache.set(id, infoID);
  381.  
  382. }
  383.  
  384.  
  385.  
  386.  
  387.  
  388.  
  389.  
  390.  
  391.  
  392.  
  393. async function isIdPresentInDatabase(id, storeName) {
  394. return new Promise((resolve) => {
  395. performDatabaseOperation(id, storeName, (store, id) => {
  396. const request = store.get(id);
  397. request.onsuccess = () => resolve(request.result !== undefined);
  398. request.onerror = () => resolve(false);
  399. });
  400. });
  401. }
  402.  
  403.  
  404.  
  405.  
  406.  
  407. async function getValueById(id, storeName) {
  408. const request = openDatabase();
  409. return new Promise((resolve, reject) => {
  410. request.onsuccess = ({ target }) => {
  411. const store = target.result.transaction(storeName, 'readonly').objectStore(storeName);
  412. store.get(id).onsuccess = e => resolve(e.target.result || null);
  413. store.get(id).onerror = () => reject('Error retrieving value');
  414. };
  415. request.onerror = () => reject('IndexedDB error');
  416. });
  417. }
  418.  
  419. async function wasAlreadyOpenedCheck(id, forceRefresh = false) {
  420.  
  421. if (forceRefresh) {
  422. let storedValue = await getValueById(id, 'previews');
  423. if (storedValue) openedPreviewCache.set(id, storedValue);
  424. return !!storedValue;
  425. }
  426.  
  427. return openedPreviewCache.has(id);
  428. }
  429.  
  430.  
  431.  
  432. function displayOpenedState(id, previewButton, forceRecheckDB = false) {
  433. let existingSpan = previewButton.querySelector('span[data-label="check"]');
  434.  
  435. wasAlreadyOpenedCheck(id, forceRecheckDB).then(isPresent => {
  436.  
  437. if (!isPresent && !!existingSpan) {
  438. existingSpan.remove();
  439. return;
  440. }
  441.  
  442. if (isPresent && !existingSpan) {
  443. let span = document.createElement('span');
  444. span.setAttribute('data-label', 'check');
  445. span.textContent = '✔️';
  446. span.style.opacity = '0';
  447. previewButton.appendChild(span);
  448. setTimeout(() => span.style.opacity = '1', 5)
  449. }
  450.  
  451. });
  452. }
  453.  
  454.  
  455. function selectThumbFromId(id) {
  456. let img = document.querySelector(`[data-test-id="virtuoso-item-list"] .contents>button img[src$="${id}"]`);
  457. if (!img) return;
  458.  
  459. thumbIcons.forEach(icon => {
  460. icon.classList.remove('selected-thumb');
  461. });
  462. let elementToSelect = img.parentElement.parentElement;
  463. elementToSelect.classList.add('selected-thumb');
  464. // console.log("Selecting",elementToSelect,'because of ID',id);
  465. }
  466.  
  467.  
  468. function latestClickSrc() {
  469. if (!latestClickedElement) return {
  470. src: null,
  471. img: null
  472. };
  473.  
  474. let latestClickImg = latestClickedElement.querySelector("img");
  475. if (!latestClickImg) return;
  476.  
  477. return {
  478. src: latestClickImg.src,
  479. img: latestClickImg
  480. };
  481. }
  482.  
  483. async function updateListenersOnNewGeneration() {
  484.  
  485. /**
  486. * Removes currentImgObserver whis is only used inside this function
  487. *
  488. * */
  489.  
  490.  
  491. let srcRestore = latestClickSrc();
  492. console.log("srcRestore:", srcRestore)
  493. if (!srcRestore) return;
  494.  
  495.  
  496. if (currentImgObserver) currentImgObserver.disconnect();
  497.  
  498. currentImgObserver = new MutationObserver(mutations => {
  499. mutations.forEach(mutation => {
  500. if (!(mutation.type === 'attributes' && mutation.attributeName === 'src')) return;
  501.  
  502.  
  503.  
  504. let srcId = extractSrcId(srcRestore.src);
  505.  
  506. console.log("Generate click detected, runnign selectThumbFromId on",srcId, "from updateListenersOnNewGeneration()");
  507.  
  508. selectThumbFromId(srcId);
  509. updateThumbs(true);
  510. currentImgObserver.disconnect();
  511. });
  512. });
  513.  
  514.  
  515. currentImgObserver.observe(srcRestore.img, { attributes: true, attributeFilter: ['src'] });
  516. }
  517.  
  518.  
  519.  
  520.  
  521.  
  522. async function addGenerateButtonListener() {
  523. /*
  524. * Replaces the previous listener to updateListenersOnNewGeneration()
  525. *
  526. * */
  527.  
  528.  
  529. while (!findButtonByInnerText('Generate', ".outline-2")) {
  530. await new Promise(resolve => setTimeout(resolve, 100));
  531. }
  532.  
  533. let generateBtn = findButtonByInnerText('Generate', ".outline-2");
  534. if (!generateBtn) return;
  535.  
  536. console.log("We have generateBtn", generateBtn)
  537.  
  538. if (generateButtonListener) generateBtn.removeEventListener('click', generateButtonListener);
  539.  
  540. generateButtonListener = () => {
  541. console.log("Generate btn clicked")
  542. updateListenersOnNewGeneration();
  543. }
  544.  
  545. generateBtn.addEventListener('click', generateButtonListener);
  546. console.log("We added generateButtonListener()", generateButtonListener)
  547. }
  548.  
  549. async function openFirstImage() {
  550. let observer = new MutationObserver(() => {
  551. if (!document.body.innerText.match(/completed/i)) {
  552.  
  553. let firstPreview = document.querySelector(imgOriginalSelector);
  554. if (!!firstPreview) {
  555. // await new Promise(resolve => setTimeout(resolve, 100));
  556. firstPreview.click();
  557. observer.disconnect();
  558. } else {
  559. console.error("Couldn't locate", firstPreview, "with", imgPreviewSelector)
  560. }
  561. }
  562. });
  563. observer.observe(document.body, { childList: true, subtree: true });
  564. }
  565.  
  566.  
  567. async function thumbListener(event) {
  568. if(isTabActive('favorites')) {
  569. console.log("Returning, because we're inside favourites");
  570. return;
  571. }
  572.  
  573. let clickedElementTarget = eventToElement(event);
  574.  
  575. let latestSrc = latestClickSrc();
  576.  
  577. let id = extractPreviewId(clickedElementTarget);
  578.  
  579. if(!id) {
  580. console.log("Element has no ID. Returning");
  581. return;
  582. }
  583.  
  584. // console.log("ID:",id);
  585.  
  586. // console.log("Clicked:", clickedElementTarget, event)
  587.  
  588.  
  589. latestClickedElement = clickedElementTarget;
  590.  
  591.  
  592. let alreadyInsideDB = await isIdPresentInDatabase(id, 'previews');
  593.  
  594. // console.log(alreadyInsideDB ? "Was already inside DB" : "Wasn't previously inside DB");
  595.  
  596.  
  597. await highlightSelected(clickedElementTarget);
  598. console.log(clickedElementTarget, "has been highlighted");
  599.  
  600. // upsertDatabase(id);
  601. latestClickedID = id;
  602. saveValue("latestClickedID", {id: latestClickedID})
  603.  
  604. preloadFullImages();
  605. updateThumbs();
  606.  
  607.  
  608. displayOpenedState(id, clickedElementTarget, true);
  609. addGenerateButtonListener();
  610.  
  611.  
  612. // createEnforceNegativeCheckbox();
  613.  
  614. if (!alreadyInsideDB) openFirstImage()
  615.  
  616. }
  617.  
  618.  
  619. function manageEnforceNegative(isChecked) {
  620. const textarea = document.querySelector('textarea[placeholder="Enter negative prompt here"]');
  621. let storedValue = localStorage.getItem('enforceNegativeNegativePrompt');
  622.  
  623. if (!isChecked && !storedValue) {
  624. localStorage.removeItem('enforceNegativeNegativePrompt');
  625. shouldEnforceNegative = false;
  626. return;
  627. }
  628. localStorage.setItem('enforceNegativeNegativePrompt', textarea.value);
  629. shouldEnforceNegative = true;
  630. }
  631.  
  632. let toggleCheckbox = (selector, isChecked) => {
  633. let checkboxLabel = document.querySelector(selector);
  634. if (!checkboxLabel) return;
  635. let checkedPath = checkboxLabel.querySelector('.checked-path');
  636.  
  637. checkedPath.style.display = !isChecked ? 'none' : 'block';
  638.  
  639. let checkbox = checkboxLabel.querySelector('input[type="checkbox"]');
  640. checkbox.checked = isChecked;
  641.  
  642. // console.log("setting",checkbox, "to", isChecked)
  643. };
  644.  
  645. function syncNegativePrompt() {
  646. toggleCheckbox('#enforce-negative', shouldEnforceNegative);
  647. if (!shouldEnforceNegative) {
  648. // console.log("if (!shouldEnforceNegative)")
  649.  
  650. return;
  651. }
  652.  
  653. let textarea = document.querySelector('textarea[placeholder="Enter negative prompt here"]');
  654. if (document.activeElement === textarea) {
  655. localStorage.removeItem('enforceNegativeNegativePrompt');
  656. shouldEnforceNegative = false;
  657. // console.log("Active element, skipping")
  658. return;
  659. }
  660.  
  661. let storedValue = localStorage.getItem('enforceNegativeNegativePrompt');
  662. if (!storedValue) {
  663. // console.log("f (!storedValue) {")
  664.  
  665. return;
  666. }
  667.  
  668. if (textarea.value.trim() === storedValue.trim()) {
  669. // console.log("alrready changed");
  670. return;
  671. }
  672.  
  673.  
  674.  
  675. textarea.value = ''; // Clear the textarea
  676. textarea.value = storedValue; // Set the new value
  677. textarea.dispatchEvent(new Event('input', { bubbles: true })); // Trigger input event
  678. console.log("set to", storedValue);
  679.  
  680.  
  681. awakeTextarea(textarea);
  682.  
  683. }
  684.  
  685.  
  686. setInterval(syncNegativePrompt, 1 * 1000);
  687.  
  688. async function slideShowDowloadButtonManager() {
  689. const observer = new MutationObserver(() => {
  690. const nextButton = document.querySelector('.pswp__button--arrow--next');
  691. if (!nextButton || !!document.querySelector('#custom-download')) return;
  692.  
  693. const button = document.createElement('button');
  694. button.id = 'custom-download';
  695. button.title = 'Download with prompt as file name';
  696. 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>`;
  697.  
  698. button.onclick = async () => {
  699. nextButton.style.cursor = 'pointer';
  700. await saveImage();
  701. };
  702.  
  703. nextButton.insertAdjacentElement('beforebegin', button);
  704. setTimeout(() => button.classList.add('show'), 10);
  705. addHideSlideshowListeners(button);
  706. });
  707.  
  708. observer.observe(document.body, { childList: true, subtree: true });
  709. window.addEventListener('beforeunload', () => {
  710. observer.disconnect();
  711. });
  712. }
  713.  
  714.  
  715. function sanitizeFilename(filename) {
  716. const maxLength = 125;
  717. const dotIndex = filename.lastIndexOf('.');
  718. const extension = dotIndex !== -1 ? filename.substring(dotIndex) : ''; // Get the file extension
  719. const baseFilename = dotIndex !== -1 ? filename.substring(0, dotIndex) : filename; // Get the base filename
  720. const sanitizedBase = baseFilename
  721. .replace(/[^a-zA-Z0-9-_\. ]/g, '_') // Replace invalid characters with underscores
  722. .replace(/\s+/g, '_') // Replace spaces with underscores
  723. .replace(/_+/g, '_') // Remove duplicate underscores
  724. .substring(0, maxLength - extension.length); // Truncate to max length minus extension
  725.  
  726. return sanitizedBase + extension; // Combine sanitized base with extension
  727. }
  728.  
  729.  
  730. async function saveImage() {
  731. const textarea = document.querySelector(promptTextareaSelector);
  732. const imgSrc = document.querySelector('#pswp__items .pswp__item[aria-hidden="false"] img.pswp__img')?.src;
  733. if (!textarea || !imgSrc) return;
  734. let filename = sanitizeFilename(`${textarea.value.trim()}.png`);
  735. await GM_download({
  736. url: imgSrc,
  737. name: filename,
  738. saveAs: false
  739. });
  740. }
  741.  
  742.  
  743. function highlightOpenThumbnail() {
  744. let firstPreviewSrc = document.querySelector(imgPreviewSelector);
  745. if (!firstPreviewSrc) return console.warn(`Element not found for selector: ${imgPreviewSelector}`);
  746.  
  747. let currentId = extractSrcId(firstPreviewSrc.src);
  748. if (!currentId) return console.warn('Current ID could not be extracted from the image source.');
  749. console.log("highlightOpenThumbnail(): Selection id", currentId, "from", firstPreviewSrc.src, 'of', firstPreviewSrc)
  750. selectThumbFromId(currentId);
  751. }
  752.  
  753. function createCheckbox(id, labelText, onChangeFunction) {
  754. const newLabel = document.createElement('label');
  755. newLabel.style.userSelect = "none";
  756. newLabel.id = id;
  757. newLabel.innerHTML = `
  758. ${labelText}
  759. <input type="checkbox" style="display:none">
  760. <svg class="sc-eDvSVe cSfylm MuiSvgIcon-root MuiSvgIcon-fontSizeMedium" focusable="false" aria-hidden="true" viewBox="0 0 24 24">
  761. <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>
  762. <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>
  763. </svg>`;
  764.  
  765. const checkbox = newLabel.querySelector('input[type="checkbox"]');
  766. const checkedPath = newLabel.querySelector('.checked-path');
  767. checkbox.checked = shouldEnforceNegative;
  768.  
  769. checkbox.addEventListener('change', function () {
  770. checkedPath.style.display = !this.checked ? 'none' : 'block';
  771. if (typeof onChangeFunction === 'function') onChangeFunction(this.checked);
  772. });
  773.  
  774. return newLabel;
  775. }
  776.  
  777. function createEnforceNegativeCheckbox() {
  778. const negativeLabel = Array.from(document.querySelectorAll('label')).find(label => label.textContent === 'Negative');
  779. if (!negativeLabel || document.getElementById('enforce-negative')) return;
  780.  
  781. negativeLabel.parentElement.insertBefore(createCheckbox('enforce-negative', 'Enforce negative for every task', manageEnforceNegative), negativeLabel);
  782. }
  783.  
  784.  
  785.  
  786. async function slideShowLifetimeMonitor() {
  787. while (true) {
  788.  
  789. while (!document.querySelector('#pswp__items')) await new Promise(resolve => setTimeout(resolve, 100));
  790. slideshowPresent = true;
  791. shortcuts.setContext('slideshowOpen', true);
  792.  
  793. if (latestClickedID) {
  794. console.log("Adding", latestClickedID, "(latestClickedID) to DB");
  795. upsertDatabase(latestClickedID);
  796. }
  797.  
  798.  
  799.  
  800.  
  801. while (!!document.querySelector('#pswp__items')) await new Promise(resolve => setTimeout(resolve, 100));
  802. slideshowPresent = false;
  803. shortcuts.setContext('slideshowOpen', false);
  804.  
  805.  
  806.  
  807. await new Promise(resolve => setTimeout(resolve, 100));
  808.  
  809. highlightOpenThumbnail();
  810. }
  811. }
  812.  
  813.  
  814. function checkOpenedImageOpacity() {
  815. let openedImage = document.querySelector('div.pswp__item[aria-hidden="false"] > div.pswp__zoom-wrap > img');
  816.  
  817. // console.log("opened",openedImage,"opacity:",parseFloat(openedImage.style.opacity));
  818.  
  819. return !!openedImage && (openedImage.complete && openedImage.naturalWidth !== 0) && (!openedImage.style.opacity || parseFloat(openedImage.style.opacity) >= 1);
  820.  
  821.  
  822. }
  823.  
  824.  
  825. async function addHideSlideshowListeners(customDownload) {
  826. if (!customDownload) return;
  827.  
  828. let firstImageShowed = false;
  829.  
  830. const elements = document.querySelectorAll('.pswp__scroll-wrap, .pswp__button--close');
  831. const bg = document.querySelector('.pswp__bg');
  832. bg.classList.add("black-bg");
  833.  
  834. if (!firstImageShowed) {
  835. firstImageShowed = checkOpenedImageOpacity();
  836. }
  837.  
  838.  
  839.  
  840. const hideDownload = async (e) => {
  841. const initialOpacity = parseFloat(bg.style.opacity) || 1;
  842. let opacityChanged = false;
  843.  
  844.  
  845. if (!firstImageShowed) {
  846. firstImageShowed = checkOpenedImageOpacity();
  847. }
  848.  
  849.  
  850. const observer = new MutationObserver(() => {
  851.  
  852.  
  853.  
  854. if (firstImageShowed && !bg.classList.contains("black-bg") && !opacityChanged) bg.classList.add("black-bg");
  855.  
  856. if (parseFloat(bg.style.opacity) !== initialOpacity) {
  857. customDownload.classList.add('hide');
  858. opacityChanged = true;
  859. bg.classList.remove("black-bg");
  860. observer.disconnect();
  861. }
  862. });
  863.  
  864. observer.observe(bg, { attributes: true });
  865. document.addEventListener('visibilitychange', () => {
  866. if (document.visibilityState === 'visible' && !opacityChanged) observer.disconnect();
  867. });
  868.  
  869. bg.classList.remove("black-bg");
  870.  
  871. await new Promise(resolve => setTimeout(resolve, 2000));
  872. if (opacityChanged) {
  873. customDownload.classList.add('hide');
  874. }
  875. observer.disconnect();
  876. };
  877.  
  878. elements.forEach(el => el.addEventListener('click', hideDownload));
  879. document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideDownload(e); });
  880. }
  881.  
  882.  
  883.  
  884.  
  885.  
  886. function isTabActive(tab = "favorites") {
  887. /*
  888.  
  889. tasks / favorites
  890.  
  891. */
  892. let tabFavourites = document.querySelector(`#radix-\\:r9\\:-trigger-${tab}[data-state="active"]`);
  893.  
  894. return !!tabFavourites && tabFavourites.getAttribute('data-state') === 'active';
  895. }
  896.  
  897.  
  898.  
  899. function createActionButton(id = "newBtn", text = "Go!", action = () => {}, remove = false) {
  900. let generateButton = findButtonByInnerText('Generate', '.outline-2');
  901. if (!generateButton) return;
  902.  
  903.  
  904. let button = document.querySelector(`#${id}`);
  905.  
  906. if (remove && !!button){
  907. button.remove();
  908. return;
  909. }
  910.  
  911. if (!!button || remove) return;
  912.  
  913. button = document.createElement('button');
  914. button.id = id;
  915. button.textContent = text;
  916. 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');
  917. button.classList.add('bg-gradient-member');
  918. generateButton.parentNode.insertBefore(button, generateButton);
  919. button.addEventListener('click', action);
  920. }
  921.  
  922. createActionButton("Copy", "Copy!", ()=>{
  923.  
  924. const textarea = document.querySelector(promptTextareaSelector);
  925. if (!textarea) return;
  926.  
  927.  
  928. GM_setClipboard(textarea.value.trim(),'text/plain');
  929.  
  930. });
  931.  
  932.  
  933.  
  934.  
  935.  
  936.  
  937. function pasteButton(remove = false) {
  938. let generateButton = findButtonByInnerText('Generate', '.outline-2');
  939. if (!generateButton) return;
  940.  
  941.  
  942. let pasteTbn = document.querySelector('#pasteButton');
  943.  
  944. if (remove && !!pasteTbn){
  945. pasteTbn.remove();
  946. return;
  947. }
  948.  
  949. if (!!pasteTbn || remove) return;
  950.  
  951. pasteTbn = document.createElement('button');
  952. pasteTbn.id = 'pasteButton';
  953. pasteTbn.textContent = "Paste! (demo)";
  954. 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');
  955. pasteTbn.classList.add('bg-gradient-member');
  956. generateButton.parentNode.insertBefore(pasteTbn, generateButton);
  957. pasteTbn.addEventListener('click', pasteIntoTextarea);
  958.  
  959. const textarea = document.querySelector(promptTextareaSelector);
  960. console.log(textarea)
  961. }
  962.  
  963. // pasteButton();
  964.  
  965.  
  966. function pasteIntoTextarea() {
  967. const textarea = document.querySelector(promptTextareaSelector);
  968. if (textarea) {
  969. const currentValue = 'Your text here';
  970.  
  971. // Focus on the textarea
  972. textarea.focus();
  973. textarea.dispatchEvent(new Event('focus', { bubbles: true }));
  974.  
  975. // Simulate paste event
  976. const clipboardData = new DataTransfer();
  977. clipboardData.setData('text/plain', currentValue);
  978.  
  979. const pasteEvent = new ClipboardEvent('paste', {
  980. bubbles: true,
  981. cancelable: true
  982. });
  983.  
  984. Object.defineProperty(pasteEvent, 'clipboardData', {
  985. get: () => clipboardData
  986. });
  987.  
  988. textarea.dispatchEvent(pasteEvent);
  989.  
  990. // Directly trigger React’s onChange handler
  991. const changeEvent = new Event('change', { bubbles: true });
  992. textarea.value = currentValue;
  993. textarea.dispatchEvent(changeEvent);
  994.  
  995. // Manually trigger input and change events to ensure React updates state
  996. textarea.dispatchEvent(new Event('input', { bubbles: true }));
  997. textarea.dispatchEvent(new Event('change', { bubbles: true }));
  998.  
  999. // Optionally simulate submit action
  1000. const submitButton = document.querySelector('button[type="submit"]');
  1001. if (submitButton) submitButton.click();
  1002. }
  1003. }
  1004.  
  1005.  
  1006.  
  1007.  
  1008.  
  1009.  
  1010.  
  1011. function favouritesRegenerateButton(remove = false) {
  1012. let generateButton = findButtonByInnerText('Generate', '.outline-2');
  1013. if (!generateButton) return;
  1014.  
  1015.  
  1016. let regenerateBtn = document.querySelector('#favouriteRegenerateAutomation');
  1017.  
  1018. if (remove && !!regenerateBtn){
  1019. regenerateBtn.remove();
  1020. return;
  1021. }
  1022.  
  1023. if (!!regenerateBtn || remove) return;
  1024.  
  1025. // console.log("Adding because remove =",remove)
  1026. regenerateBtn = document.createElement('button');
  1027. regenerateBtn.id = 'favouriteRegenerateAutomation';
  1028. regenerateBtn.textContent = regenerateBtnText;
  1029. 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');
  1030. regenerateBtn.classList.add('bg-gradient-member');
  1031. generateButton.parentNode.insertBefore(regenerateBtn, generateButton);
  1032. regenerateBtn.addEventListener('click', favouriteRegenerateAutomation);
  1033.  
  1034. let tasksTab = findButtonByInnerText('Tasks', '[role="tab"]');
  1035. tasksTab.addEventListener('click', () => favouritesRegenerateButton(true));
  1036.  
  1037. }
  1038.  
  1039.  
  1040. async function favouriteRegenerateAutomation(){
  1041.  
  1042.  
  1043.  
  1044. let regenerateBtn = document.querySelector('#favouriteRegenerateAutomation');
  1045.  
  1046. if (!!regenerateBtn && regenerateBtn.getAttribute('data-running') === 'true') {
  1047. console.log("Stopping")
  1048. stopGenerationLoop = true;
  1049. stoppedFavouriteLoopButtonDOM(regenerateBtn);
  1050. return;
  1051. }
  1052.  
  1053. let generateButton = findButtonByInnerText('Generate', '.outline-2');
  1054.  
  1055. if (!generateButton || !regenerateBtn) throw new Error('Generate button or regenerate button not found. What the actual..? Impossible!');
  1056.  
  1057. let currentlySelectedItem = document.querySelector(selectedListThumbSelector);
  1058. if(!currentlySelectedItem) {
  1059. alert("Please select the oldest item to start")
  1060. return;
  1061. }
  1062.  
  1063. regenerateBtn.textContent = 'Stop!';
  1064. regenerateBtn.classList.remove('bg-gradient-member');
  1065. regenerateBtn.setAttribute('data-running', 'true');
  1066.  
  1067.  
  1068. stopGenerationLoop = false;
  1069.  
  1070. let successCount = 0;
  1071.  
  1072. while (true) {
  1073. if(successCount > 0) await new Promise(resolve => setTimeout(resolve, 1000));
  1074.  
  1075. if(stopGenerationLoop) break;
  1076.  
  1077. let selectedButton = document.querySelector('button.outline');
  1078.  
  1079. console.info("Start wait for Generate Button")
  1080. while (!findButtonByInnerText('Generate', ".outline-2")) {
  1081. await new Promise(resolve => setTimeout(resolve, 100));
  1082. }
  1083. console.info("End wait for Generate Button")
  1084.  
  1085. generateButton.click();
  1086.  
  1087. console.log("Waiting for toast")
  1088. const toast = await waitForToastify();
  1089. console.log("Toast found");
  1090.  
  1091. let toastText = toast.textContent;
  1092.  
  1093.  
  1094. let closeBtn = toast.querySelector('.Toastify__close-button');
  1095. if(!!closeBtn) {
  1096. closeBtn.click();
  1097. console.log("Closed toast");
  1098. }
  1099.  
  1100. if(stopGenerationLoop) break;
  1101.  
  1102. if (toastText.includes('submitted')) {
  1103. console.log('Success for',document.querySelector(promptTextareaSelector).value);
  1104. successCount++;
  1105. } else if (toastText.includes('error')) {
  1106. console.log('Error, retrying...');
  1107. continue;
  1108. } else if(document.body.innerText.includes("Too many tasks in queue")) {
  1109. console.log("Too many tasks in queue");
  1110. findButtonByInnerText('OK').click();
  1111. continue;
  1112. }
  1113.  
  1114. let newestElement = document.querySelector('[data-state="active"][role="tabpanel"] button');
  1115. // console.log("Newest element:", newestElement);
  1116.  
  1117. if(selectedButton === newestElement){
  1118. console.log("End reached");
  1119. break;
  1120. }
  1121.  
  1122. if (!arrowNavigation(-1, selectedButton)) break;
  1123.  
  1124. }
  1125.  
  1126. console.log("Success count:",successCount)
  1127.  
  1128. stoppedFavouriteLoopButtonDOM(regenerateBtn);
  1129.  
  1130. }
  1131.  
  1132. function stoppedFavouriteLoopButtonDOM(regenerateBtn){
  1133. regenerateBtn.textContent = regenerateBtnText;
  1134. regenerateBtn.classList.add('bg-gradient-member');
  1135. regenerateBtn.removeAttribute('data-running');
  1136. }
  1137.  
  1138.  
  1139. async function waitForToastify() {
  1140. return new Promise(resolve => {
  1141. const checkToast = setInterval(() => {
  1142. if (stopGenerationLoop) {
  1143. clearInterval(checkToast);
  1144. return;
  1145. }
  1146. const toastElement = document.querySelector('.Toastify');
  1147. let messageText = toastElement.textContent;
  1148. if (!!toastElement && (messageText.includes('submitted') || messageText.includes('fail'))) {
  1149. latestToastMessage = messageText;
  1150. clearInterval(checkToast);
  1151. resolve(toastElement);
  1152. }
  1153. }, 100);
  1154. });
  1155. }
  1156.  
  1157.  
  1158.  
  1159.  
  1160.  
  1161. function arrowNavigation(direction = -1, givenButton = null) {
  1162. let selectedButton = givenButton || document.querySelector('button.outline');
  1163. if (!selectedButton) return;
  1164.  
  1165. let tileHeight = selectedButton.offsetHeight;
  1166.  
  1167. let scrollContainer = document.querySelector(scrollListSelector);
  1168. if (!scrollContainer) return console.error("There is no scrollContainer");
  1169.  
  1170. let positionFromTop = selectedButton.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top;
  1171.  
  1172. if (positionFromTop <= tileHeight) {
  1173. scrollContainer.scrollTop -= tileHeight * 1.5;
  1174. }
  1175.  
  1176. let buttons = Array.from(document.querySelectorAll('button'));
  1177. let selectedIndex = buttons.indexOf(selectedButton);
  1178.  
  1179. if (selectedIndex <= 0) return;
  1180.  
  1181. let previousButton = buttons[selectedIndex + direction];
  1182. if (!previousButton) {
  1183. console.log("No more buttons")
  1184. return false;
  1185. }
  1186.  
  1187. previousButton.click();
  1188. return true;
  1189. }
  1190.  
  1191.  
  1192.  
  1193.  
  1194.  
  1195.  
  1196. function updateListeners() {
  1197. if(isTabActive('favorites')){
  1198. thumbIcons = [];
  1199. favouritesRegenerateButton();
  1200. console.log("Returning because we are inside Favourites");
  1201. return;
  1202. }
  1203.  
  1204. // console.info("Running checks for listeners inside updateListeners()");
  1205. thumbIcons.forEach(icon => {
  1206. if (!icon.thumbClickListenerAdded) {
  1207. icon.addEventListener('click', thumbListener);
  1208. icon.thumbClickListenerAdded = true;
  1209. }
  1210. });
  1211.  
  1212. favouritesRegenerateButton(true); // remove
  1213.  
  1214. }
  1215.  
  1216. async function detectScroll() {
  1217. let scroller = await waitForElements(scrollListSelector);
  1218.  
  1219. scroller[0].addEventListener('scroll', () => {
  1220. requestAnimationFrame(() => updateThumbs(true));
  1221. });
  1222. }
  1223.  
  1224. const scale = (x) => {
  1225. if (x >= 2) return 1.5;
  1226. if (x < 1) return x;
  1227. return 0.5 * (x - 1) + 1;
  1228. };
  1229.  
  1230.  
  1231.  
  1232. function awakeTextarea(textarea, input = null) {
  1233. textarea.focus();
  1234. if (!input) {
  1235. textarea.value += ' ';
  1236. } else {
  1237. textarea.value = input;
  1238. }
  1239. textarea.dispatchEvent(new InputEvent('input', { bubbles: true }));
  1240. setTimeout(() => {
  1241. textarea.value = textarea.value.slice(0, -1);
  1242. textarea.dispatchEvent(new InputEvent('input', { bubbles: true }));
  1243. textarea.dispatchEvent(new Event('change', { bubbles: true }));
  1244. textarea.blur();
  1245. }, 500);
  1246. };
  1247.  
  1248.  
  1249.  
  1250. const textareaPasteFix = async (promptTextareaSelector) => {
  1251. const textarea = await waitForElement(promptTextareaSelector);
  1252. textarea.addEventListener('paste', (event) => {
  1253. const clipboardData = event.clipboardData.getData('text/plain');
  1254. const modifiedText = clipboardData
  1255. .replace(/(\d+) year old/g, '$1yo')
  1256. .replace(/(\d+) years old/g, '$1yo')
  1257. .replace(/(\d+) years/g, '$1yo')
  1258. .replace(/(\d+) years-old/g, '$1yo')
  1259. .replace(/(\d+) year-old/g, '$1yo')
  1260. .replace(/(\d+)-year-old/g, '$1yo')
  1261. .replace(/(\d+)-years-old/g, '$1yo')
  1262. .replace(/thx/g, 'thanks')
  1263. .replace('suckling', 'sucking')
  1264. .replace(/\(\(/g, '(')
  1265. .replace(/\)\)/g, ')')
  1266. .replace(/:(\d+(\.\d+)?)/g, (match, num) => {
  1267. const scaledNum = scale(parseFloat(num));
  1268. return `:${Math.round(scaledNum * 10) / 10}`;
  1269. })
  1270. .replace(/<[^>]*>/g, ''); // Remove content between '<' and '>'
  1271.  
  1272.  
  1273. if (clipboardData !== modifiedText) {
  1274. event.preventDefault();
  1275. const { selectionStart: start, selectionEnd: end } = textarea;
  1276. const textBefore = textarea.value.slice(0, start);
  1277. const textAfter = textarea.value.slice(end);
  1278. textarea.value = textBefore + modifiedText + textAfter;
  1279. textarea.selectionStart = textarea.selectionEnd = start + modifiedText.length;
  1280. awakeTextarea(textarea);
  1281. }
  1282. });
  1283. };
  1284.  
  1285.  
  1286. function createScrollToBottomButton() {
  1287. const list = document.querySelector(scrollListSelector);
  1288. const button = document.createElement('div');
  1289. Object.assign(button.style, {
  1290. position: 'fixed',
  1291. bottom: '10px',
  1292. left: '14px',
  1293. width: '50px',
  1294. height: '50px',
  1295. backgroundColor: 'rgba(0,0,0,0.6)',
  1296. borderRadius: '10px',
  1297. display: 'flex',
  1298. justifyContent: 'center',
  1299. alignItems: 'center',
  1300. cursor: 'pointer',
  1301. color: 'white',
  1302. fontSize: '24px',
  1303. zIndex: '5'
  1304. });
  1305. button.innerHTML = '&#8595;';
  1306. button.id = 'scrollDownBtn';
  1307. button.onclick = () => {
  1308.  
  1309.  
  1310. scrollingToElement = !scrollingToElement;
  1311. button.innerHTML = scrollingToElement ? '&#9634;' : '&#8595;';
  1312. if(scrollingToElement) scrollUntilElement();
  1313. };
  1314. list.appendChild(button);
  1315. }
  1316.  
  1317.  
  1318. function scrollUntilElement(id = null) {
  1319. const list = document.querySelector(scrollListSelector);
  1320. if (!list) return;
  1321.  
  1322. if (id === null) {
  1323. id = retrieveValueFromStorage('latestClickedID')?.['id'];
  1324. console.log("Finding ID", id);
  1325. }
  1326.  
  1327. const interval = setInterval(() => {
  1328.  
  1329. if(!scrollingToElement) {
  1330. clearInterval(interval);
  1331. return;
  1332. }
  1333.  
  1334. const target = id !== undefined
  1335. ? Array.from(list.querySelectorAll('img')).find(img => img.src.includes(id))
  1336. : list.querySelector('span[data-label="check"]');
  1337.  
  1338. if (target) {
  1339. clearInterval(interval);
  1340. const offset = window.innerHeight * 0.7;
  1341. const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset;
  1342. window.scrollTo({ top: targetPosition, behavior: 'smooth' });
  1343.  
  1344. let scrollBtn = document.querySelector('#scrollDownBtn');
  1345. if (!!scrollBtn) scrollBtn.innerHTML = '&#8595;';
  1346. updateThumbs();
  1347.  
  1348. } else {
  1349. list.scrollBy(0, 100);
  1350. }
  1351. }, 100);
  1352. }
  1353.  
  1354.  
  1355.  
  1356.  
  1357.  
  1358. await waitForFocus();
  1359.  
  1360. const textarea = await waitForElement(promptTextareaSelector, 60*60);
  1361.  
  1362. textarea.addEventListener('focus', () => {
  1363. shortcuts.setContext('isInput', true);
  1364. // console.log("isInput", true)
  1365. });
  1366.  
  1367. textarea.addEventListener('blur', () => {
  1368. shortcuts.setContext('isInput', false);
  1369. // console.log("isInput", false)
  1370. });
  1371.  
  1372.  
  1373.  
  1374.  
  1375.  
  1376. window.addEventListener('blur', () => {
  1377. if(!previewListCheckListener) return;
  1378. previewListCheckListener.disconnect();
  1379. });
  1380.  
  1381. window.addEventListener('focus', () => {
  1382. startScrollListListener();
  1383. });
  1384.  
  1385.  
  1386. function startScrollListListener() {
  1387. const targetNode = document.querySelector(scrollListSelector);
  1388. if (!targetNode) {
  1389. console.log("Returning because there is no scrollListSelector inside startScrollListListener()");
  1390. return;
  1391. }
  1392.  
  1393. previewListCheckListener = new MutationObserver(mutations => {
  1394. mutations.forEach(mutation => {
  1395. mutation.addedNodes.forEach(node => {
  1396. if (node.nodeType !== 1 || !node.matches('[data-label="check"]')) return;
  1397.  
  1398. const parent = node.parentElement;
  1399. const id = extractPreviewId(parent);
  1400. if (!openedPreviewCache.get(id)) {
  1401. node.remove();
  1402. console.log("Removed", node, "because", id, "not present")
  1403. }
  1404. });
  1405. });
  1406. });
  1407.  
  1408. previewListCheckListener.observe(targetNode, { childList: true, subtree: true });
  1409. }
  1410.  
  1411. startScrollListListener();
  1412.  
  1413.  
  1414.  
  1415.  
  1416. textareaPasteFix(promptTextareaSelector)
  1417.  
  1418.  
  1419. upsertDatabase(1)
  1420. detectScroll();
  1421. slideShowLifetimeMonitor();
  1422.  
  1423. slideShowDowloadButtonManager();
  1424. createEnforceNegativeCheckbox();
  1425. updateThumbs(true);
  1426.  
  1427.  
  1428. window.addEventListener('focus', () => {
  1429. updateThumbs(true);
  1430. highlightOpenThumbnail();
  1431. });
  1432.  
  1433.  
  1434. let scrollList = await waitForElement(scrollListSelector);
  1435.  
  1436. createScrollToBottomButton()
  1437.  
  1438.  
  1439. // let scrollList = document.querySelector(scrollListSelector);
  1440.  
  1441. scrollList.addEventListener('mouseenter', () => updateThumbs(true));
  1442. scrollList.addEventListener('mouseleave', () => updateThumbs(true));
  1443. scrollList.addEventListener('mousemove', () => updateThumbs());
  1444.  
  1445.  
  1446.  
  1447. GM_addStyle(`
  1448. #favouriteRegenerateAutomation:hover{
  1449. filter: brightness(1.1) contrast(1.05);
  1450. }
  1451. .black-bg{
  1452. opacity: 1 !important;
  1453. }
  1454.  
  1455. .pswp__bg{
  1456. transition: opacity .5s cubic-bezier(0.25,0.1,0.25,1);
  1457. }
  1458.  
  1459. .selected-thumb{
  1460. outline: 2px solid hsla(0, 12%, 85.3%, 0.77);
  1461. transition: outline 100ms;
  1462. }
  1463. #app .ring-2{
  1464. box-shadow: none;
  1465. }
  1466. [data-test-id="virtuoso-item-list"] .contents>button img{
  1467. cursor:pointer;
  1468. transition: filter .1s ease;
  1469. }
  1470. [data-test-id="virtuoso-item-list"] .contents>button img:hover{
  1471. filter: brightness(1.05);
  1472. }
  1473. [data-label="check"] {
  1474. position: absolute;
  1475. left: 0;
  1476. bottom: 0;
  1477. background: #ffffff2b;
  1478. border-top-right-radius: 5px;
  1479. backdrop-filter: blur(10px);
  1480. filter: brightness(1.3);
  1481. transition: opacity .5s ease;
  1482. opacity: 0;
  1483. }
  1484. #custom-download{
  1485. width: 75px;
  1486. height: 100px;
  1487. margin-top: -50px;
  1488. position: absolute;
  1489. top: 50%;
  1490. right: calc(75px + .5rem);
  1491. display: flex; justify-content: center; align-items: center;
  1492. opacity:0;
  1493. will-change: opacity;
  1494. transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1);
  1495. }
  1496. #custom-download.show{
  1497. opacity:1;
  1498. }
  1499. #custom-download.hide{
  1500. opacity:0;
  1501. }
  1502. #custom-download > svg{
  1503. fill: var(--pswp-icon-color);
  1504. /* color: var(--pswp-icon-color-secondary); */
  1505. width: 60px;
  1506. height: 60px;
  1507. }
  1508. #custom-download > svg > .pswp__icn-shadow {
  1509. stroke-width: 1px;
  1510. }
  1511. button[aria-label="Download"][type="button"]{
  1512. display:none
  1513. }
  1514. `);
  1515. })();