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