NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name BTN Show Stats Change News Item // @namespace https://openuserjs.org/users/SB100 // @description Shows stats since your last visit as a news item // @updateURL https://openuserjs.org/meta/SB100/BTN_Show_Stats_Change_News_Item.meta.js // @version 1.0.1 // @author SB100 // @copyright 2023, SB100 (https://openuserjs.org/users/SB100) // @license MIT // @match https://broadcasthe.net/index.php // ==/UserScript== // ==OpenUserJS== // @author SB100 // ==/OpenUserJS== /** * ============================= * ADVANCED OPTIONS * ============================= */ // change this number if you want to change the cache time. Currently cached for 1 minute. Set to 0 to turn caching off const CACHE_TIME = 1000 * 60 * 1; // If there have been no stats changes, 'true' will hide the news item and 'false' will show a news item with all stats set to 0 const HIDE_ON_NO_CHANGE = false; /** * ============================= * END ADVANCED OPTIONS * DO NOT MODIFY BELOW THIS LINE * ============================= */ const SIZE_TYPES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; /** * Try parsing a string into JSON, otherwise fallback */ function JsonParseWithDefault(s, fallback = null) { try { return JSON.parse(s); } catch (e) { return fallback; } } /** * Turn a string such as 77.45 GB [111] to Bytes as a floating number */ function parseStatFromString(s, decimals = 2) { const typeMatch = s.match(/[\d,]+\s([\w]+)/); const type = typeMatch && typeMatch.length >= 2 && typeMatch[1].toUpperCase(); const desiredSizeIndex = 0; const currentSizeIndex = SIZE_TYPES.findIndex( (size) => size.toUpperCase() === type ); // unknown type if (currentSizeIndex === -1) { return 0; } const size = parseFloat(s.replace(/[^\d.]+/g, '')); return parseFloat( (size * 1000 ** (currentSizeIndex - desiredSizeIndex)).toFixed(decimals) ); } /** * Turns bytes into the best representation possible */ function formatBytes(bytes) { if (bytes === 0) { return '0.00'; } const isNegative = bytes < 0; for (let i = 1; i < SIZE_TYPES.length - 1; i += 1) { const size = Math.abs(bytes) / 1000 ** i; if (size < 1) { return `${isNegative ? '-' : ''}${( Math.abs(bytes) / 1000 ** (i - 1) ).toFixed(2)} ${SIZE_TYPES[i - 1]}`; } } return '0.00'; } /** * Turns ms into a more readable time */ function msToTime(s) { const secs = Math.floor((s / 1000) % 60); const mins = Math.floor((s / (60 * 1000)) % 60); const hrs = Math.floor((s / (60 * 60 * 1000)) % 24); const days = Math.floor((s / (24 * 60 * 60 * 1000)) % 30); return `${days > 0 ? `${days} days ` : ''}${hrs > 0 ? `${hrs} hours ` : ''}${ mins > 0 ? `${mins} mins ` : '' }${secs > 0 ? `${secs} secs ` : ''}`; } /** * Parse HTML and find up, down and ratio amounts */ function getStatsFromHtml() { const statsSpans = document.querySelectorAll('#userinfo_stats li'); const bonusMatch = document .getElementById('pointsStats') .textContent.match(/[\d,]+/); const bonus = parseFloat(bonusMatch && bonusMatch[0].replaceAll(',', '')); const lumensMatch = document .getElementById('toplumens') .textContent.match(/[\d,]+/); const lumens = parseFloat(lumensMatch && lumensMatch[0].replaceAll(',', '')); return { up: parseStatFromString(statsSpans[0].querySelector('span').innerText), bonus: Number.isNaN(bonus) ? 0 : bonus, lumens: Number.isNaN(lumens) ? 0 : lumens, time: new Date().getTime(), }; } /** * Finds a valid cache cahnged object, and cleans up all of the old ones if they are outdated */ function getCacheChangeObj() { const sortedKeys = Object.keys(window.localStorage) .filter((key) => key.startsWith('cacheStats-')) .sort((a, b) => { const aTime = parseInt(a.replace('cacheStats-', ''), 10); const bTime = parseInt(b.replace('cacheStats-', ''), 10); return aTime - bTime; }); for ( let i = 0, sortedKeysLength = sortedKeys.length; i < sortedKeysLength; i += 1 ) { const key = sortedKeys[i]; const keyTime = parseInt(key.replace('cacheStats-', ''), 10); // last entry and we're still in the cache period if ( i === sortedKeysLength - 1 && new Date().getTime() - keyTime < CACHE_TIME ) { return JsonParseWithDefault(window.localStorage.getItem(key)); } window.localStorage.removeItem(key); } return null; } /** * Display the stats change in a new news item entry */ function displayDelta(change) { const timeDiff = new Date().getTime() - change.generated; const mainColumn = document.querySelector('.sidebar'); const changeElem = document.createElement('div'); changeElem.classList.add('box'); changeElem.innerHTML = ` <div class="head colhead_dark"> <strong>Stats Update</strong> </div> <ul class="stats nobullet"> <li>⬆️ ${formatBytes(change.up)}</li> <li>🪙 ${change.bonus .toString() .replace(/\B(?=(\d{3})+(?!\d))/g, ',')} bonus points</li> <li>✨ ${change.lumens .toString() .replace(/\B(?=(\d{3})+(?!\d))/g, ',')} lumens</li> </ul> <div class="extrapad" style="color: #999;" ${ timeDiff > 1000 ? `title="Values have been cached for ${msToTime(timeDiff)}"` : '' }> <center> Compared to ${msToTime(change.time)}ago ${timeDiff > 1000 ? '*' : ''} </center> </div>`; mainColumn.prepend(changeElem); } // script runner (function main() { // calculate old and new stats const newStats = getStatsFromHtml(); const oldStats = JsonParseWithDefault( window.localStorage.getItem('lastStats') || newStats, newStats ); // calculate the current change as if there were no caching const change = { up: newStats.up - oldStats.up, bonus: newStats.bonus - oldStats.bonus, lumens: newStats.lumens - oldStats.lumens, time: newStats.time - oldStats.time, generated: newStats.time, }; // check for caching - if there is an entry, and we are in the cache time, we should use that. // if there wasn't a cached value, we should cache the current one and use that as our latest stats const cached = getCacheChangeObj(); if (!cached) { if (change.up !== 0 || change.bonus !== 0 || change.lumens !== 0) { window.localStorage.setItem( `cacheStats-${newStats.time}`, JSON.stringify(change) ); } // store the new stats in local storage - this will be our new baseline for next time we show results with no cache window.localStorage.setItem('lastStats', JSON.stringify(newStats)); } const stats = cached || change; if ( HIDE_ON_NO_CHANGE && stats.up === 0 && stats.bonus === 0 && stats.lumens === 0 ) { return; } displayDelta(stats); })();