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