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