Font Awesome Pro SVG Downloader & Copier

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();
})();