Adds a button to copy SVG gears
// ==UserScript==
// @name GearGenerator Grab SVG
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Adds a button to copy SVG gears
// @author Exieros
// @match https://geargenerator.com/*
// @grant none
// @homepage https://gist.github.com/Exieros/fcc3340fd773ca17eef3d0738b5e2874
// ==/UserScript==
//Telegram @soreixe
(function() {
'use strict';
// Стили для кнопки копирования
const buttonStyles = `
.copy-svg-btn {
position: fixed !important;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
transform: none !important;
rotate: none !important;
scale: none !important;
translate: none !important;
transform-origin: initial !important;
transform-style: flat !important;
perspective: none !important;
backface-visibility: visible !important;
writing-mode: horizontal-tb !important;
direction: ltr !important;
text-orientation: mixed !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
display: inline-block !important;
vertical-align: baseline !important;
text-align: center !important;
line-height: normal !important;
}
.copy-svg-btn:hover {
background: #0056b3;
}
.copy-svg-btn.visible {
opacity: 1;
pointer-events: auto;
}
.screen-child-container {
position: relative;
}
.copy-success {
background: #28a745 !important;
}
.copy-clear-btn {
position: fixed !important;
background: #6c757d;
color: white;
border: none;
border-radius: 6px;
padding: 8px 12px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
z-index: 10000;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
transform: none !important;
rotate: none !important;
scale: none !important;
translate: none !important;
transform-origin: initial !important;
transform-style: flat !important;
perspective: none !important;
backface-visibility: visible !important;
writing-mode: horizontal-tb !important;
direction: ltr !important;
text-orientation: mixed !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
display: inline-block !important;
vertical-align: baseline !important;
text-align: center !important;
line-height: normal !important;
}
.copy-clear-btn:hover {
background: #545b62;
}
.copy-clear-btn.visible {
opacity: 1;
pointer-events: auto;
}
`;
// Добавляем стили на страницу
function addStyles() {
const style = document.createElement('style');
style.textContent = buttonStyles;
document.head.appendChild(style);
}
// Универсальная функция для копирования SVG в буфер обмена
async function copySVGToClipboard(svgElement, removeGeartext = false) {
try {
let svgCode;
if (removeGeartext) {
// Создаем копию SVG элемента и удаляем geartext, guides и firstmarker
const svgClone = svgElement.cloneNode(true);
const elementsToRemove = svgClone.querySelectorAll('.geartext, .guides, .firstmarker');
elementsToRemove.forEach(element => element.remove());
svgCode = svgClone.outerHTML;
} else {
// Получаем полный SVG код
svgCode = svgElement.outerHTML;
}
// Копируем в буфер обмена
await navigator.clipboard.writeText(svgCode);
return true;
} catch (error) {
console.error('Ошибка при копировании SVG:', error);
// Fallback метод для старых браузеров
try {
let svgCode;
if (removeGeartext) {
const svgClone = svgElement.cloneNode(true);
const elementsToRemove = svgClone.querySelectorAll('.geartext, .guides, .firstmarker');
elementsToRemove.forEach(element => element.remove());
svgCode = svgClone.outerHTML;
} else {
svgCode = svgElement.outerHTML;
}
const textArea = document.createElement('textarea');
textArea.value = svgCode;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
return true;
} catch (fallbackError) {
console.error('Fallback копирование также не удалось:', fallbackError);
return false;
}
}
}
// Универсальная функция для создания кнопок копирования
function createCopyButton(type = 'normal') {
const isNormal = type === 'normal';
const button = document.createElement('button');
button.className = isNormal ? 'copy-svg-btn' : 'copy-clear-btn';
button.textContent = isNormal ? 'Copy' : 'Copy Clear';
button.title = isNormal ? 'Копировать SVG в буфер обмена' : 'Копировать SVG без текстовых элементов и направляющих';
button.addEventListener('click', async function(e) {
e.stopPropagation();
e.preventDefault();
const container = this.containerElement;
const svgElement = container.querySelector('svg');
if (svgElement) {
const success = await copySVGToClipboard(svgElement, !isNormal);
const originalText = isNormal ? 'Copy' : 'Copy Clear';
if (success) {
// Показываем успешное копирование
this.textContent = '✓';
this.classList.add('copy-success');
setTimeout(() => {
this.textContent = originalText;
this.classList.remove('copy-success');
}, 1000);
} else {
// Показываем ошибку
this.textContent = '✗';
setTimeout(() => {
this.textContent = originalText;
}, 1000);
}
} else {
console.warn('SVG элемент не найден в контейнере');
}
});
return button;
}
// Функция для позиционирования кнопок
function positionButtons(copyButton, copyClearButton, container) {
const svgElement = container.querySelector('svg');
if (svgElement) {
const svgRect = svgElement.getBoundingClientRect();
const buttonWidth = 52; // примерная ширина кнопки "Copy"
const buttonClearWidth = 80; // примерная ширина кнопки "Copy Clear"
const buttonHeight = 34; // примерная высота кнопки
const gap = 8; // расстояние между кнопками
// Позиционируем кнопки по центру SVG
const totalWidth = buttonWidth + buttonClearWidth + gap;
const startX = svgRect.left + (svgRect.width - totalWidth) / 2;
const centerY = svgRect.top + (svgRect.height - buttonHeight) / 2;
copyButton.style.left = startX + 'px';
copyButton.style.top = centerY + 'px';
copyClearButton.style.left = (startX + buttonWidth + gap) + 'px';
copyClearButton.style.top = centerY + 'px';
} else {
// Fallback к контейнеру, если SVG не найден
const rect = container.getBoundingClientRect();
const totalWidth = 52 + 80 + 8;
const startX = rect.left + (rect.width - totalWidth) / 2;
const centerY = rect.top + (rect.height - 34) / 2;
copyButton.style.left = startX + 'px';
copyButton.style.top = centerY + 'px';
copyClearButton.style.left = (startX + 52 + 8) + 'px';
copyClearButton.style.top = centerY + 'px';
}
}
// Универсальная функция для настройки кнопок и обработчиков для контейнера
function setupButtonsForContainer(container) {
container.classList.add('screen-child-container');
// Создаем обе кнопки
const copyButton = createCopyButton('normal');
const copyClearButton = createCopyButton('clear');
copyButton.containerElement = container;
copyClearButton.containerElement = container;
document.body.appendChild(copyButton);
document.body.appendChild(copyClearButton);
// Функции для показа/скрытия кнопок
const showButtons = () => {
positionButtons(copyButton, copyClearButton, container);
copyButton.classList.add('visible');
copyClearButton.classList.add('visible');
};
const hideButtons = () => {
copyButton.classList.remove('visible');
copyClearButton.classList.remove('visible');
};
const isMouseOnButtons = (target) => {
return copyButton.contains(target) || copyClearButton.contains(target);
};
// Обработчики для контейнера
container.addEventListener('mouseenter', showButtons);
container.addEventListener('mouseleave', (e) => {
if (!isMouseOnButtons(e.relatedTarget)) hideButtons();
});
// Обработчики для кнопок
[copyButton, copyClearButton].forEach(button => {
button.addEventListener('mouseenter', showButtons);
button.addEventListener('mouseleave', (e) => {
if (!container.contains(e.relatedTarget) && !isMouseOnButtons(e.relatedTarget)) {
hideButtons();
}
});
});
// Обработчик скролла
window.addEventListener('scroll', () => {
if (copyButton.classList.contains('visible')) {
positionButtons(copyButton, copyClearButton, container);
}
});
}
// Добавляем обработчики событий для дочерних элементов контейнера screen
function setupCopyButtons() {
const screenContainer = document.getElementById('screen');
if (!screenContainer) {
console.warn('Контейнер с id="screen" не найден');
return;
}
// Получаем все прямые дочерние элементы
const children = Array.from(screenContainer.children);
children.forEach(child => {
// Проверяем, есть ли в дочернем элементе SVG
const hasSVG = child.querySelector('svg');
if (hasSVG) {
// Добавляем класс для позиционирования
child.classList.add('screen-child-container');
setupButtonsForContainer(child);
}
});
}
// Наблюдатель за изменениями DOM для динамически добавляемых элементов
function setupMutationObserver() {
const screenContainer = document.getElementById('screen');
if (!screenContainer) {
return;
}
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
const hasSVG = node.querySelector && node.querySelector('svg');
if (hasSVG && !node.classList.contains('screen-child-container')) {
setupButtonsForContainer(node);
}
}
});
}
});
});
observer.observe(screenContainer, {
childList: true,
subtree: true
});
}
// Инициализация скрипта
function init() {
// Ждем загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(init, 100);
});
return;
}
addStyles();
setupCopyButtons();
setupMutationObserver();
console.log('GearGenerator SVG Copy скрипт инициализирован');
}
// Запускаем инициализацию
init();
})();