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