From 2ae4c9c36575fbd6781229f35a82c4bae06ff089 Mon Sep 17 00:00:00 2001 From: AugenAugen <71352143+KleinerCodeDrago@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:50:05 +0100 Subject: [PATCH] 3-price-tracking-and-monitoring (#12) * Add price tracking functionality and update manifest permissions * Refactor normalizeContent function to handle numeric content with commas and dots * Refactor element selection and data storage logic * better design * import logik * remove all comments * Add export and import buttons to popup.html and display current selector and selected content in popup.js --------- Co-authored-by: KleinerCodeDrago --- background.js | 124 +++++++++++++++++++++++++++++++++++++++++++------- content.js | 82 ++++++++++++++------------------- import.html | 10 ++++ import.js | 11 +++++ manifest.json | 14 +++++- popup.css | 55 ++++++++++++++++++++++ popup.html | 60 ++++++++++-------------- popup.js | 71 ++++++++++++++++++++++++----- 8 files changed, 312 insertions(+), 115 deletions(-) create mode 100644 import.html create mode 100644 import.js create mode 100644 popup.css diff --git a/background.js b/background.js index c023d2f..f81bfee 100644 --- a/background.js +++ b/background.js @@ -3,18 +3,22 @@ function storeSelector(url, selector, content) { browser.storage.local.get(url).then(result => { let existingData = result[url] || {}; if (content !== '') { - existingData.content = content; + existingData.content = normalizeContent(content); } existingData.selector = selector; browser.storage.local.set({[url]: existingData}); }); } - browser.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === "storeSelector") { console.log("Received storeSelector message:", message); storeSelector(message.url, message.selector, message.content); + } else if (message.action === "getSelectorForCurrentTab") { + getSelectorForCurrentTab(function(data) { + sendResponse(data); + }); + return true; } }); @@ -28,11 +32,17 @@ function retrieveSelector(url) { function getSelectorForCurrentTab(callback) { browser.tabs.query({active: true, currentWindow: true}, function(tabs) { - const url = new URL(tabs[0].url); - browser.storage.local.get(url.href, function(result) { - const data = result[url.href]; + if (tabs.length === 0 || !tabs[0].url) { + callback({selector: '', content: ''}); + return; + } + + const url = tabs[0].url; + browser.storage.local.get(url, function(result) { + const data = result[url]; + console.log("Sending data to popup:", data); if (data) { - callback({selector: data.selector, content: data.content}); + callback(data); } else { callback({selector: '', content: ''}); } @@ -41,21 +51,22 @@ function getSelectorForCurrentTab(callback) { } -browser.runtime.onMessage.addListener(function(request, sender, sendResponse) { - if (request.action === "getSelectorForCurrentTab") { - getSelectorForCurrentTab(function(data) { - sendResponse(data); - }); - return true; +browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === "initiateImport") { + browser.tabs.create({url: "import.html"}); } }); - function getSelectorForCurrentTab(callback) { browser.tabs.query({active: true, currentWindow: true}, function(tabs) { - const url = new URL(tabs[0].url); - browser.storage.local.get(url.href, function(result) { - const data = result[url.href]; + if (tabs.length === 0 || !tabs[0].url) { + callback({selector: '', content: ''}); + return; + } + + const url = tabs[0].url; + browser.storage.local.get(url, function(result) { + const data = result[url]; console.log("Sending data to popup:", data); if (data) { callback(data); @@ -64,4 +75,83 @@ function getSelectorForCurrentTab(callback) { } }); }); -} \ No newline at end of file +} + +function importData(data) { + console.log("Importing data:", data); + browser.storage.local.clear(() => { + for (let url in data) { + if (data.hasOwnProperty(url)) { + let item = data[url]; + + if (item.hasOwnProperty('selector') && item.hasOwnProperty('content')) { + let storageItem = {}; + storageItem[url] = item; + browser.storage.local.set(storageItem).then(() => { + console.log("Data imported for URL:", url); + }).catch((error) => { + console.error("Error setting data for URL:", url, error); + }); + } else { + console.error("Invalid data format for URL:", url, item); + } + } + } + }); +} + + +browser.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === "importData") { + importData(message.data); + } +}); + + +function normalizeContent(content) { + let numericContent = content.replace(/[^\d,.-]/g, ''); + + let lastCommaIndex = numericContent.lastIndexOf(','); + let lastDotIndex = numericContent.lastIndexOf('.'); + + if (lastCommaIndex > lastDotIndex) { + numericContent = numericContent.substring(0, lastCommaIndex).replace(/,/g, '') + '.' + numericContent.substring(lastCommaIndex + 1); + } else { + numericContent = numericContent.replace(/,/g, ''); + } + + return numericContent; +} + + +function checkPrices() { + browser.storage.local.get(null, function(items) { + for (let url in items) { + if (!url.startsWith('http://') && !url.startsWith('https://')) { + console.error('Invalid URL:', url); + continue; + } + + let storedData = items[url]; + fetch(url) + .then(response => response.text()) + .then(html => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const element = doc.querySelector(storedData.selector); + const currentContent = element ? normalizeContent(element.innerText) : ''; + + }) + .catch(error => console.error('Error fetching the page:', error)); + } + }); +} + + +browser.alarms.create("checkPricesAlarm", { delayInMinutes: 1, periodInMinutes: 1 }); +browser.alarms.onAlarm.addListener(alarm => { + if (alarm.name === "checkPricesAlarm") { + checkPrices(); + } +}); + diff --git a/content.js b/content.js index 54a37ef..8e241c6 100644 --- a/content.js +++ b/content.js @@ -1,3 +1,37 @@ +let isSelectingElement = false; + +function enableElementSelection() { + isSelectingElement = true; + document.addEventListener('click', elementSelectionHandler, {once: true}); +} + +function elementSelectionHandler(e) { + if (!isSelectingElement) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + isSelectingElement = false; + + let path = getPathTo(e.target); + highlightElement(e.target); + + browser.runtime.sendMessage({ + action: "storeSelector", + url: window.location.href, + selector: path, + content: e.target.innerText + }); +} + +browser.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === "selectElement") { + enableElementSelection(); + } +}); + + function getPathTo(element) { if (element.id) { return `#${element.id}`; @@ -26,31 +60,6 @@ function getPathTo(element) { return path; } -document.addEventListener('click', function handler(e) { - e.preventDefault(); - e.stopPropagation(); - - setTimeout(() => { - try { - let path = getPathTo(e.target); - let content = e.target.innerText; - console.log("Captured element:", e.target); - console.log("Captured content:", content); - - browser.runtime.sendMessage({ - action: "storeSelector", - url: window.location.href, - selector: path, - content: content - }); - } catch (error) { - console.error("Error capturing content:", error); - } - }, 500); - - document.removeEventListener('click', handler); -}, {once: true}); - function highlightElement(element) { const originalBorder = element.style.border; element.style.border = '2px solid red'; @@ -58,25 +67,4 @@ function highlightElement(element) { setTimeout(() => { element.style.border = originalBorder; }, 2000); -} - -browser.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.action === "selectElement") { - document.addEventListener('click', function handler(e) { - e.preventDefault(); - e.stopPropagation(); - - let path = getPathTo(e.target); - highlightElement(e.target); - console.log("Selected Element CSS Path:", path); - - browser.runtime.sendMessage({ - action: "storeSelector", - url: window.location.href, - selector: path - }); - - document.removeEventListener('click', handler); - }, {once: true}); - } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/import.html b/import.html new file mode 100644 index 0000000..900e07d --- /dev/null +++ b/import.html @@ -0,0 +1,10 @@ + + + + Import Data + + + + + + diff --git a/import.js b/import.js new file mode 100644 index 0000000..dc6b6d5 --- /dev/null +++ b/import.js @@ -0,0 +1,11 @@ +document.getElementById('fileInput').addEventListener('change', (event) => { + const file = event.target.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const data = JSON.parse(e.target.result); + browser.runtime.sendMessage({action: "importData", data: data}); + }; + reader.readAsText(file); +}); diff --git a/manifest.json b/manifest.json index f1fbb0d..1b403eb 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,6 @@ "name": "Price Tracker", "version": "0.1", "description": "Track prices on various websites and get notified of changes.", - "permissions": ["activeTab", "storage", ""], "browser_action": { "default_popup": "popup.html", "default_icon": { @@ -12,9 +11,20 @@ "128": "icons/icon-128.png" } }, + "permissions": [ + "storage", + "alarms", + "notifications", + "" + ], "background": { - "scripts": ["background.js"] + "scripts": ["background.js"], + "persistent": false }, + "web_accessible_resources": [ + "import.html", + "import.js" + ], "content_scripts": [ { "matches": [""], diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..90d8f31 --- /dev/null +++ b/popup.css @@ -0,0 +1,55 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + color: #333; + margin: 0; + padding: 20px; +} + +#container { + background-color: white; + border-radius: 10px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 20px; + max-width: 300px; + margin: auto; +} + +h1, h2 { + color: #2a9d8f; +} + +button { + background-color: #2a9d8f; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + transition: background-color 0.3s; +} + +button:hover { + background-color: #21867a; +} + +p { + line-height: 1.6; +} + +#status { + margin-bottom: 20px; + color: #555; +} + +#selectionInfo, #watchedPrices { + background-color: #eef6f6; + border-radius: 5px; + padding: 10px; + margin-top: 20px; +} + +#currentSelector, #selectedContent { + word-break: break-all; +} diff --git a/popup.html b/popup.html index e55d938..820b341 100644 --- a/popup.html +++ b/popup.html @@ -1,43 +1,29 @@ - + - Price Tracker - + + Element Selector + -

Price Tracker

- -

Click the button to start element selection.

-

Loading...

-

- - +
+

Element Selector

+ +

Click the button to start element selection.

+
+

Current Selection

+

No selector stored for this page.

+

No content captured

+
+
+

Watched Prices

+
+
+
+ + +
+
+ - - diff --git a/popup.js b/popup.js index b2703e6..1041690 100644 --- a/popup.js +++ b/popup.js @@ -24,20 +24,67 @@ function updateButtonAndStatus() { } } +document.getElementById('exportData').addEventListener('click', () => { + browser.storage.local.get(null, function(items) { + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(items)); + const downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "price_tracker_data.json"); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); + }); +}); + +document.getElementById('importData').addEventListener('click', () => { + browser.runtime.sendMessage({action: "initiateImport"}); +}); + + + document.addEventListener('DOMContentLoaded', function() { browser.runtime.sendMessage({action: "getSelectorForCurrentTab"}) - .then(data => { - console.log("Data received in popup:", data); - if (data && 'selector' in data && 'content' in data) { - document.getElementById('currentSelector').textContent = 'Selector: ' + data.selector; - document.getElementById('selectedContent').textContent = 'Content: ' + data.content; - } else { - document.getElementById('currentSelector').textContent = 'No selector stored for this page.'; - document.getElementById('selectedContent').textContent = 'No content captured'; - } - }).catch(error => { - console.error("Error in receiving data:", error); - }); + .then(data => { + console.log("Data received in popup:", data); + if (data && 'selector' in data && data.selector !== '') { + document.getElementById('currentSelector').textContent = 'Current Selector: ' + data.selector; + document.getElementById('selectedContent').textContent = 'Selected Content: ' + (data.content || 'No content captured'); + } else { + document.getElementById('currentSelector').textContent = 'No selector stored for this page.'; + document.getElementById('selectedContent').textContent = 'No content captured'; + } + }).catch(error => { + console.error("Error in receiving data:", error); + }); + updateWatchedPricesList(); }); +function updateWatchedPricesList() { + browser.storage.local.get(null, function(items) { + const watchedPricesList = document.getElementById('watchedPricesList'); + watchedPricesList.innerHTML = ''; + + for (let url in items) { + let item = items[url]; + let div = document.createElement('div'); + div.innerHTML = ` +

URL: ${url}

+

Selector: ${item.selector}

+

Current Price: ${item.content}

+ + `; + watchedPricesList.appendChild(div); + } + + document.querySelectorAll('.deleteButton').forEach(button => { + button.addEventListener('click', function() { + let urlToDelete = this.getAttribute('data-url'); + browser.storage.local.remove(urlToDelete, function() { + updateWatchedPricesList(); + }); + }); + }); + }); +} +