Multi-Currency Converter with Context Menu

Convert between NIS, USD, EUR, GBP and other currencies with real-time rates. Right-click any amount to convert currencies.

Size

14.0 KB

Version

1.1.2

Created

Feb 5, 2026

Updated

12 days ago

1// ==UserScript==
2// @name		Multi-Currency Converter with Context Menu
3// @description		Convert between NIS, USD, EUR, GBP and other currencies with real-time rates. Right-click any amount to convert currencies.
4// @version		1.1.2
5// @match		https://*/*
6// @match		http://*/*
7// @icon		https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico
8// @grant		GM.getValue
9// @grant		GM.setValue
10// @grant		GM.xmlhttpRequest
11// @grant		GM.registerMenuCommand
12// ==/UserScript==
13(function() {
14    'use strict';
15
16    // Configuration
17    const API_URL = 'https://api.exchangerate-api.com/v4/latest/';
18    const CACHE_DURATION = 3600000; // 1 hour in milliseconds
19    const SUPPORTED_CURRENCIES = ['NIS', 'USD', 'EUR', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD', 'CNY', 'INR'];
20    
21    // Currency symbols mapping
22    const CURRENCY_SYMBOLS = {
23        'NIS': '₪',
24        'USD': '$',
25        'EUR': '€',
26        'GBP': '£',
27        'JPY': '¥',
28        'CHF': 'CHF',
29        'CAD': 'C$',
30        'AUD': 'A$',
31        'CNY': '¥',
32        'INR': '₹'
33    };
34
35    let exchangeRates = {};
36    let converterPanel = null;
37    let selectedText = '';
38    let selectedCurrency = '';
39
40    // Fetch exchange rates
41    async function fetchExchangeRates(baseCurrency = 'NIS') {
42        try {
43            const cacheKey = `rates_${baseCurrency}`;
44            const cached = await GM.getValue(cacheKey);
45            
46            if (cached) {
47                const data = JSON.parse(cached);
48                if (Date.now() - data.timestamp < CACHE_DURATION) {
49                    console.log('Using cached exchange rates');
50                    return data.rates;
51                }
52            }
53
54            console.log('Fetching fresh exchange rates...');
55            const response = await GM.xmlhttpRequest({
56                method: 'GET',
57                url: API_URL + baseCurrency,
58                responseType: 'json'
59            });
60
61            const rates = response.response.rates;
62            await GM.setValue(cacheKey, JSON.stringify({
63                rates: rates,
64                timestamp: Date.now()
65            }));
66
67            return rates;
68        } catch (error) {
69            console.error('Error fetching exchange rates:', error);
70            return null;
71        }
72    }
73
74    // Convert currency
75    function convertCurrency(amount, fromCurrency, toCurrency, rates) {
76        if (fromCurrency === toCurrency) return amount;
77        
78        // Convert to base currency (NIS) first, then to target
79        const amountInBase = amount / rates[fromCurrency];
80        return amountInBase * rates[toCurrency];
81    }
82
83    // Detect currency from text
84    function detectCurrency(text) {
85        const symbolMap = {
86            '₪': 'NIS',
87            '$': 'USD',
88            '€': 'EUR',
89            '£': 'GBP',
90            '¥': 'JPY',
91            '₹': 'INR'
92        };
93
94        for (const [symbol, currency] of Object.entries(symbolMap)) {
95            if (text.includes(symbol)) {
96                return currency;
97            }
98        }
99
100        // Check for currency codes
101        for (const currency of SUPPORTED_CURRENCIES) {
102            if (text.toUpperCase().includes(currency)) {
103                return currency;
104            }
105        }
106
107        return null;
108    }
109
110    // Extract number from text
111    function extractNumber(text) {
112        // Remove currency symbols and letters, keep numbers, dots, and commas
113        const cleaned = text.replace(/[^\d.,]/g, '');
114        // Replace comma with dot for decimal
115        const normalized = cleaned.replace(',', '.');
116        const number = parseFloat(normalized);
117        return isNaN(number) ? null : number;
118    }
119
120    // Create converter panel UI
121    function createConverterPanel() {
122        const panel = document.createElement('div');
123        panel.id = 'currency-converter-panel';
124        panel.style.cssText = `
125            position: fixed;
126            top: 50%;
127            left: 50%;
128            transform: translate(-50%, -50%);
129            background: white;
130            border: 2px solid #007bff;
131            border-radius: 12px;
132            padding: 25px;
133            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
134            z-index: 999999;
135            min-width: 400px;
136            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
137            color: #333;
138        `;
139
140        panel.innerHTML = `
141            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
142                <h3 style="margin: 0; color: #007bff; font-size: 20px;">Currency Converter</h3>
143                <button id="close-converter" style="background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px;">&times;</button>
144            </div>
145            
146            <div style="margin-bottom: 20px;">
147                <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #555;">Amount:</label>
148                <input type="number" id="amount-input" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; box-sizing: border-box;" placeholder="Enter amount">
149            </div>
150
151            <div style="display: flex; gap: 15px; margin-bottom: 20px; align-items: center;">
152                <div style="flex: 1;">
153                    <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #555;">From:</label>
154                    <select id="from-currency" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; cursor: pointer;">
155                        ${SUPPORTED_CURRENCIES.map(c => `<option value="${c}">${c} ${CURRENCY_SYMBOLS[c]}</option>`).join('')}
156                    </select>
157                </div>
158                
159                <button id="swap-currencies" style="margin-top: 28px; background: #007bff; color: white; border: none; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; font-size: 18px; transition: transform 0.2s;" title="Swap currencies"></button>
160                
161                <div style="flex: 1;">
162                    <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #555;">To:</label>
163                    <select id="to-currency" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 16px; cursor: pointer;">
164                        ${SUPPORTED_CURRENCIES.map(c => `<option value="${c}">${c} ${CURRENCY_SYMBOLS[c]}</option>`).join('')}
165                    </select>
166                </div>
167            </div>
168
169            <div id="conversion-results" style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px; min-height: 60px;">
170                <div style="text-align: center; color: #666;">Enter an amount to convert</div>
171            </div>
172
173            <div style="display: flex; gap: 10px;">
174                <button id="convert-btn" style="flex: 1; background: #007bff; color: white; border: none; padding: 14px; border-radius: 6px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s;">Convert</button>
175                <button id="refresh-rates" style="background: #28a745; color: white; border: none; padding: 14px 20px; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background 0.2s;" title="Refresh exchange rates">🔄</button>
176            </div>
177
178            <div id="rate-info" style="margin-top: 15px; text-align: center; font-size: 12px; color: #888;"></div>
179        `;
180
181        document.body.appendChild(panel);
182        return panel;
183    }
184
185    // Show converter panel
186    async function showConverterPanel(amount = '', fromCurrency = 'NIS', toCurrency = 'USD') {
187        if (!converterPanel) {
188            converterPanel = createConverterPanel();
189            setupPanelListeners();
190        }
191
192        converterPanel.style.display = 'block';
193        
194        const amountInput = document.getElementById('amount-input');
195        const fromSelect = document.getElementById('from-currency');
196        const toSelect = document.getElementById('to-currency');
197
198        amountInput.value = amount;
199        fromSelect.value = fromCurrency;
200        toSelect.value = toCurrency;
201
202        // Load rates and convert if amount is provided
203        exchangeRates = await fetchExchangeRates('NIS');
204        if (amount) {
205            performConversion();
206        }
207    }
208
209    // Setup panel event listeners
210    function setupPanelListeners() {
211        document.getElementById('close-converter').addEventListener('click', () => {
212            converterPanel.style.display = 'none';
213        });
214
215        document.getElementById('swap-currencies').addEventListener('click', () => {
216            const fromSelect = document.getElementById('from-currency');
217            const toSelect = document.getElementById('to-currency');
218            const temp = fromSelect.value;
219            fromSelect.value = toSelect.value;
220            toSelect.value = temp;
221            performConversion();
222        });
223
224        document.getElementById('convert-btn').addEventListener('click', performConversion);
225
226        document.getElementById('amount-input').addEventListener('keypress', (e) => {
227            if (e.key === 'Enter') {
228                performConversion();
229            }
230        });
231
232        document.getElementById('refresh-rates').addEventListener('click', async () => {
233            const btn = document.getElementById('refresh-rates');
234            btn.textContent = '⏳';
235            btn.disabled = true;
236            
237            // Clear cache
238            for (const currency of SUPPORTED_CURRENCIES) {
239                await GM.setValue(`rates_${currency}`, '');
240            }
241            
242            exchangeRates = await fetchExchangeRates('NIS');
243            btn.textContent = '🔄';
244            btn.disabled = false;
245            performConversion();
246        });
247
248        // Close on outside click
249        converterPanel.addEventListener('click', (e) => {
250            if (e.target === converterPanel) {
251                converterPanel.style.display = 'none';
252            }
253        });
254    }
255
256    // Perform conversion
257    async function performConversion() {
258        const amount = parseFloat(document.getElementById('amount-input').value);
259        const fromCurrency = document.getElementById('from-currency').value;
260        const toCurrency = document.getElementById('to-currency').value;
261        const resultsDiv = document.getElementById('conversion-results');
262        const rateInfo = document.getElementById('rate-info');
263
264        if (!amount || isNaN(amount)) {
265            resultsDiv.innerHTML = '<div style="text-align: center; color: #dc3545;">Please enter a valid amount</div>';
266            return;
267        }
268
269        if (!exchangeRates) {
270            exchangeRates = await fetchExchangeRates('NIS');
271        }
272
273        const result = convertCurrency(amount, fromCurrency, toCurrency, exchangeRates);
274        const rate = exchangeRates[toCurrency] / exchangeRates[fromCurrency];
275
276        resultsDiv.innerHTML = `
277            <div style="text-align: center;">
278                <div style="font-size: 14px; color: #666; margin-bottom: 8px;">
279                    ${amount.toLocaleString()} ${CURRENCY_SYMBOLS[fromCurrency]} ${fromCurrency}
280                </div>
281                <div style="font-size: 28px; font-weight: bold; color: #007bff;">
282                    ${result.toFixed(2)} ${CURRENCY_SYMBOLS[toCurrency]}
283                </div>
284                <div style="font-size: 16px; color: #333; margin-top: 4px;">
285                    ${toCurrency}
286                </div>
287            </div>
288        `;
289
290        rateInfo.textContent = `Exchange rate: 1 ${fromCurrency} = ${rate.toFixed(4)} ${toCurrency}`;
291    }
292
293    // Context menu for selected text
294    document.addEventListener('mouseup', (e) => {
295        const selection = window.getSelection();
296        selectedText = selection.toString().trim();
297        
298        if (selectedText) {
299            const amount = extractNumber(selectedText);
300            const currency = detectCurrency(selectedText);
301            
302            if (amount !== null) {
303                selectedCurrency = currency || 'USD';
304            }
305        }
306    });
307
308    // Listen for context menu
309    document.addEventListener('contextmenu', async (e) => {
310        if (selectedText) {
311            const amount = extractNumber(selectedText);
312            const currency = detectCurrency(selectedText);
313            
314            if (amount !== null && currency) {
315                // Store for context menu action
316                await GM.setValue('contextAmount', amount.toString());
317                await GM.setValue('contextCurrency', currency);
318            }
319        }
320    });
321
322    // Register menu commands
323    GM.registerMenuCommand('Open Currency Converter', () => {
324        showConverterPanel();
325    });
326
327    GM.registerMenuCommand('Convert Selected to NIS', async () => {
328        const amount = parseFloat(await GM.getValue('contextAmount', '0'));
329        const currency = await GM.getValue('contextCurrency', 'USD');
330        
331        if (amount && currency !== 'NIS') {
332            showConverterPanel(amount, currency, 'NIS');
333        }
334    });
335
336    GM.registerMenuCommand('Convert Selected to USD', async () => {
337        const amount = parseFloat(await GM.getValue('contextAmount', '0'));
338        const currency = await GM.getValue('contextCurrency', 'NIS');
339        
340        if (amount) {
341            showConverterPanel(amount, currency, 'USD');
342        }
343    });
344
345    GM.registerMenuCommand('Convert Selected to EUR', async () => {
346        const amount = parseFloat(await GM.getValue('contextAmount', '0'));
347        const currency = await GM.getValue('contextCurrency', 'NIS');
348        
349        if (amount) {
350            showConverterPanel(amount, currency, 'EUR');
351        }
352    });
353
354    // Add keyboard shortcut (Ctrl+Shift+C)
355    document.addEventListener('keydown', (e) => {
356        if (e.ctrlKey && e.shiftKey && e.key === 'C') {
357            e.preventDefault();
358            showConverterPanel();
359        }
360    });
361
362    // Initialize
363    async function init() {
364        console.log('Multi-Currency Converter initialized');
365        // Pre-fetch rates
366        exchangeRates = await fetchExchangeRates('NIS');
367    }
368
369    init();
370})();
Multi-Currency Converter with Context Menu | Robomonkey