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
1import { preferences, defaultPreferences, preferencesSchema, cache } from "@/utils/storage";
2import data from "@/public/preferences.json";
3
4declare global {
5 interface Window {
6 polyplus: Record<string, any>;
7 }
8}
9
10let values: preferencesSchema;
11let activeTag: Tags = "all";
12type Tags = "all" | "utility" | "social" | "economy" | "development" | "customization";
13
14type SettingData = {
15 name: string,
16 desc: string,
17 setting: string,
18 notes?: Array<string>,
19 config?: Array<{
20 type: "select" | "check",
21 subsetting: string,
22 label?: string,
23 values?: Record<string, any>
24 }>,
25 tags: Array<Tags>
26};
27
28export default defineUnlistedScript(() => {
29 for (const src of [
30 "/css/polytoria.css",
31 "/css/preferences.css"
32 ]) {
33 const css = browser.runtime.getURL(src as any);
34 const link = document.createElement('link');
35 link.rel = 'stylesheet';
36 link.href = css;
37 document.head.appendChild(link);
38 };
39
40 console.log('Static Settings Data:' , data);
41 const saveBtn = document.getElementById('save')!;
42 const searchInput = document.getElementById('search')! as HTMLInputElement;
43
44 const allTags = [
45 ...new Set(
46 //@ts-ignore: I do not want to deal with stupid type errors for working code right now
47 data.reduce((acc: Tags[], setting: SettingData) => {
48 if (setting.tags && Array.isArray(setting.tags)) {
49 return acc.concat(setting.tags);
50 }
51 return acc;
52 }, [] as Tags[])
53 ) as Set<Tags>,
54 ]
55
56 createTag("all");
57 for (const tag of allTags) {
58 createTag(tag);
59 }
60
61 preferences.getPreferences()
62 .then(async (preferenceValues) => {
63 values = preferenceValues;
64 window.polyplus = {
65 preferences: preferenceValues,
66 static: data
67 };
68 console.log('Loaded preferences: ', preferenceValues);
69
70 for (const _ of data as SettingData[]) {
71 const container = await createContainer(_);
72
73 container.getElementsByClassName('toggle-btn')[0].addEventListener('click', async () => {
74 saveBtn.removeAttribute('disabled');
75
76 const state = !values[_.setting].enabled;
77 values[_.setting].enabled = state;
78 updateState(container, state);
79 });
80 };
81 });
82
83 saveBtn.addEventListener('click', async () => {
84 saveBtn.setAttribute('disabled', 'true');
85 preferences.setValue(values);
86 });
87
88 searchInput.addEventListener('input', () => {
89 querySettings(searchInput.value, activeTag);
90 })
91});
92
93async function createContainer(data: SettingData): Promise<HTMLDivElement> {
94 const noteClasses = {
95 "?": "note",
96 "!": "warning",
97 "-": "secondary"
98 };
99
100 const state = await getState(data.setting);
101 const container = document.createElement('div');
102
103 container.id = data.setting;
104 container.classList.add('setting-container');
105 container.classList.add(state ? 'enabled' : 'disabled');
106
107 container.innerHTML = `
108 <span class="indicator"> </span>
109 <span class="title">
110 ${data.name}
111 </span>
112 <div class="setting-buttons">
113 <button class="btn btn-sm toggle-btn ${ state ? 'btn-danger' : 'btn-success' }">Toggle</button>
114 </div>
115 <br />
116 <span class="desc">${data.desc}</span>
117 ${(data.notes || []).map((note) => {
118 const noteType = note.charAt(0) as keyof typeof noteClasses;
119 return `<span class="${ noteClasses[noteType] }">* ${ note.slice(1) }</span>`;
120 }).join('')}
121 `;
122
123 document.getElementById('settings')!.appendChild(container);
124 if (data.config) try { createConfig(container, data) } catch(e) {};
125 return container;
126}
127
128function createConfig(div: HTMLDivElement, data: SettingData) {
129 for (const subsetting of data.config!) {
130 if (subsetting.type === 'select') {
131 const select = document.createElement('select');
132 select.classList.add('form-select', 'form-select-sm', 'mb-2');
133 select.setAttribute('style', 'width: 350px;');
134
135 for (const [key, value] of Object.entries(subsetting.values!)) {
136 const option = document.createElement('option');
137 option.value = key;
138 option.innerText = value;
139 select.appendChild(option);
140 };
141
142 select.addEventListener('change', function() {
143 document.getElementById('save')!.removeAttribute('disabled');
144 values[data.setting][subsetting.subsetting] = select.options[select.selectedIndex].value;
145 });
146
147 div.appendChild(select);
148 } else if (subsetting.type === 'check') {
149 const check = document.createElement('span');
150
151 check.classList.add('form-check', 'form-switch');
152 check.innerHTML = `
153 <input class="form-check-input" type="checkbox" role="switch" id="${subsetting.subsetting}" />
154 <label class="form-check-label" for="${subsetting.subsetting}">
155 ${subsetting.label}
156 </label>
157 `;
158
159 const checkbox = check.getElementsByClassName('form-check-input')[0] as HTMLInputElement;
160 checkbox.checked = values[data.setting][subsetting.subsetting];
161
162 check.getElementsByClassName('form-check-input')[0].addEventListener('change', () => {
163 document.getElementById('save')!.removeAttribute('disabled');
164 values[data.setting][subsetting.subsetting] = checkbox.checked;
165 });
166
167 div.appendChild(check);
168 }
169 }
170}
171
172function updateState(div: HTMLDivElement, state: boolean) {
173 const toggleBtn = div.getElementsByClassName('toggle-btn')[0];
174
175 div.classList.toggle('enabled', state);
176 div.classList.toggle('disabled', !state);
177
178 toggleBtn.classList.toggle('btn-success', !state);
179 toggleBtn.classList.toggle('btn-danger', state);
180}
181
182async function getState(name: string): Promise<boolean | null> {
183 const state = values[name] ?? (defaultPreferences as preferencesSchema)[name];
184 if (!state) return null;
185 return state.enabled;
186}
187
188function createTag(name: Tags) {
189 const tag = document.createElement('div');
190
191 tag.classList.add('col-auto');
192 tag.style.minWidth = '150px';
193
194 tag.innerHTML = `
195 <button class="btn btn-secondary btn-sm text-nowrap" style="width: 100%;">
196 ${ name.substring(0, 1).toUpperCase() + name.substring(1) }
197 </button>
198 `;
199
200 tag.children[0].addEventListener('click', function(){
201 activeTag = name;
202 querySettings((document.getElementById('search')! as HTMLInputElement).value, name);
203 });
204
205 document.getElementById('discovery')!.appendChild(tag);
206 return tag;
207}
208
209function querySettings(query: string, tag: Tags) {
210 console.time("querySettings");
211 const meetsConditions = function(data: SettingData) {
212 let result = false;
213 if (data.name.toLowerCase().includes(query)) result = true;
214 if (query.length > 4 && data.desc.toLowerCase().includes(query)) result = true;
215
216 if (!data.tags.includes(tag) && tag != "all") result = false;
217 return result;
218 }
219
220 let results = 0;
221 for (const container of Array.from(document.getElementById('settings')!.children)) {
222 const setting = data.find((setting) => setting.setting == container.id);
223 const match = meetsConditions(setting as SettingData);
224
225 if (match) {
226 results++;
227 (container as HTMLDivElement).style.display = 'block';
228 } else {
229 (container as HTMLDivElement).style.display = 'none';
230 }
231 }
232
233 console.log('Found ' + results + ' result(s)');
234 console.timeEnd("querySettings");
235}