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
at main 7.9 kB view raw
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">&nbsp;</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}