Adds download and copy buttons for Font Awesome Pro icons.
// ==UserScript== // @name Font Awesome Pro SVG Downloader & Copier // @description Adds download and copy buttons for Font Awesome Pro icons. // @icon https://fontawesome.com/images/favicon/icon.svg // @version 1.3 // @author afkarxyz // @namespace https://github.com/afkarxyz/misc-scripts/ // @supportURL https://github.com/afkarxyz/misc-scripts/issues // @license MIT // @match https://fontawesome.com/* // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; const CACHE_KEY_PREFIX = 'FA_SVG_CACHE_'; const CACHE_DURATION = 7 * 24 * 60 * 60 * 1000; const DOWNLOAD_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="12" height="12"><path d="M378.1 198.6L249.5 341.4c-6.1 6.7-14.7 10.6-23.8 10.6l-3.5 0c-9.1 0-17.7-3.8-23.8-10.6L69.9 198.6c-3.8-4.2-5.9-9.8-5.9-15.5C64 170.4 74.4 160 87.1 160l72.9 0 0-128c0-17.7 14.3-32 32-32l64 0c17.7 0 32 14.3 32 32l0 128 72.9 0c12.8 0 23.1 10.4 23.1 23.1c0 5.7-2.1 11.2-5.9 15.5zM64 352l0 64c0 17.7 14.3 32 32 32l256 0c17.7 0 32-14.3 32-32l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 64c0 53-43 96-96 96L96 512c-53 0-96-43-96-96l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32z"/></svg>`; const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="12" height="12"><path d="M192 0c-41.8 0-77.4 26.7-90.5 64L64 64C28.7 64 0 92.7 0 128L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64l-37.5 0C269.4 26.7 233.8 0 192 0zm0 64a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM72 272a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm104-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16s7.2-16 16-16zM72 368a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zm88 0c0-8.8 7.2-16 16-16l128 0c8.8 0 16 7.2 16 16s-7.2 16-16 16l-128 0c-8.8 0-16-7.2-16-16z"/></svg>`; const SUCCESS_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="12" height="12"><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z"/></svg>`; const processedIcons = new WeakSet(); function showSuccessAnimation(button, originalIcon) { const originalContent = button.innerHTML; const parser = new DOMParser(); const successSvg = parser.parseFromString(SUCCESS_ICON, 'image/svg+xml'); button.innerHTML = ''; button.appendChild(successSvg.documentElement); setTimeout(() => { button.innerHTML = originalContent; }, 250); } function clearExpiredCache() { const currentTime = Date.now(); for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key.startsWith(CACHE_KEY_PREFIX)) { try { const cachedItem = JSON.parse(localStorage.getItem(key)); if (currentTime - cachedItem.timestamp > CACHE_DURATION) { localStorage.removeItem(key); } } catch (error) { console.error('Error clearing cache:', error); } } } } function getCachedSVG(url) { clearExpiredCache(); const cacheKey = CACHE_KEY_PREFIX + btoa(url); const cachedItem = localStorage.getItem(cacheKey); if (cachedItem) { try { const parsedItem = JSON.parse(cachedItem); return parsedItem.content; } catch (error) { console.error('Error parsing cached SVG:', error); return null; } } return null; } function cacheSVG(url, svgContent) { const cacheKey = CACHE_KEY_PREFIX + btoa(url); const cacheItem = { content: svgContent, timestamp: Date.now() }; try { localStorage.setItem(cacheKey, JSON.stringify(cacheItem)); } catch (error) { console.error('Error caching SVG:', error); } } async function fetchAndCacheSVG(url, iconStyle, iconName) { const cachedSVG = getCachedSVG(url); if (cachedSVG) return cachedSVG; try { const response = await fetch(url); if (!response.ok) { if (iconStyle === 'duotone-solid' && response.status === 403) { const newUrl = url.replace('duotone-solid', 'duotone'); console.log(`Retrying with duotone style: ${newUrl}`); const retryResponse = await fetch(newUrl); if (!retryResponse.ok) throw new Error('Network response was not ok.'); let svgContent = await retryResponse.text(); svgContent = svgContent.replace(/<!--[\s\S]*?-->/g, ''); cacheSVG(url, svgContent); return svgContent; } throw new Error('Network response was not ok.'); } let svgContent = await response.text(); svgContent = svgContent.replace(/<!--[\s\S]*?-->/g, ''); cacheSVG(url, svgContent); return svgContent; } catch (error) { console.error('Error fetching SVG:', error); throw error; } } async function copySVG(url, button, iconStyle, iconName) { try { const svgContent = await fetchAndCacheSVG(url, iconStyle, iconName); await navigator.clipboard.writeText(svgContent); showSuccessAnimation(button, COPY_ICON); } catch (error) { const errorMessage = `Failed to copy SVG: ${error.message}`; console.error(errorMessage); alert(errorMessage); } } async function downloadSVG(url, filename, button, iconStyle, iconName) { try { const svgContent = await fetchAndCacheSVG(url, iconStyle, iconName); const blob = new Blob([svgContent], { type: 'image/svg+xml' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); showSuccessAnimation(button, DOWNLOAD_ICON); } catch (error) { console.error('Failed to download SVG:', error); alert('Failed to download SVG. Check the console for details.'); } } function getSelectedVersion() { const selectElement = document.getElementById('choose_aversionoffontawesome'); if (selectElement?.value) return selectElement.value.trim(); const styleLink = document.querySelector('link[rel="stylesheet"][href*="fontawesome.com/releases"]'); const versionMatch = styleLink?.getAttribute('href')?.match(/releases\/v([\d.]+)/); return versionMatch?.[1] || '6.7.1'; } function createButton(icon, title, className) { const button = document.createElement('span'); button.className = `fa-icon-button ${className}`; button.title = title; const parser = new DOMParser(); const svgDoc = parser.parseFromString(icon, 'image/svg+xml'); button.appendChild(svgDoc.documentElement); return button; } function getIconStyle(iconElement) { const classes = new Set(iconElement.classList); if (classes.has('fa-brands')) return 'brands'; const styleMap = { 'fa-solid': 'solid', 'fa-light': 'light', 'fa-thin': 'thin', 'fa-regular': 'regular' }; let style = ''; if (classes.has('fa-duotone')) style += 'duotone'; if (classes.has('fa-sharp')) style += `${style ? '-' : ''}sharp`; for (const [className, styleName] of Object.entries(styleMap)) { if (classes.has(className)) { return style ? `${style}-${styleName}` : styleName; } } return style; } function processIcon(icon) { if (processedIcons.has(icon)) return; const iconElement = icon.querySelector('i'); const iconNameElement = icon.querySelector('.icon-name'); if (!iconElement || !iconNameElement?.textContent) return; const iconName = iconNameElement.textContent.trim(); if (!iconElement || !iconName) { return; } const iconStyle = getIconStyle(iconElement); const version = getSelectedVersion(); const url = `https://site-assets.fontawesome.com/releases/v${version}/svgs/${iconStyle}/${iconName}.svg`; const filename = `${iconName}.svg`; const container = document.createElement('div'); container.className = 'fa-buttons-container'; const downloadButton = createButton(DOWNLOAD_ICON, 'Download SVG', 'fa-download-button'); const copyButton = createButton(COPY_ICON, 'Copy SVG', 'fa-copy-button'); downloadButton.addEventListener('click', () => downloadSVG(url, filename, downloadButton, iconStyle, iconName)); copyButton.addEventListener('click', () => copySVG(url, copyButton, iconStyle, iconName)); container.appendChild(copyButton); container.appendChild(downloadButton); let tagContainer = icon.querySelector('.tag'); if (!tagContainer) { tagContainer = document.createElement('div'); tagContainer.className = 'tag'; } tagContainer.innerHTML = ''; tagContainer.appendChild(container); const buttonElement = icon.querySelector('button'); if (buttonElement && !buttonElement.parentNode.querySelector('.tag')) { buttonElement.parentNode.insertBefore(tagContainer, buttonElement.nextSibling); } processedIcons.add(icon); } function processAllIcons(icons) { Array.from(icons).forEach(icon => { if (!processedIcons.has(icon)) { processIcon(icon); } }); } function setupMutationObserver() { const observer = new MutationObserver((mutations) => { const icons = document.querySelectorAll('article.wrap-icon'); processAllIcons(icons); }); observer.observe(document.body, { childList: true, subtree: true }); } const style = document.createElement('style'); style.textContent = ` .fa-buttons-container { display: inline-flex; gap: 3px; align-items: center; justify-content: center; min-width: 50px; position: absolute; top: 50%; transform: translateY(-50%); } .tag { display: flex; justify-content: center; position: relative; min-height: 28px; padding: 0 4px !important; margin: 0 !important; background: transparent !important; } .fa-icon-button { display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; border-radius: 4px; background-color: var(--fa-yellow); color: var(--fa-navy); cursor: pointer; padding: 3px; } .fa-icon-button svg { width: 100%; height: 100%; } .fa-icon-button svg path { fill: var(--fa-navy); } `; document.head.appendChild(style); const initialIcons = document.querySelectorAll('article.wrap-icon'); processAllIcons(initialIcons); setupMutationObserver(); })();