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

feat: Favorited Places toggle button on place views

Index cbc3c341 04eaa103

Changed files
+158 -24
entrypoints
utils
+27 -9
entrypoints/home.content.ts
···
main() {
preferences.getValue()
.then((values) => {
-
if (values.favoritedPlaces) favoritedPlaces();
-
if (values.bestFriends) bestFriends();
});
}
});
···
}
const placeData: Array<placeApiSchema> = await pullCache('favoritedPlaces', async () => {
const res: any = {}
for (let id of places as number[]) {
const info = (await (await fetch('https://api.polytoria.com/v1/places/' + id)).json())
res[id] = info
}
-
return res
-
}, false);
const card = container.getElementsByClassName('scrollFadeContainer')[0]
for (let i = 0; i < places.length; i++) {
-
const id = places.toSorted((a, b) => b - a)[i]
-
const details = placeData[id]
const scrollCard = document.createElement('a')
scrollCard.classList.value = 'd-none'
···
function bestFriends() {
const friendsRow = document.querySelector('.card:has(.friendsPopup) .d-flex')!;
-
const createHeadshot = async function(id: number) {
const user: userApiSchema = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json());
const headshot = document.createElement('div');
···
_bestFriends.getValue()
.then(async (friends) => {
const userData = await pullCache('bestFriends', async () => {
const res: any = {}
for (let id of friends as number[]) {
const info = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json())
res[id] = info
}
-
return res
-
}, false);
for (const id of friends) {
let headshot = document.getElementById('friend-' + id);
···
main() {
preferences.getValue()
.then((values) => {
+
if (values.favoritedPlaces.enabled) favoritedPlaces();
+
if (values.bestFriends.enabled) bestFriends();
});
}
});
···
}
const placeData: Array<placeApiSchema> = await pullCache('favoritedPlaces', async () => {
+
// API is not viable right now, static data used in it's place for now.
+
return {
+
"9656": {"id":9656,"name":"The Wayland Bridge","description":"Listen up troops,\r\n\r\nOn this battlefield your mission is to capture the enemy flag and perform as many casualties to the enemy team. The first team to reach 5 captures will claim victory. The high order will reward you for your confirmed kills, use this wisely to upgrade your arsenal.\r\n\r\nGood luck soldier.","creator":{"type":"user","id":1,"name":"Polytoria","thumbnail":"https://c0.ptacdn.com/thumbnails/avatars/4f42532128b4b6938bb4a01b0f74537ebf5e9efaa09b88d0bf7d343de536c46e-icon.png"},"thumbnail":"https://c0.ptacdn.com/places/icons/WxomBAbMNNNnnAkKxuD11Snnoz2C26wW.png","genre":"fighting","maxPlayers":24,"isActive":false,"isToolsEnabled":true,"isCopyable":false,"visits":10205,"uniqueVisits":799,"playing":0,"rating":{"likes":149,"dislikes":49,"percent":"75%"},"accessType":"everyone","accessPrice":null,"createdAt":"2024-06-14T07:48:36.720+00:00","updatedAt":"2024-07-14T09:07:53.198+00:00"}
+
};
+
+
/*
const res: any = {}
for (let id of places as number[]) {
const info = (await (await fetch('https://api.polytoria.com/v1/places/' + id)).json())
res[id] = info
}
+
return res;
+
*/
+
}, 300000, true);
const card = container.getElementsByClassName('scrollFadeContainer')[0]
for (let i = 0; i < places.length; i++) {
+
const id = places.toSorted((a, b) => parseInt(b) - parseInt(a))[i]
+
const details = placeData[parseInt(id)]
+
if (!details) {
+
console.warn("[Poly+] Missing cached place data for ID " + id);
+
continue;
+
}
const scrollCard = document.createElement('a')
scrollCard.classList.value = 'd-none'
···
function bestFriends() {
const friendsRow = document.querySelector('.card:has(.friendsPopup) .d-flex')!;
+
const createHeadshot = async function(id: string) {
const user: userApiSchema = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json());
const headshot = document.createElement('div');
···
_bestFriends.getValue()
.then(async (friends) => {
const userData = await pullCache('bestFriends', async () => {
+
// API is not viable right now, static data used in it's place for now.
+
return {
+
"1": {"id":1,"username":"Polytoria","description":"Welcome to the Polytoria profile!\r\nFor inquiries or support, please send a message to one of our staff members. Messages sent to this account will not be read!","signature":"Hello!","thumbnail":{"avatar":"https://c0.ptacdn.com/thumbnails/avatars/4f42532128b4b6938bb4a01b0f74537ebf5e9efaa09b88d0bf7d343de536c46e.png","icon":"https://c0.ptacdn.com/thumbnails/avatars/4f42532128b4b6938bb4a01b0f74537ebf5e9efaa09b88d0bf7d343de536c46e-icon.png"},"playing":null,"netWorth":3991,"placeVisits":20461,"profileViews":2567,"forumPosts":17,"assetSales":64172,"membershipType":"plusDeluxe","isStaff":true,"registeredAt":"2019-04-14T12:29:52.000+00:00","lastSeenAt":"2024-11-21T11:34:14.119+00:00"}
+
};
+
+
/*
const res: any = {}
for (let id of friends as number[]) {
const info = (await (await fetch('https://api.polytoria.com/v1/users/' + id)).json())
res[id] = info
}
+
return res;
+
*/
+
}, 300000, true);
for (const id of friends) {
let headshot = document.getElementById('friend-' + id);
+15
entrypoints/places.content/index.ts
···
···
+
import { preferences } from "@/utils/storage";
+
import * as view from "./view";
+
+
export default defineContentScript({
+
matches: ['*://polytoria.com/places/*'],
+
main() {
+
preferences.getValue()
+
.then((values) => {
+
if (window.location.pathname.split('/')[2]) {
+
// View
+
if (values.favoritedPlaces.enabled) view.favoritedPlaces();
+
}
+
});
+
}
+
});
+71
entrypoints/places.content/view.ts
···
···
+
import { _favoritedPlaces, limits } from "@/utils/storage";
+
import { expireCache } from "@/utils/utilities";
+
const placeID = window.location.pathname.split('/')[2];
+
+
export async function favoritedPlaces() {
+
let placeIDs = await _favoritedPlaces.getValue();
+
const titleCard = document.getElementById;
+
+
const button = document.createElement('button');
+
button.classList.add('btn', 'btn-primary', 'btn-sm');
+
button.style.position = 'absolute';
+
button.style.top = '0';
+
button.style.right = '0';
+
button.style.margin = '4px';
+
button.style.fontSize = '1.3em';
+
button.innerHTML = `
+
<i class="fa-regular fa-star"></i>
+
`;
+
+
const update = function() {
+
button.classList.value = 'btn btn-primary btn-sm';
+
button.disabled = false;
+
if (placeIDs.indexOf(placeID) == -1) {
+
// Not Pinned
+
if (placeIDs.length >= limits.favoritedPlaces) {
+
button.disabled = true;
+
}
+
button.children[0].classList.value = 'fa-regular fa-star';
+
} else {
+
// Pinned
+
button.children[0].classList.value = 'fa-duotone fa-star';
+
}
+
};
+
+
button.addEventListener('mouseenter', function() {
+
if (placeIDs.indexOf(placeID) != -1) {
+
button.classList.add('btn-danger');
+
button.classList.remove('btn-primary');
+
button.children[0].classList.add('fa-star-half-stroke');
+
button.children[0].classList.remove('fa-star');
+
};
+
});
+
+
button.addEventListener('mouseleave', function() {
+
if (placeIDs.indexOf(placeID) != -1) {
+
button.classList.add('btn-primary');
+
button.classList.remove('btn-danger');
+
button.children[0].classList.add('fa-star');
+
button.children[0].classList.remove('fa-star-half-stroke');
+
};
+
});
+
+
update();
+
document.querySelector('h1.my-0')!.parentElement!.appendChild(button);
+
+
button.addEventListener('click', function() {
+
if (placeIDs.indexOf(placeID) == -1) {
+
placeIDs.push(placeID);
+
} else {
+
placeIDs.splice(placeIDs.indexOf(placeID), 1);
+
}
+
+
expireCache('favoritedPlaces');
+
_favoritedPlaces.setValue(placeIDs);
+
update();
+
});
+
+
storage.watch<Array<string>>('sync:favoritedPlaces', (value, previous) => {
+
placeIDs = value!;
+
});
+
}
+1 -1
entrypoints/sitewide.content.ts
···
if (!user) {
// Error page or event page most likely, where users are not authenticated
console.warn('[Poly+] Failure to get logged in user details.');
-
return
};
const currency = bricksToCurrency(user.bricks, "USD");
···
if (!user) {
// Error page or event page most likely, where users are not authenticated
console.warn('[Poly+] Failure to get logged in user details.');
+
return;
};
const currency = bricksToCurrency(user.bricks, "USD");
+8 -3
utils/storage.ts
···
// Sync
export const preferences = storage.defineItem('sync:preferences', { fallback: defaultPreferences, version: 1 });
-
export const _favoritedPlaces = storage.defineItem('sync:favoritedPlaces', { fallback: [9656], version: 1 });
-
export const _bestFriends = storage.defineItem('sync:bestFriends', { fallback: [2782], version: 1 });
// Cache
export const cache = storage.defineItem('local:cache', {
···
favoritedPlaces: []
},
version: 1
-
});
···
// Sync
export const preferences = storage.defineItem('sync:preferences', { fallback: defaultPreferences, version: 1 });
+
export const _favoritedPlaces = storage.defineItem('sync:favoritedPlaces', { fallback: ["9656"], version: 1 });
+
export const _bestFriends = storage.defineItem('sync:bestFriends', { fallback: ["2782"], version: 1 });
// Cache
export const cache = storage.defineItem('local:cache', {
···
favoritedPlaces: []
},
version: 1
+
});
+
+
// Limits
+
export const limits = {
+
favoritedPlaces: 15
+
}
+9 -4
utils/types.ts
···
isStaff: boolean,
registeredAt: string,
lastSeenAt: string
-
}
export type placeApiSchema = {
id: number,
···
accessPrice: number|null,
createdAt: string,
updatedAt: string
-
}
export interface cacheInterface {
-
favoritedPlaces: number[],
[key: string]: any;
-
}
···
isStaff: boolean,
registeredAt: string,
lastSeenAt: string
+
};
export type placeApiSchema = {
id: number,
···
accessPrice: number|null,
createdAt: string,
updatedAt: string
+
};
export interface cacheInterface {
+
favoritedPlaces: never[],
+
[key: string]: any;
+
};
+
+
export interface cacheMetadata {
+
favoritedPlaces: number,
[key: string]: any;
+
};
+27 -7
utils/utilities.ts
···
import { cacheInterface } from "./types";
import * as currencyPackages from "./currencyPackages.json"
-
// ? maybe make this use Meta data on an actual container
-
export async function pullCache(key: string, replenish: Function, forceReplenish: boolean) {
const cacheStorage: cacheInterface = await cache.getValue();
-
if (!cacheStorage[key] || forceReplenish) {
const replenishedCache = await replenish();
cacheStorage[key] = replenishedCache;
-
//@ts-ignore: why
cache.setValue(cacheStorage);
-
return replenishedCache;
}
return cacheStorage[key];
}
export async function getUserDetails() {
···
userId: parseInt(profileLink.href.split('/')[4]),
bricks: parseInt(brickBalance.textContent!.replace(/,/g, ""))
}
-
}
export function bricksToCurrency(bricks: number, currency: string): string | null {
const _currencyPackages = currencyPackages as Record<string, Array<Array<number>>>;
···
}
}
return `~${totalValue.toFixed(2)} ${currency}`;
-
}
···
import { cacheInterface } from "./types";
import * as currencyPackages from "./currencyPackages.json"
+
export async function pullCache(key: string, replenish: Function, expiry: number, forceReplenish: boolean) {
const cacheStorage: cacheInterface = await cache.getValue();
+
const metadata = await cache.getMeta() as { [key: string]: number; };
+
+
if (!cacheStorage[key] || forceReplenish || (Date.now() - metadata[key] >= expiry)) {
+
console.info('[Poly+] "' + key + '" cache is stale replenishing...');
+
const replenishedCache = await replenish();
cacheStorage[key] = replenishedCache;
+
metadata[key] = Date.now();
+
cache.setValue(cacheStorage);
+
cache.setMeta(metadata);
}
+
return cacheStorage[key];
+
};
+
+
export async function expireCache(key: string) {
+
console.info('[Poly+] Forcefully expiring "' + key + '" cache...');
+
+
const metadata = await cache.getMeta() as { [key: string]: number };
+
metadata[key] = 0;
+
cache.setMeta(metadata);
}
export async function getUserDetails() {
···
userId: parseInt(profileLink.href.split('/')[4]),
bricks: parseInt(brickBalance.textContent!.replace(/,/g, ""))
}
+
};
export function bricksToCurrency(bricks: number, currency: string): string | null {
const _currencyPackages = currencyPackages as Record<string, Array<Array<number>>>;
···
}
}
+
if (bricks > 0) {
+
const cheapestPackage = packages[packages.length - 1];
+
const [currencyValue, bricksValue] = cheapestPackage;
+
const unitPrice = currencyValue / bricksValue;
+
totalValue += bricks * unitPrice;
+
}
+
return `~${totalValue.toFixed(2)} ${currency}`;
+
};