Greasy Fork is available in English.

Reve.art Deleter

Auto art deleter for reve.art

// ==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();
})();