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 = !values[_.setting].enabled;
93 values[_.setting].enabled = state;
94 updateState(container, state);
95 },
96 );
97 }
98 });
99
100 saveBtn.addEventListener("click", async () => {
101 saveBtn.setAttribute("disabled", "true");
102 preferences.setValue(values);
103 });
104
105 searchInput.addEventListener("input", () => {
106 querySettings(searchInput.value, activeTag);
107 });
108});
109
110async function createContainer(data: SettingData): Promise<HTMLDivElement> {
111 const noteClasses = {
112 "?": "note",
113 "!": "warning",
114 "-": "secondary",
115 };
116
117 const state = await getState(data.setting);
118 const container = document.createElement("div");
119
120 container.id = data.setting;
121 container.classList.add("setting-container");
122 container.classList.add(state ? "enabled" : "disabled");
123
124 container.innerHTML = `
125 <span class="indicator"> </span>
126 <span class="title">
127 ${data.name}
128 </span>
129 <div class="setting-buttons">
130 <button class="btn btn-sm toggle-btn ${
131 state ? "btn-danger" : "btn-success"
132 }">Toggle</button>
133 </div>
134 <br />
135 <span class="desc">${data.desc}</span>
136 ${
137 (data.notes || []).map((note) => {
138 const noteType = note.charAt(0) as keyof typeof noteClasses;
139 return `<span class="${noteClasses[noteType]}">* ${note.slice(1)}</span>`;
140 }).join("")
141 }
142 `;
143
144 document.getElementById("settings")!.appendChild(container);
145 if (data.config) {
146 try {
147 createConfig(container, data);
148 } catch (e) {}
149 }
150 return container;
151}
152
153function createConfig(div: HTMLDivElement, data: SettingData) {
154 for (const subsetting of data.config!) {
155 if (subsetting.type === "select") {
156 const select = document.createElement("select");
157 select.classList.add("form-select", "form-select-sm", "mb-2");
158 select.setAttribute("style", "width: 350px;");
159
160 for (const [key, value] of Object.entries(subsetting.values!)) {
161 const option = document.createElement("option");
162 option.value = key;
163 option.innerText = value;
164 select.appendChild(option);
165 }
166
167 select.addEventListener("change", function () {
168 document.getElementById("save")!.removeAttribute("disabled");
169 values[data.setting][subsetting.subsetting] =
170 select.options[select.selectedIndex].value;
171 });
172
173 div.appendChild(select);
174 } else if (subsetting.type === "check") {
175 const check = document.createElement("span");
176
177 check.classList.add("form-check", "form-switch");
178 check.innerHTML = `
179 <input class="form-check-input" type="checkbox" role="switch" id="${subsetting.subsetting}" />
180 <label class="form-check-label" for="${subsetting.subsetting}">
181 ${subsetting.label}
182 </label>
183 `;
184
185 const checkbox = check.getElementsByClassName(
186 "form-check-input",
187 )[0] as HTMLInputElement;
188 checkbox.checked = values[data.setting][subsetting.subsetting];
189
190 check.getElementsByClassName("form-check-input")[0].addEventListener(
191 "change",
192 () => {
193 document.getElementById("save")!.removeAttribute("disabled");
194 values[data.setting][subsetting.subsetting] = checkbox.checked;
195 },
196 );
197
198 div.appendChild(check);
199 }
200 }
201}
202
203function updateState(div: HTMLDivElement, state: boolean) {
204 const toggleBtn = div.getElementsByClassName("toggle-btn")[0];
205
206 div.classList.toggle("enabled", state);
207 div.classList.toggle("disabled", !state);
208
209 toggleBtn.classList.toggle("btn-success", !state);
210 toggleBtn.classList.toggle("btn-danger", state);
211}
212
213async function getState(name: string): Promise<boolean | null> {
214 const state = values[name] ?? (defaultPreferences as preferencesSchema)[name];
215 if (!state) return null;
216 return state.enabled;
217}
218
219function createTag(name: Tags) {
220 const tag = document.createElement("div");
221
222 tag.classList.add("col-auto");
223 tag.style.minWidth = "150px";
224
225 tag.innerHTML = `
226 <button class="btn btn-secondary btn-sm text-nowrap" style="width: 100%;">
227 ${name.substring(0, 1).toUpperCase() + name.substring(1)}
228 </button>
229 `;
230
231 tag.children[0].addEventListener("click", function () {
232 activeTag = name;
233 querySettings(
234 (document.getElementById("search")! as HTMLInputElement).value,
235 name,
236 );
237 });
238
239 document.getElementById("discovery")!.appendChild(tag);
240 return tag;
241}
242
243function querySettings(query: string, tag: Tags) {
244 console.time("querySettings");
245 const meetsConditions = function (data: SettingData) {
246 let result = false;
247 if (data.name.toLowerCase().includes(query)) result = true;
248 if (query.length > 4 && data.desc.toLowerCase().includes(query)) {
249 result = true;
250 }
251
252 if (!data.tags.includes(tag) && tag != "all") result = false;
253 return result;
254 };
255
256 let results = 0;
257 for (
258 const container of Array.from(document.getElementById("settings")!.children)
259 ) {
260 const setting = data.find((setting) => setting.setting == container.id);
261 const match = meetsConditions(setting as SettingData);
262
263 if (match) {
264 results++;
265 (container as HTMLDivElement).style.display = "block";
266 } else {
267 (container as HTMLDivElement).style.display = "none";
268 }
269 }
270
271 console.log("Found " + results + " result(s)");
272 console.timeEnd("querySettings");
273}