├── css ├── content.css └── panel.css ├── html ├── options.html ├── devtools.html ├── popup.html └── panel.html ├── img3.png ├── icon ├── icon-16.png ├── icon-19.png ├── icon-32.png ├── icon-38.png ├── icon-48.png └── icon-128.png ├── images ├── img3.png ├── img5.png ├── img6.png ├── img7.png ├── pin.png ├── puzzle.png └── bettercanvas.png ├── js ├── devtools.js ├── background.js ├── content.js └── panel.js ├── manifest.json └── README.md /css/content.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/options.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /html/devtools.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/img3.png -------------------------------------------------------------------------------- /icon/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/icon/icon-16.png -------------------------------------------------------------------------------- /icon/icon-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/icon/icon-19.png -------------------------------------------------------------------------------- /icon/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/icon/icon-32.png -------------------------------------------------------------------------------- /icon/icon-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/icon/icon-38.png -------------------------------------------------------------------------------- /icon/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/icon/icon-48.png -------------------------------------------------------------------------------- /images/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/img3.png -------------------------------------------------------------------------------- /images/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/img5.png -------------------------------------------------------------------------------- /images/img6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/img6.png -------------------------------------------------------------------------------- /images/img7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/img7.png -------------------------------------------------------------------------------- /images/pin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/pin.png -------------------------------------------------------------------------------- /js/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create("Marketplace Helper", null, 'html/panel.html'); -------------------------------------------------------------------------------- /icon/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/icon/icon-128.png -------------------------------------------------------------------------------- /images/puzzle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/puzzle.png -------------------------------------------------------------------------------- /images/bettercanvas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ksucpea/marketplacehelper/HEAD/images/bettercanvas.png -------------------------------------------------------------------------------- /js/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(() => { 2 | console.log("updated"); 3 | //chrome.storage.local.clear(); 4 | chrome.storage.local.get(["settings", "saved"], storage => { 5 | let newOptions = {}; 6 | if(!storage["settings"]) { 7 | newOptions["settings"] = { "lat": 12.345678, "long": 12.345678, "delay": 1000, "max_items": 1000 }; 8 | } 9 | if (!storage["saved"]) { 10 | newOptions["saved"] = [{"name": "all", "pathnames": []}]; 11 | } 12 | chrome.storage.local.set(newOptions); 13 | }) 14 | }); -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Better Marketplace", 4 | "description": "Improves the Facebook marketplace", 5 | "version": "1.2", 6 | "icons": { 7 | "16": "icon/icon-16.png", 8 | "32": "icon/icon-32.png", 9 | "48": "icon/icon-48.png", 10 | "128": "icon/icon-128.png" 11 | }, 12 | "action": { 13 | "default_icon": { 14 | "19": "icon/icon-19.png", 15 | "38": "icon/icon-38.png" 16 | } 17 | }, 18 | "background": { 19 | "service_worker": "js/background.js" 20 | }, 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "https://*.facebook.com/*" 25 | ], 26 | "js": [ 27 | "js/content.js" 28 | ], 29 | "css": [ 30 | "css/content.css" 31 | ], 32 | "run_at": "document_end" 33 | } 34 | ], 35 | "options_page": "html/options.html", 36 | "permissions": [ 37 | "storage", 38 | "webRequest", 39 | "tabs", 40 | "contextMenus", 41 | "unlimitedStorage" 42 | ], 43 | "host_permissions": [ 44 | "https://*.facebook.com/*" 45 | ], 46 | "devtools_page": "html/devtools.html" 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Marketplace Helper

2 | Advanced filtering, price data analysis, and additional info for Facebook Marketplace 3 | 4 | Note: this is a WIP and has several things that need to be fixed 5 | 6 |

Manual Installation

7 | 8 | - Download this repo 9 | 10 | - Go to chrome://extensions in the browser and enable developer mode 11 | 12 | - Press load unpacked and select this extension folder 13 | 14 |

Usage

15 | 16 |

Open devtools window in Facebook Marketplace (right click -> inspect element -> Marketplace Helper tab)

17 |

Collect listing data by going to a category or searching for an item and pressing the start button. Scroll down the marketplace page to load more items and put them in the queue.

18 | 19 | 20 | 21 |

Use filters or sort listings without having to refresh listings, and view price data for filtered items

22 | 23 | 24 | 25 |

Hover over items and press the arrow buttons to view more listing images

26 | 27 | -------------------------------------------------------------------------------- /html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Better Canvas 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Better Canvas

13 |
14 |
Dark mode
15 |
Automatic dark mode
Start time
End time
16 |
Click here to change dark mode colors
17 |
*Dashboard assignments
18 |
*GPA calculator
19 | 20 |
*Gradient cards
21 |
*Assignment potentials
22 |
23 |

*Refresh canvas to apply change

24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /css/panel.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Lato', sans-serif; 3 | font-family: 'Mulish', sans-serif; 4 | font-family: 'Roboto', sans-serif; 5 | font-family: 'Noto Sans', sans-serif; 6 | font-family: 'Work Sans'; 7 | font-family: 'Barlow', sans-serif; 8 | font-family: 'DM Sans', sans-serif; 9 | font-family: 'Inter', sans-serif; 10 | } 11 | 12 | 13 | html, 14 | body { 15 | margin: 0; 16 | background: #f0f2f5; 17 | } 18 | 19 | .filter-checkbox { 20 | appearance: none; 21 | background-color: transparent; 22 | margin: 0; 23 | font: inherit; 24 | color: currentColor; 25 | width: 18px; 26 | height: 18px; 27 | border-radius: 3px; 28 | border: 1px solid; 29 | border-color: #606770; 30 | transform: translateY(-0.075em); 31 | display: grid; 32 | place-content: center; 33 | } 34 | 35 | .filter-checkbox:checked { 36 | border-color: transparent; 37 | background-color: #0571ed; 38 | } 39 | 40 | .text-tiny { 41 | color: #65676b; 42 | margin: 0; 43 | } 44 | 45 | .title { 46 | margin: 18px 0 4px 0; 47 | background: -webkit-linear-gradient(315deg, #0062e0, #35b8fe); 48 | -webkit-background-clip: text; 49 | -webkit-text-fill-color: transparent; 50 | font-size: 32px; 51 | font-weight: 700; 52 | } 53 | 54 | .text-input { 55 | background: #f0f2f5; 56 | border-radius: 6px; 57 | border: 1px solid #ced0d4; 58 | padding: 8px 12px; 59 | width: 80px; 60 | font-size: 14px; 61 | font-weight: 600; 62 | } 63 | 64 | .input-long { 65 | width: 160px; 66 | } 67 | 68 | .filter-checkbox::before { 69 | background-color: #fff; 70 | content: ""; 71 | width: 18px; 72 | height: 18px; 73 | transform: scale(0); 74 | box-shadow: inset 1em 1em var(--form-control-color); 75 | transform-origin: bottom left; 76 | clip-path: polygon(75% 23%, 85% 32%, 65% 53%, 41% 75%, 17% 55%, 26% 45%, 41% 57%); 77 | } 78 | 79 | .filter-checkbox:checked::before { 80 | transform: scale(1); 81 | } 82 | 83 | img { 84 | -khtml-user-select: none; 85 | -o-user-select: none; 86 | -moz-user-select: none; 87 | -webkit-user-select: none; 88 | user-select: none; 89 | } 90 | 91 | .main { 92 | display: flex; 93 | } 94 | 95 | .filter-container { 96 | display: flex; 97 | align-items: center; 98 | border-radius: 6px; 99 | margin-top: 6px; 100 | gap: 6px; 101 | font-size: 14px; 102 | font-weight: 600; 103 | } 104 | 105 | .items { 106 | display: grid; 107 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); 108 | gap: 10px; 109 | } 110 | 111 | .items-container { 112 | background: #f0f2f5; 113 | width: 100%; 114 | } 115 | 116 | .item-container { 117 | border-radius: 8px; 118 | overflow: hidden; 119 | display: flex; 120 | width: 100%; 121 | position: relative; 122 | height: 0; 123 | padding-bottom: 100%; 124 | opacity: 100%; 125 | transition: .5s opacity; 126 | background: #adadad; 127 | } 128 | 129 | .item-link:hover {cursor: pointer;} 130 | 131 | .item-image { 132 | display: none; 133 | } 134 | 135 | .item-image.image-active { 136 | display: block; 137 | position: absolute; 138 | z-index: 0; 139 | object-fit: cover; 140 | width: 100%; 141 | height: 100%; 142 | } 143 | 144 | .item-info { 145 | position: absolute; 146 | top: 0; 147 | left: 0; 148 | height: 100%; 149 | width: 100%; 150 | background: #1414145e; 151 | padding: 15px; 152 | box-sizing: border-box; 153 | color: #fff; 154 | display: flex; 155 | flex-direction: column; 156 | justify-content: space-between; 157 | z-index: 1; 158 | } 159 | 160 | .item-price { 161 | font-weight: bold; 162 | font-size: 20px; 163 | } 164 | 165 | .saved-search { background:#f0f2f5;border-radius:6px;width:100%} 166 | .search-queries {display: none;padding:0 12px} 167 | .search-queries.open {display: block} 168 | .search-drop-btn {display: flex;align-items: center;justify-content: space-between;font-size:14px;font-weight:600;padding:12px} 169 | .search-title {margin: 0;} 170 | .search-drop-btn:hover {cursor: pointer} 171 | .search-link {margin: 0; margin-bottom: 10px} 172 | .search-link {cursor: pointer;} 173 | 174 | 175 | .settings input.text-input {width: 120px} 176 | 177 | button { 178 | display: block; 179 | background: #f0f2f5; 180 | color: #000; 181 | border: none; 182 | border-radius: 8px; 183 | padding: 0px 12px; 184 | height: 36px; 185 | font-weight: 600; 186 | font-size: 15px; 187 | margin: 0; 188 | transition: .1s background-color; 189 | cursor: pointer; 190 | } 191 | 192 | button.active { 193 | background: #e7f3ff; 194 | color: #1877f2; 195 | } 196 | 197 | button:hover { 198 | background: rgb(68, 73, 80, .15); 199 | } 200 | 201 | button.active:hover { 202 | background: rgb(25, 110, 255, .15); 203 | } 204 | 205 | .graph { 206 | gap: 1px; 207 | } 208 | 209 | .price { 210 | min-width: 50px; 211 | } 212 | 213 | .bar { 214 | height: 12px; 215 | background: #0571ed; 216 | border-radius: 3px; 217 | transition: .2s width; 218 | } 219 | 220 | .searches { 221 | display: flex; 222 | flex-wrap: wrap; 223 | gap: 4px; 224 | } 225 | 226 | .refresh { 227 | height: 5px; 228 | width: 0; 229 | background: rgb(134, 186, 255); 230 | transition: width 5s; 231 | margin-bottom: 20px; 232 | } 233 | 234 | .controls, 235 | .filters, 236 | .title, 237 | .settings, 238 | .graph { 239 | border-bottom: 1px solid #ced0d4; 240 | padding-bottom: 10px; 241 | } 242 | 243 | .items-header { 244 | display: flex; 245 | justify-content: space-between; 246 | align-items: center; 247 | box-shadow: 0px 1px 3px 0px #dfdfdf; 248 | padding: 0 16px; 249 | background: #fff; 250 | } 251 | 252 | .graph-section { 253 | display: flex; 254 | align-items: center; 255 | } 256 | 257 | .header { 258 | background: #fff; 259 | box-shadow: 1px -7px 3px 0px #dfdfdf; 260 | padding: 0 15px; 261 | position: relative; 262 | z-index: 1; 263 | min-width: 425px; 264 | max-width: 425px; 265 | } 266 | 267 | .availability-btns { 268 | display: flex; 269 | gap: 4px; 270 | } 271 | 272 | .item-container .item-arrow-left, 273 | .item-container .item-arrow-right { 274 | position: absolute; 275 | opacity: 0%; 276 | z-index: -1; 277 | font-size: 18px; 278 | background: #e4e6eb; 279 | top: 50%; 280 | height: 26px; 281 | width: 26px; 282 | border-radius: 26px; 283 | display: flex; 284 | align-items: center; 285 | justify-content: center; 286 | } 287 | 288 | .hide-item {padding: 10px; margin: -10px; stroke: #fff;height: 20px; width: 20px; opacity: 0%;z-index: -1} 289 | 290 | .item-arrow-left:hover, 291 | .item-arrow-right:hover, 292 | .hide-item:hover { 293 | cursor: pointer; 294 | } 295 | 296 | .item-arrow { 297 | fill: #1d1f23; 298 | } 299 | 300 | .item-container.hovered .item-arrow-left, 301 | .item-container.hovered .item-arrow-right, 302 | .item-container.hovered .hide-item { 303 | z-index: 10; 304 | opacity: 100%; 305 | } 306 | 307 | .item-arrow-left { 308 | left: 8px; 309 | } 310 | 311 | .item-arrow-left svg { 312 | transform: rotate(90deg); 313 | } 314 | 315 | .item-arrow-right { 316 | right: 8px; 317 | } 318 | 319 | .item-arrow-right svg { 320 | transform: rotate(-90deg); 321 | } 322 | /* 323 | .item { 324 | position: absolute; 325 | width:100%; 326 | } 327 | */ 328 | 329 | .items-scroll { 330 | position:relative; 331 | margin: 16px; 332 | } 333 | 334 | .items { 335 | position: absolute; 336 | width: 100%; 337 | } 338 | 339 | select { 340 | background: #f0f2f5; 341 | border-radius: 6px; 342 | border: 1px solid #ced0d4; 343 | padding: 8px 12px; 344 | font-weight: bold; 345 | cursor: pointer; 346 | } -------------------------------------------------------------------------------- /js/content.js: -------------------------------------------------------------------------------- 1 | 2 | let existing = {}; 3 | let interval; 4 | let newItems = 0; 5 | let queue = []; 6 | let num_items_searched = 0; 7 | let settings = {}; 8 | let currentName = "all"; 9 | 10 | document.addEventListener("DOMContentLoaded", () => { 11 | chrome.storage.local.get("settings", storage => { 12 | settings = storage.settings; 13 | }); 14 | }); 15 | 16 | function createOverlay() { 17 | 18 | let container = document.querySelector("#mh-overlay") || document.createElement("div"); 19 | container.style = "color:#fff;font-size:14px;position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background: #000000bd; z-index: 10; display: flex; align-items: center; justify-content: center;"; 20 | container.id = "mh-overlay"; 21 | document.body.appendChild(container); 22 | container.innerHTML = '
'; 23 | } 24 | 25 | /* 26 | 27 | function setNextOverlayItem(items = null) { 28 | console.log("setNextOverlayItem()"); 29 | let el = document.querySelector(".mh-overlay-inner"); 30 | let el2 = document.querySelector('div[aria-label="Collection of Marketplace items"]'); 31 | let visited = el2.querySelectorAll('a.bfbm-visited'); 32 | let listings = el2.querySelectorAll('a'); 33 | el.textContent = ""; 34 | if (items === null) { 35 | el.textContent = "Scroll to continue searching for new items"; 36 | } else { 37 | let str = ""; 38 | //const max = (items.length > 3 ? 3 : items.length); 39 | const max = items.length; 40 | el.innerHTML += "

" + items.length + " in queue

"; 41 | for (let i = 0; i < max; i++) { 42 | const listing = items[i]; 43 | var isItem = listing.href && listing.href.includes("/marketplace/item/"); 44 | if (isItem) { 45 | //str += ''; 46 | str += '

' + listing.querySelector("img").alt + "

"; 47 | } 48 | } 49 | el.innerHTML += str; 50 | } 51 | } 52 | */ 53 | /* 54 | async function getListings2() { 55 | 56 | const data = await chrome.storage.local.get("saved"); 57 | let pathnames; 58 | for (let i = 0; i < data["saved"].length; i++) { 59 | console.log(data["saved"][i]); 60 | if (data["saved"][i].name === currentName) { 61 | pathnames = data["saved"][i].pathnames; 62 | break; 63 | } 64 | } 65 | const storage = await chrome.storage.local.get(pathnames); 66 | let existing = {}; 67 | pathnames.forEach(pathname => { 68 | existing = { ...data, ...storage[pathname] }; 69 | }); 70 | let el = document.querySelector('div[aria-label="Collection of Marketplace items"]'); 71 | let visited = el.querySelectorAll('a.bfbm-visited'); 72 | let listings = el.querySelectorAll('a'); 73 | if (visited.length === listings.length) return; 74 | 75 | for (let i = visited.length; i < listings.length; i++) { 76 | const listing = listings[i]; 77 | var isItem = listing.href && listing.href.includes("/marketplace/item/"); 78 | if (isItem) { 79 | const id = isItem ? listing.href.split("/marketplace/item/")[1].split("/")[0] : -1; 80 | if (existing[id]) { 81 | listing.style.opacity = "33%"; 82 | } else { 83 | queue.push(listing); 84 | } 85 | } 86 | //listing.classList.add("bfbm-visited"); 87 | } 88 | console.log(queue); 89 | 90 | } 91 | */ 92 | 93 | async function markSeen() { 94 | const data = await chrome.storage.local.get("saved"); 95 | let pathnames; 96 | for (let i = 0; i < data["saved"].length; i++) { 97 | if (data["saved"][i].name === currentName) { 98 | pathnames = data["saved"][i].pathnames; 99 | break; 100 | } 101 | } 102 | const storage = await chrome.storage.local.get(pathnames); 103 | let existing = {}; 104 | pathnames.forEach(pathname => { 105 | existing = { ...data, ...storage[pathname] }; 106 | }); 107 | let el = document.querySelector('div[aria-label="Collection of Marketplace items"]'); 108 | const queue = el.querySelectorAll("a:not(.bfbm-visited, .bfbm-seen)"); 109 | 110 | let queueText = ""; 111 | let queueLength = 0; 112 | queue.forEach(item => { 113 | if (item.href && item.href.includes("/marketplace/item/")) { 114 | const id = item.href.split("/marketplace/item/")[1].split("/")[0]; 115 | if (existing[id]) { 116 | item.classList.add("bfbm-seen"); 117 | item.style.opacity = "33%"; 118 | } else { 119 | queueText += `

${item.querySelector("img").alt}

`; 120 | queueLength++; 121 | } 122 | } else { 123 | item.classList.add("bfbm-seen"); 124 | } 125 | }); 126 | if (queueText === "") { 127 | document.querySelector(".mh-overlay-inner").textContent = "Scroll to continue queueing items"; 128 | } else { 129 | document.querySelector(".mh-overlay-inner").innerHTML = `

${queueLength} in queue:

` + queueText; 130 | } 131 | } 132 | 133 | async function checkQueue() { 134 | await markSeen(); 135 | 136 | const el = document.querySelector('div[aria-label="Collection of Marketplace items"]'); 137 | const queue = el.querySelectorAll("a:not(.bfbm-visited, .bfbm-seen)"); 138 | 139 | const listing = queue[0]; 140 | var isItem = listing.href && listing.href.includes("/marketplace/item/"); 141 | if (isItem) { 142 | const id = listing.href.split("/marketplace/item/")[1].split("/")[0]; 143 | if (existing[id]) { 144 | listing.classList.add("bfbm-seen"); 145 | listing.style.opacity = "33%"; 146 | } else { 147 | simulateClick(listing); 148 | } 149 | } 150 | listing.classList.add("bfbm-visited"); 151 | } 152 | 153 | function simulateClick(element) { 154 | if (!element) return; 155 | const mouseDownEvent = new PointerEvent('pointerdown', { 156 | clientX: element.getBoundingClientRect().left, 157 | clientY: element.getBoundingClientRect().top, 158 | bubbles: true, 159 | cancelable: true 160 | }); 161 | element.dispatchEvent(mouseDownEvent); 162 | element.parentNode.style = "border: 2px solid #1b74e4;padding:8px; border-radius: 8px;transition:.2s padding;"; 163 | } 164 | 165 | /* 166 | function checkQueue2() { 167 | console.log("checkQueue()"); 168 | let current = queue.shift(); 169 | if (current) { 170 | const mouseDownEvent = new PointerEvent('pointerdown', { 171 | clientX: current.getBoundingClientRect().left, 172 | clientY: current.getBoundingClientRect().top, 173 | bubbles: true, 174 | cancelable: true 175 | }); 176 | current.dispatchEvent(mouseDownEvent); 177 | current.parentNode.style = "border: 2px solid #1b74e4;padding:8px; border-radius: 8px;transition:.2s padding;"; 178 | setNextOverlayItem(queue.length > 0 ? queue : null); 179 | } else { 180 | setNextOverlayItem(null); 181 | } 182 | } 183 | */ 184 | 185 | 186 | function getPathname(path) { 187 | let first, last = ""; 188 | if (path.includes("/search")) { 189 | first = path.split("/search")[1]; 190 | let x = first.split(path.includes("/?") ? "/?" : "?"); 191 | return x.length > 1 ? x[1].split("query=")[1].split("&")[0] : "unknown"; 192 | } else if (path.includes("/category/")) { 193 | first = path.split("/category/")[1].split("/")[0]; 194 | return first; 195 | } else { 196 | return "unknown"; 197 | } 198 | } 199 | 200 | function beginAutomation() { 201 | 202 | } 203 | 204 | let interval2; 205 | 206 | chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { 207 | console.log(message); 208 | if (message.type === "start") { 209 | currentName = message.name; 210 | 211 | const path = getPathname(location.href); 212 | 213 | //const isSold = location.href.includes("availability=out%20of%20stock"); 214 | 215 | chrome.storage.local.get([path, "settings"], data => { 216 | createOverlay(); 217 | clearInterval(interval2); 218 | interval2 = setInterval(checkQueue, data.settings.delay); 219 | }); 220 | 221 | } else if (message.type === "pause") { 222 | //clearInterval(interval); 223 | clearInterval(interval2); 224 | document.querySelector("#mh-overlay").style.zIndex = -1; 225 | } 226 | sendResponse(); 227 | }); -------------------------------------------------------------------------------- /html/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

Marketplace Helper

21 |

22 |
23 |

Controls

24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 |

View/save listings under:

32 | 33 |
34 |
35 | Create new saved search: 36 | 37 | 38 |
39 |
40 | Clear saved search: 41 | 42 | 43 |
44 |
45 |
46 |

Settings

47 |
latitude
48 |
longitude
49 |
queue process time (ms)
50 |

Warning: lowering this may result in getting temporarily blocked

51 |
max items shown
52 |
53 |
54 |

Filters

55 |
56 | Sort by: 57 | 62 | 66 |
67 |
68 | 69 | 70 | 71 | mi 72 |
73 |
74 | 75 | 76 | 77 | days ago 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 |

Price data

117 |

118 |

No data yet.

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 |

Saved

179 |
180 |
181 |
182 |
183 |

0 Items

184 |

185 |
186 | 187 | 188 | 189 | 190 |
191 |
192 |

No data for this yet.

193 |
194 |
195 |
196 |
197 |
198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /js/panel.js: -------------------------------------------------------------------------------- 1 | let batchTimer; 2 | let batch = {}; 3 | let requests = []; 4 | 5 | const batchCount = 10; 6 | const num_graph_sections = 13; // +1 (starts at 0) 7 | 8 | const arrowSvg = ''; 9 | const hideSvg = ''; 10 | const chevronDown = ''; 11 | 12 | function selectItemAvailability() { 13 | chrome.tabs.query({ active: true }, function (tabs) { 14 | ["#items-available", "#items-sold", "#items-all", "#items-hidden"].forEach(btn => { 15 | document.querySelector(btn).classList.remove("active"); 16 | }); 17 | 18 | if (tabs[0].url.includes("availability=out%20of%20stock")) { 19 | document.querySelector("#items-sold").classList.add("active"); 20 | } else if (tabs[0].url.includes("bfbmAllItems=true")) { 21 | document.querySelector("#items-all").classList.add("active"); 22 | } else if (tabs[0].url.includes("bfbmHidden=true")) { 23 | document.querySelector("#items-hidden").classList.add("active"); 24 | } else { 25 | document.querySelector("#items-available").classList.add("active"); 26 | } 27 | }); 28 | } 29 | 30 | async function checkBatch() { 31 | const tabs = await chrome.tabs.query({ url: ["https://www.facebook.com/marketplace/*"] }); 32 | const path = tabs[0].url; 33 | const storage = await chrome.storage.local.get(path); 34 | if (requests.length === 0) return; 35 | const availability = tabs[0].url.includes("availability=out%20of%20stock") ? "sold" : "available"; 36 | const items = await processNewItems(availability); 37 | let existing = storage[path] ? storage[path] : {}; 38 | let updatedData = { ...existing, ...items }; 39 | 40 | chrome.storage.local.set({ [path]: updatedData }).then(() => { 41 | chrome.storage.local.get(path, storage => { 42 | console.log("CHECK BATCH RESULT!", storage); 43 | }) 44 | document.querySelector(".batch-count").textContent = "refreshing"; 45 | setTimeout(() => { 46 | document.querySelector(".batch-count").textContent = ""; 47 | }, 2000); 48 | filterItems(); 49 | }); 50 | } 51 | 52 | function processNewItems(availability) { 53 | return new Promise((resolve, reject) => { 54 | let b = {}; 55 | for (let i = 0; i < requests.length; i++) { 56 | requests[i].getContent(body => { 57 | try { 58 | let parsed = JSON.parse(body); 59 | let item = parsed?.data?.viewer?.marketplace_product_details_page?.target; 60 | if (!item) return; 61 | 62 | item.availability = availability; 63 | item.hide = false; 64 | if (item["can_buyer_make_checkout_offer"] !== undefined) { 65 | item.negotiable = item.can_buyer_make_checkout_offer ? true : isItemNegotiable(item.marketplace_listing_title + " " + item.redacted_description.text); 66 | } 67 | 68 | console.log(item); 69 | 70 | const properties = ["creation_time", "availability", "hide", "negotiable", "attribute_data", "listing_photos", "primary_listing_photo", "location", "listing_price", "is_shipping_offered", "formatted_shipping_price", "location_text", "marketplace_listing_title"]; 71 | const output = {}; 72 | 73 | for (const property of properties) { 74 | if (item[property]) output[property] = item[property]; 75 | } 76 | 77 | console.log("propertyies", item.id, output); 78 | 79 | 80 | if (!b[item.id]) { 81 | b[item.id] = { ...output, "updated": true }; 82 | } else { 83 | b[item.id] = { ...b[item.id], ...output }; 84 | } 85 | 86 | console.log(b); 87 | } catch (e) { 88 | console.error("error parsing body"); 89 | } finally { 90 | if (i === requests.length - 1) { 91 | requests = []; 92 | console.log("sending", b); 93 | resolve(b); 94 | } 95 | } 96 | }); 97 | } 98 | }); 99 | } 100 | 101 | function createListing(item) { 102 | let div = document.createElement("div"); 103 | div.classList.add("item-container"); 104 | div.dataset.id = item.id; 105 | 106 | if (!item.listing_photos) { 107 | item.listing_photos = [{ "image": { "uri": item.primary_listing_photo.listing_image.uri } }] 108 | } 109 | 110 | div.innerHTML = `
111 |
112 |
113 |
114 | ${item?.listing_price?.formatted_amount_zeros_stripped} ${item?.negotiable ? " or offer" : ""} 115 |
116 | ${item?.is_shipping_offered ? `
${item?.formatted_shipping_price}
` : ""} 117 |
${convertTime(item.timeago)} ago
118 |
119 |
120 | ${hideSvg} 121 |
122 |
123 |
124 | 128 |
129 |
130 | `; 131 | 132 | //document.querySelector(".items").appendChild(div); 133 | 134 | /* lazy load images */ 135 | 136 | let lazyLoad = function () { 137 | let img = div.querySelector(".image-primary"); 138 | if (!img.classList.contains("loaded")) { 139 | img.src = item.listing_photos[0].image.uri; 140 | } 141 | div.removeEventListener("click", lazyLoad); 142 | } 143 | 144 | div.addEventListener("click", lazyLoad); 145 | 146 | /* creating picture album */ 147 | if (item.listing_photos.length > 1) { 148 | 149 | for (let i = 1; i < item.listing_photos.length; i++) { 150 | let img = document.createElement("img"); 151 | img.classList.add("item-image"); 152 | img.src = "data:,"; 153 | img.dataset.imgnum = i; 154 | div.appendChild(img); 155 | } 156 | 157 | div.innerHTML += `
${arrowSvg}
${arrowSvg}
`; 158 | [".item-arrow-left", ".item-arrow-right"].forEach(arrow => { 159 | div.querySelector(arrow).addEventListener("click", () => { 160 | let current = div.querySelector(".image-active"); 161 | let images = div.querySelectorAll(".item-image"); 162 | let imgNum = parseInt(current.dataset.imgnum); 163 | current.classList.remove("image-active"); 164 | let next; 165 | if (arrow === ".item-arrow-left") { 166 | next = imgNum === 0 ? item.listing_photos.length - 1 : imgNum - 1; 167 | } else { 168 | next = imgNum === item.listing_photos.length - 1 ? 0 : imgNum + 1; 169 | } 170 | if (images[next].src === "data:,") images[next].src = item.listing_photos[next].image.uri; 171 | images[next].classList.add("image-active"); 172 | }) 173 | }); 174 | } 175 | 176 | /* event listeners */ 177 | div.querySelector(".item-image").addEventListener("load", function (e) { 178 | e.target.parentNode.style.opacity = "100%"; 179 | e.target.removeEventListener("load", this); 180 | }); 181 | div.addEventListener("mouseover", () => { 182 | div.classList.add("hovered"); 183 | }); 184 | div.addEventListener("mouseleave", () => { 185 | div.classList.remove("hovered"); 186 | }); 187 | 188 | div.querySelector(".item-link").addEventListener("click", () => { 189 | chrome.tabs.create({ url: `https://facebook.com/marketplace/item/${item.id}` }); 190 | }); 191 | 192 | div.querySelector(".hide-item").addEventListener("click", () => { 193 | batch[item.id] = { ...item, hide: true }; 194 | checkBatch(); 195 | div.remove(); 196 | }); 197 | 198 | return div; 199 | } 200 | 201 | 202 | 203 | let currentItems = []; 204 | 205 | function getSavedPathnames(save, data) { 206 | for (let i = 0; i < data["saved"].length; i++) { 207 | if (data["saved"][i].name === save) { 208 | return data["saved"][i].pathnames; 209 | } 210 | } 211 | return []; 212 | } 213 | 214 | async function filterItems() { 215 | const tabs = await chrome.tabs.query({ url: ["https://www.facebook.com/marketplace/*"] }); 216 | const save = document.querySelector("#save-under").value; 217 | const saved = await chrome.storage.local.get("saved"); 218 | let pathnames = getSavedPathnames(save, saved); 219 | const storage = await chrome.storage.local.get([...pathnames, "settings"]); 220 | 221 | const options = { 222 | "sort": document.querySelector("#sort").value, 223 | "direction": document.querySelector("#direction").value, 224 | "hideDistance": { "checked": document.querySelector("#hideDistance").checked, "radius": parseInt(document.querySelector("#hideDistanceVal").value) || 50 }, 225 | "hideTimeOver": { "checked": document.querySelector("#hideTimeOver").checked, "days": parseFloat(document.querySelector("#hideTimeOverVal").value) || 1 }, 226 | "hidePriceUnder": { "checked": document.querySelector("#hidePriceUnder").checked, "price": parseInt(document.querySelector("#hidePriceUnderVal").value) || 0 }, 227 | "hidePriceOver": { "checked": document.querySelector("#hidePriceOver").checked, "price": parseInt(document.querySelector("#hidePriceOverVal").value) || 1000 }, 228 | "beforeYear": { "checked": document.querySelector("#beforeYear").checked, "year": parseInt(document.querySelector("#beforeYearVal").value) || 2023 }, 229 | "showNegotiable": { "checked": document.querySelector("#showNegotiable").checked }, 230 | "explicitWords": { "checked": document.querySelector("#explicitWords").checked, "words": document.querySelector("#explicitWordsVal").value }, 231 | "explicitWordsHide": { "checked": document.querySelector("#explicitWordsHide").checked, "words": document.querySelector("#explicitWordsHideVal").value }, 232 | "hideEmojis": { "checked": document.querySelector("#hideEmojis").checked }, 233 | "availability": tabs[0].url.includes("availability=out%20of%20stock") ? "sold" : "available", 234 | "lat": storage.settings.lat, 235 | "long": storage.settings.long, 236 | } 237 | 238 | // combine data 239 | let data = {}; 240 | pathnames.forEach(pathname => { 241 | data = { ...data, ...storage[pathname] }; 242 | }); 243 | 244 | let keys = Object.keys(data); 245 | let toSort = []; 246 | let filtered = []; 247 | currentItems = []; 248 | let totalPrice = 0, totalAfterFilter = 0, low = 10000, high = 0; 249 | 250 | // setup items to be sorted 251 | keys.forEach(key => { 252 | if (totalAfterFilter >= storage["settings"]["max_items"]) return; 253 | const item = data[key]; 254 | let allowAfterFilter = true; 255 | let xlat = parseFloat(item?.location?.latitude || 0), xlong = parseFloat(item?.location?.longitude || 0); 256 | let pythx = Math.sqrt(Math.pow(options.lat - xlat, 2) + Math.pow(options.long - xlong, 2)); 257 | let prc = parseInt(item?.listing_price?.amount || 0); 258 | let distance = parseInt(pythx * 69); 259 | let now = (new Date().getTime()) / 1000; 260 | let timeago = now - item.creation_time; 261 | 262 | if (options.explicitWords.checked === true) { 263 | let words = options.explicitWords.words.split(","); 264 | let description = (" " + item.marketplace_listing_title + " " + item.redacted_description.text + " ").toLowerCase(); 265 | let found = false; 266 | words.forEach(word => { 267 | if (word === "" || found === true) return; 268 | if (description.includes(" " + word.toLowerCase().trim() + " ")) { 269 | found = true; 270 | } 271 | }); 272 | allowAfterFilter = found; 273 | } 274 | 275 | if (options.explicitWordsHide.checked === true) { 276 | let words = options.explicitWordsHide.words.split(","); 277 | let description = (" " + item.marketplace_listing_title + " " + item.redacted_description.text + " ").toLowerCase(); 278 | words.forEach(word => { 279 | if (word !== "") { 280 | if (description.includes(word.toLowerCase())) { 281 | allowAfterFilter = false; 282 | } 283 | } 284 | }); 285 | } 286 | 287 | if (options.hideEmojis.checked === true) { 288 | let description = (" " + item.marketplace_listing_title + " " + item.redacted_description.text + " "); 289 | if ((description.match(/([\uD800-\uDBFF][\uDC00-\uDFFF])/g))) allowAfterFilter = false; 290 | 291 | } 292 | 293 | if (options.beforeYear.checked === true) { 294 | let year = new Date(parseInt(options.beforeYear.year), 0); 295 | if ((item.marketplace_listing_seller.join_time * 1000) > year.getTime()) { 296 | allowAfterFilter = false; 297 | } 298 | } 299 | 300 | if ((options.hideDistance.checked === true && distance > options.hideDistance.radius) || 301 | (options.hidePriceUnder.checked === true && prc <= options.hidePriceUnder.price) || 302 | (options.hidePriceOver.checked === true && prc > options.hidePriceOver.price) || 303 | (options.hideTimeOver.checked === true && timeago > (options.hideTimeOver.days * 24 * 60 * 60)) || 304 | (options.showNegotiable.checked === true && item.negotiable === false) || 305 | (options.availability === "available" && item.availability === "sold") || 306 | (options.availability === "sold" && item.availability === "available") || 307 | (item.hide === true)) { 308 | allowAfterFilter = false; 309 | } 310 | 311 | item.distance = distance; 312 | item.timeago = timeago; 313 | item.allowAfterFilter = allowAfterFilter; 314 | //toSort.push(data[key]); 315 | 316 | if (allowAfterFilter) { 317 | totalPrice += prc; 318 | if (low > prc) low = prc; 319 | if (high < prc) high = prc; 320 | totalAfterFilter++; 321 | filtered.push(item); 322 | } 323 | }); 324 | 325 | // setting up graph 326 | let avg = parseInt(totalPrice / totalAfterFilter); 327 | document.querySelector("#avg-price").textContent = `Average: $${avg}`; 328 | let incr = (avg - low) / (num_graph_sections / 2); 329 | if ((incr * (num_graph_sections + 1)) > high) { // if the highest item is less than the increment 330 | incr = (high - low) / num_graph_sections; 331 | } 332 | 333 | // reset graph 334 | document.querySelectorAll(".graph-bars > div").forEach((sec, i) => { 335 | sec.querySelector(".bar").style.width = 0; 336 | sec.querySelector(".price").textContent = "$" + parseInt(low + (i * incr)) + (i === num_graph_sections ? "+" : ""); 337 | }); 338 | 339 | 340 | let sections = []; 341 | let max_section = 0; 342 | for (let i = 0; i <= num_graph_sections; i++) { 343 | sections[i] = 0; 344 | } 345 | 346 | filtered = filtered.sort(sortBy(options.sort, options.direction)); 347 | 348 | for (const item of filtered) { 349 | let section = (parseInt(item?.listing_price?.amount) === 0 || parseInt(item?.listing_price?.amount) === low) && incr === 0 ? 0 : parseInt((parseInt(item?.listing_price?.amount) - low) / incr); 350 | if (section >= num_graph_sections) section = num_graph_sections; 351 | sections[section]++; 352 | if (sections[section] > sections[max_section]) { 353 | max_section = section; 354 | } 355 | try { 356 | const listing = createListing(item); 357 | currentItems.push(listing); 358 | } catch (e) { 359 | console.warn(e); 360 | } 361 | } 362 | 363 | 364 | /* determine the length of a single bar */ 365 | /* the section with most items should span 100% of the graph */ 366 | let bars = document.querySelector(".graph-bars"); 367 | let percentage = 100 / sections[max_section]; 368 | 369 | /* increase each bar section */ 370 | for (let i = 0; i <= num_graph_sections; i++) { 371 | let bar = bars.querySelector(`div[data-incr="${i}"] > .bar`); 372 | bar.style.width = (sections[i] * percentage) + "%"; 373 | } 374 | 375 | /* displaying indicators if there is no data */ 376 | if (keys.length > 0) { 377 | document.querySelector("#no-items").style.display = "none"; 378 | document.querySelector("#graph-no-data").style.display = "none"; 379 | document.querySelector(".graph-bars").style.display = "block"; 380 | } else { 381 | document.querySelector("#no-items").style.display = "block"; 382 | document.querySelector("#graph-no-data").style.display = "block"; 383 | document.querySelector("#avg-price").textContent = "Average: n/a"; 384 | document.querySelector(".graph-bars").style.display = "none"; 385 | } 386 | document.querySelector(".items-count").textContent = totalAfterFilter + " Items"; 387 | 388 | display(); 389 | return; 390 | } 391 | 392 | function getDimensions() { 393 | const items = document.querySelector(".items").getBoundingClientRect(); 394 | const cols = Math.floor(items.width / 280); 395 | const height = items.width / cols; 396 | return { "height": height, "cols": cols } 397 | } 398 | 399 | const wh = window.innerHeight; 400 | const gridGap = 10; 401 | 402 | function renderItems(s = null) { 403 | const { height, cols } = getDimensions(); 404 | const scrollTop = document.body.scrollTop; 405 | const start = s ? s : (Math.floor(scrollTop / height) * cols); 406 | const begin = document.createDocumentFragment(); 407 | const numItems = Math.ceil(wh / height) * cols; 408 | const end = start + numItems + (2 * cols); 409 | 410 | for (let i = start; i < end; i++) { 411 | if (i >= currentItems.length) break; 412 | begin.appendChild(currentItems[i]); 413 | } 414 | document.querySelector(".items").innerHTML = ""; 415 | document.querySelector(".items").appendChild(begin); 416 | document.querySelector(".items").style.top = (Math.floor(start / cols) * height) + "px"; 417 | document.querySelector(".items-scroll").style.height = ((currentItems.length / cols) * height) + "px"; 418 | 419 | } 420 | 421 | function display() { 422 | renderItems(0); 423 | document.onscroll = renderItems; 424 | } 425 | 426 | /* 427 | function display222(startIndex = 0) { 428 | 429 | console.log("displaying...", currentItems); 430 | 431 | ih = getCurrentItemHeight(); 432 | 433 | document.querySelector(".items-scroll").style.height = ((currentItems.length / numCols) * ih) + "px"; 434 | 435 | let begin = document.createDocumentFragment(); 436 | 437 | const numItems = Math.ceil(wh / ih) * numCols; 438 | 439 | for (let i = startIndex; i < numItems + numCols; i++) { 440 | 441 | if (i >= currentItems.length) break; 442 | 443 | let container = document.createElement("div"); 444 | container.classList.add("item"); 445 | container.appendChild(currentItems[i]); 446 | 447 | begin.appendChild(container); 448 | } 449 | 450 | 451 | document.querySelector(".items").innerHTML = ""; 452 | document.querySelector(".items").appendChild(begin); 453 | 454 | let items = document.querySelectorAll(".item"); 455 | 456 | 457 | 458 | 459 | const virtualScroll = () => { 460 | const scrollTop = document.body.scrollTop; 461 | var maxScroll = ((currentItems.length / numCols) * ih) - wh; 462 | 463 | let x = Math.floor(scrollTop / ih); 464 | let y = (x * ih); 465 | 466 | 467 | const start = Math.min(Math.floor((scrollTop / ih)) * numCols); 468 | if (start === prev || scrollTop >= maxScroll) return; 469 | 470 | document.querySelector(".items").style.top = y + "px"; 471 | 472 | renderItems(start); 473 | 474 | for (var j = 0; j < numItems + numCols; j++) { 475 | if (j + start > currentItems.length - 1) return; 476 | const item = currentItems[j + start]; 477 | items[j].innerHTML = ""; 478 | items[j].appendChild(item); 479 | } 480 | 481 | prev = start; 482 | 483 | } 484 | document.onscroll = virtualScroll; 485 | //document.addEventListener("scroll", virtualScroll); 486 | } 487 | */ 488 | 489 | function pause() { 490 | sendMessage({ type: "pause" }); 491 | document.getElementById("pause").classList.add("active"); 492 | document.getElementById("start").classList.remove("active"); 493 | } 494 | 495 | function reset() { 496 | document.querySelector(".items").textContent = ""; 497 | } 498 | 499 | /* 500 | function collectMarketplace() { 501 | document.querySelector(".results").querySelectorAll(".cl-search-result").forEach(item => { 502 | 503 | }); 504 | } 505 | 506 | function collectCraigslist() { 507 | 508 | } 509 | */ 510 | 511 | /* 512 | function refresh() { 513 | let urls = [/*"https://washingtondc.craigslist.org/search/college-park-md/sss?lat=38.976&lon=-76.9482&search_distance=8.3#search=1~gallery~0~0","https://www.facebook.com/marketplace/category/recently-posted?deliveryMethod=local_pick_up&sortBy=creation_time_descend&exact=true"]; 514 | console.log(urls); 515 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 516 | chrome.tabs.update(tabs[0].id, { url: tabs[0].url === urls[0] ? urls[1] : urls[0] }).then(() => { 517 | console.log("tab"); 518 | if (tabs[0].url === urls[0]) { 519 | collectCraigslist(); 520 | } else if (tabs[0].url === urls[1]) { 521 | collectMarketplace(); 522 | } 523 | }); 524 | }); 525 | } 526 | */ 527 | 528 | let refreshInterval; 529 | 530 | const loadSettings = async (callback = () => { }) => { 531 | const tabs = await chrome.tabs.query({ url: ["https://www.facebook.com/marketplace/*"] }); 532 | const storage = await chrome.storage.local.get(["settings", "saved"]); 533 | 534 | document.querySelector("#long").value = storage.settings.long; 535 | document.querySelector("#lat").value = storage.settings.lat; 536 | document.querySelector("#delay").value = storage.settings.delay; 537 | document.querySelector("#max_items").value = storage.settings.max_items; 538 | 539 | 540 | if (!storage.saved || storage.saved.length === 0) { 541 | 542 | } else { 543 | let set = false; 544 | document.querySelector("#save-under").textContent = ""; 545 | storage.saved.forEach(item => { 546 | let selected = ""; 547 | if (set === false) { 548 | item.pathnames.forEach(path => { 549 | if (tabs[0].url === path) { 550 | document.querySelector("#save-under").value = item.name; 551 | selected = "selected"; 552 | set = true; 553 | filterItems(); 554 | } 555 | }) 556 | } 557 | document.querySelector("#save-under").innerHTML += ''; 558 | }); 559 | 560 | const searches = document.querySelector(".searches"); 561 | searches.textContent = ""; 562 | storage.saved.forEach(item => { 563 | let container = document.createElement("div"); 564 | container.className = "saved-search"; 565 | container.innerHTML = `

${item.name}

${chevronDown}
`; 566 | item.pathnames.forEach(path => { 567 | let link = document.createElement("p"); 568 | link.className = "search-link"; 569 | link.textContent = path; 570 | link.addEventListener("click", () => { 571 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 572 | chrome.tabs.update(tabs[0].id, { url: path }); 573 | }); 574 | }); 575 | container.querySelector(".search-queries").prepend(link); 576 | }); 577 | container.querySelector(".search-drop-btn").addEventListener("click", () => { 578 | container.querySelector(".search-queries").classList.toggle("open"); 579 | }) 580 | searches.appendChild(container); 581 | }); 582 | 583 | 584 | } 585 | callback(); 586 | } 587 | 588 | async function start() { 589 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); 590 | const save = document.querySelector("#save-under").value; 591 | const storage = await chrome.storage.local.get("saved"); 592 | 593 | // add the path under the saved search if not already 594 | for (let i = 0; i < storage["saved"].length; i++) { 595 | if (storage["saved"][i].name !== save) continue; 596 | if (!storage["saved"][i].pathnames.includes(tabs[0].url)) { 597 | storage["saved"][i].pathnames.push(tabs[0].url); 598 | await chrome.storage.local.set({ "saved": storage["saved"] }); 599 | } 600 | break; 601 | } 602 | 603 | sendMessage({ "type": "start", "name": save }); 604 | document.getElementById("start").classList.add("active"); 605 | document.getElementById("pause").classList.remove("active"); 606 | } 607 | 608 | document.addEventListener("DOMContentLoaded", function () { 609 | 610 | loadSettings(() => { updateResize(); filterItems() }); 611 | batchTimer = setInterval(checkBatch, 5000); 612 | 613 | chrome.devtools.network.onRequestFinished.addListener((request) => { 614 | if (request.request && request.request.url && request.request.url.includes('https://www.facebook.com/api/graphql/')) { 615 | requests.push(request); 616 | } 617 | }); 618 | 619 | selectItemAvailability(); 620 | 621 | // new search is performed 622 | chrome.tabs.onUpdated.addListener((id, info, tab) => { 623 | if (tab.url.includes("https://www.facebook.com/marketplace/") && info.url && (info.url.includes("/search/") || info.url.includes("/category/"))) { 624 | selectItemAvailability(); 625 | if (info.status === "loading") { 626 | batch = {}; 627 | reset(); 628 | pause(); 629 | //checkBatch(true); 630 | } else if (info.status === "complete") { 631 | pause(); 632 | } 633 | } 634 | }); 635 | 636 | chrome.runtime.onMessage.addListener(function (request, sender, x) { 637 | if (request.type === "numListings") { 638 | } else if (request.type === "existingItem") { 639 | request.data.seen = true; 640 | } 641 | }); 642 | 643 | // refilter after changing filters 644 | ["sort", "hideEmojis", "beforeYear", "beforeYearVal", "direction", "hideDistance", "hideDistanceVal", "hideTimeOver", "hidetimeOverVal", "hidePriceUnder", "hidePriceUnderVal", "hidePriceOver", "hidePriceOverVal", "showNegotiable", "explicitWords", "explicitWordsVal", "explicitWordsHide", "explicitWordsHideVal"].forEach(filter => { 645 | document.querySelector("#" + filter).addEventListener("change", () => { 646 | filterItems(); 647 | }); 648 | }); 649 | 650 | // send start message to content script 651 | document.querySelector("#start").addEventListener("click", start); 652 | 653 | // send pause message to content script 654 | document.querySelector("#pause").addEventListener("click", pause); 655 | 656 | // not working 657 | document.querySelector("#items-available").addEventListener("click", function (e) { 658 | chrome.storage.local.set({ "bfbm-params": { ...storage["bfbm-params"], "availability": "available" } }).then(() => { 659 | e.target.classList.add("active"); 660 | document.querySelector("#items-sold").classList.remove("active"); 661 | reset(); 662 | checkBatch(); 663 | }); 664 | }); 665 | 666 | // not working 667 | document.querySelector("#items-sold").addEventListener("click", function (e) { 668 | chrome.storage.local.set({ "bfbm-params": { ...storage["bfbm-params"], "availability": "sold" } }).then(() => { 669 | e.target.classList.add("active"); 670 | document.querySelector("#items-available").classList.remove("active"); 671 | reset(); 672 | checkBatch(); 673 | }); 674 | }); 675 | 676 | // not working 677 | document.querySelector("#items-hidden").addEventListener("click", () => { 678 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 679 | chrome.tabs.update(tabs[0].id, { url: tabs[0].url + (tabs[0].url.includes("?") ? "&" : "?") + "bfbmHidden=true" }).then(() => { 680 | 681 | }); 682 | }); 683 | }); 684 | 685 | // setting inputs 686 | document.querySelector("#lat").addEventListener("change", function (e) { 687 | chrome.storage.local.get("settings", storage => { 688 | chrome.storage.local.set({ "settings": { ...storage.settings, "lat": parseFloat(e.target.value) } }); 689 | }) 690 | }); 691 | 692 | document.querySelector("#long").addEventListener("change", function (e) { 693 | chrome.storage.local.get("settings", storage => { 694 | chrome.storage.local.set({ "settings": { ...storage.settings, "long": parseFloat(e.target.value) } }); 695 | }) 696 | }); 697 | 698 | document.querySelector("#delay").addEventListener("change", function (e) { 699 | chrome.storage.local.get("settings", storage => { 700 | chrome.storage.local.set({ "settings": { ...storage.settings, "delay": parseFloat(e.target.value) } }); 701 | }) 702 | }); 703 | 704 | document.querySelector("#max_items").addEventListener("change", function (e) { 705 | chrome.storage.local.get("settings", storage => { 706 | chrome.storage.local.set({ "settings": { ...storage.settings, "max_items": parseFloat(e.target.value) } }); 707 | }) 708 | }); 709 | 710 | document.querySelector("#create-save").addEventListener("click", createSave); 711 | document.querySelector("#clear-save").addEventListener("click", clearSave); 712 | document.querySelector("#save-under").addEventListener("change", filterItems); 713 | }); 714 | 715 | async function createSave() { 716 | const save = document.querySelector("#create-save-name").value; 717 | if (save === "") return; 718 | const storage = await chrome.storage.local.get("saved"); 719 | let existing = storage["saved"] ? storage["saved"] : []; 720 | if (existing.some(x => x.name === save)) return; 721 | existing.push({ "name": save, "pathnames": [] }); 722 | await chrome.storage.local.set({ "saved": existing }); 723 | loadSettings(() => { 724 | document.querySelector("#save-under").value = save; 725 | document.querySelector("#save-under").querySelector("option[value=" + save + "]").selected = true; 726 | filterItems(); 727 | }); 728 | } 729 | 730 | async function clearSave() { 731 | const save = document.querySelector("#clear-save-name").value; 732 | if (save === "") return; 733 | const storage = await chrome.storage.local.get("saved"); 734 | const existing = storage["saved"] ? storage["saved"] : []; 735 | 736 | const wiped = {}; 737 | for (let i = 0; i < existing.length; i++) { 738 | if (existing[i]["name"] !== save) continue; 739 | existing[i]["pathnames"].forEach(path => { 740 | wiped[path] = {}; 741 | }); 742 | existing["pathnames"] = []; 743 | } 744 | 745 | await chrome.storage.local.set({ ...wiped, "saved": existing }); 746 | loadSettings(() => { 747 | document.querySelector("#save-under").value = save; 748 | document.querySelector("#save-under").querySelector("option[value=" + save + "]").selected = true; 749 | filterItems(); 750 | }); 751 | } 752 | 753 | // helpers 754 | 755 | function getPathname(path) { 756 | let first, last = ""; 757 | if (path.includes("/search")) { 758 | first = path.split("/search")[1]; 759 | let x = first.split(path.includes("/?") ? "/?" : "?"); 760 | return x.length > 1 ? x[1].split("query=")[1].split("&")[0] : "unknown"; 761 | } else if (path.includes("/category/")) { 762 | first = path.split("/category/")[1].split("/")[0]; 763 | return first; 764 | } else { 765 | return "unknown"; 766 | } 767 | } 768 | 769 | function isItemNegotiable(description) { 770 | description = description.toLowerCase(); 771 | if (description.includes("best offer") || description.includes("negotiable") || description.includes(" obo") || description.includes("willing to negotiate")) { 772 | return true; 773 | } 774 | return false; 775 | } 776 | 777 | function sendMessage(message) { 778 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 779 | chrome.tabs.sendMessage(tabs[0].id, message, function (response) { 780 | //console.log(response); 781 | }); 782 | }); 783 | } 784 | 785 | function sortBy(sort, direction) { 786 | console.log(sort, direction); 787 | switch (sort) { 788 | case "time": 789 | return (a, b) => { 790 | let x = parseInt(a.creation_time), y = parseInt(b.creation_time); 791 | //return x > y ? -1 : x < y ? 1 : 0; 792 | return direction === "dec" ? (x < y ? -1 : 1) : (x > y ? -1 : 1); 793 | } 794 | case "price": 795 | return (a, b) => { 796 | let x = parseInt(a.listing_price.amount), y = parseInt(b.listing_price.amount); 797 | return direction === "dec" ? (x > y ? -1 : 1) : (x < y ? -1 : 1); 798 | } 799 | case "distance": 800 | const lat = document.querySelector("#lat").value; 801 | const long = document.querySelector("#long").value; 802 | return (a, b) => { 803 | let xlat = parseFloat(a?.location?.latitude || 0), xlong = parseFloat(a?.location?.longitude || 0), ylat = parseFloat(b?.location?.latitude || 0), ylong = parseFloat(b?.location?.longitude || 0); 804 | let pythx = Math.pow(lat - xlat, 2) + Math.pow(long - xlong, 2); 805 | let pythy = Math.pow(lat - ylat, 2) + Math.pow(long - ylong, 2); 806 | //console.log(pythx + " < " + pythy); 807 | return pythx < pythy ? -1 : 1; 808 | } 809 | } 810 | } 811 | 812 | function convertTime(time) { 813 | let unit = "seconds"; 814 | if (time > 60) { 815 | unit = "minutes"; 816 | time /= 60; 817 | if (time > 60) { 818 | unit = "hours"; 819 | time /= 60; 820 | if (time > 24) { 821 | unit = "days"; 822 | time /= 24; 823 | } 824 | } 825 | } 826 | return parseInt(time) + " " + unit; 827 | } --------------------------------------------------------------------------------