├── 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 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
181 |
182 |
192 |
No data for this yet.
193 |
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 |
125 |
${item.marketplace_listing_title}
126 |
${item.location_text.text} (${item.distance}mi)
127 |
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 += '' + item.name + ' ';
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 | }
--------------------------------------------------------------------------------