Reve.art Deleter

Auto art deleter for reve.art

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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