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">&nbsp;</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}