// ==UserScript== // @name Stash Batch Save // @namespace https://github.com/7dJx1qP/stash-userscripts // @description Adds a batch save button to scenes tagger // @version 0.5.3 // @author 7dJx1qP // @match http://localhost:9999/* // @grant unsafeWindow // @require https://raw.githubusercontent.com/7dJx1qP/stash-userscripts/master/src\StashUserscriptLibrary.js // ==/UserScript== (function () { 'use strict'; const { stash, Stash, waitForElementId, waitForElementClass, waitForElementByXpath, getElementByXpath, getElementsByXpath, getClosestAncestor, sortElementChildren, createElementFromHTML, } = unsafeWindow.stash; document.body.appendChild(document.createElement('style')).textContent = ` .search-item > div.row:first-child > div.col-md-6.my-1 > div:first-child { display: flex; flex-direction: column; } .tagger-remove { order: 10; } `; let running = false; const buttons = []; let maxCount = 0; let sceneId = null; function run() { if (!running) return; const button = buttons.pop(); stash.setProgress((maxCount - buttons.length) / maxCount * 100); if (button) { const searchItem = getClosestAncestor(button, '.search-item'); if (searchItem.classList.contains('d-none')) { setTimeout(() => { run(); }, 0); return; } const { id } = stash.parseSearchItem(searchItem); sceneId = id; if (!button.disabled) { button.click(); } else { buttons.push(button); } } else { stop(); } } function processSceneUpdate(evt) { if (running && evt.detail.data?.sceneUpdate?.id === sceneId) { setTimeout(() => { run(); }, 0); } } const btnId = 'batch-save'; const startLabel = 'Save All'; const stopLabel = 'Stop Save'; const btn = document.createElement("button"); btn.setAttribute("id", btnId); btn.classList.add('btn', 'btn-primary', 'ml-3'); btn.innerHTML = startLabel; btn.onclick = () => { if (running) { stop(); } else { start(); } }; function start() { if (!confirm("Are you sure you want to batch save?")) return; btn.innerHTML = stopLabel; btn.classList.remove('btn-primary'); btn.classList.add('btn-danger'); running = true; stash.setProgress(0); buttons.length = 0; for (const button of document.querySelectorAll('.btn.btn-primary')) { if (button.innerText === 'Save') { buttons.push(button); } } maxCount = buttons.length; stash.addEventListener('stash:response', processSceneUpdate); run(); } function stop() { btn.innerHTML = startLabel; btn.classList.remove('btn-danger'); btn.classList.add('btn-primary'); running = false; stash.setProgress(0); sceneId = null; stash.removeEventListener('stash:response', processSceneUpdate); } stash.addEventListener('tagger:mutations:header', evt => { const el = getElementByXpath("//button[text()='Scrape All']"); if (el && !document.getElementById(btnId)) { const container = el.parentElement; container.appendChild(btn); sortElementChildren(container); el.classList.add('ml-3'); } }); function checkSaveButtonDisplay() { const taggerContainer = document.querySelector('.tagger-container'); const saveButton = getElementByXpath("//button[text()='Save']", taggerContainer); btn.style.display = saveButton ? 'inline-block' : 'none'; } stash.addEventListener('tagger:mutations:searchitems', checkSaveButtonDisplay); async function initRemoveButtons() { const nodes = getElementsByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']"); const buttons = []; let node = null; while (node = nodes.iterateNext()) { buttons.push(node); } for (const button of buttons) { const searchItem = getClosestAncestor(button, '.search-item'); const removeButtonExists = searchItem.querySelector('.tagger-remove'); if (removeButtonExists) { continue; } const removeEl = createElementFromHTML('
'); const removeButton = removeEl.querySelector('button'); button.parentElement.parentElement.appendChild(removeEl); removeButton.addEventListener('click', async () => { searchItem.classList.add('d-none'); }); } } stash.addEventListener('page:studio:scenes', function () { waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); }); stash.addEventListener('page:performer:scenes', function () { waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); }); stash.addEventListener('page:scenes', function () { waitForElementByXpath("//button[contains(@class, 'btn-primary') and text()='Scrape by fragment']", initRemoveButtons); }); })();