Gelbooru Auto Tags

Save named tag presets and combine them on the fly — auto-append your active preset(s) to every Gelbooru search.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Gelbooru Auto Tags
// @namespace    http://tampermonkey.net/
// @version      4.0
// @description  Save named tag presets and combine them on the fly — auto-append your active preset(s) to every Gelbooru search.
// @match        *://*.gelbooru.com/*
// @run-at       document-start
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // --- Storage keys ----------------------------------------------------

    const STORAGE_ENABLED       = 'gelbooru-auto-tags-enabled';
    const STORAGE_PRESETS       = 'gelbooru-auto-tags-presets';
    const STORAGE_ACTIVE        = 'gelbooru-auto-tags-active';
    const STORAGE_LAST_APPLIED  = 'gelbooru-auto-tags-last-applied';
    const LEGACY_LIST           = 'gelbooru-auto-tags-list';
    const LEGACY_ENABLED        = 'gelbooru-auto-sort-score-enabled';

    function newId() {
        return 'p_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8);
    }

    // --- Migration -------------------------------------------------------

    function migrate() {
        if (localStorage.getItem(STORAGE_PRESETS) !== null) return;

        // From v2 (single tag list -> one preset called "Default")
        const v2List = localStorage.getItem(LEGACY_LIST);
        if (v2List) {
            try {
                const tags = JSON.parse(v2List);
                if (Array.isArray(tags) && tags.length > 0) {
                    const id = newId();
                    localStorage.setItem(STORAGE_PRESETS, JSON.stringify([
                        { id, name: 'Default', tags }
                    ]));
                    localStorage.setItem(STORAGE_ACTIVE, JSON.stringify([id]));
                    return;
                }
            } catch (e) { /* ignore */ }
        }

        // Fresh install: seed with one preset
        const id = newId();
        localStorage.setItem(STORAGE_PRESETS, JSON.stringify([
            { id, name: 'Score sorted', tags: ['sort:score'] }
        ]));
        localStorage.setItem(STORAGE_ACTIVE, JSON.stringify([id]));

        // Carry over enable state from very old v1 if present
        if (localStorage.getItem(LEGACY_ENABLED) !== null && localStorage.getItem(STORAGE_ENABLED) === null) {
            localStorage.setItem(STORAGE_ENABLED, localStorage.getItem(LEGACY_ENABLED));
        }
    }
    migrate();

    // --- State accessors -------------------------------------------------

    function isEnabled() {
        const v = localStorage.getItem(STORAGE_ENABLED);
        return v === null ? true : v === 'true';
    }
    function setEnabled(val) {
        localStorage.setItem(STORAGE_ENABLED, val ? 'true' : 'false');
    }

    function getPresets() {
        try {
            const arr = JSON.parse(localStorage.getItem(STORAGE_PRESETS) || '[]');
            if (!Array.isArray(arr)) return [];
            return arr.filter(p =>
                p && typeof p.id === 'string' &&
                typeof p.name === 'string' &&
                Array.isArray(p.tags)
            );
        } catch (e) {
            return [];
        }
    }
    function setPresets(presets) {
        localStorage.setItem(STORAGE_PRESETS, JSON.stringify(presets));
    }

    // Active preset IDs are stored as a JSON array. We also accept a bare
    // string for backwards compatibility with v3.x (single active preset).
    function getActiveIds() {
        const raw = localStorage.getItem(STORAGE_ACTIVE);
        if (!raw) return [];
        try {
            const arr = JSON.parse(raw);
            if (Array.isArray(arr)) return arr.filter(x => typeof x === 'string');
        } catch (e) { /* fall through to legacy single-string format */ }
        return [raw];
    }
    function setActiveIds(ids) {
        if (!ids || ids.length === 0) {
            localStorage.removeItem(STORAGE_ACTIVE);
        } else {
            localStorage.setItem(STORAGE_ACTIVE, JSON.stringify(ids));
        }
    }
    function isActive(id) {
        return getActiveIds().includes(id);
    }

    function getActivePresets() {
        const ids = getActiveIds();
        if (ids.length === 0) return [];
        const presets = getPresets();
        return ids.map(id => presets.find(p => p.id === id)).filter(Boolean);
    }
    // Combined tags from every active preset, deduplicated case-insensitively,
    // preserving the order presets were activated in.
    function getActiveTags() {
        const tags = [];
        const seen = new Set();
        for (const preset of getActivePresets()) {
            for (const tag of preset.tags) {
                const key = (tag || '').trim().toLowerCase();
                if (!key || seen.has(key)) continue;
                seen.add(key);
                tags.push(tag);
            }
        }
        return tags;
    }

    // Tags that the script last auto-added to a search.
    // Used to remove the previous preset's tags when switching presets.
    function getLastApplied() {
        try {
            const arr = JSON.parse(localStorage.getItem(STORAGE_LAST_APPLIED) || '[]');
            return Array.isArray(arr) ? arr.filter(t => typeof t === 'string') : [];
        } catch (e) {
            return [];
        }
    }
    function setLastApplied(tags) {
        localStorage.setItem(STORAGE_LAST_APPLIED, JSON.stringify(tags || []));
    }

    // --- Tag logic -------------------------------------------------------

    function tokenize(tags) {
        return (tags || '').split(/[\s+]+/).filter(Boolean);
    }
    function hasExactTag(tags, tag) {
        const t = tag.toLowerCase();
        return tokenize(tags).some(x => x.toLowerCase() === t);
    }
    function hasSortDirective(tags) {
        return tokenize(tags).some(x => /^sort:/i.test(x));
    }

    // Apply auto-tags to a search string:
    //   1. Remove tags that the previous preset added but aren't in the current preset.
    //   2. Add the current preset's tags (skipping ones already present).
    function applyAutoTags(tags) {
        let result = tags || '';

        const activeTags = getActiveTags();
        const activeSet = new Set(activeTags.map(t => t.toLowerCase()));

        // Step 1: clean up tags from the previous preset that the new preset doesn't want
        const lastApplied = getLastApplied();
        const toRemove = lastApplied.filter(t => !activeSet.has(t.toLowerCase()));
        if (toRemove.length > 0) {
            const removeSet = new Set(toRemove.map(t => t.toLowerCase()));
            const sep = result.includes('+') ? '+' : ' ';
            result = tokenize(result).filter(t => !removeSet.has(t.toLowerCase())).join(sep);
        }

        // Step 2: add tags from the current preset
        for (const tag of activeTags) {
            const trimmed = (tag || '').trim();
            if (!trimmed) continue;
            if (/^sort:/i.test(trimmed) && hasSortDirective(result)) continue;
            if (hasExactTag(result, trimmed)) continue;
            const sep = result === '' ? '' : (result.includes('+') ? '+' : ' ');
            result = result + sep + trimmed;
        }

        return result;
    }

    // --- Core behavior ---------------------------------------------------

    function fixCurrentUrl() {
        if (!isEnabled()) return;

        const params = new URLSearchParams(window.location.search);
        if (params.get('page') !== 'post' || params.get('s') !== 'list') return;

        const tags = params.get('tags') || '';
        const updated = applyAutoTags(tags);

        // Record what we just applied so the next preset switch knows what to clean up
        setLastApplied(getActiveTags());

        if (updated === tags) return;

        params.set('tags', updated);
        const newUrl = window.location.pathname + '?' + params.toString() + window.location.hash;
        window.location.replace(newUrl);
    }

    function hookSearchForms() {
        document.addEventListener('submit', function (e) {
            if (!isEnabled()) return;
            const form = e.target;
            if (!(form instanceof HTMLFormElement)) return;
            const tagInput = form.querySelector('input[name="tags"]');
            if (!tagInput) return;
            tagInput.value = applyAutoTags(tagInput.value);
            setLastApplied(getActiveTags());
        }, true);
    }

    // --- UI --------------------------------------------------------------

    let editingDraft = null; // { id: string|null, name: string, tags: string[] }

    function truncate(str, max) {
        if (!str) return '';
        return str.length > max ? str.slice(0, max - 1) + '…' : str;
    }

    function injectStyles() {
        const style = document.createElement('style');
        style.textContent = `
            #gb-at-root, #gb-at-root * { box-sizing: border-box; }

            #gb-at-pill {
                position: fixed;
                bottom: 16px;
                right: 16px;
                z-index: 99999;
                display: flex;
                align-items: center;
                gap: 8px;
                padding: 8px 12px;
                background: rgba(20, 20, 28, 0.92);
                color: #e8e8ec;
                font-family: system-ui, -apple-system, sans-serif;
                font-size: 12px;
                border: 1px solid rgba(255,255,255,0.12);
                border-radius: 999px;
                box-shadow: 0 4px 14px rgba(0,0,0,0.35);
                user-select: none;
                opacity: 0.85;
                transition: opacity 0.2s ease, transform 0.2s ease;
            }
            #gb-at-pill:hover { opacity: 1; transform: translateY(-1px); }

            #gb-at-pill .gb-at-toggle {
                position: relative;
                width: 30px;
                height: 16px;
                background: #555;
                border-radius: 999px;
                transition: background 0.2s ease;
                flex-shrink: 0;
                cursor: pointer;
            }
            #gb-at-pill .gb-at-toggle::after {
                content: "";
                position: absolute;
                top: 2px; left: 2px;
                width: 12px; height: 12px;
                background: #fff;
                border-radius: 50%;
                transition: left 0.2s ease;
            }
            #gb-at-pill.on .gb-at-toggle { background: #4ea1ff; }
            #gb-at-pill.on .gb-at-toggle::after { left: 16px; }

            #gb-at-pill .gb-at-sep { opacity: 0.35; }
            #gb-at-pill .gb-at-active-name {
                font-weight: 600;
                max-width: 140px;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            #gb-at-pill .gb-at-active-name.muted { font-weight: 400; opacity: 0.55; font-style: italic; }
            #gb-at-pill .gb-at-count { opacity: 0.6; font-size: 11px; }
            #gb-at-pill .gb-at-gear {
                cursor: pointer;
                padding: 2px 4px;
                border-radius: 4px;
                opacity: 0.7;
                font-size: 14px;
                line-height: 1;
            }
            #gb-at-pill .gb-at-gear:hover {
                opacity: 1;
                background: rgba(255,255,255,0.08);
            }

            #gb-at-panel {
                position: fixed;
                bottom: 64px;
                right: 16px;
                z-index: 99999;
                width: 320px;
                max-height: calc(100vh - 96px);
                overflow-y: auto;
                padding: 14px;
                background: rgba(24, 24, 32, 0.98);
                color: #e8e8ec;
                font-family: system-ui, -apple-system, sans-serif;
                font-size: 13px;
                border: 1px solid rgba(255,255,255,0.14);
                border-radius: 12px;
                box-shadow: 0 10px 30px rgba(0,0,0,0.5);
                display: none;
            }
            #gb-at-panel.open { display: block; }

            #gb-at-panel .gb-at-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                margin-bottom: 10px;
                font-weight: 600;
            }
            #gb-at-panel .gb-at-close {
                cursor: pointer;
                opacity: 0.6;
                padding: 0 6px;
                font-size: 18px;
                line-height: 1;
            }
            #gb-at-panel .gb-at-close:hover { opacity: 1; }

            #gb-at-panel .gb-at-label {
                font-size: 11px;
                text-transform: uppercase;
                letter-spacing: 0.05em;
                opacity: 0.55;
                margin: 6px 0 6px;
            }

            /* Preset list rows */
            #gb-at-panel .gb-at-presets {
                display: flex;
                flex-direction: column;
                gap: 4px;
                margin-bottom: 10px;
            }
            #gb-at-panel .gb-at-preset {
                display: flex;
                align-items: center;
                gap: 8px;
                padding: 8px 10px;
                background: rgba(255,255,255,0.03);
                border: 1px solid rgba(255,255,255,0.08);
                border-radius: 8px;
                cursor: pointer;
                transition: background 0.15s ease, border-color 0.15s ease;
            }
            #gb-at-panel .gb-at-preset:hover {
                background: rgba(255,255,255,0.06);
                border-color: rgba(255,255,255,0.15);
            }
            #gb-at-panel .gb-at-preset.active {
                background: rgba(78, 161, 255, 0.12);
                border-color: rgba(78, 161, 255, 0.5);
            }
            #gb-at-panel .gb-at-checkbox {
                width: 16px; height: 16px;
                border: 2px solid rgba(255,255,255,0.3);
                border-radius: 4px;
                flex-shrink: 0;
                position: relative;
                cursor: pointer;
                transition: background 0.15s ease, border-color 0.15s ease;
            }
            #gb-at-panel .gb-at-checkbox:hover {
                border-color: rgba(255,255,255,0.55);
            }
            #gb-at-panel .gb-at-preset.active .gb-at-checkbox {
                border-color: #4ea1ff;
                background: #4ea1ff;
            }
            #gb-at-panel .gb-at-preset.active .gb-at-checkbox::after {
                content: '';
                position: absolute;
                left: 3px; top: 0px;
                width: 4px; height: 8px;
                border: solid white;
                border-width: 0 2px 2px 0;
                transform: rotate(45deg);
            }
            #gb-at-panel .gb-at-preset-info {
                flex: 1;
                min-width: 0;
            }
            #gb-at-panel .gb-at-preset-name {
                font-weight: 600;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
            }
            #gb-at-panel .gb-at-preset-tags {
                font-size: 11px;
                opacity: 0.55;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                margin-top: 2px;
            }
            #gb-at-panel .gb-at-preset-actions {
                display: flex;
                gap: 2px;
                flex-shrink: 0;
            }
            #gb-at-panel .gb-at-icon-btn {
                cursor: pointer;
                width: 24px; height: 24px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                border-radius: 4px;
                opacity: 0.6;
                font-size: 13px;
            }
            #gb-at-panel .gb-at-icon-btn:hover {
                opacity: 1;
                background: rgba(255,255,255,0.1);
            }

            #gb-at-panel .gb-at-empty {
                opacity: 0.4;
                font-style: italic;
                font-size: 12px;
                padding: 8px 0;
                text-align: center;
            }

            /* Edit form */
            #gb-at-panel .gb-at-name-input {
                width: 100%;
                padding: 8px 10px;
                background: rgba(0,0,0,0.4);
                border: 1px solid rgba(255,255,255,0.15);
                border-radius: 6px;
                color: #e8e8ec;
                font-size: 14px;
                font-weight: 600;
                font-family: inherit;
                outline: none;
                margin-bottom: 12px;
            }
            #gb-at-panel .gb-at-name-input:focus {
                border-color: rgba(78, 161, 255, 0.6);
            }

            #gb-at-panel .gb-at-chips {
                display: flex;
                flex-wrap: wrap;
                gap: 5px;
                min-height: 24px;
                margin-bottom: 10px;
            }
            #gb-at-panel .gb-at-chip {
                display: inline-flex;
                align-items: center;
                gap: 5px;
                padding: 3px 4px 3px 9px;
                background: rgba(78, 161, 255, 0.18);
                border: 1px solid rgba(78, 161, 255, 0.35);
                border-radius: 999px;
                font-size: 12px;
                max-width: 100%;
                word-break: break-all;
            }
            #gb-at-panel .gb-at-chip-x {
                cursor: pointer;
                width: 16px; height: 16px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                border-radius: 50%;
                opacity: 0.6;
                font-size: 14px;
                line-height: 1;
                flex-shrink: 0;
            }
            #gb-at-panel .gb-at-chip-x:hover {
                opacity: 1;
                background: rgba(255,255,255,0.1);
            }

            #gb-at-panel .gb-at-input-row {
                display: flex;
                gap: 6px;
                margin-bottom: 12px;
            }
            #gb-at-panel input.gb-at-tag-input {
                flex: 1;
                min-width: 0;
                padding: 6px 8px;
                background: rgba(0,0,0,0.4);
                border: 1px solid rgba(255,255,255,0.15);
                border-radius: 6px;
                color: #e8e8ec;
                font-size: 12px;
                font-family: inherit;
                outline: none;
            }
            #gb-at-panel input.gb-at-tag-input:focus {
                border-color: rgba(78, 161, 255, 0.6);
            }

            #gb-at-panel .gb-at-btn-row {
                display: flex;
                justify-content: space-between;
                gap: 6px;
                margin-top: 10px;
            }
            #gb-at-panel button.gb-at-btn {
                padding: 7px 14px;
                background: rgba(255,255,255,0.06);
                border: 1px solid rgba(255,255,255,0.12);
                border-radius: 6px;
                color: #e8e8ec;
                font-size: 12px;
                font-weight: 600;
                cursor: pointer;
                font-family: inherit;
            }
            #gb-at-panel button.gb-at-btn:hover { background: rgba(255,255,255,0.12); }
            #gb-at-panel button.gb-at-btn.primary {
                background: #4ea1ff;
                border-color: #4ea1ff;
                color: #fff;
            }
            #gb-at-panel button.gb-at-btn.primary:hover { background: #6cb4ff; border-color: #6cb4ff; }
            #gb-at-panel button.gb-at-btn.full { width: 100%; }
            #gb-at-panel button.gb-at-btn.add {
                padding: 6px 12px;
                background: #4ea1ff;
                border-color: #4ea1ff;
                color: #fff;
            }
            #gb-at-panel button.gb-at-btn.add:hover { background: #6cb4ff; }

            #gb-at-panel .gb-at-hint {
                margin-top: 8px;
                font-size: 11px;
                opacity: 0.45;
                line-height: 1.4;
            }
            #gb-at-panel .gb-at-hint code {
                background: rgba(255,255,255,0.07);
                padding: 1px 4px;
                border-radius: 3px;
                font-size: 10px;
            }
        `;
        document.head.appendChild(style);
    }

    function buildPanelListView() {
        const presets = getPresets();

        let html = `
            <div class="gb-at-header">
                <span>Auto-tag presets</span>
                <span class="gb-at-close" data-action="close">×</span>
            </div>
            <div class="gb-at-label">Click a preset to use only it · Use the checkbox to combine</div>
            <div class="gb-at-presets">
        `;

        if (presets.length === 0) {
            html += `<div class="gb-at-empty">No presets yet — create one below.</div>`;
        } else {
            for (const p of presets) {
                const active = isActive(p.id);
                html += `
                    <div class="gb-at-preset ${active ? 'active' : ''}" data-action="solo" data-preset-id="${p.id}">
                        <div class="gb-at-checkbox" data-action="toggle-active" data-preset-id="${p.id}" title="Toggle in combination"></div>
                        <div class="gb-at-preset-info">
                            <div class="gb-at-preset-name"></div>
                            <div class="gb-at-preset-tags"></div>
                        </div>
                        <div class="gb-at-preset-actions">
                            <span class="gb-at-icon-btn" data-action="edit" data-preset-id="${p.id}" title="Edit">✏</span>
                            <span class="gb-at-icon-btn" data-action="delete" data-preset-id="${p.id}" title="Delete">🗑</span>
                        </div>
                    </div>
                `;
            }
        }

        html += `
            </div>
            <button class="gb-at-btn full primary" data-action="new">+ New preset</button>
            <div class="gb-at-hint">Click a row to use only that preset. Click the checkbox on the left to add or remove a preset from the active mix without affecting the others.</div>
        `;

        return html;
    }

    function buildPanelEditView() {
        const isNew = editingDraft.id === null;
        return `
            <div class="gb-at-header">
                <span>${isNew ? 'New preset' : 'Edit preset'}</span>
                <span class="gb-at-close" data-action="cancel-edit">×</span>
            </div>
            <div class="gb-at-label">Preset name</div>
            <input class="gb-at-name-input" type="text" placeholder="e.g. Glasses mode" value="">
            <div class="gb-at-label">Tags</div>
            <div class="gb-at-chips"></div>
            <div class="gb-at-input-row">
                <input class="gb-at-tag-input" type="text" placeholder="add a tag…">
                <button class="gb-at-btn add" data-action="add-tag">Add</button>
            </div>
            <div class="gb-at-btn-row">
                <button class="gb-at-btn" data-action="cancel-edit">Cancel</button>
                <button class="gb-at-btn primary" data-action="save">Save</button>
            </div>
            <div class="gb-at-hint">Press Enter in the tag box to add. Multiple tags can be separated by spaces.</div>
        `;
    }

    function injectUI() {
        if (document.getElementById('gb-at-root')) return;
        injectStyles();

        const root = document.createElement('div');
        root.id = 'gb-at-root';
        root.innerHTML = `
            <div id="gb-at-pill" title="Auto-tags">
                <span class="gb-at-toggle" data-action="toggle"></span>
                <span class="gb-at-active-name"></span>
                <span class="gb-at-count"></span>
                <span class="gb-at-sep">|</span>
                <span class="gb-at-gear" data-action="open" title="Manage presets">⚙</span>
            </div>
            <div id="gb-at-panel"></div>
        `;
        document.body.appendChild(root);

        const pill = root.querySelector('#gb-at-pill');
        const panel = root.querySelector('#gb-at-panel');
        const nameEl = pill.querySelector('.gb-at-active-name');
        const countEl = pill.querySelector('.gb-at-count');

        function renderPill() {
            const on = isEnabled();
            const activePresets = getActivePresets();
            pill.classList.toggle('on', on);

            if (activePresets.length === 0) {
                nameEl.textContent = 'no preset';
                nameEl.classList.add('muted');
                countEl.textContent = '';
            } else if (activePresets.length === 1) {
                nameEl.textContent = truncate(activePresets[0].name, 18);
                nameEl.classList.remove('muted');
                const tagCount = getActiveTags().length;
                countEl.textContent = tagCount > 0 ? `(${tagCount})` : '';
            } else {
                const first = activePresets[0].name;
                const more = activePresets.length - 1;
                nameEl.textContent = `${truncate(first, 12)} +${more}`;
                nameEl.classList.remove('muted');
                const tagCount = getActiveTags().length;
                countEl.textContent = tagCount > 0 ? `(${tagCount})` : '';
            }
        }

        function renderPanel() {
            if (editingDraft) {
                panel.innerHTML = buildPanelEditView();
                const nameInput = panel.querySelector('.gb-at-name-input');
                nameInput.value = editingDraft.name;
                nameInput.addEventListener('input', () => {
                    editingDraft.name = nameInput.value;
                });
                renderEditChips();
                const tagInput = panel.querySelector('.gb-at-tag-input');
                tagInput.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter') {
                        e.preventDefault();
                        addTagFromInput(tagInput);
                    }
                });
                setTimeout(() => nameInput.focus(), 0);
            } else {
                panel.innerHTML = buildPanelListView();
                const presets = getPresets();
                const rows = panel.querySelectorAll('.gb-at-preset');
                rows.forEach((row, i) => {
                    const p = presets[i];
                    if (!p) return;
                    row.querySelector('.gb-at-preset-name').textContent = p.name || '(unnamed)';
                    const tagPreview = p.tags.length > 0 ? p.tags.join(', ') : '(no tags)';
                    row.querySelector('.gb-at-preset-tags').textContent = tagPreview;
                });
            }
        }

        function renderEditChips() {
            const chipsEl = panel.querySelector('.gb-at-chips');
            chipsEl.innerHTML = '';
            if (editingDraft.tags.length === 0) {
                const empty = document.createElement('span');
                empty.className = 'gb-at-empty';
                empty.style.padding = '0';
                empty.textContent = 'No tags yet.';
                chipsEl.appendChild(empty);
                return;
            }
            for (const tag of editingDraft.tags) {
                const chip = document.createElement('span');
                chip.className = 'gb-at-chip';
                const text = document.createElement('span');
                text.textContent = tag;
                const x = document.createElement('span');
                x.className = 'gb-at-chip-x';
                x.title = 'Remove';
                x.textContent = '×';
                x.addEventListener('click', () => {
                    editingDraft.tags = editingDraft.tags.filter(t => t !== tag);
                    renderEditChips();
                });
                chip.appendChild(text);
                chip.appendChild(x);
                chipsEl.appendChild(chip);
            }
        }

        function addTagFromInput(input) {
            const raw = input.value.trim();
            if (!raw) return;
            const incoming = raw.split(/\s+/).filter(Boolean);
            const seen = new Set(editingDraft.tags.map(t => t.toLowerCase()));
            for (const t of incoming) {
                if (!seen.has(t.toLowerCase())) {
                    editingDraft.tags.push(t);
                    seen.add(t.toLowerCase());
                }
            }
            input.value = '';
            renderEditChips();
        }

        function openPanel() {
            editingDraft = null;
            renderPanel();
            panel.classList.add('open');
        }
        function closePanel() {
            panel.classList.remove('open');
            editingDraft = null;
        }

        function startEdit(presetId) {
            const p = getPresets().find(x => x.id === presetId);
            if (!p) return;
            editingDraft = { id: p.id, name: p.name, tags: p.tags.slice() };
            renderPanel();
        }

        function startNew() {
            editingDraft = { id: null, name: '', tags: [] };
            renderPanel();
        }

        function saveDraft() {
            const name = (editingDraft.name || '').trim();
            if (!name) {
                alert('Please give the preset a name.');
                return;
            }
            const presets = getPresets();
            if (editingDraft.id === null) {
                const id = newId();
                presets.push({ id, name, tags: editingDraft.tags.slice() });
                setPresets(presets);
                // If nothing was active, make this new preset active
                if (getActiveIds().length === 0) setActiveIds([id]);
            } else {
                const idx = presets.findIndex(p => p.id === editingDraft.id);
                if (idx >= 0) {
                    presets[idx] = { id: editingDraft.id, name, tags: editingDraft.tags.slice() };
                    setPresets(presets);
                }
            }
            editingDraft = null;
            renderPanel();
            renderPill();
            fixCurrentUrl();
        }

        function cancelEdit() {
            editingDraft = null;
            renderPanel();
        }

        function deletePreset(presetId) {
            const p = getPresets().find(x => x.id === presetId);
            if (!p) return;
            if (!confirm(`Delete preset "${p.name}"?`)) return;
            const remaining = getPresets().filter(x => x.id !== presetId);
            setPresets(remaining);
            // Drop the deleted preset from the active list
            const newActive = getActiveIds().filter(id => id !== presetId);
            setActiveIds(newActive);
            renderPanel();
            renderPill();
            fixCurrentUrl();
        }

        function soloPreset(presetId) {
            // "Use only this preset" — replace the active list with just this one
            setActiveIds([presetId]);
            renderPill();
            renderPanel();
            fixCurrentUrl();
        }

        function toggleActive(presetId) {
            // Add or remove this preset from the active list without affecting others
            const ids = getActiveIds();
            if (ids.includes(presetId)) {
                setActiveIds(ids.filter(id => id !== presetId));
            } else {
                setActiveIds(ids.concat(presetId));
            }
            renderPill();
            renderPanel();
            fixCurrentUrl();
        }

        // Event delegation
        root.addEventListener('click', (e) => {
            let el = e.target;
            while (el && el !== root && !(el.dataset && el.dataset.action)) {
                el = el.parentElement;
            }
            if (!el || el === root) return;
            const action = el.dataset.action;
            const presetId = el.dataset.presetId;

            if (action !== 'solo') e.stopPropagation();

            switch (action) {
                case 'toggle': {
                    const on = !isEnabled();
                    setEnabled(on);
                    renderPill();
                    if (on) fixCurrentUrl();
                    break;
                }
                case 'open':
                    if (panel.classList.contains('open')) closePanel();
                    else openPanel();
                    break;
                case 'close':
                    closePanel();
                    break;
                case 'solo':
                    if (presetId) soloPreset(presetId);
                    break;
                case 'toggle-active':
                    if (presetId) toggleActive(presetId);
                    break;
                case 'edit':
                    if (presetId) startEdit(presetId);
                    break;
                case 'delete':
                    if (presetId) deletePreset(presetId);
                    break;
                case 'new':
                    startNew();
                    break;
                case 'save':
                    saveDraft();
                    break;
                case 'cancel-edit':
                    cancelEdit();
                    break;
                case 'add-tag': {
                    const input = panel.querySelector('.gb-at-tag-input');
                    if (input) addTagFromInput(input);
                    break;
                }
            }
        });

        // Click outside panel to close it.
        // We use composedPath() instead of contains(e.target) because some
        // in-panel handlers (e.g. removing a tag chip) re-render the panel,
        // detaching e.target from the DOM before this handler runs. The path
        // is captured at dispatch time, so it's still accurate.
        document.addEventListener('click', (e) => {
            if (!panel.classList.contains('open')) return;
            const path = e.composedPath();
            if (path.includes(panel) || path.includes(pill)) return;
            closePanel();
        });

        renderPill();
    }

    // --- Init ------------------------------------------------------------

    fixCurrentUrl();

    function onReady() {
        hookSearchForms();
        injectUI();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', onReady);
    } else {
        onReady();
    }
})();