Reve.art Deleter

Auto art deleter for reve.art

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Reve.art Deleter
// @namespace    https://greasyfork.org/en/users/781396-yad
// @version      1.0
// @description  Auto art deleter for reve.art
// @author       YAD
// @match        https://preview.reve.art/app
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        clickDelay: 1000,
        retryAttempts: 3,
        retryDelay: 500,
        uiWidth: '280px',
        collapsedWidth: '40px',
        toggleBtnSize: '20px'
    };

    GM_addStyle(`
        #reve-art-deleter-ui {
            position: fixed;
            bottom: 10px;
            right: 10px;
            z-index: 9999;
            background: #1e1e2d;
            border: 1px solid #2d2d3d;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
            color: #e0e0e0;
            font-family: Arial, sans-serif;
            width: ${CONFIG.uiWidth};
            transition: all 0.3s ease;
            overflow: hidden;
        }
        #reve-art-deleter-ui.collapsed {
            width: ${CONFIG.collapsedWidth};
            height: ${CONFIG.collapsedWidth};
        }
        #reve-art-deleter-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px 10px;
            background: #2d2d3d;
            cursor: pointer;
        }
        #reve-art-deleter-title {
            font-size: 14px;
            color: #fff;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        #reve-art-deleter-toggle {
            width: ${CONFIG.toggleBtnSize};
            height: ${CONFIG.toggleBtnSize};
            display: flex;
            align-items: center;
            justify-content: center;
            background: none;
            border: none;
            color: #fff;
            font-size: 16px;
            cursor: pointer;
            flex-shrink: 0;
        }
        #reve-art-deleter-ui.collapsed #reve-art-deleter-title,
        #reve-art-deleter-ui.collapsed #reve-art-deleter-content {
            display: none;
        }
        #reve-art-deleter-content {
            padding: 10px;
        }
        .reve-art-control-row {
            display: grid;
            grid-template-columns: auto 1fr;
            gap: 8px;
            align-items: center;
            margin-bottom: 8px;
        }
        .reve-art-label {
            font-size: 12px;
            white-space: nowrap;
        }
        .reve-art-input {
            padding: 4px;
            background: #2d2d3d;
            border: 1px solid #3d3d4d;
            color: #fff;
            border-radius: 3px;
            width: 100%;
        }
        .reve-art-button-group {
            display: flex;
            gap: 8px;
            margin: 8px 0;
        }
        .reve-art-button {
            padding: 6px;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            flex: 1;
            color: #fff;
        }
        #reve-art-scan-btn { background: #3d3d4d; }
        #reve-art-delete-btn { background: #dc3545; font-weight: bold; }
        #reve-art-stop-btn {
            background: #ff9500;
            font-weight: bold;
            display: none;
        }
        #reve-art-stop-btn.active { display: block; }
        #reve-art-grid-info {
            font-size: 11px;
            color: #aaa;
            margin-bottom: 8px;
            line-height: 1.3;
        }
        #reve-art-status {
            font-size: 11px;
            color: #aaa;
            max-height: 120px;
            overflow-y: auto;
            background: #2d2d3d;
            padding: 6px;
            border-radius: 3px;
            white-space: pre-wrap;
        }
        .grid-cell { position: relative !important; }
        .index-label {
            position: absolute;
            top: 2px;
            left: 2px;
            color: #fff;
            background: rgba(0,0,0,0.7);
            padding: 1px 4px;
            font-size: 10px;
            border-radius: 2px;
            z-index: 1000;
        }
    `);

    const ui = document.createElement('div');
    ui.id = 'reve-art-deleter-ui';
    ui.innerHTML = `
        <div id="reve-art-deleter-header">
            <div id="reve-art-deleter-title">REVE ART Deleter</div>
            <button id="reve-art-deleter-toggle">≡</button>
        </div>
        <div id="reve-art-deleter-content">
            <div class="reve-art-control-row">
                <label class="reve-art-label" for="reve-art-start-index">From:</label>
                <input class="reve-art-input" type="number" id="reve-art-start-index" min="0" value="0">
            </div>
            <div class="reve-art-control-row">
                <label class="reve-art-label" for="reve-art-end-index">To:</label>
                <input class="reve-art-input" type="number" id="reve-art-end-index" min="0" value="4">
            </div>
            <div class="reve-art-control-row">
                <label class="reve-art-label" for="reve-art-delay">Delay:</label>
                <input class="reve-art-input" type="number" id="reve-art-delay" min="500" value="${CONFIG.clickDelay}">
            </div>
            <div class="reve-art-control-row">
                <label class="reve-art-label" for="reve-art-skip-indexes">Skip:</label>
                <input class="reve-art-input" type="text" id="reve-art-skip-indexes" placeholder="e.g. 2,5,7">
            </div>
            <div class="reve-art-button-group">
                <button id="reve-art-scan-btn" class="reve-art-button">Scan</button>
                <button id="reve-art-delete-btn" class="reve-art-button">Delete</button>
                <button id="reve-art-stop-btn" class="reve-art-button">Stop</button>
            </div>
            <div id="reve-art-grid-info">No scan performed</div>
            <div id="reve-art-status">Ready</div>
        </div>
    `;
    document.body.appendChild(ui);

    const toggleBtn = document.getElementById('reve-art-deleter-toggle');
    toggleBtn.addEventListener('click', e => {
        e.stopPropagation();
        ui.classList.toggle('collapsed');
        toggleBtn.textContent = ui.classList.contains('collapsed') ? '≡' : '×';
    });

    const header = document.getElementById('reve-art-deleter-header');
    header.addEventListener('mousedown', e => {
        e.preventDefault();
        if (e.target.id !== 'reve-art-deleter-header') return;

        const startX = e.clientX, startY = e.clientY;
        const startLeft = ui.offsetLeft, startTop = ui.offsetTop;

        const moveHandler = e => {
            ui.style.left = `${startLeft + e.clientX - startX}px`;
            ui.style.top = `${startTop + e.clientY - startY}px`;
            ui.style.right = ui.style.bottom = 'auto';
        };

        const upHandler = () => {
            document.removeEventListener('mousemove', moveHandler);
            document.removeEventListener('mouseup', upHandler);
        };

        document.addEventListener('mousemove', moveHandler);
        document.addEventListener('mouseup', upHandler);
    });

    const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

    async function findElement(selector, root = document, attempts = CONFIG.retryAttempts) {
        for (let i = 0; i < attempts; i++) {
            const element = root.querySelector(selector);
            if (element) return element;

            for (const el of root.querySelectorAll('*')) {
                if (el.shadowRoot) {
                    const shadowElement = await findElement(selector, el.shadowRoot, 1);
                    if (shadowElement) return shadowElement;
                }
            }
            if (i < attempts - 1) await wait(CONFIG.retryDelay);
        }
        return null;
    }

    async function getGridCells() {
        const cells = [];
        const gridCells = await findElement('.grid');
        if (!gridCells) return cells;

        gridCells.querySelectorAll('.grid-cell').forEach((cell, index) => {
            const link = cell.querySelector('a.aspect-ratio-container');
            if (link) cells.push({ element: cell, link, index: cell.dataset.index || index });
        });
        return cells;
    }

    function addIndexLabels(cells) {
        cells.forEach(cell => {
            const existingLabel = cell.element.querySelector('.index-label');
            if (existingLabel) existingLabel.remove();

            const label = document.createElement('div');
            label.className = 'index-label';
            label.textContent = cell.index;
            label.style.cssText = 'position:absolute;top:2px;left:2px;color:#fff;background:rgba(0,0,0,0.7);padding:1px 4px;font-size:10px;border-radius:2px;z-index:1000';
            cell.element.style.position = 'relative';
            cell.element.appendChild(label);
        });
    }

    async function clickElement(element, description) {
        if (!element) {
            appendStatus(`❌ ${description} not found`);
            return false;
        }

        try {
            element.scrollIntoView({ behavior: 'smooth', block: 'center' });
            await wait(300);
            element.click();
            await wait(300);

            if (description.includes('Grid cell') && !element.classList.contains('selected')) {
                throw new Error('Not selected after click');
            }
            appendStatus(`✓ ${description}`);
            return true;
        } catch (error) {
            try {
                const rect = element.getBoundingClientRect();
                element.dispatchEvent(new MouseEvent('click', {
                    bubbles: true,
                    cancelable: true,
                    view: window,
                    clientX: rect.left + rect.width/2,
                    clientY: rect.top + rect.height/2
                }));
                await wait(300);
                appendStatus(`✓ ${description} (simulated)`);
                return true;
            } catch (e) {
                appendStatus(`❌ Failed ${description}: ${e.message}`);
                return false;
            }
        }
    }

    const appendStatus = text => {
        const statusEl = document.getElementById('reve-art-status');
        statusEl.textContent += (statusEl.textContent ? '\n' : '') + text;
        statusEl.scrollTop = statusEl.scrollHeight;
    };

    const clearStatus = () => document.getElementById('reve-art-status').textContent = '';

    async function updateGridInfo() {
        const cells = await getGridCells();
        const infoEl = document.getElementById('reve-art-grid-info');

        if (!cells.length) {
            infoEl.textContent = 'No grid cells found';
            infoEl.style.color = '#ff6b6b';
            return;
        }

        addIndexLabels(cells);
        const indexes = cells.map(cell => cell.index);
        infoEl.innerHTML = `${cells.length} items (${Math.min(...indexes)}-${Math.max(...indexes)})<br>Next: ${indexes.slice(0, 3).join(', ')}${indexes.length > 3 ? '...' : ''}`;
        infoEl.style.color = '#aaa';
    }

    const parseSkipIndexes = input => input ? new Set(input.split(',').map(num => parseInt(num.trim())).filter(num => !isNaN(num))) : new Set();

    let shouldStop = false;
    async function deleteRange(start, end, delay, skipIndexes) {
        clearStatus();
        appendStatus(`Deleting ${start} to ${end}...`);
        
        const stopBtn = document.getElementById('reve-art-stop-btn');
        const deleteBtn = document.getElementById('reve-art-delete-btn');
        stopBtn.classList.add('active');
        deleteBtn.disabled = true;
        shouldStop = false;

        const cells = await getGridCells();
        const targetCells = cells.filter(cell => {
            const idx = parseInt(cell.index);
            return !isNaN(idx) && idx >= start && idx <= end && !skipIndexes.has(idx);
        });

        appendStatus(`Found ${cells.length} items`);
        appendStatus(`Processing ${targetCells.length} in range`);
        if (skipIndexes.size) appendStatus(`Skipping: ${Array.from(skipIndexes).join(', ')}`);

        let successCount = 0;
        for (const cell of targetCells) {
            if (shouldStop) {
                appendStatus('🛑 Stopped by user');
                break;
            }

            appendStatus(`--- ${cell.index} ---`);
            try {
                if (!(await clickElement(cell.link, `Select ${cell.index}`) &&
                      await clickElement(await findElement('rv-icon-button sl-icon[name="trash"]'), 'Trash') &&
                      await clickElement(await findElement('sl-button[variant="danger"]'), 'Confirm'))) {
                    throw new Error('Click sequence failed');
                }
                successCount++;
                appendStatus(`✅ Deleted ${cell.index}`);
                await wait(delay);
            } catch (error) {
                appendStatus(`❌ Error: ${error.message}`);
            }
        }

        appendStatus(`\nCompleted: ${successCount}/${targetCells.length}`);
        stopBtn.classList.remove('active');
        deleteBtn.disabled = false;
        updateGridInfo();
    }

    document.getElementById('reve-art-scan-btn').addEventListener('click', updateGridInfo);
    document.getElementById('reve-art-delete-btn').addEventListener('click', () => {
        const start = parseInt(document.getElementById('reve-art-start-index').value);
        const end = parseInt(document.getElementById('reve-art-end-index').value);
        const delay = parseInt(document.getElementById('reve-art-delay').value);
        const skipIndexes = parseSkipIndexes(document.getElementById('reve-art-skip-indexes').value);

        if (start > end) {
            appendStatus('Error: Start must be ≤ end');
            return;
        }
        deleteRange(start, end, delay, skipIndexes);
    });

    document.getElementById('reve-art-stop-btn').addEventListener('click', () => {
        shouldStop = true;
        appendStatus('Stopping after current operation...');
    });

    updateGridInfo();
})();