ZedHelper

Misc helper tools for Zed City

// ==UserScript==
// @name         ZedHelper
// @description  Misc helper tools for Zed City
// @version      0.4.14
// @namespace    kvassh.zedhelper
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zed.city
// @homepage     https://greasyfork.org/en/scripts/527868-zedhelper
// @author       Kvassh
// @match        https://www.zed.city/*
// @run-at       document-end
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.onurlchange
// @connect      api.zed.city
// ==/UserScript==

/** 
 * ZedHelper
 * 
 * Features:
 * - Displays market value for items in inventory
 * - Calculates your inventory networth based on current market values
 * - Extra nav menu with some useful shortcuts (togglable in settings)
 * - Autopopulates gym train input fields to use maximum energy
 * - Autopopulates input field for junk shop with 360 item buy qty
 * - Show value of trades at Radio Tower
 * - Show timer for various features (Raid, Junk Store limit)
 *
 * If you have any questions, feel free to reach out to Kvassh [12853] in Zed City
 * 
 * Changelog:
 * - 0.4.14: Another fix for the duplicate timer bar issue, hopefully 100% fixed now.
 * - 0.4.13: Fix error with duplicate timer bar appearing.
 *           Add link to respective functions for timers also when not ready
 * - 0.4.12: Fix another bug in time parsing for timer bar
 * - 0.4.11: Fix bug in time parsing for timer bar
 * - 0.4.10: Fix correct link for Raid timer shortcut
 * - 0.4.9: Add timer for Raid
 * - 0.4.8: Add timer for Junk Store limit
 * - 0.4.7: Fix container width for mobile in Settings page.
 * - 0.4.6: Add ZH icon to statusbar that points to new Settings page.
 *          Add setting for toggling extra nav menu on or off.
 *          Include cash on hand when calculating networth.
 * - 0.4.5: Avoid duplicate inventory networth elements
 *          Less padding for item values in inventory to fit better on mobile.
 * - 0.4.4: Change homepage and downloadURL to use greasyfork.org + change icon to zed.city favicon.
 * - 0.4.3: Use navigation navigate eventlistener instead to detect page change.
 * - 0.4.2: Try to force window eventlistener for urlchange to work on mobile.
 * - 0.4.1: Show warning if market values has not been cached yet. 
 *          Show warning on Radio Tower if the cached data is old.
 *          Indicate if the trade is good or bad with checkmark on Radio Tower.
 *          Fixed bug on inventory page where it would potentially not update prices if changing to next page in inventorylist.
 * - 0.4: Add value of trades at Radio Tower.
 * - 0.3: Fix bug in gym autopopulate + add new autopopulate in junk store for 360 items.
 * - 0.2: Add feature to autopopulate gym input fields.
 * - 0.1: Initial release.
*/

