Auto Currency Converter

Automatically converts prices to your preferred currency on shopping sites, travel sites, and anywhere prices are displayed.

// ==UserScript==
// @name         Auto Currency Converter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Automatically converts prices to your preferred currency on shopping sites, travel sites, and anywhere prices are displayed.
// @author       xvcf
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
	"use strict";

	const config = {
		targetCurrency: GM_getValue("targetCurrency", "EUR"),
		localCurrency: GM_getValue("localCurrency", "auto"),
		updateInterval: 3600000,
		maxRetries: 3,
		debug: false,
		ratesCacheDuration: 3600000,
	};

	let exchangeRates = {};
	let processedElements = new WeakSet();
	let isRatesLoaded = false;

	function log(message, ...args) {
		if (config.debug) {
			console.log(`[Currency Converter] ${message}`, ...args);
		}
	}

	const currencySymbols = {
		USD: "$",
		EUR: "€",
		GBP: "£",
		JPY: "¥",
		CNY: "¥",
		KRW: "₩",
		INR: "₹",
		RUB: "₽",
		BRL: "R$",
		CAD: "C$",
		AUD: "A$",
		CHF: "CHF",
	};

	const pricePatterns = [
		/([£$€¥₹₽₩₪₺])\s*([0-9]{1,3}(?:[,.]?[0-9]{3})*(?:[.,][0-9]{1,2})?)/g,
		/([0-9]{1,3}(?:[,.]?[0-9]{3})*(?:[.,][0-9]{1,2})?)\s*([A-Z]{3})\b/g,
		/\b([A-Z]{3})\s+([0-9]{1,3}(?:[,.]?[0-9]{3})*(?:[.,][0-9]{1,2})?)/g,
		/\b([A-Z]{3})([0-9]{1,3}(?:[,.]?[0-9]{3})*(?:[.,][0-9]{1,2})?)/g,
	];

	function detectCurrencyFromSymbol(symbol) {
		const symbolMap = {
			$: "USD",
			"€": "EUR",
			"£": "GBP",
			"¥": "JPY",
			"₹": "INR",
			"₽": "RUB",
			"₩": "KRW",
			"₪": "ILS",
			"₺": "TRY",
		};
		return symbolMap[symbol] || null;
	}

	function detectCurrencyFromDomain() {
		const domain = window.location.hostname;
		const domainMap = {
			"amazon.com": "USD",
			"amazon.co.uk": "GBP",
			"amazon.de": "EUR",
			"amazon.fr": "EUR",
			"amazon.ca": "CAD",
			"amazon.co.jp": "JPY",
			"ebay.com": "USD",
			"ebay.co.uk": "GBP",
			"ebay.de": "EUR",
			"booking.com": "EUR",
			"airbnb.com": "USD",
			"expedia.com": "USD",
			"walmart.com": "USD",
			"target.com": "USD",
			"bestbuy.com": "USD",
			"zalando.com": "EUR",
			"zara.com": "EUR",
			"hm.com": "EUR",
			"nike.com": "USD",
			"adidas.com": "EUR",
		};

		for (const [site, currency] of Object.entries(domainMap)) {
			if (domain.includes(site)) {
				return currency;
			}
		}
		return "USD";
	}

	function getLocalCurrency() {
		if (config.localCurrency === "auto") {
			return detectCurrencyFromDomain();
		}
		return config.localCurrency;
	}

	async function fetchExchangeRates() {
		const cachedRates = GM_getValue("exchangeRates", null);
		const cacheTime = GM_getValue("ratesCacheTime", 0);
		const now = Date.now();

		if (cachedRates && now - cacheTime < config.ratesCacheDuration) {
			exchangeRates = JSON.parse(cachedRates);
			isRatesLoaded = true;
			log("Using cached exchange rates");
			return;
		}

		for (let attempt = 0; attempt < config.maxRetries; attempt++) {
			try {
				await new Promise((resolve, reject) => {
					GM_xmlhttpRequest({
						method: "GET",
						url: "https://api.exchangerate-api.com/v4/latest/USD",
						onload: function (response) {
							try {
								const data = JSON.parse(response.responseText);
								if (data && data.rates) {
									exchangeRates = data.rates;
									exchangeRates["USD"] = 1;
									GM_setValue(
										"exchangeRates",
										JSON.stringify(exchangeRates)
									);
									GM_setValue("ratesCacheTime", now);
									isRatesLoaded = true;
									log("Exchange rates loaded successfully");
									resolve();
								} else {
									reject(
										new Error("Invalid response format")
									);
								}
							} catch (e) {
								reject(e);
							}
						},
						onerror: function (error) {
							reject(error);
						},
					});
				});
				return;
			} catch (error) {
				log(`Attempt ${attempt + 1} failed:`, error);
				if (attempt === config.maxRetries - 1) {
					const fallbackRates = GM_getValue("exchangeRates", null);
					if (fallbackRates) {
						exchangeRates = JSON.parse(fallbackRates);
						isRatesLoaded = true;
						log("Using fallback cached rates");
					} else {
						log("Failed to load exchange rates");
					}
				}
				await new Promise((resolve) => setTimeout(resolve, 1000));
			}
		}
	}

	function parsePrice(priceText) {
		const cleanText = priceText.replace(
			/[^\d.,£$€¥₹₽₩₪₺₦₵₨₱₸₼₾₮₲₴₪₫₡₧A-Z\s]/g,
			""
		);

		for (const pattern of pricePatterns) {
			pattern.lastIndex = 0;
			const match = pattern.exec(cleanText);
			if (match) {
				let amount, currency;

				if (
					match[1] &&
					match[2] &&
					!isNaN(parseFloat(match[2].replace(/[,]/g, ""))) &&
					/^[£$€¥₹₽₩₪₺₦₵₨₱₸₼₾₮₲₴₫₡₧]$/.test(match[1])
				) {
					currency = detectCurrencyFromSymbol(match[1]);
					amount = parseFloat(match[2].replace(/[,]/g, ""));
				} else if (
					match[1] &&
					match[2] &&
					match[2].length === 3 &&
					!isNaN(parseFloat(match[1].replace(/[,]/g, "")))
				) {
					amount = parseFloat(match[1].replace(/[,]/g, ""));
					currency = match[2].toUpperCase();
				} else if (
					match[1] &&
					match[2] &&
					match[1].length === 3 &&
					!isNaN(parseFloat(match[2].replace(/[,]/g, "")))
				) {
					currency = match[1].toUpperCase();
					amount = parseFloat(match[2].replace(/[,]/g, ""));
				} else if (
					match[1] &&
					!match[2] &&
					!isNaN(parseFloat(match[1].replace(/[,]/g, "")))
				) {
					amount = parseFloat(match[1].replace(/[,]/g, ""));
					currency = getLocalCurrency();
				}

				if (
					amount &&
					currency &&
					exchangeRates[currency] &&
					amount > 0
				) {
					return { amount, currency };
				}
			}
		}
		return null;
	}

	function convertCurrency(amount, fromCurrency, toCurrency) {
		if (!exchangeRates[fromCurrency] || !exchangeRates[toCurrency]) {
			return null;
		}

		const usdAmount = amount / exchangeRates[fromCurrency];
		const convertedAmount = usdAmount * exchangeRates[toCurrency];
		return convertedAmount;
	}

	function formatCurrency(amount, currency) {
		const symbol = currencySymbols[currency] || currency;
		const formatted = new Intl.NumberFormat("en-US", {
			minimumFractionDigits: 2,
			maximumFractionDigits: 2,
		}).format(amount);

		if (symbol.length === 1 && /^[£$€¥₹₽₩₪₺₦₵₨₱₸₼₾₮₲₴₫₡₧]$/.test(symbol)) {
			return `${symbol}${formatted}`;
		} else {
			return `${formatted} ${symbol}`;
		}
	}

	function createConvertedElement(
		convertedPrice,
		originalPrice,
		originalCurrency
	) {
		const wrapper = document.createElement("span");
		wrapper.className = "currency-converter-wrapper";

		const converted = document.createElement("span");
		converted.className = "currency-converted";
		converted.textContent = convertedPrice;
		converted.style.cssText = `
            font-weight: bold;
            color: #e74c3c;
            background: linear-gradient(135deg, #ffeaa7 0%, #fab1a0 100%);
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 0.9em;
            margin: 0 4px;
            display: inline-block;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            transition: all 0.3s ease;
        `;

		wrapper.appendChild(converted);

		converted.addEventListener("mouseenter", () => {
			converted.style.transform = "scale(1.05)";
			converted.style.boxShadow = "0 4px 8px rgba(0,0,0,0.2)";
		});

		converted.addEventListener("mouseleave", () => {
			converted.style.transform = "scale(1)";
			converted.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
		});

		return wrapper;
	}

	function processTextNode(textNode) {
		if (processedElements.has(textNode) || !textNode.textContent.trim()) {
			return;
		}

		const parent = textNode.parentNode;
		if (!parent) {
			return;
		}

		if (
			parent.classList.contains("currency-converter-wrapper") ||
			parent.classList.contains("currency-converted") ||
			parent.closest(".currency-converter-wrapper")
		) {
			return;
		}

		const text = textNode.textContent;
		const priceInfo = parsePrice(text);

		if (priceInfo) {
			if (priceInfo.currency !== config.targetCurrency) {
				const convertedAmount = convertCurrency(
					priceInfo.amount,
					priceInfo.currency,
					config.targetCurrency
				);

				if (convertedAmount) {
					const convertedPrice = formatCurrency(
						convertedAmount,
						config.targetCurrency
					);
					const convertedElement = createConvertedElement(
						convertedPrice,
						text,
						priceInfo.currency
					);

					const fragment = document.createDocumentFragment();
					fragment.appendChild(convertedElement);

					parent.insertBefore(fragment, textNode.nextSibling);
					processedElements.add(textNode);

					log(
						`Converted ${formatCurrency(
							priceInfo.amount,
							priceInfo.currency
						)} to ${convertedPrice}`
					);
				}
			}
		}
	}

	function findAndConvertPrices() {
		if (!isRatesLoaded) return;

		const walker = document.createTreeWalker(
			document.body,
			NodeFilter.SHOW_TEXT,
			{
				acceptNode: function (node) {
					if (
						node.parentNode.tagName === "SCRIPT" ||
						node.parentNode.tagName === "STYLE" ||
						node.parentNode.closest(".currency-converter-wrapper")
					) {
						return NodeFilter.FILTER_REJECT;
					}
					return NodeFilter.FILTER_ACCEPT;
				},
			}
		);

		const textNodes = [];
		let node;
		while ((node = walker.nextNode())) {
			textNodes.push(node);
		}

		textNodes.forEach(processTextNode);
	}

	function createToggleButton() {
		if (document.getElementById("currency-converter-toggle")) {
			return;
		}

		const button = document.createElement("button");
		button.id = "currency-converter-toggle";
		button.innerHTML = "💱";
		button.title = "Currency Converter Settings";
		button.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border: none;
            color: white;
            font-size: 20px;
            cursor: pointer;
            z-index: 2147483646;
            box-shadow: 0 4px 12px rgba(0,0,0,0.2);
            transition: all 0.3s ease;
        `;

		button.addEventListener("mouseenter", () => {
			button.style.transform = "scale(1.1)";
			button.style.boxShadow = "0 6px 16px rgba(0,0,0,0.3)";
		});

		button.addEventListener("mouseleave", () => {
			button.style.transform = "scale(1)";
			button.style.boxShadow = "0 4px 12px rgba(0,0,0,0.2)";
		});

		button.addEventListener("click", () => {
			const panel = document.getElementById(
				"currency-converter-settings"
			);
			if (panel) {
				panel.style.display =
					panel.style.display === "none" ? "block" : "none";
			}
		});

		document.body.appendChild(button);
	}

	function createSettingsPanel() {
		if (document.getElementById("currency-converter-settings")) {
			return;
		}

		const panel = document.createElement("div");
		panel.id = "currency-converter-settings";
		panel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            padding: 15px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 2147483647;
            font-family: Arial, sans-serif;
            font-size: 14px;
            max-width: 300px;
            display: none;
        `;

		panel.innerHTML = `
            <h3 style="margin: 0 0 10px 0; color: #2c3e50;">Currency Converter</h3>
            <label style="display: block; margin-bottom: 10px;">
                Local Currency (for this site):
                <select id="local-currency-select" style="width: 100%; padding: 5px; margin-top: 5px;">
                    <option value="auto">Auto-detect (${detectCurrencyFromDomain()})</option>
                    <option value="USD">USD ($)</option>
                    <option value="EUR">EUR (€)</option>
                    <option value="GBP">GBP (£)</option>
                    <option value="JPY">JPY (¥)</option>
                    <option value="CNY">CNY (¥)</option>
                    <option value="KRW">KRW (₩)</option>
                    <option value="INR">INR (₹)</option>
                    <option value="RUB">RUB (₽)</option>
                    <option value="BRL">BRL (R$)</option>
                    <option value="CAD">CAD (C$)</option>
                    <option value="AUD">AUD (A$)</option>
                    <option value="CHF">CHF (Fr)</option>
                </select>
            </label>
            <label style="display: block; margin-bottom: 10px;">
                Convert To:
                <select id="target-currency-select" style="width: 100%; padding: 5px; margin-top: 5px;">
                    <option value="USD">USD ($)</option>
                    <option value="EUR">EUR (€)</option>
                    <option value="GBP">GBP (£)</option>
                    <option value="JPY">JPY (¥)</option>
                    <option value="CNY">CNY (¥)</option>
                    <option value="KRW">KRW (₩)</option>
                    <option value="INR">INR (₹)</option>
                    <option value="RUB">RUB (₽)</option>
                    <option value="BRL">BRL (R$)</option>
                    <option value="CAD">CAD (C$)</option>
                    <option value="AUD">AUD (A$)</option>
                    <option value="CHF">CHF (Fr)</option>
                </select>
            </label>
            <button id="convert-now-btn" style="width: 100%; padding: 8px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; margin-bottom: 10px;">Save</button>
            <button id="close-settings-btn" style="width: 100%; padding: 8px; background: #95a5a6; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button>
        `;

		document.body.appendChild(panel);

		const localSelect = panel.querySelector("#local-currency-select");
		const targetSelect = panel.querySelector("#target-currency-select");

		localSelect.value = config.localCurrency;
		targetSelect.value = config.targetCurrency;

		localSelect.addEventListener("change", (e) => {
			config.localCurrency = e.target.value;
			GM_setValue("localCurrency", config.localCurrency);
			refreshConversions();
		});

		targetSelect.addEventListener("change", (e) => {
			config.targetCurrency = e.target.value;
			GM_setValue("targetCurrency", config.targetCurrency);
			refreshConversions();
		});

		panel
			.querySelector("#convert-now-btn")
			.addEventListener("click", () => {
				refreshConversions();
			});

		panel
			.querySelector("#close-settings-btn")
			.addEventListener("click", () => {
				panel.style.display = "none";
			});

		return panel;
	}

	function refreshConversions() {
		document
			.querySelectorAll(".currency-converter-wrapper")
			.forEach((el) => el.remove());

		processedElements = new WeakSet();

		findAndConvertPrices();
	}

	async function initialize() {
		if (window.currencyConverterInitialized) return;
		window.currencyConverterInitialized = true;

		await fetchExchangeRates();
		if (!isRatesLoaded) return;

		createToggleButton();
		createSettingsPanel();
		findAndConvertPrices();

		if (!window.currencyConverterObserver) {
			window.currencyConverterObserver = new MutationObserver(() => {
				setTimeout(findAndConvertPrices, 1000);
			});
			window.currencyConverterObserver.observe(document.body, {
				childList: true,
				subtree: true,
			});
		}
	}

	if (document.readyState === "loading") {
		document.addEventListener("DOMContentLoaded", initialize);
	} else {
		initialize();
	}
})();