-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
226 lines (197 loc) · 7.19 KB
/
content.js
File metadata and controls
226 lines (197 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
let isEnabled = false;
let highlightedElements = [];
let debounceTimeout = null;
let bitcoinPrice = null;
let lastPriceUpdate = 0;
// Improved regular expression for USD prices
const usdPriceRegex = /\$\s*\d+(?:,\d{3})*(?:\.\d{2})?|\$\d+(?:,\d{3})*(?:\.\d{2})?/g;
// Function to fetch current Bitcoin price
async function updateBitcoinPrice() {
try {
const now = Date.now();
// Only update price every 60 seconds
if (now - lastPriceUpdate < 60000 && bitcoinPrice !== null) {
return bitcoinPrice;
}
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd');
const data = await response.json();
bitcoinPrice = data.bitcoin.usd;
lastPriceUpdate = now;
return bitcoinPrice;
} catch (error) {
console.error('Error fetching Bitcoin price:', error);
return null;
}
}
// Function to calculate BTC and sats amounts from USD
function calculateCryptoAmount(usdAmount, btcPrice) {
if (!btcPrice) return null;
// Remove $ and commas, then convert to number
const amount = Number(usdAmount.replace(/[$,]/g, ''));
const btcAmount = amount / btcPrice;
const satsAmount = Math.round(btcAmount * 100000000); // Convert to satoshis
return {
btc: btcAmount,
sats: satsAmount
};
}
// Format number with commas
function formatWithCommas(number) {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Debounce function to limit how often we process changes
function debounce(func, wait) {
return function executedFunction(...args) {
const later = () => {
clearTimeout(debounceTimeout);
func(...args);
};
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(later, wait);
};
}
// Check if an element should be skipped
function shouldSkipElement(element) {
const skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME', 'OBJECT', 'VIDEO', 'AUDIO'];
const skipClasses = ['usd-price-highlight'];
return skipTags.includes(element.tagName) ||
skipClasses.some(className => element.classList.contains(className)) ||
element.closest('[aria-hidden="true"]');
}
// Check if element is an inline element that might contain prices
function isInlineElement(element) {
const inlineTags = ['STRONG', 'B', 'I', 'EM', 'SPAN', 'A', 'SUB', 'SUP', 'MARK', 'SMALL'];
return inlineTags.includes(element.tagName);
}
async function processNode(node) {
if (!node || !isEnabled) return;
const btcPrice = await updateBitcoinPrice();
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
if (text && text.includes('$') && usdPriceRegex.test(text)) {
const span = document.createElement('span');
span.innerHTML = text.replace(usdPriceRegex, match => {
const cryptoAmount = calculateCryptoAmount(match, btcPrice);
const tooltip = cryptoAmount ?
`≈ ${cryptoAmount.btc.toFixed(8)} BTC (${formatWithCommas(cryptoAmount.sats)} sats) @ $${btcPrice.toLocaleString()}` :
'Unable to calculate BTC price';
return `<span class="usd-price-highlight" title="${tooltip}">${match}</span>`;
});
node.parentNode.replaceChild(span, node);
const highlights = span.getElementsByClassName('usd-price-highlight');
highlightedElements.push(...highlights);
}
} else if (node.nodeType === Node.ELEMENT_NODE && !shouldSkipElement(node)) {
// Special handling for inline elements that might contain prices
if (isInlineElement(node) && node.textContent.includes('$')) {
const text = node.textContent;
if (usdPriceRegex.test(text)) {
const newElement = document.createElement(node.tagName);
Array.from(node.attributes).forEach(attr => {
newElement.setAttribute(attr.name, attr.value);
});
newElement.innerHTML = text.replace(usdPriceRegex, match => {
const cryptoAmount = calculateCryptoAmount(match, btcPrice);
const tooltip = cryptoAmount ?
`≈ ${cryptoAmount.btc.toFixed(8)} BTC (${formatWithCommas(cryptoAmount.sats)} sats) @ $${btcPrice.toLocaleString()}` :
'Unable to calculate BTC price';
return `<span class="usd-price-highlight" title="${tooltip}">${match}</span>`;
});
node.parentNode.replaceChild(newElement, node);
const highlights = newElement.getElementsByClassName('usd-price-highlight');
highlightedElements.push(...highlights);
return;
}
}
// Process child nodes
for (const childNode of Array.from(node.childNodes)) {
await processNode(childNode);
}
}
}
async function findAndHighlightPrices() {
if (!isEnabled) return;
// Remove existing highlights
highlightedElements.forEach(el => {
if (el && el.parentNode) {
const text = el.textContent;
el.parentNode.replaceChild(document.createTextNode(text), el);
}
});
highlightedElements = [];
// Start checking from main content area if possible
const mainContent = document.querySelector('main') || document.querySelector('#content') || document.body;
await processNode(mainContent);
}
// Create a more aggressive observer for price-specific changes
function createPriceObserver() {
return new MutationObserver((mutations) => {
if (!isEnabled) return;
let shouldProcess = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.textContent && node.textContent.includes('$')) {
shouldProcess = true;
break;
}
}
} else if (mutation.type === 'characterData') {
if (mutation.target.textContent && mutation.target.textContent.includes('$')) {
shouldProcess = true;
break;
}
}
if (shouldProcess) break;
}
if (shouldProcess) {
debouncedHighlight();
}
});
}
// Debounced version of findAndHighlightPrices
const debouncedHighlight = debounce(() => {
findAndHighlightPrices().catch(console.error);
}, 100);
// Listen for messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'toggleHighlight') {
isEnabled = request.isEnabled;
debouncedHighlight();
}
});
// Load initial state
chrome.storage.local.get(['isEnabled'], function(result) {
isEnabled = result.isEnabled || false;
if (isEnabled) {
if (document.readyState === 'complete') {
debouncedHighlight();
} else {
window.addEventListener('load', debouncedHighlight);
}
}
});
// Create and start the observer
const priceObserver = createPriceObserver();
// Start observing once the DOM is ready
function startObserving() {
priceObserver.observe(document.body, {
childList: true,
subtree: true,
characterData: true,
characterDataOldValue: true
});
}
if (document.readyState === 'complete') {
startObserving();
} else {
window.addEventListener('load', startObserving);
}
// Additional check for dynamic content loading
window.addEventListener('DOMContentLoaded', debouncedHighlight);
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') {
setTimeout(debouncedHighlight, 1000);
setTimeout(debouncedHighlight, 2000);
}
});