Dynamic Element Toggle Panel at web page

Объединённая изолированная панель управления элементами (Shadow DOM, надёжная синхронизация, минимальная нагрузка)

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Dynamic Element Toggle Panel at web page
// @namespace    gemini
// @version      3.1.0
// @description  Объединённая изолированная панель управления элементами (Shadow DOM, надёжная синхронизация, минимальная нагрузка)
// @author       Wizzergod
// @icon         https://images.icon-icons.com/2072/PNG/96/disable_eye_hidden_hide_internet_security_view_icon_127055.png
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-body
// @noframes     true
// @inject-into  content
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ---- Константы и конфигурация ----
    const STORAGE_KEY = 'DynamicTogglePanelConfig';
    const HOSTNAME = location.hostname;
    const MAX_FALLBACK_ATTEMPTS = 6;
    const FALLBACK_DELAY_MS = 700;
    const APPLY_DEBOUNCE_MS = 120;

    // ---- Утилиты хранения ----
    const getFullStorage = () => GM_getValue(STORAGE_KEY, {});
    const saveFullStorage = (obj) => GM_setValue(STORAGE_KEY, obj);
    const getSiteConfig = () => (getFullStorage()[HOSTNAME] || []);
    const saveSiteConfig = (arr) => {
        const s = getFullStorage();
        if (arr.length) s[HOSTNAME] = arr; else delete s[HOSTNAME];
        saveFullStorage(s);
    };

    // ---- Изоляция UI через Shadow DOM ----
    const host = document.createElement('div');
    host.id = 'dyn-panel-shadow-host';
    host.style.position = 'fixed';
    host.style.top = '0';
    host.style.left = '0';
    host.style.zIndex = '2147483647';

    (document.body || document.documentElement).appendChild(host);
    const shadow = host.attachShadow({ mode: 'open' });

    // ---- Обновленный и приглаженный CSS ----
    const css = `
        :host { all: initial; z-index: 99989; }

        #dyn-open-btn {
            position: fixed;
            top: 40%;
            left: -30px;
            width: 40px;
            height: 80px;
            background: #1b2838;
            border: 2px solid #2a475e;
            border-radius: 0 12px 12px 0;
            color: white;
            font-size: 18px;
            cursor: pointer;
            z-index: 2147483647;
            box-shadow: 0 2px 10px rgba(0,0,0,.5);
            opacity: 0.6;
            transition: left .3s ease, opacity .2s, background .2s;
            transition-delay: .6s;
        }

        #dyn-open-btn:hover {
            left: 0px;
            background: #2a475e;
            opacity: 0.9;
            transition-delay: 0s;
        }

        #dyn-panel {
            position: fixed;
            top: 10px;
            left: 10px;
            min-width: 320px;
            max-width: 510px;
            background: #1b2838;
            border: 2px solid #2a475e;
            border-radius: 12px;
            padding: 10px;
            color: white;
            z-index: 2147483647;
            font-family: Arial, sans-serif;
            box-shadow: 0 4px 20px rgba(0,0,0,.7);
            max-height: 95vh;
            overflow-y: auto;
            scrollbar-width: thin;
            bottom: 10px;
            opacity: 0;
            transform: scale(0.95) translateY(-10px);
            pointer-events: none;
            transition: opacity 0.2s ease, transform 0.2s ease;
        }

        #dyn-panel.open {
            opacity: 1;
            transform: scale(1) translateY(0);
            pointer-events: auto;
        }

        .dyn-panel-title { margin: 0 0 15px 0; text-align: center; font-size: 16px; font-weight: bold; }
        #dyn-toggles-container { max-height: 575px; min-height: 280px; overflow-y: auto; background: #101720cc; padding: 5px; border-radius: 12px; scrollbar-width: thin; border: 1px solid #2a475e; }
        #dyn-no-items { font-size: 13px; opacity: 0.7; text-align: center; margin: 10px 0; }

        .toggle-item { display: flex; justify-content: space-between; align-items: center; margin: 0 0 4px 0; border: 1px solid transparent; transition: background .2s, border .2s; }
        .toggle-item:hover { background: rgba(185, 255, 199, 0.03); border: 1px solid #2a475e; }
        .toggle-item:nth-child(odd) { background: rgba(255,255,255,0.03); border-radius: 12px; padding: 4px 6px; }
        .toggle-item:nth-child(even) { background: #192635; border-radius: 12px; padding: 4px 6px; }

        .toggle-item-controls { display: flex; align-items: center; flex-grow: 1; margin-left: 8px; cursor: pointer; }
        .toggle-label { font-size: 14px; flex-grow: 1; margin-right: 10px; word-break: break-word; user-select: none; cursor: pointer; }

        .toggle-switch { position: relative; display: inline-block; width: 40px; height: 20px; flex-shrink: 0; cursor: pointer; }
        .toggle-switch input { opacity: 0; width: 0; height: 0; }
        .toggle-slider { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #2a475e; transition: .3s; border-radius: 12px; }
        .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background: white; transition: .3s; border-radius: 12px; }

        input:checked + .toggle-slider { background-color: #5c7e10; }
        input:checked + .toggle-slider:before { transform: translateX(20px); }

        .panel-buttons { display: flex; gap: 5px; margin-top: 15px; }
        .panel-btn { flex: 1; padding: 7px; border: none; border-radius: 12px; cursor: pointer; font-size: 12px; font-weight: bold; transition: background-color 0.2s ease; }

        .show-all-btn { background: #5c7e10; color: white; }
        .show-all-btn:hover { background: #749f14; }
        .hide-all-btn { background: #7e1010; color: white; }
        .hide-all-btn:hover { background: #b21616; }

        .dyn-panel-separator { border: 0; border-top: 1px solid #2a475e; margin: 12px 0; }
        .dyn-add-form { display: flex; flex-direction: column; gap: 10px; }
        .dyn-panel-author { font-size: 13px; font-weight: bold; margin: 5px 0 0 0; text-align: center; opacity: 0.6; }

        .panel-input { width: 100%; padding: 8px; background: #101822; border: 1px solid #2a475e; color: white; border-radius: 12px; box-sizing: border-box; font-size: 13px; transition: background-color 0.2s ease; }
        .panel-input:hover, .panel-input:focus { background: #151f2d; outline: none; }

        .add-btn { background: #5c7e10; color: white; padding: 8px; }
        .add-btn:hover { background: #749f14; }

        .delete-btn { background: #2a475e; color: white; border: none; border-radius: 12px; width: 22px; height: 22px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; padding: 0; flex-shrink: 0; transition: background-color 0.2s ease; }
        .delete-btn:hover { background: #ca2121; }

        .clear-all-btn { background: #a01a1a; color: white; width: 100%; }
        .clear-all-btn:hover { background: #ca2121; }
    `;

    const styleEl = document.createElement('style');
    styleEl.textContent = css;
    shadow.appendChild(styleEl);

    // ---- Компоненты UI ----
    const root = document.createElement('div');
    shadow.appendChild(root);

    function buildUI() {
        root.innerHTML = '';
        const openBtn = document.createElement('button');
        openBtn.id = 'dyn-open-btn';
        openBtn.innerHTML = '☰';
        openBtn.title = 'Управление элементами';

        const panel = document.createElement('div');
        panel.id = 'dyn-panel';

        const toggles = document.createElement('div');
        toggles.id = 'dyn-toggles-container';
        panel.appendChild(toggles);

        const btns = document.createElement('div');
        btns.className = 'panel-buttons';
        const showAll = document.createElement('button');
        showAll.className = 'panel-btn show-all-btn';
        showAll.textContent = 'Показать всё 🔴';
        const hideAll = document.createElement('button');
        hideAll.className = 'panel-btn hide-all-btn';
        hideAll.textContent = 'Скрыть всё 🟢';
        btns.appendChild(showAll);
        btns.appendChild(hideAll);
        panel.appendChild(btns);

        panel.appendChild(Object.assign(document.createElement('hr'), { className: 'dyn-panel-separator' }));
        const form = document.createElement('div');
        form.className = 'dyn-add-form';
        const selectorInput = document.createElement('input');
        selectorInput.className = 'panel-input';
        selectorInput.placeholder = 'CSS селектор';
        form.appendChild(selectorInput);
        const nameInput = document.createElement('input');
        nameInput.className = 'panel-input';
        nameInput.placeholder = 'Название';
        form.appendChild(nameInput);
        const addBtn = document.createElement('button');
        addBtn.className = 'panel-btn add-btn';
        addBtn.textContent = 'Добавить';
        form.appendChild(addBtn);
        panel.appendChild(form);

        panel.appendChild(Object.assign(document.createElement('hr'), { className: 'dyn-panel-separator' }));
        const clearBtn = document.createElement('button');
        clearBtn.className = 'panel-btn clear-all-btn';
        clearBtn.textContent = 'Сбросить всё для сайта';
        panel.appendChild(clearBtn);

        panel.appendChild(Object.assign(document.createElement('hr'), { className: 'dyn-panel-separator' }));
        const sub = document.createElement('h4');
        sub.className = 'dyn-panel-author';
        sub.textContent = '❮ By Wizzergod 2026 ❯';
        panel.appendChild(sub);

        root.appendChild(openBtn);
        root.appendChild(panel);

        // ---- Логика событий открытия/закрытия ----
        openBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            const isOpen = panel.classList.toggle('open');
            if (isOpen) populateToggles();
        });

        document.addEventListener('click', (e) => {
            if (panel.classList.contains('open') && !e.composedPath().includes(host)) {
                panel.classList.remove('open');
            }
        });

        addBtn.addEventListener('click', () => {
            const res = addItem(selectorInput.value.trim(), nameInput.value.trim());
            if (res) {
                selectorInput.value = '';
                nameInput.value = '';
                populateToggles();
            }
        });

        showAll.addEventListener('click', () => { updateAll(true); populateToggles(); });
        hideAll.addEventListener('click', () => { updateAll(false); populateToggles(); });
        clearBtn.addEventListener('click', () => {
            if (confirm('Сбросить все настройки для этого сайта?')) {
                const cfg = getSiteConfig();
                cfg.forEach(i => applyVisibility(i.selector, true));
                saveSiteConfig([]);
                populateToggles();
            }
        });
    }

    // ---- UI: наполнение и элементы ----
    function populateToggles() {
        const cont = root.querySelector('#dyn-toggles-container');
        if (!cont) return;
        cont.innerHTML = '';
        const cfg = getSiteConfig();
        if (cfg.length === 0) {
            cont.innerHTML = '<div id="dyn-no-items">Нет элементов</div>';
            return;
        }
        cfg.forEach(item => cont.appendChild(makeToggle(item)));
    }

    function makeToggle(item) {
        const row = document.createElement('div');
        row.className = 'toggle-item';
        row.dataset.selector = item.selector;

        const del = document.createElement('button');
        del.className = 'delete-btn';
        del.textContent = '⛔';
        del.title = 'Удалить';
        del.addEventListener('click', (e) => {
            e.stopPropagation();
            if (confirm(`Удалить "${item.name}"?`)) removeItem(item.selector);
        });

        const box = document.createElement('label');
        box.className = 'toggle-item-controls';

        const lbl = document.createElement('span');
        lbl.className = 'toggle-label';
        lbl.textContent = item.name;
        lbl.title = item.selector;

        const sw = document.createElement('span');
        sw.className = 'toggle-switch';

        const inp = document.createElement('input');
        inp.type = 'checkbox';
        inp.checked = !!item.visible;

        const sld = document.createElement('span');
        sld.className = 'toggle-slider';

        inp.addEventListener('change', () => {
            applyVisibility(item.selector, inp.checked);
            saveItemVisibility(item.selector, inp.checked);
        });

        sw.appendChild(inp);
        sw.appendChild(sld);
        box.appendChild(lbl);
        box.appendChild(sw);
        row.appendChild(del);
        row.appendChild(box);

        return row;
    }

    // ---- CRUD для настроек ----
    function addItem(selector, name) {
        if (!selector || !name) { alert('Заполните оба поля'); return false; }

        // Валидация корректности CSS селектора
        try {
            document.createDocumentFragment().querySelector(selector);
        } catch(e) {
            alert('Недопустимый CSS селектор. Пожалуйста, проверьте синтаксис.');
            return false;
        }

        const cfg = getSiteConfig();
        if (cfg.find(i => i.selector === selector)) { alert('Этот селектор уже добавлен'); return false; }

        cfg.push({ selector: selector, name: name, visible: true });
        saveSiteConfig(cfg);
        applyVisibility(selector, true);
        return true;
    }

    function removeItem(selector) {
        const cfg = getSiteConfig().filter(i => i.selector !== selector);
        applyVisibility(selector, true);
        saveSiteConfig(cfg);
        populateToggles();
    }

    function saveItemVisibility(selector, isVisible) {
        const cfg = getSiteConfig();
        const it = cfg.find(i => i.selector === selector);
        if (it) { it.visible = isVisible; saveSiteConfig(cfg); }
    }

    function updateAll(isVisible) {
        const cfg = getSiteConfig();
        cfg.forEach(i => { i.visible = isVisible; applyVisibility(i.selector, isVisible); });
        saveSiteConfig(cfg);
    }

    // ---- Надежное применение видимости ----
    function applyVisibility(selector, visible) {
        try {
            document.querySelectorAll(selector).forEach(el => {
                el.style.setProperty('display', visible ? '' : 'none', 'important');
            });
        } catch(e) {
            console.error('[TogglePanel] Ошибка применения селектора:', selector, e);
        }
    }

    let queuedSelectors = new Set();
    let applyTimer = null;

    function scheduleApply(selector) {
        queuedSelectors.add(selector);
        debounceApply();
    }

    function debounceApply() {
        if (applyTimer) clearTimeout(applyTimer);
        applyTimer = setTimeout(() => {
            const arr = Array.from(queuedSelectors);
            queuedSelectors.clear();
            if (arr.length === 0) return;

            const runLogic = () => {
                const cfg = getSiteConfig();
                arr.forEach(s => {
                    const target = cfg.find(i => i.selector === s);
                    applyVisibility(s, target ? target.visible : true);
                });
            };

            if ('requestIdleCallback' in window) {
                requestIdleCallback(runLogic, { timeout: 1000 });
            } else {
                setTimeout(runLogic, 0);
            }
        }, APPLY_DEBOUNCE_MS);
    }

    // ---- Оптимизированный MutationObserver ----
    function startObserver() {
        const observer = new MutationObserver(mutations => {
            const cfg = getSiteConfig();
            if (cfg.length === 0) return;

            let needUpdate = false;
            // Быстрый выход из цикла мутаций, чтобы убрать лишнюю нагрузку O(N)
            for (const m of mutations) {
                if ((m.addedNodes && m.addedNodes.length > 0) || m.type === 'attributes') {
                    needUpdate = true;
                    break;
                }
            }

            if (needUpdate) {
                cfg.forEach(i => queuedSelectors.add(i.selector));
                debounceApply();
            }
        });

        observer.observe(document.documentElement || document.body, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['class', 'id', 'style']
        });

        // Фоллбек-ретраи для динамических SPA-приложений
        (function fallback(attempt) {
            if (attempt > MAX_FALLBACK_ATTEMPTS) return;
            const cfg = getSiteConfig();
            if (cfg.length > 0) {
                cfg.forEach(i => applyVisibility(i.selector, i.visible));
            }
            setTimeout(() => fallback(attempt + 1), FALLBACK_DELAY_MS);
        })(0);
    }

    function initApplyAll() {
        const cfg = getSiteConfig();
        if (cfg.length === 0) return;
        cfg.forEach(i => {
            applyVisibility(i.selector, i.visible);
            scheduleApply(i.selector);
        });
    }

    // ---- Инициализация ----
    buildUI();

    try {
        initApplyAll();
        if ('requestIdleCallback' in window) {
            requestIdleCallback(startObserver, { timeout: 2000 });
        } else {
            setTimeout(startObserver, 600);
        }
    } catch(e) {
        console.error('[TogglePanel] Init error', e);
    }

    // ---- Экспорт в консоль для отладки ----
    try {
        Object.defineProperty(window, '__DynamicTogglePanel', {
            value: { getSiteConfig, saveSiteConfig, applyVisibility },
            writable: false,
            configurable: true
        });
    } catch(e) {}
})();