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