Gemini Bulk Delete

Bulk delete Gemini conversations

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gemini Bulk Delete
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  Bulk delete Gemini conversations
// @author       Antigravity
// @match        https://gemini.google.com/app*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @noframes
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // --- 1. Mini-Framework Utils ---

    // Feature detection for Trusted Types
    const policy = window.trustedTypes && window.trustedTypes.createPolicy ?
        window.trustedTypes.createPolicy('geminiBulkDeletePolicy', { createHTML: s => s }) :
        { createHTML: s => s };

    // Tagged Template Literal for safe HTML
    function html(strings, ...values) {
        const raw = strings.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
        return policy.createHTML(raw);
    }

    // Reactivity System
    function reactive(target, onChange) {
        return new Proxy(target, {
            set(obj, prop, value) {
                if (obj[prop] !== value) {
                    obj[prop] = value;
                    onChange(prop, value);
                }
                return true;
            }
        });
    }

    // --- 2. Styles ---
    const CHECKBOX_CLASS = 'gemini-bulk-del-checkbox';

    function addStyles() {
        if (document.getElementById('gemini-bulk-style')) return;
        const style = document.createElement('style');
        style.id = 'gemini-bulk-style';
        style.textContent = `
            :root {
                --gemini-bulk-bg: rgba(30, 30, 30, 0.9);
                --gemini-bulk-border: rgba(255, 255, 255, 0.1);
                --gemini-bulk-text: #e3e3e3;
                --gemini-bulk-accent: #8ab4f8;
                --gemini-bulk-accent-hover: #aecbfa;
                --gemini-bulk-danger: #ea4335;
                --gemini-bulk-danger-hover: #f28b82;
            }
            .${CHECKBOX_CLASS} {
                appearance: none;
                -webkit-appearance: none;
                width: 20px;
                height: 20px;
                border: 2px solid var(--gemini-bulk-border);
                border-radius: 6px;
                margin-right: 12px;
                cursor: pointer;
                position: relative;
                transition: all 0.2s ease;
                background-color: transparent;
                flex-shrink: 0;
                z-index: 1000;
            }
            .${CHECKBOX_CLASS}:checked {
                background-color: var(--gemini-bulk-accent);
                border-color: var(--gemini-bulk-accent);
            }
            .${CHECKBOX_CLASS}:checked::after {
                content: '';
                position: absolute;
                left: 50%;
                top: 50%;
                width: 5px;
                height: 10px;
                border: solid #1e1e1e;
                border-width: 0 2px 2px 0;
                transform: translate(-50%, -50%) rotate(45deg);
                margin-top: -2px;
            }
            .${CHECKBOX_CLASS}:hover {
                border-color: var(--gemini-bulk-accent-hover);
            }
            .gemini-bulk-selected {
                background-color: var(--gemini-bulk-border);
            }
            /* Floating Bar Component */
            .gemini-floating-bar {
                position: fixed;
                bottom: 30px;
                left: 50%;
                transform: translateX(-50%) translateY(100px);
                background: var(--gemini-bulk-bg);
                backdrop-filter: blur(12px);
                -webkit-backdrop-filter: blur(12px);
                color: var(--gemini-bulk-text);
                padding: 12px 24px;
                border-radius: 16px;
                display: flex;
                align-items: center;
                gap: 20px;
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
                border: 1px solid var(--gemini-bulk-border);
                z-index: 9999;
                font-family: 'Google Sans', Roboto, sans-serif;
                font-size: 14px;
                font-weight: 500;
                transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.4s ease;
                opacity: 0;
                pointer-events: none;
            }
            .gemini-floating-bar.visible {
                transform: translateX(-50%) translateY(0);
                opacity: 1;
                pointer-events: auto;
            }
            .gemini-floating-bar button {
                background: var(--gemini-bulk-danger);
                color: #fff;
                border: none;
                padding: 10px 20px;
                border-radius: 20px;
                cursor: pointer;
                font-weight: 600;
                font-size: 14px;
                letter-spacing: 0.5px;
                transition: background-color 0.2s, transform 0.1s;
                box-shadow: 0 2px 8px rgba(234, 67, 53, 0.3);
            }
            .gemini-floating-bar button:hover {
                background: var(--gemini-bulk-danger-hover);
                box-shadow: 0 4px 12px rgba(234, 67, 53, 0.4);
            }
            .gemini-floating-bar button:active {
                transform: scale(0.96);
            }
        `;
        document.head.appendChild(style);
    }

    // --- 3. App Core ---

    class FloatingBar {
        constructor(store, onDelete) {
            this.store = store;
            this.onDelete = onDelete;
            this.el = null;
        }

        template() {
            const count = this.store.selectedCount;
            const visibleClass = count > 0 ? 'visible' : '';

            return html`
                <div id="gemini-bulk-del-bar" class="gemini-floating-bar ${visibleClass}">
                    <span>${count} selected</span>
                    <button id="gemini-bulk-del-btn">Delete</button>
                </div>
            `;
        }

        mount(container) {
            const wrapper = document.createElement('div');
            wrapper.innerHTML = this.template();
            this.el = wrapper.firstElementChild;
            container.appendChild(this.el);
            this.bindEvents();
        }

        update() {
            if (!this.el) {
                return;
            }

            const count = this.store.selectedCount;
            const countSpan = this.el.querySelector('span');
            if (countSpan) {
                countSpan.textContent = `${count} selected`;
            }

            if (count > 0) {
                this.el.classList.add('visible');
            } else {
                this.el.classList.remove('visible');
            }
        }

        bindEvents() {
            const btn = this.el.querySelector('#gemini-bulk-del-btn');
            if (btn) btn.addEventListener('click', this.onDelete);
        }
    }

    class App {
        constructor() {
            this.store = reactive({ selectedCount: 0 }, () => this.update());
            this.floatingBar = new FloatingBar(this.store, () => this.deleteSelectedItems());
        }

        init() {
            console.log('[Bulk Delete] Vanilla App Init');
            addStyles();
            this.floatingBar.mount(document.body);
            this.initObserver();
            this.injectCheckboxes();
        }

        update() {
            this.floatingBar.update();
        }

        initObserver() {
            const observer = new MutationObserver((mutations) => {
                let shouldUpdate = false;
                for (const m of mutations) {
                    if (m.addedNodes.length > 0) {
                        shouldUpdate = true;
                        break;
                    }
                }
                if (shouldUpdate) this.injectCheckboxes();
            });
            observer.observe(document.body, { childList: true, subtree: true });
        }

        injectCheckboxes() {
            const links = document.querySelectorAll('a[href^="/app/"][data-test-id="conversation"]');
            links.forEach(link => {
                if (link.dataset.bulkDeleteProcessed) return;

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.className = CHECKBOX_CLASS;

                // Bind events
                checkbox.addEventListener('click', (e) => e.stopPropagation());
                checkbox.addEventListener('change', (e) => this.handleCheckboxChange(e));

                link.insertBefore(checkbox, link.firstChild);
                link.dataset.bulkDeleteProcessed = 'true';
                link.style.display = 'flex';
                link.style.alignItems = 'center';
            });
        }

        handleCheckboxChange(e) {
            const link = e.target.closest('a');
            if (e.target.checked) {
                link.classList.add('gemini-bulk-selected');
            } else {
                link.classList.remove('gemini-bulk-selected');
            }
            this.syncState();
        }

        syncState() {
            this.store.selectedCount = document.querySelectorAll(`.${CHECKBOX_CLASS}:checked`).length;
        }

        async deleteSelectedItems() {
            console.log('[Bulk Delete] Starting deletion...');

            const checkboxes = document.querySelectorAll(`.${CHECKBOX_CLASS}:checked`);
            if (!checkboxes.length) {
                return;
            }

            for (const checkbox of checkboxes) {
                const menuButton = checkbox
                    .closest('div')
                    .querySelector('button[data-test-id="actions-menu-button"]');

                if (menuButton) {
                    await this.deleteItem(menuButton);
                }
            }

            this.syncState();

            console.log('[Bulk Delete] Finished');
        }

        async deleteItem(menuButton) {
            menuButton.click();

            await new Promise(resolve => setTimeout(resolve, 100));

            const deleteOption = document.querySelector('[role="menuitem"][data-test-id="delete-button"]');
            if (!deleteOption) {
                return;
            }

            deleteOption.click();

            await new Promise(resolve => setTimeout(resolve, 100));

            const confirmButton = document.querySelector('button[data-test-id="confirm-button"]');
            if (!confirmButton) {
                return;
            }

            confirmButton.click();
            await new Promise(resolve => setTimeout(resolve, 100));
        }
    }

    // --- Boot ---
    const app = new App();
    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        app.init();
    } else {
        window.addEventListener('load', () => app.init());
    }
})();