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