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, _favoritedPlaces, _bestFriends } from "@/utils/storage"; 2import { placeApiSchema, userApiSchema } from "@/utils/types"; 3import { pullCache } from "@/utils/utilities"; 4import * as api from "@/utils/api"; 5 6export default defineContentScript({ 7 matches: ['https://polytoria.com/', 'https://polytoria.com/home'], 8 main() { 9 preferences.getPreferences() 10 .then((values) => { 11 if (values.favoritedPlaces.enabled) favoritedPlaces(); 12 if (values.bestFriends.enabled) bestFriends(); 13 if (values.irlBrickPrice.enabled) irlBrickPrice(); 14 }); 15 } 16}); 17 18function favoritedPlaces() { 19 _favoritedPlaces.getValue() 20 .then(async (places) => { 21 const container = document.createElement('div'); 22 container.innerHTML = ` 23 <div class="row reqFadeAnim px-2 px-lg-0"> 24 <div class="col"> 25 <h6 class="dash-ctitle2">Jump right back into your favorite places</h6> 26 <h5 class="dash-ctitle">Favorited Places</h5> 27 </div> 28 </div> 29 <div class="card card-dash mcard mb-3"> 30 <div class="card-body p-0 m-1 scrollFadeContainer"> 31 <div class="text-center p-5"> 32 <div class="spinner-border text-muted" role="status"> 33 <span class="visually-hidden">Loading...</span> 34 </div> 35 </div> 36 </div> 37 </div> 38 `; 39 40 const column = document.getElementsByClassName('col-lg-8')[0]; 41 if (document.getElementsByClassName('home-event-container')[0] === undefined) { 42 column.insertBefore(container, column.children[0]); 43 } else { 44 column.insertBefore(container, column.children[1]); 45 } 46 47 const placeData: Array<placeApiSchema> = await pullCache( 48 'favoritedPlaces', 49 async () => await api.batch('public', 'places/', places), 50 300000, 51 false 52 ); 53 54 const card = container.getElementsByClassName('scrollFadeContainer')[0] 55 for (let i = 0; i < places.length; i++) { 56 const id = places.toSorted((a, b) => parseInt(b) - parseInt(a))[i] 57 const details = placeData[parseInt(id)] 58 if (!details) { 59 console.warn("[Poly+] Missing cached place data for ID " + id); 60 continue; 61 } 62 63 const scrollCard = document.createElement('a') 64 scrollCard.classList.value = 'd-none' 65 scrollCard.href = '/places/' + id 66 scrollCard.innerHTML = ` 67 <div class="scrollFade card me-2 place-card force-desktop text-center mb-2" style="opacity: 1;"> 68 <div class="card-body"> 69 <div class="ratings-header"> 70 <img src="${details.thumbnail}" class="place-card-image" style="position: relative;"> 71 <div class="p+pinned_games_playing" style="position: absolute;background: linear-gradient(to bottom, #000000f7, transparent, transparent, transparent);width: 100%;height: 100%;top: 0;left: 0;border-radius: 15px;padding-top: 12px;color: gray;font-size: 0.8rem;"> 72 <i class="fa-duotone fa-users"></i> 73 <span> 74 ${details.playing} 75 Playing 76 </span> 77 </div> 78 </div> 79 <div> 80 <div class="mt-2 mb-1 place-card-title"> 81 ${details.name} 82 </div> 83 </div> 84 </div> 85 </div> 86 ` 87 88 if (!details.isActive) { 89 const PlayerCountText = scrollCard.getElementsByClassName('p+pinned_games_playing')[0]; 90 PlayerCountText.children[0].classList.value = 'text-warning fa-duotone fa-lock'; 91 PlayerCountText.children[1].remove(); 92 } 93 94 card.appendChild(scrollCard); 95 } 96 card.children[0].remove(); 97 card.classList.add('d-flex'); 98 Array.from(card.children).forEach((place) => { place.classList.remove('d-none') }); 99 }); 100}; 101 102function bestFriends() { 103 const friendsRow = document.querySelector('.card:has(.friendsPopup) .d-flex')!; 104 105 const createHeadshot = async function(id: string) { 106 const user: userApiSchema = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json()); 107 const headshot = document.createElement('div'); 108 109 // ? surely a better way to do this but who cares 110 headshot.classList.add('friend-circle'); 111 headshot.setAttribute('data-user-id', id.toString()); 112 headshot.setAttribute('data-username', user.username); 113 headshot.setAttribute('data-is-online', 'false'); 114 headshot.setAttribute('data-location', 'offline'); 115 116 headshot.innerHTML = ` 117 <img width="90" height="auto" src="${user.thumbnail.icon}" alt="${user.username}" class="img-fluid rounded-circle border border-2 "> 118 <div class="friend-name text-truncate mt-1"> 119 <div style="font-size: 0.5rem; line-height: 0.5rem; display: inline-block;"> 120 <span class="text-muted"> 121 <i class="fas fa-dot-circle"></i> 122 </span> 123 </div> 124 ${user.username} 125 </div> 126 `; 127 128 friendsRow.prepend(headshot); 129 return headshot; 130 }; 131 132 _bestFriends.getValue() 133 .then(async (friends) => { 134 const userData = await pullCache( 135 'bestFriends', 136 async () => await api.batch('public', 'users/', friends), 137 300000, 138 false 139 ); 140 141 for (const id of friends) { 142 if (!userData[id]) { 143 console.warn("[Poly+] Missing cached user data for ID " + id); 144 continue; 145 }; 146 147 let headshot = document.getElementById('friend-' + id); 148 if (!headshot) headshot = await createHeadshot(id); 149 friendsRow.prepend(headshot, friendsRow.children[0]); 150 }; 151 }); 152}; 153 154function irlBrickPrice() { 155 const trendingItems = Array.from(document.querySelectorAll('a[href^="/store"]:has(.place-card)')); 156 157 for (const item of trendingItems) { 158 const priceTag = item.getElementsByClassName('text-success')[0]; 159 const currency = bricksToCurrency(parseInt(priceTag.textContent!), "USD"); 160 161 if (currency) { 162 const spanTag = document.createElement('span'); 163 spanTag.classList.add('text-muted'); 164 spanTag.style.fontSize = '0.7rem'; 165 spanTag.style.fontWeight = 'lighter'; 166 spanTag.innerText = ` (${currency})`; 167 priceTag.appendChild(spanTag); 168 }; 169 } 170};