Tix Pro (for Roblox)

Extremely simple $RBLX & Robux price conversion widget with multiple currencies.

// ==UserScript==
// @name         Tix Pro (for Roblox)
// @namespace    rblx.cash
// @version      1337
// @description  Extremely simple $RBLX & Robux price conversion widget with multiple currencies.
// @author       mobs2r
// @match        https://www.roblox.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @connect      query1.finance.yahoo.com
// @connect      api.exchangerate-api.com
// @connect      api.frankfurter.app
// ==/UserScript==

(function() {
'use strict';
const ROBUX_RATE_USD = 0.0035;
const CURRENCIES = {
    USD: { symbol: '$', name: 'US Dollar', flag: '🇺🇸' },
    EUR: { symbol: '€', name: 'Euro', flag: '🇪🇺' },
    GBP: { symbol: '£', name: 'British Pound', flag: '🇬🇧' },
    CAD: { symbol: '$', name: 'Canadian Dollar', flag: '🇨🇦' },
    AUD: { symbol: '$', name: 'Australian Dollar', flag: '🇦🇺' }
};

function createTicker() {
    const tickerContainer = document.createElement('div');
    tickerContainer.id = 'rblx-ticker-container';
    tickerContainer.style.cssText = `position:fixed;top:15px;right:15px;background:rgba(30,30,30,0.95);border-radius:8px;z-index:9999;box-shadow:0 4px 20px rgba(0,0,0,0.5);border:1px solid #3a3a3a;width:40px;height:40px;font-family:'Gotham SSm A','Gotham SSm B',Arial,sans-serif;cursor:move;transition:all 0.2s ease;overflow:hidden;display:flex;flex-direction:column;align-items:center;`;
    const toggleBtn = document.createElement('div');
    toggleBtn.id = 'rblx-toggle';
    toggleBtn.style.cssText = `width:100%;height:40px;display:flex;align-items:center;justify-content:center;position:relative;`;
    const logoContainer = document.createElement('div');
    logoContainer.id = 'rblx-logo-container';
    logoContainer.style.cssText = `display:flex;align-items:center;justify-content:center;width:24px;height:24px;cursor:pointer;`;
    const logoSvg = document.createElementNS('http://www.w3.org/2000/svg','svg');
    logoSvg.setAttribute('width','24');
    logoSvg.setAttribute('height','24');
    logoSvg.setAttribute('viewBox','0 0 24 24');
    logoSvg.id = 'rblx-logo';
    logoSvg.style.cssText = 'transition:transform 0.2s;';
    const rect = document.createElementNS('http://www.w3.org/2000/svg','rect');
    rect.setAttribute('x','2');
    rect.setAttribute('y','2');
    rect.setAttribute('width','20');
    rect.setAttribute('height','20');
    rect.setAttribute('rx','4');
    rect.setAttribute('fill','#00ff88');
    const text = document.createElementNS('http://www.w3.org/2000/svg','text');
    text.setAttribute('x','12');
    text.setAttribute('y','12');
    text.setAttribute('text-anchor','middle');
    text.setAttribute('dominant-baseline','middle');
    text.setAttribute('font-size','14');
    text.setAttribute('font-weight','bold');
    text.setAttribute('fill','#1e1e1e');
    text.textContent = 'R';
    logoSvg.appendChild(rect);
    logoSvg.appendChild(text);
    logoContainer.appendChild(logoSvg);
    const title = document.createElement('span');
    title.id = 'rblx-title';
    title.textContent = 'Tix Pro';
    title.style.cssText = `display:none;font-size:14px;font-weight:bold;margin-left:5px;color:#00ff88;text-transform:uppercase;letter-spacing:0.5px;`;
    toggleBtn.appendChild(logoContainer);
    toggleBtn.appendChild(title);
    const content = document.createElement('div');
    content.id = 'rblx-content';
    content.style.cssText = `display:none;width:100%;padding:15px;`;
    const stockRow = document.createElement('div');
    stockRow.style.cssText = `display:flex;justify-content:space-between;margin-bottom:12px;`;
    const stockLabel = document.createElement('span');
    stockLabel.textContent = '$RBLX:';
    stockLabel.style.cssText = `font-size:14px;color:#b8b8b8;`;
    const stockValue = document.createElement('span');
    stockValue.id = 'rblx-stock-value';
    stockValue.textContent = 'Loading...';
    stockValue.style.cssText = `font-size:14px;font-weight:700;color:#fff;`;
    stockRow.appendChild(stockLabel);
    stockRow.appendChild(stockValue);
    const robuxRow = document.createElement('div');
    robuxRow.style.cssText = `display:flex;justify-content:space-between;margin-bottom:15px;`;
    const robuxLabel = document.createElement('span');
    robuxLabel.textContent = 'Robux:';
    robuxLabel.style.cssText = `font-size:14px;color:#b8b8b8;`;
    const robuxValue = document.createElement('span');
    robuxValue.id = 'rblx-robux-value';
    robuxValue.textContent = 'Loading...';
    robuxValue.style.cssText = `font-size:14px;font-weight:700;color:#fff;display:flex;align-items:center;`;
    const robuxCurrencyIcon = document.createElement('span');
    robuxCurrencyIcon.id = 'rblx-robux-currency';
    robuxCurrencyIcon.style.cssText = `margin-right:4px;font-size:12px;`;
    robuxValue.prepend(robuxCurrencyIcon);
    robuxRow.appendChild(robuxLabel);
    robuxRow.appendChild(robuxValue);
    const currencyRow = document.createElement('div');
    currencyRow.style.cssText = `display:flex;justify-content:space-between;align-items:center;margin-top:15px;padding-top:15px;border-top:1px solid #3a3a3a;`;
    const currencyLabel = document.createElement('span');
    currencyLabel.textContent = 'Currency:';
    currencyLabel.style.cssText = `font-size:13px;color:#b8b8b8;`;
    const currencySwitcher = document.createElement('div');
    currencySwitcher.id = 'rblx-currency-switcher';
    currencySwitcher.style.cssText = `display:flex;gap:8px;`;
    Object.keys(CURRENCIES).forEach(currency => {
        const flag = document.createElement('span');
        flag.textContent = CURRENCIES[currency].flag;
        flag.dataset.currency = currency;
        flag.style.cssText = `cursor:pointer;font-size:18px;transition:transform 0.2s;`;
        flag.addEventListener('click',function(e){e.stopPropagation();switchCurrency(currency);setTimeout(()=>location.reload(),300);});
        currencySwitcher.appendChild(flag);
    });
    currencyRow.appendChild(currencyLabel);
    currencyRow.appendChild(currencySwitcher);
    content.appendChild(stockRow);
    content.appendChild(robuxRow);
    content.appendChild(currencyRow);
    tickerContainer.appendChild(toggleBtn);
    tickerContainer.appendChild(content);
    document.body.appendChild(tickerContainer);
    makeDraggable(tickerContainer);
    logoContainer.addEventListener('click',function(e){e.stopPropagation();toggleDockedState(tickerContainer);});
    return{container:tickerContainer,stockValue,robuxValue,robuxCurrencyIcon,currencySwitcher};
}

function makeDraggable(element){
    let isDragging=false;let offsetX,offsetY;
    element.addEventListener('mousedown',function(e){
        if(e.target.id==='rblx-logo-container'||e.target.id==='rblx-logo'||e.target.tagName==='text')return;
        isDragging=true;
        offsetX=e.clientX-element.getBoundingClientRect().left;
        offsetY=e.clientY-element.getBoundingClientRect().top;
        element.style.cursor='grabbing';
        element.style.zIndex='10000';
        element.style.boxShadow='0 10px 30px rgba(0,0,0,0.5)';
        element.style.transition='none';
        e.preventDefault();
    });
    document.addEventListener('mousemove',function(e){
        if(!isDragging)return;
        const x=e.clientX-offsetX;
        const y=e.clientY-offsetY;
        const maxX=window.innerWidth-element.offsetWidth;
        const maxY=window.innerHeight-element.offsetHeight;
        element.style.left=Math.max(0,Math.min(maxX,x))+'px';
        element.style.top=Math.max(0,Math.min(maxY,y))+'px';
        element.style.right='auto';
        element.style.bottom='auto';
    });
    document.addEventListener('mouseup',function(){
        if(!isDragging)return;
        isDragging=false;
        element.style.cursor='move';
        element.style.zIndex='9999';
        element.style.boxShadow='0 4px 20px rgba(0,0,0,0.5)';
        element.style.transition='all 0.2s ease';
        GM_setValue('widgetPosition',{left:element.style.left,top:element.style.top,open:element.classList.contains('open')});
    });
}

function toggleDockedState(element){
    if(element.style.transition==='none')return;
    const savedPosition=GM_getValue('widgetPosition');
    let left=parseInt(savedPosition?.left||'15px',10);
    let top=parseInt(savedPosition?.top||'15px',10);
    if(element.classList.contains('open')){
        element.classList.remove('open');
        element.style.width='40px';
        element.style.height='40px';
        document.getElementById('rblx-content').style.display='none';
        const maxX=window.innerWidth-40;
        const maxY=window.innerHeight-40;
        left=Math.max(0,Math.min(maxX,left));
        top=Math.max(0,Math.min(maxY,top));
    }else{
        element.classList.add('open');
        element.style.width='250px';
        element.style.height='auto';
        document.getElementById('rblx-content').style.display='block';
        const maxX=window.innerWidth-250;
        const maxY=window.innerHeight-200;
        left=Math.max(0,Math.min(maxX,left));
        top=Math.max(0,Math.min(maxY,top));
    }
    element.style.left=left+'px';
    element.style.top=top+'px';
    element.style.right='auto';
    element.style.bottom='auto';
    element.style.cursor='move';
    GM_setValue('widgetPosition',{left:element.style.left,top:element.style.top,open:element.classList.contains('open')});
}

function fetchStockData(){
    return new Promise((resolve,reject)=>{
        GM_xmlhttpRequest({method:"GET",url:"https://query1.finance.yahoo.com/v8/finance/chart/RBLX",onload:function(response){
            try{const data=JSON.parse(response.responseText);if(data.chart?.result?.[0]?.meta){const price=data.chart.result[0].meta.regularMarketPrice;const change=data.chart.result[0].meta.regularMarketChange;resolve({price,change});}else{reject('Invalid stock data response');}}catch(e){reject('Error parsing stock data: '+e.message);}},onerror:function(error){reject('Stock fetch error: '+error.statusText);},ontimeout:function(){reject('Stock fetch timeout');}});
    });
}

function fetchExchangeRates(){
    return new Promise((resolve,reject)=>{
        GM_xmlhttpRequest({method:"GET",url:"https://api.exchangerate-api.com/v4/latest/USD",onload:function(response){
            try{const data=JSON.parse(response.responseText);if(data.rates){resolve(data.rates);}else{fetchExchangeRatesFallback().then(resolve).catch(reject);}}catch(e){fetchExchangeRatesFallback().then(resolve).catch(reject);}},onerror:function(error){fetchExchangeRatesFallback().then(resolve).catch(reject);},ontimeout:function(){fetchExchangeRatesFallback().then(resolve).catch(reject);}});
    });
}

function fetchExchangeRatesFallback(){
    return new Promise((resolve,reject)=>{
        GM_xmlhttpRequest({method:"GET",url:"https://api.frankfurter.app/latest?from=USD",onload:function(response){
            try{const data=JSON.parse(response.responseText);if(data.rates){resolve(data.rates);}else{reject('No rates data in fallback API response');}}catch(e){reject('Error parsing fallback exchange rates: '+e.message);}},onerror:function(error){reject('Fallback exchange rate fetch error: '+error.statusText);},ontimeout:function(){reject('Fallback exchange rate fetch timeout');}});
    });
}

function switchCurrency(currency){
    GM_setValue('selectedCurrency',currency);
}

async function updateTicker(currency){
    const currencyData=CURRENCIES[currency]||CURRENCIES.USD;
    try{
        const[stockData,exchangeRates]=await Promise.all([fetchStockData(),fetchExchangeRates()]);
        const rate=exchangeRates[currency]||1;
        const stockValue=document.getElementById('rblx-stock-value');
        stockValue.textContent=`${(stockData.price*rate).toFixed(2)}`;
        stockValue.className=stockData.change>=0?'positive':'negative';
        const robuxValue=document.getElementById('rblx-robux-value');
        const robuxCurrencyIcon=document.getElementById('rblx-robux-currency');
        robuxValue.textContent=`${(ROBUX_RATE_USD*rate).toFixed(4)}`;
        robuxCurrencyIcon.textContent=currencyData.symbol;
        document.querySelectorAll('#rblx-currency-switcher span').forEach(flag=>{
            flag.style.opacity=flag.dataset.currency===currency?'1':'0.6';
            flag.style.transform=flag.dataset.currency===currency?'scale(1.2)':'scale(1)';
        });
    }catch(error){
        console.error('Tix Pro Error:',error);
        document.getElementById('rblx-stock-value').textContent='Error';
        document.getElementById('rblx-robux-value').textContent='Error';
        document.getElementById('rblx-stock-value').className='negative';
    }
}

function addStyles(){
    GM_addStyle(`
        .positive{color:#00ff88!important;}
        .negative{color:#ff4d4d!important;}
        #rblx-currency-switcher span:hover{transform:scale(1.2);opacity:0.9!important;}
        #rblx-logo-container:hover{transform:scale(1.1);}
        #rblx-ticker-container.open{width:250px;height:auto;padding-bottom:10px;}
        #rblx-ticker-container{transition:all 0.2s ease,box-shadow 0.2s ease;}
        #rblx-ticker-container:hover{transform:translateY(-3px);box-shadow:0 8px 25px rgba(0,0,0,0.6);}
        #rblx-ticker-container.open #rblx-title{display:inline-block;}
        .status-indicator{display:inline-block;width:10px;height:10px;border-radius:50%;margin-left:8px;vertical-align:middle;}
        .status-active{background-color:#00ff88;box-shadow:0 0 5px #00ff88;}
        .status-inactive{background-color:#ff4d4d;box-shadow:0 0 5px #ff4d4d;}
        #rblx-currency-switcher span{opacity:0.6;transition:all 0.2s ease;}
        #rblx-currency-switcher span:hover{opacity:0.9;}
    `);
}

async function init(){
    addStyles();
    const{container}=createTicker();
    const savedCurrency=GM_getValue('selectedCurrency','USD');
    const savedPosition=GM_getValue('widgetPosition');
    if(savedPosition){
        const left=parseInt(savedPosition.left,10);
        const top=parseInt(savedPosition.top,10);
        const maxX=window.innerWidth-(savedPosition.open?250:40);
        const maxY=window.innerHeight-(savedPosition.open?200:40);
        container.style.left=`${Math.max(0,Math.min(maxX,left))}px`;
        container.style.top=`${Math.max(0,Math.min(maxY,top))}px`;
        if(savedPosition.open){
            container.classList.add('open');
            container.style.width='250px';
            container.style.height='auto';
            document.getElementById('rblx-content').style.display='block';
        }
    }else{
        container.classList.remove('open');
        container.style.width='40px';
        container.style.height='40px';
        document.getElementById('rblx-content').style.display='none';
    }
    await updateTicker(savedCurrency);
}

init();
})();