Clean URL Width Posts Parameter on Page Load

Удаляет параметры &width=* и шаблоны _crop_*x* из текущего URL и перезагружает страницу с обновленной анимированной кнопкой Optimize

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Clean URL Width Posts Parameter on Page Load
// @namespace    gemini
// @version      1.7.2
// @description  Удаляет параметры &width=* и шаблоны _crop_*x* из текущего URL и перезагружает страницу с обновленной анимированной кнопкой Optimize
// @author       Wizzergod & Gemini
// @icon         https://cdn-icons-png.flaticon.com/512/9788/9788821.png
// @resource     icon256 https://cdn-icons-png.flaticon.com/256/9788/9788821.png
// @resource     icon512 https://cdn-icons-png.flaticon.com/512/9788/9788821.png
// @homepageURL  https://greasyfork.org/ru/users/712283-wizzergod
// @run-at       document-start
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @match        *://store-images.s-microsoft.com/image/*
// @match        *://*/*_bg_crop_*
// @match        *://*/*width=*
// @match        *://*/*.png*
// @match        *://*/*.jpg*
// @match        *://*/*.webp*
// @match        *://*/*.jpeg*
// @match        *://*/*.jfif*
// @match        *://*/*=s*
// @match        *://*/*=w*
// @match        *://*/*=h*
// @match        *://*/*scale*
// @match        *://*/*orig*
// @match        *://*/scale*
// @match        *://*/orig*
// @match        *://*/*x*
// @match        *://*/*x*_*
// @match        *://yt3.ggpht.com/*
// @match        *://*.ggpht.com/*
// @match        *://ggpht.com/*
// @match        *://play-lh.googleusercontent.com/*
// @match        *://*.googleusercontent.com/*
// @match        *://googleusercontent.com/*
// @match        *://gcore-pic.xnxx-cdn.com/*
// @match        *://hqdefault/*
// @include      *://hqdefault/*
// @match        *://pic.rutubelist.ru/video/*
// @include      *://pic.rutubelist.ru/video/*
// @include      *://img.youtube.com/vi/*
// @include      *://img.youtube.com/*
// @include      *://youtube.com/vi/*
// @match        *://steamuserimages-a.akamaihd.net/*
// @match        *://steamuserimages-*.akamaihd.net/*
// @match        *://*/*.img*
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    // =================================================================================================
    // 1. КЛЮЧИ ДЛЯ ХРАНЕНИЯ В БД
    // =================================================================================================
    const STORAGE_KEYS = {
        PATTERNS: 'cleanurl_patterns',
        PATTERN_SETTINGS: 'pattern_settings'
    };

    // =================================================================================================
    // 2. ПАТТЕРНЫ ПО УМОЛЧАНИЮ
    // =================================================================================================
    const DEFAULT_PATTERNS = [];

    // =================================================================================================
    // 3. ФУНКЦИИ РАБОТЫ С БД И СИСТЕМНЫЕ СОБЫТИЯ
    // =================================================================================================
    const getRawPatterns = () => {
        const patterns = GM_getValue(STORAGE_KEYS.PATTERNS, null);
        if (!patterns || !Array.isArray(patterns)) {
            GM_setValue(STORAGE_KEYS.PATTERNS, DEFAULT_PATTERNS);
            return DEFAULT_PATTERNS;
        }
        return patterns;
    };

    const getPatterns = () => {
        return convertToRegExp(getRawPatterns());
    };

    const savePatterns = (patterns) => {
        try {
            const serializablePatterns = patterns.map(pattern => ({
                cleanpattern: pattern.cleanpattern instanceof RegExp ? pattern.cleanpattern.toString() : String(pattern.cleanpattern),
                replacement: typeof pattern.replacement === 'function' ? pattern.replacement.toString() : pattern.replacement,
                description: pattern.description
            }));
            GM_setValue(STORAGE_KEYS.PATTERNS, serializablePatterns);
            dispatchUpdate();
        } catch (e) {
            console.error('Error saving patterns:', e);
        }
    };

    const convertToRegExp = (patterns) => {
        return patterns.map(pattern => {
            let regex;
            try {
                let patternStr = pattern.patternStr || pattern.cleanpattern;
                if (typeof patternStr === 'string') {
                    if (patternStr.startsWith('/')) {
                        const lastSlash = patternStr.lastIndexOf('/');
                        const flags = patternStr.substring(lastSlash + 1);
                        const patternContent = patternStr.substring(1, lastSlash);
                        regex = new RegExp(patternContent, flags);
                    } else {
                        regex = new RegExp(patternStr, 'g');
                    }
                } else if (patternStr instanceof RegExp) {
                    regex = patternStr;
                } else {
                    regex = new RegExp('', 'g');
                }
            } catch(e) {
                console.error('Error converting pattern:', e);
                regex = new RegExp('', 'g');
            }

            let replacement = pattern.replacement;
            if (typeof replacement === 'string' && (replacement.trim().startsWith('function') || replacement.trim().startsWith('(match'))) {
                try {
                    replacement = new Function('match', 'p1', 'p2', 'p3', 'p4', 'p5', 'return ' + replacement);
                } catch(e) {}
            }

            return {
                cleanpattern: regex,
                replacement: replacement,
                description: pattern.description
            };
        });
    };

    const getPatternSettings = () => {
        const domain = window.location.hostname;
        const allSettings = GM_getValue(STORAGE_KEYS.PATTERN_SETTINGS, {});
        return allSettings[domain] || {};
    };

    const savePatternSettings = (settings) => {
        const domain = window.location.hostname;
        const allSettings = GM_getValue(STORAGE_KEYS.PATTERN_SETTINGS, {});
        allSettings[domain] = settings;
        GM_setValue(STORAGE_KEYS.PATTERN_SETTINGS, allSettings);
        dispatchUpdate();
    };

    const dispatchUpdate = () => {
        document.dispatchEvent(new CustomEvent('patternsUpdated'));
    };

    const addPattern = (pattern, replacement, description) => {
        const patterns = getPatterns();
        patterns.push({ cleanpattern: pattern, replacement: replacement, description: description });
        savePatterns(patterns);
        return patterns;
    };

    const removePattern = (index) => {
        const patterns = getPatterns();
        if (index >= 0 && index < patterns.length) {
            patterns.splice(index, 1);
            savePatterns(patterns);

            const settings = getPatternSettings();
            const newSettings = {};
            patterns.forEach((_, idx) => {
                if (settings[`pattern_${idx}`] !== undefined) {
                    newSettings[`pattern_${idx}`] = settings[`pattern_${idx}`];
                }
            });
            savePatternSettings(newSettings);
        }
        return patterns;
    };

    const updatePattern = (index, pattern, replacement, description) => {
        const patterns = getPatterns();
        if (index >= 0 && index < patterns.length) {
            patterns[index] = { cleanpattern: pattern, replacement: replacement, description: description };
            savePatterns(patterns);
        }
        return patterns;
    };

    // =================================================================================================
    // 4. ФУНКЦИЯ ОЧИСТКИ URL И АВТОЗАПУСК
    // =================================================================================================
    const applyPattern = (pattern, replacement) => {
        try {
            let url = new URL(window.location.href);
            let fullUrl = url.toString();
            let newFullUrl = fullUrl.replace(pattern, replacement);

            if (newFullUrl !== fullUrl) {
                return newFullUrl;
            }

            url.pathname = url.pathname.replace(pattern, replacement);
            url.search = url.search.replace(pattern, replacement);
            return url.toString();
        } catch (e) {
            return window.location.href.replace(pattern, replacement);
        }
    };

    const autoApplyPatterns = () => {
        const patterns = getPatterns();
        const settings = getPatternSettings();
        let currentUrl = window.location.href;
        let newUrl = currentUrl;
        let changed = false;
        patterns.forEach(({ cleanpattern, replacement }, index) => {
            if (settings[`pattern_${index}`] && currentUrl.match(cleanpattern)) {
                newUrl = applyPattern(cleanpattern, replacement);
                if (newUrl !== currentUrl) {
                    changed = true;
                    currentUrl = newUrl;
                }
            }
        });
        if (changed) {
            window.history.replaceState(null, '', newUrl);
            window.location.reload();
        }
    };

    autoApplyPatterns();

    // =================================================================================================
    // 5. СТИЛИ (Новая плавно выезжающая кнопка Optimize и адаптивный контейнер)
    // =================================================================================================
    GM_addStyle(`
        #clean-url-shadow-host {
            position: fixed;
            top: 0;
            left: 0;
            z-index: 2147483647;
            pointer-events: none;
        }

        /* Обновленная кнопка Optimize с анимацией выезда */
        #cleantoggleButton {
            position: fixed;
            bottom: -30px; /* Спрятана на 30px вниз по умолчанию */
            left: 45%;
            z-index: 99990;
            width: 220px;   /* Фиксированная ширина */
            height: 40px;  /* Фиксированная высота */
            padding: 10px;
            box-sizing: border-box;

            background-color: #2a475e;
            color: #000;
            border: 0.1px solid rgba(255, 255, 255, 0.74);
            border-radius: 12px 12px 0 0; /* Скругление только сверху */

            cursor: pointer;
            text-align: center;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
            font-family: 'Compacta', sans-serif;
            font-size: 1rem;
            opacity: 0.6;

            /* Анимация плавного скрытия с задержкой 0.6s */
            transition: bottom .3s ease, color .3s ease, opacity .2s, background .2s;
            transition-delay: .6s;
            pointer-events: auto; /* Включаем клики обратно */
        }

        /* Эффект при наведении курсора или если меню открыто */
        #cleantoggleButton:hover, #cleantoggleButton.menu-open {
            background-color: #e1e1e1b5;
            bottom: -1px;  /* Полностью выезжает наверх */
            opacity: 0.8;
            transition-delay: 0s; /* Выезжает мгновенно */
        }

        /* Смещен вверх до 42px, чтобы не перекрывать новую кнопку (40px) */
        #cleanpatternButtonContainer {
            position: fixed;
            bottom: 42px;
            left: 4px;
            right: 4px;
            z-index: 99991;
            display: none;
            flex-direction: row;
            flex-wrap: wrap-reverse;
            gap: 4px;
            max-width: calc(100vw - 8px);
            align-content: flex-end;
            background-color: rgb(12 12 12 / 77%);
            padding: 4px;
            border: 1px solid rgba(255, 255, 255, 0.12);
            border-radius: 1px;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.8);
            pointer-events: auto;
        }

        .cleanpattern-button {
            padding: 4px 8px;
            background-color: #1e1e1ea8;
            border: 1px solid #333;
            cursor: pointer;
            border-radius: 1px;
            transition: all 0.15s ease;
            text-align: center;
            font-family: sans-serif;
            font-size: 16px;
            line-height: 20px;
            color: #ccc;
            user-select: none;
        }

        .cleanpattern-button:hover {
            background-color: #282828;
            border: 1px solid #00aff0;
            color: #00aff0;
        }

        .active-cleanpattern {
            background-color: #163248 !important;
            border: 1px solid #00aff0 !important;
            color: #00aff0 !important;
        }

        .checked-gradient-border {
            border: 1px solid transparent !important;
            border-image: linear-gradient(to right, #4caf50, #8a2be2) 1 !important;
        }

        .pattern-checkbox {
            margin: 0 6px 0 0;
            vertical-align: middle;
            cursor: pointer;
        }

        .checkbox-label {
            display: flex;
            align-items: center;
            cursor: pointer;
            color: inherit;
        }

        .cu-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100vw;
            height: 100vh;
            background-color: #1e1e1ea8;
            z-index: 2147483647;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .cu-window {
            background-color: #1e1e1ea8;
            width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh;
            display: flex; flex-direction: column;
            box-sizing: border-box;
        }
        .cu-header {
            display: flex;
            justify-content: space-between; align-items: center;
            padding: 12px 20px; border-bottom: 1px solid #222; background-color: #181818de;
        }
        .cu-content { flex: 1; overflow-y: auto; padding: 20px; }

        .cu-grid-list {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
            gap: 12px;
            margin-bottom: 12px;
        }

        .cu-btn {
            background-color: #2322229e;
            color: #ddd; border: 1px solid #444;
            padding: 6px 12px; border-radius: 1px; cursor: pointer; font-size: 16px;
            transition: background 0.15s;
        }
        .cu-btn:hover { background-color: #2e2e2e; border-color: #555; }
        .cu-btn-primary { background-color: #144b7a; border-color: #195d96; }
        .cu-btn-primary:hover { background-color: #18588f; }
        .cu-btn-danger { background-color: #63171e; border-color: #821f28; }
        .cu-btn-danger:hover { background-color: #751b23; }

        .cu-item {
            background-color: rgb(12 12 12 / 77%);
            padding: 12px;
            border-radius: 1px; border: 1px solid #222;
            display: flex; flex-direction: column; justify-content: space-between;
        }
        .cu-db-actions {
            display: flex;
            gap: 8px; background: #181818de; padding: 12px 20px;
            border-top: 1px solid #222; justify-content: flex-end;
        }
    `);

    // =================================================================================================
    // 6. ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ И ОКНА НАСТРОЕК
    // =================================================================================================
    const regexToString = (regex) => {
        if (regex instanceof RegExp) return regex.toString();
        if (typeof regex === 'object' && regex.source !== undefined) return `/${regex.source}/${regex.flags || ''}`;
        return String(regex);
    };

    const stringToRegex = (str) => {
        str = str.trim();
        if (str.startsWith('/')) {
            const lastSlash = str.lastIndexOf('/');
            if (lastSlash > 0) {
                const flags = str.substring(lastSlash + 1);
                const pattern = str.substring(1, lastSlash);
                return new RegExp(pattern, flags);
            }
        }
        return new RegExp(str, 'g');
    };

    let settingsWindow = null;

    const showSettingsWindow = () => {
        if (settingsWindow) { settingsWindow.remove(); settingsWindow = null; }

        const overlay = document.createElement('div');
        overlay.className = 'cu-overlay';
        const windowDiv = document.createElement('div');
        windowDiv.className = 'cu-window';

        const header = document.createElement('div');
        header.className = 'cu-header';

        const title = document.createElement('h2');
        title.textContent = '⚙️ Управление паттернами URL Cleaner';
        title.style.cssText = 'color: #fff; font-size: 18px; margin: 0; font-family: sans-serif;';
        const closeBtn = document.createElement('button');
        closeBtn.className = 'cu-btn cu-btn-danger';
        closeBtn.textContent = '✖';
        closeBtn.style.padding = '2px 10px';
        closeBtn.onclick = () => { overlay.remove(); settingsWindow = null; };

        header.appendChild(title);
        header.appendChild(closeBtn);

        const contentArea = document.createElement('div');
        contentArea.className = 'cu-content';
        const patternsList = document.createElement('div');
        patternsList.className = 'cu-grid-list';

        const refreshList = () => {
            const patterns = getPatterns();
            patternsList.innerHTML = '';

            if (patterns.length === 0) {
                const emptyMsg = document.createElement('div');
                emptyMsg.textContent = '📭 Нет добавленных паттернов.';
                emptyMsg.style.cssText = 'color: #555; text-align: center; padding: 15px; font-size: 16px;';
                patternsList.appendChild(emptyMsg);
                return;
            }

            patterns.forEach((pattern, index) => {
                const patternItem = document.createElement('div');
                patternItem.className = 'cu-item';

                const desc = document.createElement('div');
                desc.textContent = pattern.description;
                desc.style.cssText = 'color: #4caf50; font-weight: bold; margin-bottom: 6px; font-size: 16px;';

                let regexStr = regexToString(pattern.cleanpattern);
                const regex = document.createElement('div');
                regex.textContent = `📝 Регекс: ${regexStr}`;
                regex.style.cssText = 'color: #ffa500; font-size: 14px; font-family: monospace; margin-bottom: 4px; word-break: break-all;';

                let replacementText = typeof pattern.replacement === 'function' ? '[Функция]' : `"${pattern.replacement}"`;
                const replacement = document.createElement('div');
                replacement.textContent = `🔄 Замена: ${replacementText}`;
                replacement.style.cssText = 'color: #87ceeb; font-size: 14px; margin-bottom: 10px; word-break: break-all;';

                const btnContainer = document.createElement('div');
                btnContainer.style.cssText = 'display: flex; gap: 6px; margin-top: auto;';
                const editBtn = document.createElement('button');
                editBtn.className = 'cu-btn';
                editBtn.textContent = '✏️ Правка';
                editBtn.onclick = () => {
                    showPatternEditor(index, pattern, () => { refreshList(); });
                };

                const deleteBtn = document.createElement('button');
                deleteBtn.className = 'cu-btn cu-btn-danger';
                deleteBtn.textContent = '🗑️ Удалить';
                deleteBtn.onclick = () => {
                    if (confirm(`Удалить паттерн "${pattern.description}"?`)) {
                        removePattern(index);
                        refreshList();
                    }
                };

                btnContainer.appendChild(editBtn);
                btnContainer.appendChild(deleteBtn);
                patternItem.appendChild(desc);
                patternItem.appendChild(regex);
                patternItem.appendChild(replacement);
                patternItem.appendChild(btnContainer);
                patternsList.appendChild(patternItem);
            });
        };

        const addBtn = document.createElement('button');
        addBtn.className = 'cu-btn cu-btn-primary';
        addBtn.textContent = '+ Добавить новый паттерн';
        addBtn.style.cssText = 'width: 100%; margin-bottom: 12px; padding: 6px; font-weight: bold; font-size: 16px;';
        addBtn.onclick = () => {
            showPatternEditor(null, null, () => { refreshList(); });
        };

        contentArea.appendChild(addBtn);
        contentArea.appendChild(patternsList);

        const dbActionsPanel = document.createElement('div');
        dbActionsPanel.className = 'cu-db-actions';

        const exportBtn = document.createElement('button');
        exportBtn.className = 'cu-btn';
        exportBtn.textContent = '📤 Экспорт БД';
        exportBtn.onclick = () => {
            const dataStr = JSON.stringify(getRawPatterns(), null, 2);
            const blob = new Blob([dataStr], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `cleanurl_patterns_backup_${window.location.hostname}.json`;
            a.click();
            URL.revokeObjectURL(url);
        };

        const importBtn = document.createElement('button');
        importBtn.className = 'cu-btn';
        importBtn.textContent = '📥 Импорт БД';
        importBtn.onclick = () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.onchange = (e) => {
                const file = e.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = (evt) => {
                    try {
                        const imported = JSON.parse(evt.target.result);
                        if (Array.isArray(imported)) {
                            if (confirm('Перезаписать текущие паттерны импортированными данными?')) {
                                GM_setValue(STORAGE_KEYS.PATTERNS, imported);
                                dispatchUpdate();
                                refreshList();
                            }
                        } else {
                            alert('❌ Неверный формат файла. Ожидался массив.');
                        }
                    } catch (err) {
                        alert('❌ Ошибка чтения JSON: ' + err.message);
                    }
                };
                reader.readAsText(file);
            };
            input.click();
        };

        dbActionsPanel.appendChild(exportBtn);
        dbActionsPanel.appendChild(importBtn);

        windowDiv.appendChild(header);
        windowDiv.appendChild(contentArea);
        windowDiv.appendChild(dbActionsPanel);
        overlay.appendChild(windowDiv);
        document.body.appendChild(overlay);

        settingsWindow = overlay;
        refreshList();
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) { overlay.remove(); settingsWindow = null; }
        });
    };

    const showPatternEditor = (index, pattern, onSave) => {
        const overlay = document.createElement('div');
        overlay.className = 'cu-overlay';
        overlay.style.zIndex = '2147483647';

        const editor = document.createElement('div');
        editor.className = 'cu-window';

        const header = document.createElement('div');
        header.className = 'cu-header';

        const title = document.createElement('h3');
        title.textContent = index !== null ? '✏️ Редактировать паттерн' : '➕ Добавить паттерн';
        title.style.cssText = 'color: #fff; margin: 0; font-size: 18px; font-family: sans-serif;';
        const closeBtn = document.createElement('button');
        closeBtn.className = 'cu-btn cu-btn-danger';
        closeBtn.textContent = '✖';
        closeBtn.style.padding = '2px 10px';
        closeBtn.onclick = () => overlay.remove();
        header.appendChild(title);
        header.appendChild(closeBtn);

        const contentArea = document.createElement('div');
        contentArea.className = 'cu-content';
        contentArea.style.cssText = 'padding: 20px; display: flex; flex-direction: column; gap: 16px;';

        const descGroup = document.createElement('div');
        const descLabel = document.createElement('div');
        descLabel.textContent = '📝 Описание паттерна:';
        descLabel.style.cssText = 'color: #aaa; margin-bottom: 6px; font-size: 16px;';
        const descInput = document.createElement('input');
        descInput.type = 'text';
        descInput.value = pattern ? pattern.description : '';
        descInput.style.cssText = 'width: 100%; padding: 10px; border: 1px solid #444; background-color: #222; color: #fff; font-size: 16px; box-sizing: border-box; border-radius: 1px;';
        descGroup.appendChild(descLabel);
        descGroup.appendChild(descInput);

        const regexGroup = document.createElement('div');
        const regexLabel = document.createElement('div');
        regexLabel.textContent = '🔍 Регулярное выражение (RegExp):';
        regexLabel.style.cssText = 'color: #aaa; margin-bottom: 6px; font-size: 16px;';
        const regexInput = document.createElement('textarea');
        regexInput.value = pattern && pattern.cleanpattern ? regexToString(pattern.cleanpattern) : '';
        regexInput.style.cssText = 'width: 100%; height: 120px; padding: 10px; border: 1px solid #444; background-color: #222; color: #fff; font-family: monospace; font-size: 16px; resize: vertical; box-sizing: border-box; border-radius: 1px;';
        regexGroup.appendChild(regexLabel);
        regexGroup.appendChild(regexInput);

        const replaceGroup = document.createElement('div');
        const replaceLabel = document.createElement('div');
        replaceLabel.textContent = '🔄 На что заменять (оставьте пустым для удаления параметров):';
        replaceLabel.style.cssText = 'color: #aaa; margin-bottom: 6px; font-size: 16px;';
        const replaceInput = document.createElement('input');
        replaceInput.type = 'text';
        replaceInput.value = pattern ? (typeof pattern.replacement === 'function' ? pattern.replacement.toString() : pattern.replacement) : '';
        replaceInput.style.cssText = 'width: 100%; padding: 10px; border: 1px solid #444; background-color: #222; color: #fff; font-size: 16px; box-sizing: border-box; border-radius: 1px;';
        replaceGroup.appendChild(replaceLabel);
        replaceGroup.appendChild(replaceInput);

        contentArea.appendChild(descGroup);
        contentArea.appendChild(regexGroup);
        contentArea.appendChild(replaceGroup);

        const actionsPanel = document.createElement('div');
        actionsPanel.className = 'cu-db-actions';
        const saveBtn = document.createElement('button');
        saveBtn.className = 'cu-btn cu-btn-primary';
        saveBtn.textContent = '💾 Сохранить паттерн';
        saveBtn.style.padding = '10px 20px';
        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'cu-btn';
        cancelBtn.textContent = '❌ Отмена';
        cancelBtn.style.padding = '10px 20px';

        saveBtn.onclick = () => {
            try {
                const regexStr = regexInput.value.trim();
                if (!regexStr) return;

                let regex = stringToRegex(regexStr);
                let replacement = replaceInput.value;
                if (index !== null) {
                    updatePattern(index, regex, replacement, descInput.value);
                } else {
                    addPattern(regex, replacement, descInput.value);
                }

                overlay.remove();
                if (onSave) onSave();
            } catch (e) {
                alert('❌ Ошибка в регулярном выражении:\n' + e.message);
            }
        };

        cancelBtn.onclick = () => overlay.remove();

        actionsPanel.appendChild(saveBtn);
        actionsPanel.appendChild(cancelBtn);

        editor.appendChild(header);
        editor.appendChild(contentArea);
        editor.appendChild(actionsPanel);
        overlay.appendChild(editor);
        document.body.appendChild(overlay);
    };

    // =================================================================================================
    // 7. ОСНОВНОЙ ИНТЕРФЕЙС И ОБРАБОТКА ИЗМЕНЕНИЙ СОСТОЯНИЯ
    // =================================================================================================
    const buildUI = () => {
        let host = document.getElementById('clean-url-shadow-host');
        if (!host) {
            host = document.createElement('div');
            host.id = 'clean-url-shadow-host';
            document.body.appendChild(host);
        } else {
            host.innerHTML = '';
        }

        const toggleButton = document.createElement('button');
        toggleButton.id = 'cleantoggleButton';
        toggleButton.textContent = '🌀 Optimize 🔺';
        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'cleanpatternButtonContainer';

        const updateButtons = () => {
            const patterns = getPatterns();
            const currentUrl = window.location.href;
            const settings = getPatternSettings();

            buttonContainer.innerHTML = '';

            const settingsBtn = document.createElement('div');
            settingsBtn.className = 'cleanpattern-button';
            settingsBtn.textContent = '⚙️ Управление паттернами';
            settingsBtn.style.backgroundColor = '#144b7a';
            settingsBtn.style.color = 'white';
            settingsBtn.style.borderColor = '#195d96';
            settingsBtn.onclick = (e) => {
                e.stopPropagation();
                showSettingsWindow();
            };
            buttonContainer.appendChild(settingsBtn);

            patterns.forEach((config, index) => {
                let isActive = false;
                try {
                    isActive = config.cleanpattern && currentUrl.match(config.cleanpattern) !== null;
                } catch(e) {}

                const button = document.createElement('div');
                button.className = 'cleanpattern-button';
                if (isActive) button.classList.add('active-cleanpattern');

                const isPatternChecked = settings[`pattern_${index}`] || false;
                if (isPatternChecked) button.classList.add('checked-gradient-border');

                const label = document.createElement('label');
                label.className = 'checkbox-label';

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.className = 'pattern-checkbox';
                checkbox.checked = isPatternChecked;
                checkbox.onchange = (e) => {
                    e.stopPropagation();
                    const newSettings = getPatternSettings();
                    newSettings[`pattern_${index}`] = checkbox.checked;
                    savePatternSettings(newSettings);

                    if (checkbox.checked) {
                        button.classList.add('checked-gradient-border');
                    } else {
                        button.classList.remove('checked-gradient-border');
                    }
                };
                const textSpan = document.createElement('span');
                textSpan.textContent = config.description;

                label.appendChild(checkbox);
                label.appendChild(textSpan);
                button.appendChild(label);
                button.onclick = (e) => {
                    if (e.target === checkbox) return;
                    try {
                        const newUrl = applyPattern(config.cleanpattern, config.replacement);
                        if (newUrl !== window.location.href) {
                            window.history.replaceState(null, '', newUrl);
                            window.location.reload();
                        }
                    } catch(e) {
                        console.error('Error applying pattern:', e);
                    }
                };

                buttonContainer.appendChild(button);
            });
        };

        toggleButton.onclick = (e) => {
            e.stopPropagation();
            const isVisible = buttonContainer.style.display === 'flex';
            buttonContainer.style.display = isVisible ? 'none' : 'flex';
            toggleButton.textContent = isVisible ? '🌀 Optimize 🔺' : '🌀 Optimize 🔻';

            // Фиксация кнопки в верхнем положении, чтобы она не пряталась при открытом меню
            if (isVisible) {
                toggleButton.classList.remove('menu-open');
            } else {
                toggleButton.classList.add('menu-open');
            }

            if (!isVisible) updateButtons();
        };

        host.appendChild(buttonContainer);
        host.appendChild(toggleButton);

        document.addEventListener('patternsUpdated', () => {
            updateButtons();
        });

        document.addEventListener('click', (e) => {
            if (buttonContainer.style.display === 'flex') {
                if (!host.contains(e.target)) {
                    buttonContainer.style.display = 'none';
                    toggleButton.textContent = '🌀 Optimize 🔺';
                    toggleButton.classList.remove('menu-open'); // Возвращаем анимацию закрытия кнопки
                }
            }
        });

        updateButtons();
    };

    if (document.body) {
        buildUI();
    } else {
        document.addEventListener('DOMContentLoaded', buildUI);
    }

})();