Last active
October 5, 2025 08:38
-
-
Save Exieros/fcc3340fd773ca17eef3d0738b5e2874 to your computer and use it in GitHub Desktop.
The browser userscript allows you to copy SVG gears from the geargenerator.com website
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment