Created
March 26, 2026 15:06
-
-
Save d-bucur/5cf82584ec9e2e5dec2481dfac3cbac2 to your computer and use it in GitHub Desktop.
Modified version of https://github.com/Manderius/PrUn_Tooltips with some small QoL feature
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name PrUn Tooltips by Rynx | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2.2 | |
| // @description Adds FIO powered market tooltips to Apex console | |
| // @author Manderius (Rynx), Jani Mustonen (seequ), inspired by Tim Davis (binarygod, @timthedevguy) | |
| // @match https://apex.prosperousuniverse.com/ | |
| // @grant none | |
| // @require http://ajax.googleapis.com/ajax/libs/jquery/1.10.0/jquery.min.js | |
| // @downloadURL https://raw.githubusercontent.com/Manderius/PrUn_Tooltips/main/tooltips.js | |
| // @updateURL https://raw.githubusercontent.com/Manderius/PrUn_Tooltips/main/tooltips.js | |
| // ==/UserScript== | |
| let $ = jQuery; | |
| let prices = []; | |
| let last_update = null; | |
| let updates_on = null; | |
| const styles = | |
| '.prun-tooltip-base{display:flex;pointer-events:none;position:absolute!important;font-family:"Droid Sans",sans-serif;font-size:10px;color:#bbb;z-index:100000;}.prun-tooltip-box{flex:1 1 auto}.prun-tooltip-content{box-sizing:border-box;max-height:100%;max-width:100%;overflow:auto}.prun-tooltip-fade{opacity:0;-webkit-transition-property:opacity;-moz-transition-property:opacity;-o-transition-property:opacity;-ms-transition-property:opacity;transition-property:opacity}.prun-tooltip-fade.prun-tooltip-show{opacity:1}.prun-tooltip-sidetip .prun-tooltip-box{background:#222;border:1px solid #2b485a;box-shadow:0 0 5px rgba(63,162,222,.5);border-radius:0}.prun-tooltip-sidetip.prun-tooltip-right .prun-tooltip-box{margin-left:0}.prun-tooltip-sidetip .prun-tooltip-content{line-height:10px;padding:0}.prun-tooltip-sidetip .prun-tooltip-arrow{overflow:hidden;display:none;position:absolute}.prun-tooltip-content H1{border-bottom:1px solid #2b485a;background-color:rgba(63,162,222,.15);padding-bottom:8px;padding-top:9px;padding-left:10px;margin:0;font-weight:400;padding-right:10px;font-size:12px}'; | |
| const tooltip_html = ` | |
| <div | |
| class="prun-tooltip-base prun-tooltip-sidetip prun-tooltip-right prun-tooltip-fade prun-tooltip-show" | |
| > | |
| <div class="prun-tooltip-box" style="margin: 0px"> | |
| <div class="prun-tooltip-content"> | |
| <div class="PrUn_tooltip_content"> | |
| <h1>{TITLE}</h1> | |
| <table class="PrUnTools_Table"> | |
| <thead> | |
| <tr> | |
| <th></th> | |
| <th>AI1</th> | |
| <th>CI1</th> | |
| <th>IC1</th> | |
| <th>NC1</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td>Ask</td> | |
| <td class="accounting-cell">{Ask.AI1}</td> | |
| <td class="accounting-cell">{Ask.CI1}</td> | |
| <td class="accounting-cell">{Ask.IC1}</td> | |
| <td class="accounting-cell">{Ask.NC1}</td> | |
| </tr> | |
| <tr> | |
| <td>Bid</td> | |
| <td class="accounting-cell">{Buy.AI1}</td> | |
| <td class="accounting-cell">{Buy.CI1}</td> | |
| <td class="accounting-cell">{Buy.IC1}</td> | |
| <td class="accounting-cell">{Buy.NC1}</td> | |
| </tr> | |
| <tr> | |
| <td>Average</td> | |
| <td class="accounting-cell">{Avg.AI1}</td> | |
| <td class="accounting-cell">{Avg.CI1}</td> | |
| <td class="accounting-cell">{Avg.IC1}</td> | |
| <td class="accounting-cell">{Avg.NC1}</td> | |
| </tr> | |
| <tr class="top-border-cell"> | |
| <td>Supply</td> | |
| <td class="accounting-cell">{Supply.AI1}</td> | |
| <td class="accounting-cell">{Supply.CI1}</td> | |
| <td class="accounting-cell">{Supply.IC1}</td> | |
| <td class="accounting-cell">{Supply.NC1}</td> | |
| </tr> | |
| <tr> | |
| <td>Demand</td> | |
| <td class="accounting-cell">{Demand.AI1}</td> | |
| <td class="accounting-cell">{Demand.CI1}</td> | |
| <td class="accounting-cell">{Demand.IC1}</td> | |
| <td class="accounting-cell">{Demand.NC1}</td> | |
| </tr> | |
| </tbody> | |
| <tfoot> | |
| <tr class="bottom-border-cell"> | |
| <td colspan="5">Updates on {UPDATE}</td> | |
| </tr> | |
| </tfoot> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| const tooltip_html_nodata = ` | |
| <div | |
| class="prun-tooltip-base prun-tooltip-sidetip prun-tooltip-right prun-tooltip-fade prun-tooltip-show" | |
| > | |
| <div class="prun-tooltip-box" style="margin: 0px"> | |
| <div class="prun-tooltip-content"> | |
| <div class="PrUn_tooltip_content"> | |
| <h1>{TITLE}</h1> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| function getPrices(callback) { | |
| // Check if last_update is null or if now is past the updates on | |
| if (!last_update || new Date() > updates_on) { | |
| // Get market data from FIO | |
| $.ajax({ | |
| type: "GET", | |
| url: "https://rest.fnar.net/exchange/all", | |
| success: function (output, status, xhr) { | |
| // Grab data | |
| prices = output; | |
| // Set last update to now | |
| last_update = new Date(); | |
| // Set updates_on to 5 minutes from now | |
| updates_on = new Date(last_update.getTime() + 5 * 60000); | |
| callback(output); | |
| }, | |
| error: function () { | |
| console.log("Error in API call"); | |
| }, | |
| }); | |
| } else { | |
| // No update needed go ahead and parse the data | |
| callback(prices); | |
| } | |
| } | |
| function displayPrice(price, amount) { | |
| if (!price) return 'null'; | |
| if (!amount) return price.toLocaleString(); | |
| const total = price * amount; | |
| return `${price.toLocaleString()} <i>(${total.toLocaleString()})</i>`; | |
| } | |
| function generateTooltipContent(ticker, title, amount) { | |
| let html = tooltip_html.replace("{UPDATE}", updates_on.toLocaleString()); | |
| // Find Material in FIO data | |
| let market_data = prices.filter((obj) => { | |
| return obj.MaterialTicker === ticker; | |
| }); | |
| if (market_data.length === 0) { | |
| return createElement(tooltip_html_nodata.replace("{TITLE}", title)); | |
| } | |
| // Filter should return all 4 markets worth of data, populate our tooltip | |
| market_data.forEach(function (ticker_data) { | |
| html = html.replace( | |
| `{Ask.${ticker_data.ExchangeCode}}`, | |
| displayPrice(ticker_data.Ask, amount) | |
| ); | |
| html = html.replace( | |
| `{Buy.${ticker_data.ExchangeCode}}`, | |
| displayPrice(ticker_data.Bid, amount) | |
| ); | |
| html = html.replace( | |
| `{Avg.${ticker_data.ExchangeCode}}`, | |
| displayPrice(ticker_data.PriceAverage, amount) | |
| ); | |
| html = html.replace( | |
| `{Supply.${ticker_data.ExchangeCode}}`, | |
| ticker_data.Supply ? ticker_data.Supply.toLocaleString() : "null" | |
| ); | |
| html = html.replace( | |
| `{Demand.${ticker_data.ExchangeCode}}`, | |
| ticker_data.Demand ? ticker_data.Demand.toLocaleString() : "null" | |
| ); | |
| html = html.replace(`{TITLE}`, title); | |
| }); | |
| // Replace any nulls with '--' | |
| html = html.replaceAll("null", "--"); | |
| return createElement(html); | |
| } | |
| function createElement(html) { | |
| var div = document.createElement("div"); | |
| div.innerHTML = html.trim(); | |
| return div.firstChild; | |
| } | |
| function showTooltip(item, ticker) { | |
| if ($(`#tooltip_${ticker}`).length > 0) { | |
| return document.getElementById(`tooltip_${ticker}`); | |
| } | |
| const title = $(item).data("title"); | |
| const amount = $(item).parent().find(".MaterialIcon__type-very-small___UMzQ3ir").text(); | |
| const content = generateTooltipContent(ticker, title, amount ? +amount.replace(',', '').replace('.', '') : null); | |
| content.id = `tooltip_${ticker}`; | |
| // Positioning | |
| document.body.appendChild(content); | |
| const positionFromLeft = | |
| item.getBoundingClientRect().right + item.offsetWidth / 6; | |
| const canFitOnRight = | |
| positionFromLeft + content.offsetWidth < window.innerWidth; | |
| if (canFitOnRight) { | |
| content.style.left = positionFromLeft + "px"; | |
| } else { | |
| content.style.left = | |
| item.getBoundingClientRect().left - | |
| item.offsetWidth / 6 - | |
| content.offsetWidth + | |
| "px"; | |
| } | |
| let positionFromTop = | |
| item.getBoundingClientRect().top + | |
| item.offsetHeight / 2 - | |
| content.offsetHeight / 2; | |
| const doesBottomOverflow = | |
| positionFromTop + content.offsetHeight > window.innerHeight; | |
| const doesTopOverflow = positionFromTop < 0; | |
| if (doesBottomOverflow) { | |
| content.style.top = | |
| window.innerHeight - content.offsetHeight - 3 + "px"; | |
| } else if (doesTopOverflow) { | |
| content.style.top = "3px"; | |
| } else { | |
| content.style.top = positionFromTop + "px"; | |
| } | |
| return content; | |
| } | |
| function hideTooltip(tooltip) { | |
| try { | |
| tooltip.remove(); | |
| } catch (e) { } | |
| } | |
| function addEventListenersToItems(items) { | |
| items.forEach((item) => { | |
| if (item.hasListener) return; | |
| item.hasListener = true; | |
| const ticker = $(item).find(".ColoredIcon__label___OU1I4oP").text(); | |
| $(item).children().attr("title", ""); | |
| // remove hover tooltip | |
| $(item).data("title", $(item).attr("title")) | |
| $(item).attr("title", ""); | |
| let tooltip; | |
| item.addEventListener("mouseover", () => { | |
| tooltip = showTooltip(item, ticker); | |
| }); | |
| item.addEventListener("mouseout", () => { | |
| hideTooltip(tooltip); | |
| }); | |
| item.addEventListener("mousedown", () => { | |
| hideTooltip(tooltip); | |
| }); | |
| }); | |
| } | |
| function setupTooltips(items) { | |
| getPrices(() => addEventListenersToItems(items)); | |
| } | |
| function monitorOnElementCreated(selector, callback, onlyOnce = true) { | |
| const getElementsFromNodes = (nodes) => | |
| Array.from(nodes) | |
| .flatMap((node) => | |
| node.nodeType === 3 | |
| ? [] | |
| : Array.from(node.querySelectorAll?.(selector) ?? []) | |
| ) | |
| .filter((item) => item !== null); | |
| let onMutationsObserved = function (mutations) { | |
| mutations.forEach(function (mutation) { | |
| if (mutation.addedNodes.length) { | |
| const elements = getElementsFromNodes(mutation.addedNodes); | |
| if (elements && elements.length > 0) { | |
| if (onlyOnce) { | |
| observer.disconnect(); | |
| callback(elements[0]); | |
| return; | |
| } | |
| callback(elements); | |
| } | |
| } | |
| }); | |
| }; | |
| let containerSelector = "body"; | |
| let target = document.querySelector(containerSelector); | |
| let config = { childList: true, subtree: true }; | |
| let MutationObserver = | |
| window.MutationObserver || window.WebKitMutationObserver; | |
| let observer = new MutationObserver(onMutationsObserved); | |
| observer.observe(target, config); | |
| } | |
| function addStyle(styleString) { | |
| var style = document.createElement("style"); | |
| if (style.styleSheet) { | |
| style.styleSheet.cssText = styleString; | |
| } else { | |
| style.appendChild(document.createTextNode(styleString)); | |
| } | |
| document.getElementsByTagName("head")[0].appendChild(style); | |
| } | |
| function waitForApexLoad() { | |
| getPrices(() => { }); | |
| const insideFrameSelector = ".ColoredIcon__container___djaR4r2"; | |
| monitorOnElementCreated( | |
| insideFrameSelector, | |
| (items) => setTimeout(() => setupTooltips(items), 100), | |
| false | |
| ); | |
| const onLoad = () => { | |
| addStyle(styles); | |
| }; | |
| const selector = "#TOUR_TARGET_BUTTON_BUFFER_NEW"; | |
| monitorOnElementCreated(selector, onLoad); | |
| } | |
| (function () { | |
| "use strict"; | |
| waitForApexLoad(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment