Reve.art Deleter

Auto art deleter for reve.art

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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