Удаляет параметры &width=* и шаблоны _crop_*x* из текущего URL и перезагружает страницу с обновленной анимированной кнопкой Optimize
// ==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);
}
})();