(function() {
    'use strict';

    // Add CSS for displaying prices (optional, but makes it look nicer)
    GM_addStyle(`
        .market-price {
            color:#999999;
            float:right;
            position:absolute;
            top:18px;
            right:100px;
        }
        .green {
            color: #00cc66;
        }
        .red {
            color: #ff6666;
        }
        .gray {
            color: #888;
        }
        .zedhelper-networth {
            text-align: center;
            margin: 10px auto;
            color: #ccc;
            font-size: 1.6rem;
        }
        .zedhelper-inventory-warning {
            text-align: center;
            margin: 10px auto;
            color: #ccc;
            font-size: 0.8rem;
        }
        .radio-warning {
            text-align: center;
        }
        .zedhelper-timer-bar {
            margin-top:0px;
        }
        .zedhelper-timer-span {
            padding: 0 10px;
        }
        .zedhelper-timer-span a {
            text-decoration:none;
        }
    `);

    const baseApiUrl = 'https://api.zed.city';

    /** Dont modify anything below this line */

    let module = "index";
    let checkForInventoryUpdates = null;








    /** Utils */

    function get(key) {
        return localStorage.getItem(`kvassh.zedhelper.${key}`);
    }
    function set(key, value) {
        localStorage.setItem(`kvassh.zedhelper.${key}`, value);
    }

    function log(msg) {
        const spacer = "          ";
        const ts = new Date();
        console.log("ZedHelper (" + ts.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' } )+ ") " +
            "[" + module + "]" + ((module.length < spacer.length) ? spacer.substring(0, spacer.length - module.length) : "") + ": " +
            (typeof msg === 'object' ? JSON.stringify(msg) : msg));
    }

    function waitForElement(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
    
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
    
            // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function getCodename(itemName) {
        let codename = itemName.toString().toLowerCase().replace(' ', '_').trim().split(/\n/)[0];

        let nametable = {
            "arrows": "ammo_arrows",
            "bows": "ammo_bows",
            "logs": "craft_log",
            "nails": "craft_nails",
            "rope": "craft_rope",
            "scrap": "craft_scrap",
            "wire": "craft_wire",
            "army_helmet": "defense_army_helmet",
            "camo_hat": "defense_camo_hat",
            "camo_vest": "defense_camo_vest",
            "e-cola": "ecola",
            "lighter":"misc_lighter",
            "lockpick":"misc_lockpick",
            "pickaxe":"misc_pickaxe",
            "security_card":"defense_security_card",
            "zed_coin": "points",
            "baseball_bat": "weapon_baseball_bat",
            "bow":"weapon_bow",
            "chainsaw":"weapon_chainsaw",
            "spear":"weapon_spear",
            "switchblade":"weapon_switchblade",
        };

        for (const [key, value] of Object.entries(nametable)) {
            if (codename === key) {
                return value;
            }
        }
        return codename;
    }

    function formatNumber(number) {
        const formatter = new Intl.NumberFormat('nb-NO', {
            maximumFractionDigits: 0,
            

        });
        return formatter.format(number);
    }


















    /** XHR Interceptor */

    const originalXHR = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (...args) {
        this.addEventListener('load', function () {
            const url = this.responseURL;

            // if (url.includes("/getOffers")) {
            //     const item = JSON.parse(this.responseText)[0];
            //     log(`Caching market value for: ${item['name']} (${item['codename']})`);
            //     set(`mv_${item["codename"]}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() }));
            // }

            if (url.endsWith("/getMarket")) {
                const items = JSON.parse(this.responseText).items;
                let itemsCached = 0;
                for (let item of items) {
                    let codename = getCodename(item["name"]);
                    set(`mv_${codename}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() }));
                    itemsCached++;
                }
                set(`mv_lastupdate`, Date.now());
                log(`Cached market value for ${itemsCached} items.`);
            }

            else if (url.endsWith("/loadItems")) {
                const data = JSON.parse(this.responseText);
                const items = data.items;
                let networthVendor = 0;
                let networthMarket = 0;
                for (let item of items) {

                    networthVendor += item.value * item.quantity;

                    const codename = item.codename;
                    if(get(`mv_${codename}`)) {
                        const mv = JSON.parse(get(`mv_${codename}`));
                        networthMarket += mv.marketValue * item.quantity;
                    } else {
                        networthMarket += item.value * item.quantity;
                    }
                }
                set(`mv_networth_vendor`, networthVendor);
                set(`mv_networth_market`, networthMarket);
                log(`cached inventory networth (vendor: ${networthVendor}, market: ${networthMarket})`);
            }

            else if (url.endsWith("/getStats")) {
                const data = JSON.parse(this.responseText);
                set(`energy`, data.energy);
                set(`morale`, data.morale);
                set(`rad`, data.rad);
                set(`refills`, data.refills);
                set(`money`, data.money);
                set(`xpUntilNextRank`, parseInt(data.xp_end-data.experience));

                set(`raidCooldownSecondsLeft`, data.raid_cooldown); 
                set(`raidCooldownTime`, Date.now()); 
            }

            else if (url.endsWith("/getRadioTower")) {
                const data = JSON.parse(this.responseText);
                saveCurrentTradeValues(data);
                set(`radio_lastupdate`, Date.now());
            }

            else if (url.endsWith("/getStore?store_id=junk")) {
                const data = JSON.parse(this.responseText);
                if (data.hasOwnProperty('limits')) {
                    set(`junkStoreLimitSecondsLeft`, data.limits.reset_time); 
                    set(`junkStoreLimitTime`, Date.now()); 
                } else {
                    set(`junkStoreLimitSecondsLeft`, 0); 
                    set(`junkStoreLimitTime`, 0); 
                }
            }

        });
        originalXHR.apply(this, args);
    };


















    /** Main script */

    log("Starting up ZedHelper!");

    let navigationTimeout = null;

    let urlChangeHandler = async () => {
        if (navigationTimeout === null) {

            const page = location.pathname;

            // Ensure we dont watch for inventory updates after changing subpage
            clearInterval(checkForInventoryUpdates);
            checkForInventoryUpdates = null;

            // Update the timer bar
            addZedHelperIconAndTimerBar();

            if (page.includes("inventory")) {
                module = "inventory";
                // log("Waiting for inventory list...");
                
                waitForElement("#q-app > div > div.q-page-container > main > div > div:nth-child(4) > div > div.grid-cont.no-padding").then(() => {
                    waitForElement(".item-row").then(() => {
                        log("Inventory list loaded! Adding market prices...");
                        addMarketPrices();
                    });
                });

                waitForElement("#q-app > div > div.q-page-container > main > div").then(() => {
                    showNetworth();
                });
            }
            else if (page.includes("market-listings")) {
                module = "market";
                log("Navigated to Market Listings - Watching for element to add new listing...");
                waitForElement("div > div > button.q-btn.q-btn-item.bg-positive").then(() => {
                    log("Detected form for adding new market listing... showing market values for inventory!");
                    addMarketPrices();
                })
            }
            else if (page.includes("stronghold/2375014")) {
                module = "gym";
                log("Navigated to Gym");
                autoPopulateTrainInput();
            }
            else if (page.includes("stronghold/2375016")) {
                module = "crafting";
                log("Navigated to Crafting Bench");
            }
            else if (page.includes("stronghold/2375017")) {
                module = "furnace";
                log("Navigated to Furnace");
            }
            else if (page.includes("stronghold/2375019")) {
                module = "radio";
                log("Navigated to Radio Tower");
                showTradeValues();
            }
            else if (page.includes("/store/junk")) {
                module = "store";
                log("Setting up auto input for junk store - 360 items");
                autoPopulate360Items();
            }
            else if (page.includes("/zedhelper")) {
                showSettingsPage();
            }
            else {
                module = "unknown";
                log(`Unknown subpage: ${page}`);
            }

            navigationTimeout = setTimeout(() => {
                clearTimeout(navigationTimeout);
                navigationTimeout = null;
            }, 250);
        }
    }

    // try {
    //     window.addEventListener('urlchange', async (event) => {
    //         urlChangeHandler();
    //     });
    // } catch (error) {
    //     log("FATAL ERROR: Could not add EventListener for window urlchange: " + JSON.stringify(error));
    // }

    try {
        navigation.addEventListener('navigate', () => {
            setTimeout(() => {
                urlChangeHandler();
            },100);
        });
    } catch (error) {
        log("FATAL ERROR: Could not add EventListener for navigation navigate: " + JSON.stringify(error));
    }













    /** Add a second nav menu with some useful shortcuts */

    // document.querySelector("#q-app > div > header > div:nth-child(2) > div > div > div").app
    const secondNavBar = document.createElement('div');
    secondNavBar.innerHTML = `
<div>
    <div class="gt-xs bg-grey-3 text-grey-5 text-h6">
        <div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside q-tabs--mobile-with-arrows q-tabs--dense" role="tablist" inside-arrows="">

            <div class="q-tabs__content scroll--mobile row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center">

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375017">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                        <div class="q-tab__label">Furnace</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>
                
                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375014">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                        <div class="q-tab__label">Gym</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/scavenge/2">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Scrapyard</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/market">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Market</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/store/junk">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Junk Store</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/zedhelper">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Settings</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

            </div>
        </div>
    </div>
    `;  

    if(get('extraNavMenu') && (get('extraNavMenu') === 'true' || get('extraNavMenu') === true)) {
        log("Enabling extra navigation menu");
        waitForElement("#q-app > div > header").then(() => {
            document.querySelector("#q-app > div > header").appendChild(secondNavBar);
        });
    }











    /** Add icon for ZedHelper settings + timer bar */

    function addZedHelperIconAndTimerBar() {

        const zedHelperIcon = document.createElement('div');
        zedHelperIcon.classList = 'zedhelper-icon-bar';
        zedHelperIcon.innerHTML = `
    <div class="row items-center">
        <b><a href="/zedhelper" title="ZedHelper Settings" style="color:dodgerblue;text-decoration:none;font-weight:bold;">ZH</a></b>
    </div>
        `;

        const timerBar = document.createElement('div');
        timerBar.classList = "row q-col-gutter-md justify-center items-center zedhelper-timer-bar";

        let timeDiff = 0;
        let timeLeft = 0;
        let timeLeftFormatted = "";

        let html = `<div class="q-tab__label">`;

        const raidCooldownSecondsLeft = get('raidCooldownSecondsLeft');
        const raidCooldownTime = get('raidCooldownTime');
        if (raidCooldownSecondsLeft && raidCooldownTime) {
            timeDiff = (Date.now() - raidCooldownTime)/1000;
            timeLeft = raidCooldownSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span">Raid: <a href="/raids">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;

        const junkStoreLimitSecondsLeft = get('junkStoreLimitSecondsLeft');
        const junkStoreLimitTime = get('junkStoreLimitTime');
        if (junkStoreLimitSecondsLeft && junkStoreLimitTime) {
            timeDiff = (Date.now() - junkStoreLimitTime)/1000;
            timeLeft = junkStoreLimitSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span">Junk Store: <a href="/store/junk">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;

        html += `</div>`;

        timerBar.innerHTML = html;

        const selector = ".q-col-gutter-md.justify-center.items-center.currency-stats";
        log("Searching for statusbar...");
        waitForElement(selector).then((el) => {

            try {
                document.querySelectorAll('.zedhelper-icon-bar').entries().forEach((entry) => {
                    log("Remove entry of icon bar: " + entry[1]);
                    entry[1].remove();
                });
                document.querySelectorAll('.zedhelper-timer-bar').entries().forEach((entry) => {
                    log("Remove entry of timer bar: " + entry[1]);
                    entry[1].remove();
                });
            } catch (error) {
                // eat exception
            }
            log("Appending ZedHelper icon to statusbar + adding new bar for timers!");
            el.appendChild(zedHelperIcon);
            el.parentElement.appendChild(timerBar);

            // document.querySelector(selector).parentElement.appendChild(zedHelperIcon);
        });
    }
    // addZedHelperIconAndTimerBar();









    /** Settings page */

    function showSettingsPage() {
        const selector = "#q-app > div > div.q-page-container > div";
        waitForElement(selector).then((el) => {
            el.style.top = "40%";
            el.style.width = "90%";
            el.style.border = "2px inset #333";
            el.style.padding = "10px";
            el.innerHTML = `
            <h3>ZedHelper Settings</h3>
            <p>Userscript written by <a href="https://www.zed.city/profile/12853">Kvassh</a><br>
            For any questions or feedback, please reach out to me in Zed City or Discord.</p>

            <br><br><hr><br>
            
            <div style="text-align:left;">
                <label>Enable extra nav menu? <input type="checkbox" id="extraNavMenu" name="extraNavMenu" value="true" ${get('extraNavMenu') === true | get('extraNavMenu') === 'true' ? 'checked' : ''}></label>
            </style>

            <br><br>
            <div id="zedhelper-settings-output" style="height:50px; display:block;"> </div>
            
            `;
            setTimeout(() => {
                document.querySelector("#extraNavMenu").addEventListener('change', (event) => {
                    set('extraNavMenu', event.target.checked);
                    document.querySelector('#zedhelper-settings-output').innerHTML = '<b class="green">Settings saved! &check;<br>You might need to refresh page for some settings like the extra nav menu.</b>';
                    setTimeout(() => {
                        document.querySelector('#zedhelper-settings-output').innerHTML = " ";
                    },1000);
                });
            }, 100);
        });
    }










    /** Gym functions */

    function autoPopulateTrainInput() {

        const energy = get("energy");
        if (energy > 5) {
            const trainsAvailable = Math.floor(energy/5);
            log(`Current energy: ${energy} - Autopopulating ${trainsAvailable} into the input fields`);

            waitForElement("input.q-field__native").then(() => {
                
                const inputs = document.querySelectorAll("input.q-field__native");
                for (let input of inputs) {
                    input.value = trainsAvailable;
                    input.dispatchEvent(new Event("input", { bubbles: true }));
                }

            });
        } else {
            log("Current energy is 5 or lower, don't autopopulate input fields");
        }
    }
                    




    /** Radio Tower functions */
    function showTradeValues() {
        try {

            const timeDiff = get("radio_lastupdate") ? (Date.now() - get("radio_lastupdate"))/1000 : 0;
            log(`Trade values last updated: ${timeDiff} sec ago`);
            if (timeDiff > 60*60*12) {
                log("Trade values are old. Please visit the Radio Tower to cache new values.");

                const el = document.createElement('div');
                el.classList.add('radio-warning');
                waitForElement("div.overlay-cont").then(() => {
                    const container = document.querySelector("div.overlay-cont");
                    // document.querySelector("#q-app > div > div.q-page-container > main > div > div:nth-child(11) > div.overlay-cont > div > div > div > div > div.text-center.text-no-bg-light.subtext-large.q-my-md")
                    el.innerHTML = `Radio trades data are old - please refresh <a href="stronghold/2375019">Radio Tower</a> to cache new values.`;
                    container.prepend(el);
                });
                return;
            }

            const trades = JSON.parse(get(`tradeValues`));
            // [{"give":96,"return":460},{"give":1425,"return":11900},{"give":3000,"return":2380}]
            log("Current trades to show:");
            log(trades);

            waitForElement(".q-pa-md").then(() => {
                const tradeContainers = document.querySelectorAll(".q-pa-md");
                let i = 0;
                for (let tradeContainer of tradeContainers) {
                    const valueEl = document.createElement('div');
                    valueEl.classList.add('trade-value');
                    valueEl.innerHTML = `
                    <div style="float:left;">
                        ${trades[i].giveqty} items worth<br><span class="red">$</span> ${formatNumber(trades[i].give)}
                    </div>
                    <div style="float:right;">
                        ${trades[i].returnqty} items worth<br><span class="green">$</span> ${formatNumber(trades[i].return)}
                    </div>
                    <div style="clear:both;font-size:1.2rem;">
                        ${parseInt(trades[i].return) > parseInt(trades[i].give) ? '<span class="green">&check;</span>' : '<span class="red">&cross;</span>'}
                    </div>
                    `;
                    tradeContainer.appendChild(valueEl);
                    i++;
                }
            });
        } catch(error) {
            log("No trade values found");
        }
    }

    function saveCurrentTradeValues(data) {
        try {
            const trades = [];
            for (let trade of data.items) {
                // trade -> vars -> items -> <item_requirement_1> -> codename/req_qty
                // trade -> vars -> output -> <item_list-1> -> codename/quantity
                let worthGive = 0;
                let worthReturn = 0;
                let qtyGive = 0;
                let qtyReturn = 0;
                const items = trade.vars.items;
                Object.keys(items).forEach( (key,val) => {
                    const marketValue = JSON.parse(get(`mv_${items[key].codename}`)).marketValue;
                    worthGive += (marketValue*items[key].req_qty);
                    qtyGive += items[key].req_qty;
                });
                const output = trade.vars.output;
                Object.keys(output).forEach( (key,val) => {
                    const marketValue = JSON.parse(get(`mv_${output[key].codename}`)).marketValue;
                    worthReturn += (marketValue*output[key].quantity);
                    qtyReturn += output[key].quantity;
                });
                log(`Trade: ${trade.name} - Give: ${worthGive} - Return: ${worthReturn}`);
                trades.push({ "give": worthGive, "return": worthReturn, "giveqty": qtyGive, "returnqty": qtyReturn });
            }
            set(`tradeValues`, JSON.stringify(trades));
        } catch(error) {
            log("Error saving trade values");
            set(`tradeValues`, null);
        }
    }



    /** Store functions */

    function autoPopulate360Items() {
        const selector = "input[type=number].q-placeholder";
        waitForElement(selector).then(() => {
            const el = document.querySelector(selector);
            el.value = 360;
            el.dispatchEvent(new Event("input", { bubbles: true }));
        });
    }



    /** Functions related to market/inventory */
    
    // async function fetchMarketPrice(itemCodename) {
    //     try {
    //         const response = await fetch(`${baseApiUrl}/getOffers?item=${itemCodename}`, {
    //             "headers": {
    //                 "accept": "application/json, text/plain, */*",
    //                 "accept-language": "en-US,en;q=0.9,no;q=0.8",
    //                 "content-type": "application/json",
    //                 "cookie": document.cookie, // Use current cookies from the browser
    //                 "Referer": "https://www.zed.city/"
    //             },
    //             "body": "{\"page\":1,\"descending\":false}",
    //             "method": "POST"
    //         });

    //         if (!response.ok) {
    //             log(`Error fetching price for ${itemCodename}: ${response.status}`);
    //             return null;
    //         }

    //         const data = await response.json();
    //         if (data && data.length > 0) {  // Check if there are any offers
    //             return data[0].market_price; // Return the first offer's price
    //         } else {
    //           return null; // No offers found
    //         }
    //     } catch (error) {
    //         log(`Error fetching price for ${itemCodename}: ${error}`);
    //         return null;
    //     }
    // }


    // Function to process inventory items and add prices
    async function addMarketPrices() {

        const items = document.querySelectorAll('.item-row');

        if (!items) {
          log("No inventory items found. Check your selectors.");
          return;
        }

        const mvLastUpdateEl = document.createElement('div');
        mvLastUpdateEl.classList.add('zedhelper-inventory-warning');
        const mvLastUpdated = get('mv_lastupdate');
        if (mvLastUpdated) {
            const timeDiff = (Date.now() - mvLastUpdated)/1000;
            log(`Market values last updated: ${timeDiff} sec ago`);
            if (timeDiff > 60*60*24) {
                log("Market values are older than 24 hours. Please visit the market page to cache new values.");
                mvLastUpdateEl.innerHTML = `Market values are older than a day - please visit the <a href="market">Market</a> page to cache new values.`;
            }
        }
        else {
            log("Market values not cached. Please visit the market page to cache values.");
            mvLastUpdateEl.innerHTML = `
            Market value has not been cached yet.<br>
            Please visit the <a href="market">Market</a> page first to calculate worth on your inventory.
            `;
        }
        const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)";
        waitForElement(selector).then(() => {
            document.querySelector(selector).prepend(mvLastUpdateEl);
        });

        // Delete any existing market value elements
        const existingMarketValues = document.querySelectorAll('.market-price');
        for (let mvEl of existingMarketValues) {
            mvEl.remove();
        }

        for (let item of items) {

            const codename = getCodename(item.querySelector('.q-item__label').innerText);
            let qty = 1;
            try {
                qty = item.querySelector('.item-qty').innerText;
                if (qty.includes("%")) {
                    qty = 1;
                } else {
                    qty = parseInt(qty.replace(/[^0-9]/g, ''));
                }
            } catch (error) {
                // eat exception
            }
            if (Number.isNaN(qty)) {
                qty = 1;
            }
            log(`Adding market value for ${codename} x ${qty}`);
            
            let data = null;
            if(get(`mv_${codename}`)) {
                data = JSON.parse(get(`mv_${codename}`));
            } 

            const priceElement = document.createElement('span');
            priceElement.classList.add('market-price');
            
            if (data !== null) {
                const datetime = new Date(data.tz).toISOString();
                priceElement.innerHTML = `<span title="${datetime}">
<b class="green">$</b> ${formatNumber(data.marketValue * qty)} 
<small>(<b class="green">$</b> ${formatNumber(data.marketValue)})</small>
</span>`;
            } else {
                priceElement.innerHTML = `<span class="gray">N/A</span>`;
            }
            item.querySelector('.q-item__label').appendChild(priceElement);
        }

        // Setup interval to check if inventory list changes
        let firstItemRowCodename = "";
        try {
            firstItemRowCodename = getCodename(items[0].querySelector('.q-item__label').innerText);
        } catch (error) {
            // eat exception
        }

        checkForInventoryUpdates = setInterval(() => {
            let newItems = document.querySelectorAll('.item-row');
            if (newItems.length !== items.length) {
                log("Inventory list has changed. Updating prices...");
                clearInterval(checkForInventoryUpdates);
                checkForInventoryUpdates = null;
                addMarketPrices();
                return;
            }
            let newFirstItemRowCodename = ""; 
            try {
                newFirstItemRowCodename = getCodename(newItems[0].querySelector('.q-item__label').innerText);
            } catch (error) {
                // eat exception
            }
            if (firstItemRowCodename != newFirstItemRowCodename) {
                log("Inventory list has changed. Updating prices...");
                clearInterval(checkForInventoryUpdates);
                checkForInventoryUpdates = null;
                addMarketPrices();
                return;
            }
        },250);
    }
    function showNetworth() {
        const networthVendor = get(`mv_networth_vendor`) || 0;
        const networthMarket = get(`mv_networth_market`) || 0;
        const networthCash = get(`money`) || 0;
        const networth = parseInt(networthMarket) + parseInt(networthCash);

        const existingElement = document.querySelector('.zedhelper-networth');
        if (existingElement) {
            existingElement.remove8();
        }

        const el = document.createElement('div');
        el.classList.add('zedhelper-networth');
        
        el.innerHTML = `
        Networth:
        <span title="Value of items only if sold to vendor: $ ${formatNumber(networthVendor)}">
            <b class="green">$</b> ${formatNumber(networth)}
        </span>
        `;
        
        const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)";
        waitForElement(selector).then(() => {
            document.querySelector(selector).prepend(el);
        });
    }

    
})();

































/* EXAMPLE RESPONSE /getOffers:
    [
        {
            "name":"Advanced Tools",
            "codename":"advanced_tools",
            "type":"resources_craft_basic",
            "quantity":2,
            "value":10,
            "vars":{
                "buy":10,"sell":5,"desc":"","weight":"1","ash_value":"20"
            },
            "market_id":15490,
            "market_price":19500,
            "quantity_sold":3,
            "user":{
                "id":11703,"username":"bump","avatar":"","online":1739285402
            }
        },
    ]
*/

/* EXAMPLE RESPONSE /getMarket
{
    "items": 
    [
        {
            "name":"Advanced Tools",
            "codename":"advanced_tools",
            "type":"resources_craft_basic",
            "quantity":35,
            "value":10,
            "vars": {
                "buy":10,
                "sell":5,
                "desc":"",
                "weight":"1",
                "ash_value":"20"
            },
            "market_id":14020,
            "market_price":19500,
            "quantity_sold":0
        },      
    ]
}   

*/