A rewrite of Poly+, my quality-of-life browser extension for Polytoria. Built entirely fresh using the WXT extension framework, Typescript, and with added better overall code quality.
extension
1const SaveBtn = document.getElementById('Save');
2const Elements = Array.from(document.getElementsByClassName('setting-container'));
3
4var RecentSave;
5var Settings;
6
7var Utilities;
8(async () => {
9 Utilities = await import(chrome.runtime.getURL('resources/utils.js'));
10 Utilities = Utilities.default;
11 LoadCurrent();
12
13 document.getElementById('PinnedGames-limit').innerText = Utilities.Limits.PinnedGames;
14 //document.getElementById('ImprovedFrLists-limit').innerText = Utilities.Limits.ImprovedFrLists;
15 //document.getElementById('ItemWishlist-limit').innerText = Utilities.Limits.ItemWishlist;
16 document.getElementById('HoardersList-pageLimit').innerText = Utilities.Limits.HoardersListPages;
17})();
18
19// Handle buttons at the bottom of the page
20document.getElementById('ResetDefaults').addEventListener('click', function () {
21 document.getElementById('ResetDefaults-Modal').showModal();
22});
23SaveBtn.addEventListener('click', Save);
24
25// Handle modal buttons for Reset Defaults modal
26document.getElementById('ResetDefaults-Modal-Yes').addEventListener('click', function () {
27 Settings = Utilities.DefaultSettings;
28 Save();
29 setTimeout(function () {
30 LoadCurrent();
31 document.getElementById('ResetDefaults-Modal').close();
32 }, 400);
33});
34document.getElementById('ResetDefaults-Modal-No').addEventListener('click', function () {
35 document.getElementById('ResetDefaults-Modal').close();
36});
37
38// Loop thru each setting container and handle toggling, selecting, opening modal, etc
39Elements.forEach((element) => {
40 let Button = element.getElementsByClassName('toggle-btn')[0];
41 let Options = element.getElementsByClassName('options-btn')[0];
42 let Select = element.getElementsByTagName('select')[0];
43 let Checkbox = element.getElementsByTagName('input');
44
45 if (Button) {
46 Button.addEventListener('click', function () {
47 SetSetting(Button, 'bool');
48 });
49 }
50
51 if (Select) {
52 Select.addEventListener('change', function () {
53 if (Select.getAttribute('data-useValue') !== null) {
54 let Value = Select.options[Select.selectedIndex].value;
55 if (!isNaN(Value)) {
56 Value = parseInt(Value);
57 }
58 SetSetting(Select, Value, false);
59 } else {
60 SetSetting(Select, Select.selectedIndex, false);
61 }
62 });
63 }
64
65 if (Checkbox) {
66 Array.from(Checkbox).forEach(check => {
67 check.addEventListener('change', function () {
68 SetSetting(check, check.checked, false);
69 });
70 })
71 }
72
73 if (Options) {
74 const Modal = document.getElementById(Options.getAttribute('data-modal') + '-Modal');
75 const ModalButtons = Modal.getElementsByTagName('button');
76 const ModalInputs = Modal.getElementsByTagName('input');
77 const ModalSelect = Modal.getElementsByTagName('select');
78
79 Options.addEventListener('click', function () {
80 Array.from(ModalButtons)
81 .filter((x) => !x.classList.contains('ignore'))
82 .forEach((button) => {
83 button.addEventListener('click', function () {
84 const Setting = button.getAttribute('data-setting');
85
86 if (Setting === '[save]') {
87 // Save Modal Button
88
89 // Save Modal Inputs
90 Array.from(ModalInputs)
91 .filter((x) => !x.classList.contains('ignore'))
92 .forEach((input) => {
93 SetSetting(input, input.value, false, Modal.getAttribute('data-setting'));
94 });
95
96 // Save Modal Select Menus
97 Array.from(ModalSelect)
98 .filter((x) => !x.classList.contains('ignore'))
99 .forEach((select) => {
100 SetSetting(select, select.selectedIndex, false, Modal.getAttribute('data-setting'));
101 });
102
103 Save();
104 setTimeout(function () {
105 LoadCurrent();
106 Modal.close();
107 }, 400);
108 } else if (Setting === '[reset-default]') {
109 // Reset to Defaults Modal Button
110
111 if (confirm("Are you sure you'd like to reset these options to their defaults?") === true) {
112 Settings[Modal.getAttribute('data-setting')] = Utilities.DefaultSettings[Modal.getAttribute('data-setting')];
113 Save();
114 Modal.close();
115 }
116 } else if (Setting === '[cancel]') {
117 // Cancel Changes Button
118
119 Modal.close();
120 } else {
121 // Default Toggle Button
122
123 SetSetting(button, 'bool', false, Modal.getAttribute('data-setting'));
124 }
125 });
126 });
127
128 Array.from(ModalInputs)
129 .filter((x) => !x.classList.contains('ignore'))
130 .forEach((input) => {
131 const Status = GetSettingValue(input, Modal.getAttribute('data-setting'));
132 if (Status !== 'undefined' && Status !== undefined) {
133 input.value = Status;
134 } else {
135 input.value = '';
136 }
137 });
138
139 Array.from(ModalSelect)
140 .filter((x) => !x.classList.contains('ignore'))
141 .forEach((select) => {
142 const Status = GetSettingValue(select, Modal.getAttribute('data-setting'));
143 if (Status !== 'undefined' && Status !== undefined) {
144 select.selectedIndex = Status;
145 }
146 });
147
148 Modal.showModal();
149 });
150 }
151});
152
153function LoadCurrent() {
154 chrome.storage.sync.get(['PolyPlus_Settings'], function (result) {
155 Settings = Utilities.MergeObjects(result.PolyPlus_Settings || Utilities.DefaultSettings, Utilities.DefaultSettings);
156 RecentSave = structuredClone(Settings)
157
158 console.log('Current Settings: ', Settings);
159
160 Elements.forEach((element) => {
161 UpdateElementState(element);
162 });
163 });
164}
165
166function SetSetting(element, value, update, modalParent) {
167 document.title = '*unsaved | Poly+ Settings'
168 const name = element.getAttribute('data-setting');
169 let parent = element.getAttribute('data-parent');
170
171 if (modalParent !== undefined) {
172 console.log(modalParent);
173 parent = modalParent;
174 }
175
176 if (value === 'bool') {
177 value = !GetSettingValue(element, modalParent);
178 }
179 if (parent !== null) {
180 let Parent = Object.values(Settings)[Object.keys(Settings).indexOf(parent)];
181 if (!isNaN(element.getAttribute('data-parent')) && element.getAttribute('data-parent') !== null) {
182 Parent = Parent[parseInt(element.getAttribute('data-parent'))];
183 }
184 Parent[name] = value;
185 } else {
186 Settings[name] = value;
187 }
188 if (update !== false) {
189 UpdateElementState(document.querySelector(`.setting-container:has([data-setting="${name}"])${parent !== null ? `:has([data-parent="${parent}"])` : ''}`), value);
190 }
191 if (SaveBtn.getAttribute('disabled')) {
192 SaveBtn.removeAttribute('disabled');
193
194 // Handle leaving the settings page before saving
195 window.onbeforeunload = function (e) {
196 return "Are you sure you'd like to leave? Your Poly+ settings haven't been saved."
197 };
198 }
199 /*
200 if (AreIdentical(Settings, RecentSave) === true) {
201 document.title = 'Poly+ Settings'
202 SaveBtn.disabled = true
203 }
204 */
205}
206
207function GetSettingValue(element, modalParent) {
208 const name = element.getAttribute('data-setting');
209 let parent = element.getAttribute('data-parent');
210
211 if (modalParent !== undefined) {
212 parent = modalParent;
213 }
214
215 let Status = name;
216 if (parent !== null) {
217 let Parent = Object.values(Settings)[Object.keys(Settings).indexOf(parent)];
218 if (!isNaN(element.getAttribute('data-parent')) && element.getAttribute('data-parent') !== null) {
219 Parent = Parent[parseInt(element.getAttribute('data-parent'))];
220 Status = Parent[name];
221 } else {
222 Status = Object.values(Parent)[Object.keys(Parent).indexOf(name)];
223 }
224 } else {
225 Status = Settings[Status];
226 }
227
228 if (element.tagName === 'SELECT' && element.getAttribute('data-useValue') === 'true') {
229 Status = Array.from(element.children).indexOf(element.querySelector('option[value="' + Status + '"]'))
230 }
231
232 return Status;
233}
234
235function UpdateElementState(element, status) {
236 const Button = element.getElementsByClassName('toggle-btn')[0];
237
238 if (status === undefined) {
239 status = GetSettingValue(Button);
240 }
241
242 if (status === true) {
243 element.classList.add('enabled');
244 element.classList.remove('disabled');
245 Button.innerText = 'Disable';
246 Button.classList.add('btn-danger');
247 Button.classList.remove('btn-success');
248 } else {
249 element.classList.add('disabled');
250 element.classList.remove('enabled');
251 Button.innerText = 'Enable';
252 Button.classList.add('btn-success');
253 Button.classList.remove('btn-danger');
254 }
255
256 let SelectInput = element.getElementsByTagName('select')[0];
257 if (SelectInput) {
258 SelectInput.selectedIndex = GetSettingValue(SelectInput);
259 }
260
261 let Checkbox = Array.from(element.getElementsByTagName('input'));
262 if (Checkbox.length > 0) {
263 Checkbox.forEach((check) => {
264 check.checked = GetSettingValue(check);
265 });
266 }
267}
268
269function Save() {
270 document.title = 'Poly+ Settings';
271 SaveBtn.setAttribute('disabled', 'true');
272 chrome.storage.sync.set({PolyPlus_Settings: Settings}, function () {
273 console.log('Saved successfully!');
274 RecentSave = Settings
275 });
276
277 // Handle leaving the settings page after saving
278 window.onbeforeunload = null
279
280 console.log('Save:', Settings);
281}
282
283let LoadThemeFromJSONBtn = document.getElementById('LoadThemeFromJSONBtn');
284let SaveThemeToJSONInput = document.getElementById('SaveThemeToJSONInput');
285let CopyThemeJSONBtn = document.getElementById('CopyThemeJSONBtn');
286LoadThemeFromJSONBtn.addEventListener('click', function () {
287 LoadThemeJSON(LoadThemeFromJSONBtn.previousElementSibling.value);
288});
289document
290 .getElementById('theme-creator')
291 .getElementsByTagName('button')[1]
292 .addEventListener('click', function () {
293 SaveThemeToJSONInput.value = JSON.stringify(Settings.ThemeCreator);
294 });
295CopyThemeJSONBtn.addEventListener('click', function () {
296 if (SaveThemeToJSONInput.value.length > 0) {
297 navigator.clipboard
298 .writeText(SaveThemeToJSONInput.value)
299 .then(() => {
300 alert('Successfully copied theme data to clipboard!');
301 })
302 .catch(() => {
303 alert('Failure to copy theme data to clipboard.');
304 });
305 }
306});
307
308function LoadThemeJSON(string) {
309 try {
310 let JSONTable = JSON.parse(string);
311 if (JSONTable.length === Utilities.DefaultSettings.ThemeCreator.length) {
312 if (confirm("Are you sure you'd like to replace this theme with the theme specified in the JSON?") === true) {
313 LoadThemeFromJSONBtn.previousElementSibling.value = '';
314 document.getElementById('ThemeCreator-Modal').close();
315 for (let i = 0; i < JSONTable.length; i++) {
316 if (JSONTable[i][0] !== '#') {
317 JSONTable[i] = '';
318 }
319 }
320 Settings.ThemeCreator = Utilities.MergeObjects(JSONTable, Utilities.DefaultSettings.ThemeCreator);
321 Save();
322 console.log(JSONTable.length, JSONTable, 'applied');
323 document.getElementById('ThemeCreator').getElementsByTagName('button')[1].click();
324 }
325 } else {
326 alert('JSON is not a theme!');
327 }
328 } catch (error) {
329 alert('JSON is invalid!');
330 }
331}
332
333chrome.storage.sync.get(['PolyPlus_AutoAds'], function(result){
334 let AutoAds = result.PolyPlus_AutoAds || [];
335
336 const Modal = document.getElementById("AutoAdBidding-Modal")
337 const AddButton = document.getElementById('auto-ad-bidding-add')
338
339 AddButton.addEventListener('click', async function() {
340 const Page = new DOMParser().parseFromString((await (await fetch('https://polytoria.com/create/ad/' + AddButton.previousElementSibling.value)).text()), 'text/html')
341
342
343 })
344
345 const AddRow = function(index, info) {
346 const Row = document.createElement('tr')
347 Row.innerHTML = `
348 <th scope="row">${index+1}</th>
349 <td><a href="https://polytoria.com/create/ad/${info.id}">"${info.name}"</a></td>
350 <td class="text-success"><span class="pi">$</span> 150</td>
351 <td>
352 <select class="form-select ignore">
353 <option value="daily" selected>Daily</option>
354 <option value="weekly">Weekly</option>
355 <option value="monthly">Monthly</option>
356 </select>
357 </td>
358 <td>
359 <div role="group" class="btn-group w-100">
360 <button class="btn btn-success w-25">
361 BID
362 </button>
363 <button class="btn btn-orange w-25">
364 EDIT
365 </button>
366 <button class="btn btn-danger w-25">
367 DELETE
368 </button>
369 </div>
370 </td>
371 `
372 }
373})
374
375function AreIdentical(obj1, obj2) {
376 if (obj1.length !== obj2.length) { return false }
377 return JSON.stringify(obj1) === JSON.stringify(obj2)
378}
379
380function FormatBool(bool) {
381 if (bool === true) {
382 return 'enabled';
383 } else {
384 return 'disabled';
385 }
386}
387
388const Manifest = chrome.runtime.getManifest();
389let BuildType = 'Stable';
390if (Manifest.version_name !== undefined) {
391 BuildType = 'Pre-Release';
392}
393
394const FooterText = document.getElementById('footer-text');
395FooterText.children[0].innerHTML = `Version: v${Manifest.version} | Build Type: ${BuildType}`;
396
397const CheckForUpdatesButton = document.getElementById('check-for-updates');
398function CheckForUpdates() {
399 CheckForUpdatesButton.removeEventListener('click', CheckForUpdates);
400 CheckForUpdatesButton.disabled = true;
401 fetch('https://polyplus.vercel.app/data/version.json')
402 .then((response) => {
403 if (!response.ok) {
404 throw new Error('Network not ok');
405 }
406 return response.json();
407 })
408 .then((data) => {
409 if (data.version === Manifest.version || Math.floor((data.version - Manifest.version) * 10) === 0) {
410 CheckForUpdatesButton.innerHTML = '<b>No updates available</b>';
411 alert('No updates available');
412 } else {
413 const NumberOfUpdatesAvailable = Math.floor((data.version - Version) * 10);
414 CheckForUpdatesButton.innerHTML = '<b>' + NumberOfUpdatesAvailable + ' update(s) available</b>';
415 alert(NumberOfUpdatesAvailable + ' updates available');
416 }
417 })
418 .catch((error) => {
419 console.log(error);
420 });
421}
422CheckForUpdatesButton.addEventListener('click', CheckForUpdates);
423
424/*
425fetch(chrome.runtime.getURL('resources/currencies.json'))
426 .then((response) => {
427 if (!response.ok) {
428 throw new Error('Network not ok');
429 }
430 return response.json();
431 })
432 .then((data) => {
433 const DateText = new Date(data.Date).toLocaleDateString('en-US', {day: 'numeric', month: 'long', year: 'numeric'});
434 document.getElementById('IRLPriceWithCurrency-Date').innerText = DateText;
435 })
436 .catch((error) => {
437 console.log(error);
438 });
439*/
440
441chrome.storage.local.get(['PolyPlus_OutOfDate', 'PolyPlus_LiveVersion', 'PolyPlus_ReleaseNotes', 'PolyPlus_SkipUpdate'], function (result) {
442 const OutOfDate = result.PolyPlus_OutOfDate || false;
443 const SkipUpdate = result.PolyPlus_SkipUpdate || null;
444 const LiveVersion = result.PolyPlus_LiveVersion || Manifest.version;
445 if (OutOfDate === true && SkipUpdate !== LiveVersion) {
446 const Banner = document.createElement('div');
447 Banner.classList = 'alert position-sticky p-3';
448 Banner.style = 'top: 30px; box-shadow: 0 0 20px 2px #000; z-index: 2000; background: rgb(163 39 39);';
449 Banner.innerHTML = `
450 <b>New Update Available!</b>
451 <br>
452 Your Poly+ installation is out of date! If you would like to get the latest and greatest features, improvements, and bug fixes click on one of the links below to dismiss this banner!
453 <br>
454 <div role="group" class="btn-group w-100 mt-2">
455 <a href="${result.PolyPlus_ReleaseNotes}" class="btn btn-primary btn-sm w-25" target="_blank">Go to Release Notes</a>
456 <button id="skip-this-update" class="btn btn-warning btn-sm w-25">(Not Recommended) Skip this Update</button>
457 </div>
458 `;
459 document.getElementById('page').insertBefore(Banner, document.getElementById('page').children[1]);
460
461 const SkipButton = document.getElementById('skip-this-update');
462 SkipButton.addEventListener('click', function () {
463 Banner.remove();
464 chrome.storage.local.set({PolyPlus_SkipUpdate: result.PolyPlus_LiveVersion}, function () {
465 console.log('set skip update to live version: ', result.PolyPlus_LiveVersion);
466 });
467 });
468 }
469});