您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();