NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name BTN Sticky Next Class Info
// @namespace https://openuserjs.org/users/SB100
// @description Shows what you need to progress to the next class on every page
// @updateURL https://openuserjs.org/meta/SB100/BTN_Sticky_Next_Class_Info.meta.js
// @version 1.0.1
// @author SB100
// @copyright 2023, SB100 (https://openuserjs.org/users/SB100)
// @license MIT
// @grant GM.xmlHttpRequest
// @match https://broadcasthe.net/*
// @connect broadcasthe.net
// ==/UserScript==
// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==
/**
* =============================
* ADVANCED OPTIONS
* =============================
*/
// change this number if you want to change the cache time. Default: cached for 30 minutes. Set to 0 to turn caching off
const CACHE_TIME = 1000 * 60 * 30;
/**
* =============================
* END ADVANCED OPTIONS
* DO NOT MODIFY BELOW THIS LINE
* =============================
*/
/**
* Try parsing a string into JSON, otherwise fallback
*/
function JsonParseWithDefault(s, fallback = null) {
try {
return JSON.parse(s);
}
catch (e) {
return fallback;
}
}
/**
* Turn a HTML string into a HTML element so that we can run querySelector calls against it
*/
function htmlToElement(html) {
const template = document.createElement('template');
template.innerHTML = html.trim();
return template.content;
}
/**
* Query a url for its HTML
*/
function query(url) {
let resolver;
let rejecter;
const p = new Promise((resolveFn, rejectFn) => {
resolver = resolveFn;
rejecter = rejectFn;
});
GM.xmlHttpRequest({
method: 'get',
url,
timeout: 10000,
onloadstart: () => {},
onload: (result) => {
if (result.status !== 200) {
rejecter(
new Error(
`[BTN Next Class Info] Error received from remote call: ${url}`
)
);
return;
}
if (typeof result.response === 'undefined') {
rejecter(
new Error(`[BTN Next Class Info] No result received from ${url}`)
);
return;
}
resolver(htmlToElement(result.response));
},
onerror: (result) => {
rejecter(result);
},
ontimeout: (result) => {
rejecter(result);
},
});
return p;
}
/**
* Finds a valid cache object, and cleans up all the old ones if they are outdated
*/
function getCacheObj() {
const sortedKeys = Object.keys(window.localStorage)
.filter((key) => key.startsWith('nextClass-'))
.sort((a, b) => {
const aTime = parseInt(a.replace('nextClass-', ''), 10);
const bTime = parseInt(b.replace('nextClass-', ''), 10);
return aTime - bTime;
});
for (
let i = 0, sortedKeysLength = sortedKeys.length; i < sortedKeysLength; i += 1
) {
const key = sortedKeys[i];
const keyTime = parseInt(key.replace('nextClass-', ''), 10);
// last entry and we're still in the cache period
if (
i === sortedKeysLength - 1 &&
new Date().getTime() - keyTime < CACHE_TIME
) {
const result = JsonParseWithDefault(window.localStorage.getItem(key));
if (result === null || Object.keys(result).length === 0) {
return null;
}
// add time this was generated
result.generated = keyTime;
return result;
}
window.localStorage.removeItem(key);
}
return null;
}
async function queryForNextClassInfo() {
const sections = [{
name: 'data',
needRegex: /Need (.*)\./,
haveRegex: /You have (.*)\./,
},
{
name: 'bp',
needRegex: /Need (.*) bonus points\./,
haveRegex: /You have (.*)\./,
},
{
name: 'snatches',
needRegex: /Need (.*) snatches\./,
haveRegex: /You have (.*)\./,
},
{
name: 'time',
needRegex: /Need to have been a member for (.*)\./,
haveRegex: /You have been a member for (.*)\./,
},
{
name: 'hnr',
needRegex: /Need to have: (\d+)/,
haveRegex: /You have: (\d+)/,
},
];
const html = await query(
'https://broadcasthe.net/user.php?action=next_class'
);
const nextClass = html
.querySelector('#content .thin h2')
?.innerText?.match(/Required stats to progress to (.*)\./)?.[1];
const results = Array.from(
html.querySelectorAll('#content .thin table tr:not(.colhead, .center)')
).reduce(
(result, row, idx) => {
const sectionData = sections[idx];
const need = row
.querySelector('td:nth-child(2)')
?.innerText?.match(sectionData.needRegex)?.[1];
const have = row
.querySelector('td:nth-child(3)')
?.innerText?.match(sectionData.haveRegex)?.[1];
const reached =
row.querySelector('td:nth-child(4) img')?.getAttribute('title') ===
'Requirement reached';
// eslint-disable-next-line no-param-reassign
result[sectionData.name] = {
need,
have,
reached,
};
return result;
}, {
nextClass
}
);
// store in cache
window.localStorage.setItem(
`nextClass-${new Date().getTime()}`,
JSON.stringify(results)
);
// add in when this was generated
results.generated = 'current';
return results;
}
function addStatsToDOM(stats) {
console.log(stats);
const container = document.createElement('div');
container.id = 'btn-next-stats__container';
container.innerHTML = `<span class="btn-next-stats__item">
<a href="/rules.php?p=class">Next class</a>: <a href="/user.php?action=next_class">${
stats.nextClass
}</a></span> •
<span class="btn-next-stats__item btn-next-stats__item--${
stats.data.reached ? 'reached' : 'unreached'
}">Data: ${stats.data.have} / ${stats.data.need}</span> •
<span class="btn-next-stats__item btn-next-stats__item--${
stats.bp.reached ? 'reached' : 'unreached'
}">BP: ${stats.bp.have} / ${stats.bp.need}</span> •
<span class="btn-next-stats__item btn-next-stats__item--${
stats.snatches.reached ? 'reached' : 'unreached'
}">Snatches: ${stats.snatches.have} / ${stats.snatches.need}</span> •
<span class="btn-next-stats__item btn-next-stats__item--${
stats.time.reached ? 'reached' : 'unreached'
}">Time: ${stats.time.have} / ${stats.time.need}</span> •
<span class="btn-next-stats__item btn-next-stats__item--${
stats.hnr.reached ? 'reached' : 'unreached'
}">H&Rs: ${stats.hnr.have} / ${stats.hnr.need}</span>
`;
const content = document.getElementById('content');
content.insertBefore(container, content.firstChild);
}
function getCssTheme() {
const external = document.querySelector(
'link[type="text/css"][title="External CSS"]'
);
if (external) {
if (external.href.includes('btn-future.css')) {
return 'btn-future';
}
}
return 'smptev3';
}
function createStyleTag(cssTheme) {
const css = `#btn-next-stats__container {
width: 100%;
padding: 5px 15px;
position: sticky;
top: 0px;
${cssTheme === 'btn-future' ? 'margin-top: -10px;' : ''}
${
cssTheme === 'btn-future'
? 'background-color: rgba(47, 43, 76, 0.85);'
: 'background-color: rgba(0, 0, 0, 0.15);'
}
border-radius: 5px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
${cssTheme !== 'btn-future' ? 'margin-bottom: 10px;' : ''}
z-index: 1;
}
.btn-next-stats__item {
display: inline-block;
padding: 5px;
border-radius: 3px;
}
.btn-next-stats__item--unreached {
color: rgb(202, 49, 66);
background-color: rgba(81, 20, 26, 0.3);
}
.btn-next-stats__item--reached {
color: rgb(75, 137, 92);
background-color: rgba(30, 55, 37, 0.3);
}
`;
const style = document.createElement('style');
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
// script runner
(async function main() {
// add style tags
createStyleTag(getCssTheme());
// get it
const nextClassStats = getCacheObj() || (await queryForNextClassInfo());
// insert into DOM to display
addStatsToDOM(nextClassStats);
})();