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); })();