Markethunt plugin for Mousehunt

Adds a price chart and Markethunt integration to the MH marketplace screen.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Markethunt plugin for Mousehunt
// @author       Program
// @namespace    https://greasyfork.org/en/users/886222-program
// @license      MIT
// @version      1.7.0
// @description  Adds a price chart and Markethunt integration to the MH marketplace screen.
// @resource     jq_confirm_css https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.css
// @resource     jq_toast_css https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.css
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-confirm/3.3.2/jquery-confirm.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-toast-plugin/1.3.2/jquery.toast.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/highcharts/9.3.2/highstock.min.js
// @include      https://www.mousehuntgame.com/*
// @grant        GM_addStyle
// @grant        GM_getResourceText
//
// ==/UserScript==

const markethuntDomain = 'markethunt.win';
const markethuntApiDomain = 'api.markethunt.win';

MutationObserver =
    window.MutationObserver ||
    window.WebKitMutationObserver ||
    window.MozMutationObserver;

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

const RoundToIntLocaleStringOpts = {
    maximumFractionDigits: 0
}

const isDarkMode = (() => {
    let isDark = null;

    return () => {
        if (isDark == null) {
            isDark = !!getComputedStyle(document.documentElement).getPropertyValue('--mhdm-white');
        }

        return isDark;
    }
})();

/*******************************
 * 
 *  Plugin settings  
 * 
 *******************************/

class SettingsController {
    // TODO: make settings property private and convert init into static initializer once greasyfork adds support
    static settings;

    static init() {
        let settingsObj = {};

        if (localStorage.markethuntSettings !== undefined) {
            settingsObj = JSON.parse(localStorage.markethuntSettings);
        }

        this.settings = new Proxy(settingsObj, {
            set(obj, prop, value) {
                obj[prop] = value;
                localStorage.markethuntSettings = JSON.stringify(obj);
                return true;
            }
        });
    }

    static getStartChartAtZero() {
        if (this.settings.startChartAtZero === undefined) {
            return false;
        } else {
            return this.settings.startChartAtZero;
        }
    }

    static setStartChartAtZero(value) {
        this.settings.startChartAtZero = value;
    }

    static getEnablePortfolioButtons() {
        if (this.settings.enablePortfolioButtons === undefined) {
            return true;
        } else {
            return this.settings.enablePortfolioButtons;
        }
    }

    static setEnablePortfolioButtons(value) {
        this.settings.enablePortfolioButtons = value;
    }

    static getEnableFloatingVolumeLabels() {
        if (this.settings.enableFloatingVolumeLabels === undefined) {
            return false;
        } else {
            return this.settings.enableFloatingVolumeLabels;
        }
    }

    static setEnableFloatingVolumeLabels(value) {
        this.settings.enableFloatingVolumeLabels = value;
    }

    static getEnableChartAnimation() {
        if (this.settings.enableChartAnimation === undefined) {
            return true;
        } else {
            return this.settings.enableChartAnimation;
        }
    }

    static setEnableChartAnimation(value) {
        this.settings.enableChartAnimation = value;
    }
}

SettingsController.init();

function openPluginSettings() {
    $.alert({
        title: 'Markethunt Plugin Settings',
        content: `
            <div id="markethunt-settings-container">
                <h1>Chart settings</h1>
                <label for="checkbox-start-chart-at-zero" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-start-chart-at-zero" type="checkbox" ${SettingsController.getStartChartAtZero() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Y-axis starts at 0</b><br>
                        Make the Y-axis start at 0 gold/SB
                    </div>
                </label>
                <label for="checkbox-enable-floating-volume-labels" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-enable-floating-volume-labels" type="checkbox" ${SettingsController.getEnableFloatingVolumeLabels() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Volume labels</b><br>
                        Place floating labels indicating volume amount on the left side of the chart
                    </div>
                </label>
                <label for="checkbox-enable-chart-animation" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-enable-chart-animation" type="checkbox" ${SettingsController.getEnableChartAnimation() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Chart animation</b><br>
                        Enable chart animations
                    </div>
                </label>
                <h1>Other settings</h1>
                <label for="checkbox-enable-portfolio-buttons" class="cl-switch markethunt-settings-row">
                    <div class="markethunt-settings-row-input">
                        <input id="checkbox-enable-portfolio-buttons" type="checkbox" ${SettingsController.getEnablePortfolioButtons() ? 'checked' : ''}>
                        <span class="switcher"></span>
                    </div>
                    <div class="label markethunt-settings-row-description">
                        <b>Portfolio quick-add buttons</b><br>
                        Place "Add to portfolio" buttons in your marketplace history and journal log
                    </div>
                </label>
            </div>
        `,
        boxWidth: '450px',
        useBootstrap: false,
        closeIcon: true,
        draggable: true,
        onOpen: function(){
            const startChartAtZeroCheckbox = document.getElementById("checkbox-start-chart-at-zero");
            startChartAtZeroCheckbox.addEventListener('change', function(event) {
                SettingsController.setStartChartAtZero(event.currentTarget.checked);
            });

            const enablePortfolioButtonsCheckbox = document.getElementById("checkbox-enable-portfolio-buttons");
            enablePortfolioButtonsCheckbox.addEventListener('change', function(event) {
                SettingsController.setEnablePortfolioButtons(event.currentTarget.checked);
            });

            const enableFloatingVolumeLabelsCheckbox = document.getElementById("checkbox-enable-floating-volume-labels");
            enableFloatingVolumeLabelsCheckbox.addEventListener('change', function(event) {
                SettingsController.setEnableFloatingVolumeLabels(event.currentTarget.checked);
            });

            const enableChartAnimationCheckbox = document.getElementById("checkbox-enable-chart-animation");
            enableChartAnimationCheckbox.addEventListener('change', function(event) {
                SettingsController.setEnableChartAnimation(event.currentTarget.checked);
            });
        }
    });
}

/*******************************
 * 
 *  Chart functions  
 * 
 *******************************/

// chart vars
const UtcTimezone = "T00:00:00+00:00"

// style
const primaryLineColor = "#4f52aa";
const secondaryLineColor = "#b91a05";
const sbiLineColor = "#00c000"
const volumeColor = "#51cda0";
const volumeLabelColor = '#3ab28a';

const eventBandColor = "#f2f2f2";
const eventBandFontColor = "#888888"; // recommend to have same or close color as yGridLineColor for visual clarity
const xGridLineColor = "#bbbbbb";
const yGridLineColor = "#aaaaaa";
const yGridLineColorLighter = "#dddddd";
const axisLabelColor = "#444444";
const crosshairColor = "#252525";

// dark mode style overrides
const darkMode = {
    backgroundColor: "#222222",
    eventBandColor: "#303030",
    eventBandFontColor: "#909090",
    primaryLineColor: "#9e8dfc",
    crosshairColor: "#e0e0e0"
}

const chartFont = "tahoma,arial,sans-serif";

// set global opts
Highcharts.setOptions({
    chart: {
        style: {
            fontFamily: chartFont,
        },
        spacingLeft: 0,
        spacingRight: 5,
        spacingTop: 7,
        spacingBottom: 6,
    },
    lang: {
        rangeSelectorZoom :""
    },
    plotOptions: {
        series: {
            showInLegend: true,
        },
    },
    // must keep scrollbar enabled for dynamic scrolling, so hide the scrollbar instead
    scrollbar: {
        height: 0,
        buttonArrowColor: "#ffffff00",
    },
    title: {
        enabled: false,
    },
    credits: {
        enabled: false,
    },
    rangeSelector: {
        buttonPosition: {
            y: 5,
        },
        inputEnabled: false,
        labelStyle: {
            color: axisLabelColor,
        },
        verticalAlign: 'top',
        x: -5.5,
    },
    legend: {
        align: 'right',
        verticalAlign: 'top',
        y: -23,
        padding: 0,
        itemStyle: {
            color: '#000000',
            fontSize: "13px",
        },
    },
    tooltip: {
        animation: false,
        shared: true,
        split: false,
        headerFormat: '<span style="font-size: 11px; font-weight: bold">{point.key}</span><br/>',
        backgroundColor: 'rgba(255, 255, 255, 1)',
        hideDelay: 0, // makes tooltip feel more responsive when crossing gap between plots
        style: {
            color: '#000000',
            fontSize: '11px',
            fontFamily: chartFont,
        }
    },
    navigator: {
        height: 25,
        margin: 0,
        maskInside: false,
        enabled: false,
    },
    xAxis: {
        tickColor: xGridLineColor,
        gridLineColor: xGridLineColor,
        labels: {
            style: {
                color: axisLabelColor,
                fontSize: '11px',
            }
        }
    },
    yAxis: {
        gridLineColor: yGridLineColor,
        labels: {
            style: {
                color: axisLabelColor,
                fontSize: '11px',
            },
            y: 3,
        }
    }
});

function setDarkThemeGlobalOpts() {
    Highcharts.setOptions({
        chart: {
            backgroundColor: darkMode.backgroundColor
        },
        legend: {
            itemStyle: {
                color: '#e0e0e0'
            },
            itemHoverStyle: {
                color: '#f0f0f0'
            },
            itemHiddenStyle: {
                color: '#777777'
            },
            title: {
                style: {
                    color: '#c0c0c0'
                }
            }
        },
        xAxis: {
            gridLineColor: '#707070',
            labels: {
                style: {
                    color: '#e0e0e0'
                }
            },
            lineColor: '#707070',
            minorGridLineColor: '#505050',
            tickColor: '#707070',
        },
        yAxis: {
            gridLineColor: '#707070',
            labels: {
                style: {
                    color: '#e0e0e0'
                }
            },
            lineColor: '#707070',
            minorGridLineColor: '#505050',
            tickColor: '#707070',
        },
        tooltip: {
            backgroundColor: '#000000',
            style: {
                color: '#f0f0f0'
            }
        },
        rangeSelector: {
            buttonTheme: {
                fill: '#444444',
                stroke: '#000000',
                style: {
                    color: '#cccccc'
                },
                states: {
                    hover: {
                        fill: '#707070',
                        stroke: '#000000',
                        style: {
                            color: 'white'
                        }
                    },
                    select: {
                        fill: '#000000',
                        stroke: '#000000',
                        style: {
                            color: 'white'
                        }
                    }
                }
            },
        },
    });
}

function UtcIsoDateToMillis(dateStr) {
    return (new Date(dateStr + UtcTimezone)).getTime();
}

function formatSISuffix(num, decimalPlaces) {
    const suffixes = ["", "K", "M", "B"];
    let order = Math.max(Math.floor(Math.log(num) / Math.log(1000)), 0);
    if (order > suffixes.length - 1) {
        order = suffixes.length - 1;
    }
    let significand = num / Math.pow(1000, order);
    return significand.toFixed(decimalPlaces) + suffixes[order];
}

function eventBand(IsoStrFrom, IsoStrTo, labelText) {
    return {
        from: UtcIsoDateToMillis(IsoStrFrom),
        to: UtcIsoDateToMillis(IsoStrTo),
        color: isDarkMode() ? darkMode.eventBandColor : eventBandColor,
        label: {
            text: labelText,
            rotation: 270,
            textAlign: 'right',
            y: 5, // pixels from top of chart
            x: 4, // fix slight centering issue
            style: {
                color: isDarkMode() ? darkMode.eventBandFontColor : eventBandFontColor,
                fontSize: '12px',
                fontFamily: chartFont,
            },
        },
    }
}

function updateEventData() {
    $.getJSON(`https://${markethuntApiDomain}/events?plugin_ver=${GM_info.script.version}`, function (response) {
        localStorage.markethuntEventDatesV2 = JSON.stringify(response);
        localStorage.markethuntEventDatesV2LastRetrieval = Date.now();
    });
}

function renderChartWithItemId(itemId, containerId, forceRender = false) {
    const containerElement = document.getElementById(containerId);

    if (forceRender === false && containerElement.dataset.lastRendered) {
        return;
    }

    itemId = Number(itemId);
    let eventData = [];

    if (localStorage.markethuntEventDatesV2LastRetrieval !== undefined) {
        JSON.parse(localStorage.markethuntEventDatesV2).forEach(event => eventData.push(eventBand(event.start_date, event.end_date, event.short_name)));

        if (Date.now() - Number(localStorage.markethuntEventDatesV2LastRetrieval) > 2 * 86400 * 1000) {
            updateEventData();
        }
    } else {
        updateEventData();
    }

    function renderChart(response) {
        // set HUD
        if (response.market_data.length > 0) {
            const newestPrice = response.market_data[response.market_data.length - 1];
            const utcTodayMillis = UtcIsoDateToMillis(new Date().toISOString().substring(0, 10));

            const priceDisplay = document.getElementById("infoboxPrice");
            const sbPriceDisplay = document.getElementById("infoboxSbPrice");
            const tradeVolDisplay = document.getElementById("infoboxTradevol");
            const goldVolDisplay = document.getElementById("infoboxGoldvol");
            const weeklyVolDisplay = document.getElementById("infobox7dTradevol");
            const weeklyGoldVolDisplay = document.getElementById("infobox7dGoldvol");

            // set gold price
            priceDisplay.innerHTML = newestPrice.price.toLocaleString();

            // set sb price
            try {
                let sbPriceText;
                let sbPrice = newestPrice.sb_price;
                
                if (sbPrice >= 100) {
                    sbPriceText = Math.round(sbPrice).toLocaleString();
                } else {
                    sbPriceText = sbPrice.toFixed(2).toLocaleString();
                }
                sbPriceDisplay.innerHTML = sbPriceText;
            } catch (e) {
                // do nothing
            }

            // set yesterday's trade volume
            let volText = '0';
            if (utcTodayMillis - UtcIsoDateToMillis(newestPrice.date) <= 86400 * 1000 && newestPrice.volume !== null) {
                volText = newestPrice.volume.toLocaleString();
            }
            tradeVolDisplay.innerHTML = volText;

            // set yesterday's gold volume
            let goldVolText = '0';
            if (utcTodayMillis - UtcIsoDateToMillis(newestPrice.date) <= 86400 * 1000 && newestPrice.volume !== null) {
                goldVolText = formatSISuffix(newestPrice.volume * newestPrice.price, 2);
            }
            goldVolDisplay.innerHTML = goldVolText;

            // set last week's trade volume
            let weeklyVolText = response.market_data.reduce(function(sum, dataPoint) {
                if (utcTodayMillis - UtcIsoDateToMillis(dataPoint.date) <= 7 * 86400 * 1000) {
                    return sum + (dataPoint.volume !== null ? dataPoint.volume : 0);
                } else {
                    return sum;
                }
            }, 0);
            weeklyVolDisplay.innerHTML = weeklyVolText.toLocaleString();

            // set last week's gold volume
            let weeklyGoldVol = response.market_data.reduce(function(sum, dataPoint) {
                if (utcTodayMillis - UtcIsoDateToMillis(dataPoint.date) <= 7 * 86400 * 1000) {
                    return sum + (dataPoint.volume !== null ? dataPoint.volume * dataPoint.price : 0);
                } else {
                    return sum;
                }
            }, 0);
            weeklyGoldVolDisplay.innerHTML = (weeklyGoldVol === 0) ? '0' : formatSISuffix(weeklyGoldVol, 2);
        }

        // process data for highcharts
        var dailyPrices = [];
        var dailyVolumes = [];
        var dailySbPrices = [];
        for (var i = 0; i < response.market_data.length; i++) {
            dailyPrices.push([
                UtcIsoDateToMillis(response.market_data[i].date),
                Number(response.market_data[i].price)
            ]);
            dailyVolumes.push([
                UtcIsoDateToMillis(response.market_data[i].date),
                Number(response.market_data[i].volume)
            ]);
            dailySbPrices.push([
                UtcIsoDateToMillis(response.market_data[i].date),
                Number(response.market_data[i].sb_price)
            ]);
        }

        if (isDarkMode()) {
            setDarkThemeGlobalOpts();
        }

        // Create the chart
        let chart = new Highcharts.stockChart(containerId, {
            chart: {
                // zoom animations
                animation: SettingsController.getEnableChartAnimation() ? { 'duration': 500 } : false,
            },
            plotOptions: {
                series: {
                    // initial animation
                    animation: SettingsController.getEnableChartAnimation() ? { 'duration': 900 } : false,
                    dataGrouping: {
                        enabled: itemId === 114,
                        units: [['day', [1]], ['week', [1]]],
                        groupPixelWidth: 3,
                    },
                },
            },
            rangeSelector: {
                buttons: [
                    {
                        type: 'month',
                        count: 1,
                        text: '1M'
                    }, {
                        type: 'month',
                        count: 3,
                        text: '3M'
                    }, {
                        type: 'month',
                        count: 6,
                        text: '6M'
                    }, {
                        type: 'year',
                        count: 1,
                        text: '1Y',
                    }, {
                        type: 'all',
                        text: 'All'
                    },
                ],
                selected: 3,
            },
            legend: {
                enabled: true
            },
            tooltip: {
                xDateFormat: '%b %e, %Y',
            },
            series: [
                {
                    name: 'Average price',
                    id: 'dailyPrice',
                    data: dailyPrices,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 0,
                    color: isDarkMode() ? darkMode.primaryLineColor : primaryLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString()}g</b><br/>`;
                        },
                    },
                    zIndex: 1,
                }, {
                    name: 'Volume',
                    type: 'column',
                    data: dailyVolumes,
                    pointPadding: 0, // disable point and group padding to simulate column area chart
                    groupPadding: 0,
                    yAxis: 2,
                    color: volumeColor,
                    tooltip: {
                        pointFormatter: function() {
                            let volumeAmtText = this.y !== 0 ? this.y.toLocaleString() : 'n/a';
                            return `<span style="color:${this.color}">\u25CF</span>`
                                    + ` ${this.series.name}:`
                                    + ` <b>${volumeAmtText}</b><br/>`;
                        },
                    },
                    zIndex: 0,
                }, {
                    name: 'SB Price',
                    id: 'sbi',
                    data: dailySbPrices,
                    visible: false,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 1,
                    color: sbiLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            let sbiText;

                            if (this.y >= 1000) {
                                sbiText = Math.round(this.y).toLocaleString();
                            } else if (this.y >= 100) {
                                sbiText = this.y.toFixed(1).toLocaleString();
                            } else if (this.y >= 10) {
                                sbiText = this.y.toFixed(2).toLocaleString();
                            } else {
                                sbiText = this.y.toFixed(3).toLocaleString();
                            }
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` SB Index:`
                                + ` <b>${sbiText} SB</b><br/>`;
                        },
                    },
                    zIndex: 2,
                },
            ],
            yAxis: [
                {
                    min: SettingsController.getStartChartAtZero() ? 0 : null,
                    labels: {
                        formatter: function() {
                            return this.value.toLocaleString() + 'g';
                        },
                        x: -8,
                    },
                    showLastLabel: true, // show label at top of chart
                    crosshair: {
                        dashStyle: 'ShortDot',
                        color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
                    },
                    opposite: false,
                    alignTicks: false, // disabled, otherwise autoranger will create too large a Y-window
                }, {
                    min: SettingsController.getStartChartAtZero() ? 0 : null,
                    gridLineWidth: 0,
                    labels: {
                        formatter: function() {
                            return this.value.toLocaleString() + ' SB';
                        },
                        x: 5,
                    },
                    showLastLabel: true, // show label at top of chart
                    opposite: true,
                    alignTicks: false,
                }, {
                    top: '75%',
                    height: '25%',
                    offset: 0,
                    min: 0,
                    opposite: false,
                    tickPixelInterval: 35,
                    allowDecimals: false,
                    alignTicks: false,
                    gridLineWidth: 0,
                    labels: {
                        enabled: SettingsController.getEnableFloatingVolumeLabels(),
                        align: 'left',
                        x: 0,
                        style: {
                            color: volumeLabelColor,
                        },
                    },
                    showLastLabel: true,
                    showFirstLabel: false,
            }],
            xAxis: {
                type: 'datetime',
                ordinal: false, // show continuous x axis if dates are missing
                plotBands: eventData,
                crosshair: {
                    dashStyle: 'ShortDot',
                    color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
                },
                dateTimeLabelFormats:{
                    day: '%b %e',
                    week: '%b %e, \'%y',
                    month: '%b %Y',
                    year: '%Y'
                },
                tickPixelInterval: 120,
            },
        });

        containerElement.dataset.lastRendered = Date.now().toString();
    }

    $.getJSON(`https://${markethuntApiDomain}/items/${itemId}?plugin_ver=${GM_info.script.version}`, function (response) {
        renderChart(response);
    });
}

function renderStockChartWithItemId(itemId, containerId, forceRender = false) {
    const containerElement = document.getElementById(containerId);

    if (forceRender === false && containerElement.dataset.lastRendered) {
        return;
    }

    itemId = Number(itemId);
    let eventData = [];

    if (localStorage.markethuntEventDatesV2LastRetrieval !== undefined) {
        JSON.parse(localStorage.markethuntEventDatesV2).forEach(event => eventData.push(eventBand(event.start_date, event.end_date, event.short_name)));
    }

    function renderStockChart(response) {
        const bid_data = [];
        const ask_data = [];
        const supply_data = [];

        response.stock_data.forEach(x => {
            bid_data.push([x.timestamp, x.bid]);
            ask_data.push([x.timestamp, x.ask]);
            supply_data.push([x.timestamp, x.supply]);
        })

        if (isDarkMode()) {
            setDarkThemeGlobalOpts();
        }

        // Create the chart
        let chart = new Highcharts.stockChart(containerId, {
            chart: {
                // zoom animations
                animation: SettingsController.getEnableChartAnimation() ? { 'duration': 500 } : false,
            },
            plotOptions: {
                series: {
                    // initial animation
                    animation: SettingsController.getEnableChartAnimation() ? { 'duration': 900 } : false,
                    dataGrouping: {
                        enabled: true,
                        units: [['hour', [2, 4, 6]], ['day', [1]], ['week', [1]]],
                        groupPixelWidth: 2,
                        dateTimeLabelFormats: {
                            hour: ['%b %e, %Y %H:%M UTC', '%b %e, %Y %H:%M UTC'],
                            day: ['%b %e, %Y']
                        }
                    },
                },
            },
            rangeSelector: {
                buttons: [
                    {
                        type: 'day',
                        count: 7,
                        text: '7D'
                    }, {
                        type: 'month',
                        count: 1,
                        text: '1M'
                    }, {
                        type: 'month',
                        count: 3,
                        text: '3M'
                    }, {
                        type: 'month',
                        count: 6,
                        text: '6M'
                    }, {
                        type: 'year',
                        count: 1,
                        text: '1Y'
                    }, {
                        type: 'all',
                        text: 'All'
                    },
                ],
                selected: 1,
            },
            legend: {
                enabled: true
            },
            tooltip: {
                xDateFormat: '%b %e, %Y %H:%M UTC',
            },
            series: [
                {
                    name: 'Ask',
                    id: 'ask',
                    data: ask_data,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 0,
                    color: isDarkMode() ? darkMode.primaryLineColor : primaryLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}g</b><br/>`;
                        },
                    },
                    zIndex: 1,
                }, {
                    name: 'Bid',
                    id: 'bid',
                    data: bid_data,
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 0,
                    color: secondaryLineColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}g</b><br/>`;
                        },
                    },
                    zIndex: 2,
                }, {
                    name: 'Supply',
                    id: 'supply',
                    data: supply_data,
                    type: 'area',
                    lineWidth: 1.5,
                    states: {
                        hover: {
                            lineWidthPlus: 0,
                            halo: false, // disable translucent halo on marker hover
                        }
                    },
                    yAxis: 1,
                    color: volumeColor,
                    marker: {
                        states: {
                            hover: {
                                lineWidth: 0,
                            }
                        },
                    },
                    tooltip: {
                        pointFormatter: function() {
                            return `<span style="color:${this.color}">\u25CF</span>`
                                + ` ${this.series.name}:`
                                + ` <b>${this.y.toLocaleString(undefined, RoundToIntLocaleStringOpts)}</b><br/>`;
                        },
                    },
                    zIndex: 0,
                },
            ],
            yAxis: [
                {
                    min: SettingsController.getStartChartAtZero() ? 0 : null,
                    labels: {
                        formatter: function() {
                            return this.value.toLocaleString() + 'g';
                        },
                        x: -8,
                    },
                    showLastLabel: true, // show label at top of chart
                    opposite: false,
                    alignTicks: false
                }, {
                    top: '75%',
                    height: '25%',
                    offset: 0,
                    min: 0,
                    opposite: false,
                    tickPixelInterval: 35,
                    allowDecimals: false,
                    alignTicks: false,
                    gridLineWidth: 0,
                    labels: {
                        enabled: SettingsController.getEnableFloatingVolumeLabels(),
                        align: 'left',
                        x: 0,
                        style: {
                            color: volumeLabelColor,
                        },
                    },
                    showLastLabel: true,
                    showFirstLabel: false,
                }],
            xAxis: {
                type: 'datetime',
                ordinal: false, // show continuous x axis if dates are missing
                plotBands: eventData,
                crosshair: {
                    dashStyle: 'ShortDot',
                    color: isDarkMode() ? darkMode.crosshairColor : crosshairColor,
                },
                dateTimeLabelFormats:{
                    day: '%b %e',
                    week: '%b %e, \'%y',
                    month: '%b %Y',
                    year: '%Y'
                },
                tickPixelInterval: 120,
            }
        });

        containerElement.dataset.lastRendered = Date.now().toString();
    }

    $.getJSON(`https://${markethuntApiDomain}/items/${itemId}/stock?&plugin_ver=${GM_info.script.version}`, function (response) {
        renderStockChart(response);
    });
}

if (localStorage.markethuntEventDatesV2LastRetrieval === undefined) {
    updateEventData();
}

/*******************************
 * 
 *  Marketplace view observer  
 * 
 *******************************/

// chart-icon-90px.png minified with TinyPNG then converted to base 64
const chartIconImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAMAAAAPdrEwAAAAVFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU4H24AAAAG3RSTlMABvTkbYjGz8kamvoO0EYg6H0koqabYVSqNDILUie0AAABU0lEQVRYw+2V2Y6DMAx" +
    "FyUIgwLB1mxn//3+OhEQd6o5EypUqVT6PoBwcbmIXiqIo78b42traG7w5ftFCFeE1L+bFja7b0x0PVtesDmC1ZbWFpS/V2PQnYgIyfXOmBA9MP3KG/HlI+r9uY46vpj+It7f1tQvWhryL3lNC2" +
    "wz/BFhn3/CuoS3taU4CPK2Pv7tcc++IYbkIsDxaMv+W+RpGG9wawQ1Q8lPcT27JF147BTuG69y0qZEDzOsYYeKSm3tEwxP52WR2DMb1BSPlU3bHECULeX6f86JkwWBfU9ep+dIhZ4olpsdOwj2" +
    "bNdVjCzWlowVXmmMDNFYPLbTkVeXBsW/8toW6JPli12Z3QwnFns2i1bxZvJqR6cPUMn2UWqY/gtWUoGqxTJwZlFqeGZhanhmwmhI+Xi3SR6hl+jC1TB+spgRVq1rVqla1qlUNUSuKooD4Az6O4" +
    "MtRLQLhAAAAAElFTkSuQmCC";

// sb.png minified with TinyPNG then converted to base 64
const sbImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAABcVBMVEX+/v/5///+/fzwyJTuxJD1////6cn416z31ab206SOi4ny///t///b" +
    "///J///2/P/9/Pj4+Pjq6/b8+PPw8PPh4+7+9+3K5s//7M7/6Mb+5cP63bfv17b53LP21an1zJzWuprzzJDRrovpuoHmuXzktXu6k3Rd0HOzi2vp///Q//+z//+t///n/v/0+P/q7v/t7v79" +
    "+PjW//P/+/P2+vHZ7PHn6e3/+eft5+f/9ubi8eT/9OPw59//8t377dzh3tz+7dXN7dTa09Sv1tP35tHO1czX4cu+3cLazcLCysL44L3Uw7vDuLj32rar3LGvtbHq167gyK6S16nlyanl06je" +
    "w6jy06fxz6Z8oqSooKDszJ7Ls56MypvoxZqel5fQsZblwJWVkJB0xY3nvI10y4vswIv30IrvwInRp4jxyYeIg4XdtYPQq4LNpH65mnt9e3u+sXr0xHjZq3dZwHTPonR1dXHgqnC6kGtpbGnh" +
    "qWEs0UvWjFe8AAAA4klEQVQY02PACvgYITSvlbo4mCEY4V9awZUf4+ieUqUOFmFK5OKKjMtKCioW9zPRBAowAhFIJUSnFhBrczMwAJGIkKiomQhIkFWHj0GXQc+An4df3yfPlRUoxMNgaGFv" +
    "6uTpHF1SpqIA0StWWaCqzBwlL8+RngFxhnlhSJiblxSbhCRzEViE1ShNWlaGnZMzIFU1HqLLWFGOnZOZmYWFRcUD6g1FFg52DrnY3HINIahIpnJ2jpqGmlJCsjdUJFBJIViGTZJNOjwUKiLr" +
    "KyXhYGtpbediAxURExYWYGIAQgGgDwEEwCDFO/6WiQAAAABJRU5ErkJggg==";

const settingsImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAACKElEQVR4nO2YMUucQRCGH9Cgoi" +
    "YgNolVqhAVm6SyNoGAyP0Q8TT+ABG0C3ZRbMU2gprC4vA/HNx5tQZTxSimCDHKJwsjHEe8b2bX1cV8DwzICTPvu+x+M7tQUFBwl3wFspzYI2G+Kwz8AjpJkCGF+Jt4Q4JMGQzMkiCLBgNfSIhhYAP4" +
    "bTBwBWwCr+5D4AegDiyI2Bu6gU/ApUF4a1wCK0BPy4IsSE1XO4gnQKOlaA1YAg4ChLdGQ3LW/vG70+DN3B2K9I1ZX/EDwEkCBk6BQR8DawmIzyRWreLdQfqbgPCs6bCPWQxUAorVZN+OAL0SI3Ke6g" +
    "F5K1rxJc8Cf4BpoKNNbve/GeDCs0ZJY+A1cOghfkK7QsA7DxOHok3FC6BqSO5W3krZkL8qmkw8A/aVe77dtrkNN1JrmuG+aPGiCziKOFl+zMl9JBqCOMsp0jwfWRlVNLFgrnKK9AXk7ld8/6MbcCJ8" +
    "eXofBk5zirgmlewW6lIcYtdhfZmPeYi1n9F6wGe0Eeszam1kbjyIedeoWhqZzyhxIeOBlvcxR4mSMXGziXLOo1WnrHzUYS50nK5Lhx2VHtEnf88H3qMr/E8XGuQalyUSn/G81P9IQPxP30s9Mmk+tI" +
    "EykR62NE1IG9+A5RgPW+2eFnvkWTDkaTEDtoHnsZ4WNbwE1o1G3IS7C4yTEFsGAzskyJzBgNsiyfHWYGCSBOkAzpUGmg9tUuwpxB8/tMiCgsfENevgYdmM/xZUAAAAAElFTkSuQmCC";

const kofiImageData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAABm1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
    "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AAAD/Xluzs7PIyMj/XV7/Wmz/W2b/XW" +
    "D//f3/WXH/Wm7/W2n/XGT8/Pz/+Pn/XGLQ0ND/W2q6urq3t7esrKympqb/V3j/WHX/+vr19fX/ydDCwsKwsLCioqKMjIx9fX1YWFhKSko7Ozv9/f3//Pz5+fn/9fXm5ub/2tzb29v/0dXNzc3Ex" +
    "MT/pqf/mqX/kKX/n56enp6RkZH/kpD/joz/i4mIiIj/h4b/hIJubm5iYmJNTU0jIyMeHh4PDw8MDAz/4+TS0tL/xcj/u8jFxcW+vr7/rbT/lqz/k6D/jZ3/gZz/i5r/ipmYmJiXl5f/mJaVlZX/" +
    "lJWUlJT/kJP/eYz/comCgoJ5eXl0dHRra2s0NDQsLCwnJycTExMRERH4jQN3AAAAKXRSTlMAiqaR9J+W+/h6dQ+5rI6Jg4BvZ1ZJNycH1svGwmM9MyshCe/ksqE/Gb0RW4gAAAH8SURBVEjH7dR" +
    "nVxpBGIbhiVnpKki1JWrq+4KKi7pgaAqIIGrsGks09vTee//ZbmFndhTQ73p9fu4zs+fsLrlwnvhdl2posnnthNN5DU8h1PuNQSNuh2oY+7jzDy1WwtTjItSWGT/Ahm5D0AOnmfuDJhrU1Qgy28" +
    "XirzEJYGYfvWcJelBRmgFIYOvxQJx/8XrlSRTE7Kuvb7JiOXD4PO1YmobJ3+ZOGiyo+5V0Oj0y8m32bWSgr+/OsqgFdYQE2jEP8B3dNBgGWVZeRyKRAWU9OBgOP6YBuYITACE08cGyYT00FOv/w" +
    "gK78BdgGBv44KW2Dqvr/t7eLRZ0C3s0YA/93LgOBoP3jl0pgSY+eCjfJKatVbM0sLdiCqCAbj6ApZg81tbqAVrQ1uG8jrsSTO9ZuojmZjkY/aSuNfdHQbGAMvNOBmANb7B36SloxQbdP4iCSlor" +
    "bK7OAUDu0OJnQQ400Q19fxc4k6lDs5voTPgM9GJd3W/xeym3i+ZmYgjioJv6oNw/yrbvN38U9xFbbnFfXAgocSm4zvawiDLB4QkQgyZMAiPOTwETQofHZyc8F+ahmnGkd2c68CdUIZXw6sngtnD" +
    "wqOoBbaQCG/4vJOLxeKL8X0kmk6lUfvXd5wkU6AEcqwUra/GRyrqaL+salb+j0+myWm02b4BcOK+OAN6AtFxkFAmAAAAAAElFTkSuQmCC";

const mpObserverTarget = document.querySelector("#overlayPopup");

function CurrentViewedItemId() {
    const itemIdRaw = mpObserverTarget.querySelector(".marketplaceView-item.view")?.dataset.itemId;
    return itemIdRaw ? Number(itemIdRaw) : null;
}

const mpObserver = new MutationObserver(function () {
    // Check if the Marketplace interface is open
    if (!mpObserverTarget.querySelector(".marketplaceView")) {
        return;
    }

    // detect item page and inject chart
    const backButton = mpObserverTarget.querySelector("a.marketplaceView-breadcrumb");
    if (backButton) {
        const targetContainer = mpObserverTarget.querySelector(".marketplaceView-item-description");

        if (targetContainer && !mpObserverTarget.querySelector("#chartArea")) {
            // Disconnect and reconnect later to prevent mutation loop
            mpObserver.disconnect();

            // Setup chart divs
            const itemId = CurrentViewedItemId();

            targetContainer.insertAdjacentHTML(
                "beforebegin",
                `<div id="chartArea" style="display: flex; padding: 0 20px 0 20px; height: 315px;">
                    <div style="flex-grow: 1; position: relative">
                        <div id="chartContainer" style="text-align: center; height: 100%; width: 100%">
                            <img style="opacity: 0.07; margin-top: 105px" src="${chartIconImageData}" alt="Chart icon" class="${isDarkMode() ? 'inverted' : ''}">
                            <div style="color: grey">Loading ...</div>
                        </div>
                        <div id="stockChartContainer" style="text-align: center; position: absolute; background-color: ${isDarkMode() ? darkMode.backgroundColor : 'white'}; top: 0; left: 0; width: 100%; height: 100%; display: none">
                            <img style="opacity: 0.07; margin-top: 105px" src="${chartIconImageData}" alt="Chart icon" class="${isDarkMode() ? 'inverted' : ''}">
                            <div style="color: grey">Loading ...</div>
                        </div>
                    </div>
                    <div id="markethuntInfobox" style="text-align: center; display: flex; flex-direction: column; padding: 34px 0 12px 5px; position: relative;">
                        <div class="marketplaceView-item-averagePrice infobox-stat infobox-small-spans infobox-striped">
                            Trade volume:<br>
                            <span id="infoboxTradevol">--</span><br>
                            <span id="infoboxGoldvol" class="marketplaceView-goldValue">--</span>
                        </div>
                        <div class="marketplaceView-item-averagePrice infobox-stat infobox-small-spans">
                            7-day trade volume:<br>
                            <span id="infobox7dTradevol">--</span><br>
                            <span id="infobox7dGoldvol" class="marketplaceView-goldValue">--</span>
                        </div>
                        <div style="user-select: none; text-align: left">
                            <label class="cl-switch" for="markethuntShowStockData" style="cursor: pointer">
                                <input type="checkbox" id="markethuntShowStockData">
                                <span class="switcher"></span>
                                <span class="label">Stock chart</span>
                            </label>
                        </div>
                        <div style="flex-grow: 1"></div> <!-- spacer div -->
                        <div>
                            <div style="display: flex;">
                                <div style="flex-grow: 1;"></div>
                                <div>
                                    <a id="markethuntSettingsLink" href="#">
                                        <img src="${settingsImageData}" class="markethunt-settings-btn-img ${isDarkMode() ? 'inverted' : ''}">
                                    </a>
                                </div>
                                <div>
                                    <a href="https://ko-fi.com/vsong_program" target="_blank" alt="Donation Link">
                                        <img src="${kofiImageData}" class="markethunt-settings-btn-img" alt="Settings">
                                    </a>
                                </div>
                                <div style="flex-grow: 1;"></div>
                            </div>
                            <div style="font-size: 0.8em; color: grey">v${GM_info.script.version}</div>
                        </div>
                    </div>
                </div>`
            );

            const itemPriceContainer = mpObserverTarget.querySelector(".marketplaceView-item-averagePrice");
            itemPriceContainer.classList.add("infobox-stat");
            itemPriceContainer.insertAdjacentHTML(
                "beforeend",
                `<br><span id="infoboxSbPrice" class="marketplaceView-sbValue">--</span><img style="vertical-align: bottom" src="${sbImageData}" alt="SB icon" />`
            );

            const itemPriceDisplay = itemPriceContainer.querySelector("span");
            itemPriceDisplay.id = "infoboxPrice";

            const infoBox = document.getElementById("markethuntInfobox");
            infoBox.prepend(itemPriceContainer);

            // Set infobox minimum width to prevent layout shifts, *then* reset price display
            const infoBoxInitialWidth = $(infoBox).width();
            infoBox.style.minWidth = `${infoBoxInitialWidth}px`;

            itemPriceDisplay.innerHTML = "--";

            // Set stock chart checkbox listener
            const stockChartCheckbox = document.getElementById('markethuntShowStockData');
            stockChartCheckbox?.addEventListener('change', (e) => {
                if (e.target.checked) {
                    document.getElementById('stockChartContainer').style.display = 'block';
                    renderStockChartWithItemId(itemId, 'stockChartContainer');
                } else {
                    document.getElementById('stockChartContainer').style.display = 'none';
                    renderChartWithItemId(itemId, 'chartContainer');
                }
            });

            // Set Plugin Settings listener
            const settingsLink = document.getElementById("markethuntSettingsLink");
            settingsLink.addEventListener('click', openPluginSettings);

            // Render chart
            renderChartWithItemId(itemId, "chartContainer");

            // Re-observe after mutation-inducing logic
            mpObserver.observe(mpObserverTarget, {
                childList: true,
                subtree: true
            });
        }
    }

    // detect history page and inject portfolio buttons
    const historyTab = mpObserverTarget.querySelector("[data-tab=history].active");
    if (SettingsController.getEnablePortfolioButtons() && historyTab) {
        mpObserver.disconnect();

        let rowElem = mpObserverTarget.querySelectorAll(".marketplaceMyListings tr.buy");
        rowElem.forEach(function(row) {
            if (!row.querySelector(".mousehuntActionButton.tiny.addPortfolio")) {
                let itemElem = row.querySelector(".marketplaceView-itemImage");
                const itemId = itemElem.getAttribute("data-item-id");

                let qtyElem = row.querySelector("td.marketplaceView-table-numeric");
                const qty = Number(qtyElem.innerText.replace(/\D/g, ''));

                let priceElem = row.querySelector("td.marketplaceView-table-numeric .marketplaceView-goldValue");
                const price = Number(priceElem.innerText.replace(/\D/g, ''));

                let buttonContainer = row.querySelector("td.marketplaceView-table-actions");
                let addPortfolioBtn = document.createElement("a");
                addPortfolioBtn.href = `https://${markethuntDomain}/portfolio.php?action=add_position&item_id=${itemId}&add_qty=${qty}&add_mark=${price}`;
                addPortfolioBtn.innerHTML = "<span>+ Portfolio</span>";
                addPortfolioBtn.className = "mousehuntActionButton tiny addPortfolio lightBlue";
                addPortfolioBtn.target = "_blank";
                addPortfolioBtn.style.display = "block";
                addPortfolioBtn.style.marginTop = "2px";
                buttonContainer.appendChild(addPortfolioBtn);
            }
        });

        mpObserver.observe(mpObserverTarget, {
            childList: true,
            subtree: true
        });
    }
});

// Initial observe
mpObserver.observe(mpObserverTarget, {
    childList: true,
    subtree: true
});

const marketplaceCssOverrides = `
.marketplaceView-item {
    padding-top: 10px;
}
.marketplaceView-item-content {
    padding-top: 10px;
    padding-bottom: 0px;
    min-height: 0px;
}
.marketplaceView-item-descriptionContainer {
    padding-bottom: 5px;
    padding-top: 5px;
}
.marketplaceView-item-averagePrice {
    margin-top: 5px;
}
.marketplaceView-item-footer {
    padding-top: 10px;
    padding-bottom: 10px;
}
.markethunt-cross-link {
    color: #000;
    font-size: 10px;
    background: #fff;
    display: block;
    padding: 0.1em 0;
    margin: 0.4em 0;
    box-shadow: #797979 1px 1px 1px 0;
    border-radius: 0.2em;
    border: 1px solid #a8a8a8;
}
.markethunt-cross-link:hover {
    color: white;
    background-color: ${primaryLineColor};
}
.markethunt-settings-btn-img {
    height: 26px;
    padding: 3px;
    margin: 0 5px 0 5px;
    border-radius: 999px;
    box-shadow: 0px 0px 3px gray;
}
.markethunt-settings-btn-img:hover {
    box-shadow: 0px 0px 3px 1px gray;
}
.inverted {
    filter: invert(1);
}
.markethunt-settings-row-input {
    display: flex;
    align-items: center;
    padding-right: 5px;
}
.markethunt-settings-row {
    display: flex;
    padding: 5px;
}
.markethunt-settings-row-description {
}
.marketplaceView-item-averagePrice.infobox-stat {
    text-align: left;
    margin-bottom: 14px;
    white-space: nowrap;
}
.marketplaceView-item-leftBlock .marketplaceHome-block-viewAll {
    margin-top: 5px;
}
.infobox-striped {
}
.infobox-small-spans span {
    font-size: 11px;
}
.infobox-small-spans .marketplaceView-goldValue::after {
    width: 17px;
    height: 13px;
}
`;

const materialSwitchCss = `
.cl-switch input[type="checkbox"] {
    display: none;
    visibility: hidden;
}

.cl-switch .switcher {
    display: inline-block;
    border-radius: 100px;
    width: 2.25em;
    height: 1em;
    background-color: #ccc;
    position: relative;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    vertical-align: middle;
    cursor: pointer;
}

.cl-switch .switcher:before {
    content: "";
    display: block;
    width: 1.3em;
    height: 1.3em;
    background-color: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
    border-radius: 50%;
    margin-top: -0.15em;
    position: absolute;
    top: 0;
    left: 0;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
    margin-right: 0;
    -webkit-transition: all 0.2s;
    -moz-transition: all 0.2s;
    -ms-transition: all 0.2s;
    -o-transition: all 0.2s;
    transition: all 0.2s;
}

.cl-switch .switcher:active:before {
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6), 0 0 0 1em rgba(63, 81, 181, 0.3);
    transition: all, 0.1s;
}

.cl-switch .label {
    cursor: pointer;
    vertical-align: middle;
}

.cl-switch input[type="checkbox"]:checked+.switcher {
    background-color: #8591d5;
}

.cl-switch input[type="checkbox"]:checked+.switcher:before {
    left: 100%;
    margin-left: -1.3em;
    background-color: #3f51b5;
}

.cl-switch [disabled]:not([disabled="false"])+.switcher {
    background: #ccc !important;
}

.cl-switch [disabled]:not([disabled="false"])+.switcher:active:before {
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important;
}

.cl-switch [disabled]:not([disabled="false"])+.switcher:before {
    background-color: #e2e2e2 !important;
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.2) !important;
}`;

/*******************************
 * 
 *  Journal observer  
 * 
 *******************************/

// add_portfolio_journal.png minified with TinyPNG then converted to base 64
const addPfolioBtnImgData = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAALBAMAAACEzBAKAAAAFVBMVEUAAAAAAAD/swD06DD6zhj///8PDgMsru0CAAAAAXRSTlMA" +
      "QObYZgAAADhJREFUCNdjCBQEAwEGYWMwMERmmBkbCoIZhkAobMgg4igo6Cjo4sggpKQIhEqKyAwgQGEwQk0GAIl6DBhSGEjXAAAAAElFTkSuQmCC";

function addJournalButtons(supplyTransferJournalEntries) {
    supplyTransferJournalEntries.forEach(function(supplyTransferEntry) {
        const journalActionsElem = supplyTransferEntry.querySelector(".journalactions");
        const textElem = supplyTransferEntry.querySelector(".journaltext");

        if (journalActionsElem.querySelector("a.actionportfolio")) {
            return;
        }
        if (textElem.textContent.includes("SUPER|brie+") || textElem.textContent.includes("Passing Parcel")) {
            return;
        }
        // Disable button on sending transfers until portfolio sending feature implemented
        if (textElem.textContent.includes("I sent")) {
            return;
        }

        const addPortfolioBtn = document.createElement("a");
        addPortfolioBtn.href = "#";
        addPortfolioBtn.className = "actionportfolio";
        addPortfolioBtn.addEventListener('click', addSbTradeToPortfolio);
        journalActionsElem.prepend(addPortfolioBtn)
    });
}

async function updateItemMetadata() {
    console.log("Retrieving marketplace item data");
    return new Promise((resolve, reject) => {
        hg.utils.Marketplace.getMarketplaceData(
            function (response) {
                const itemMetadata = response.marketplace_items.reduce(
                    function (items, item) {
                        items[normalizeItemName(item.name)] = item.item_id;
                        return items;
                    },
                    {}
                );
                localStorage.markethuntItemMetadata = JSON.stringify(itemMetadata);
                localStorage.markethuntItemMetadataLastRetrieval = Date.now();
                resolve(itemMetadata);
            },
            function (e) {
                reject(e);
            }
        );
    });
}

function normalizeItemName(name) {
    return name.trim();
}

async function addSbTradeToPortfolio(event) {
    event.preventDefault(); // prevent scroll to top

    const targetTransferJournalEntry = event.target.parentNode.parentNode.parentNode;
    const textElem = targetTransferJournalEntry.querySelector(".journaltext");
    const targetEntryId = Number(targetTransferJournalEntry.dataset.entryId);
    // group 1 = qty, group 2 = item name, group 3 = trade partner snuid
    const regex = /^I received (\d[\d,]*) (.+?) from <a href.+snuid=(\w+)/

    // get item and partner data
    const targetEntryMatch = textElem.innerHTML.match(regex);
    const targetItemQty = Number(targetEntryMatch[1].replace(",", ""));
    const targetItemName = targetEntryMatch[2];
    const partnerSnuid = targetEntryMatch[3];
    const partnerName = textElem.querySelector('a').innerHTML;

    // get item ID
    let targetItemId = undefined;
    if (localStorage.markethuntItemMetadata !== undefined) {
        const itemMetadata = JSON.parse(localStorage.markethuntItemMetadata);
        targetItemId = itemMetadata[normalizeItemName(targetItemName)];
    }

    if (targetItemId === undefined) {
        $.toast({
            text: "Please wait ...",
            heading: localStorage.markethuntItemMetadata === undefined ? 'Downloading item data' : 'Reloading item data',
            icon: 'info',
            position: 'top-left',
            loader: false,  // Whether to show loader or not. True by default
        });
        const itemMetadata = await updateItemMetadata();
        await sleep(600); // allow user to read toast before opening new tab
        targetItemId = itemMetadata[normalizeItemName(targetItemName)];
    }

    // detect all sb send entries
    const allSupplyTransferJournalEntries = document.querySelectorAll("#journalContainer div.entry.supplytransferitem");
    const matchingSbSendEntries = Array.from(allSupplyTransferJournalEntries).reduce(
        function(results, journalEntry) {
            const innerHTML = journalEntry.querySelector(".journaltext").innerHTML;
            if (!innerHTML.includes(partnerSnuid)) {
                return results;
            }
            const candidateSbMatch = innerHTML.match(/^I sent (\d[\d,]*) SUPER\|brie\+ to <a href/);
            if (!candidateSbMatch) {
                return results;
            }
            const candidateSbSent = Number(candidateSbMatch[1].replace(",", ""));
            const candidateEntryId = Number(journalEntry.dataset.entryId);
            results.push({sbSent: candidateSbSent, entryId: candidateEntryId});
            return results;
        },
        []
    );

    // choose best sb send entry
    let bestSbSendEntryMatch = null;
    let bestMatchDistance = null;
    matchingSbSendEntries.forEach(function(candidateEntry) {
        const entryPairDistance = Math.abs(targetEntryId - candidateEntry.entryId);
        if (bestMatchDistance === null || bestMatchDistance > entryPairDistance) {
            bestSbSendEntryMatch = candidateEntry;
            bestMatchDistance = entryPairDistance;
        }
    });

    let avgSbPriceString = "none";
    if (bestSbSendEntryMatch !== null) {
        const avgSbPrice = bestSbSendEntryMatch.sbSent / targetItemQty;
        avgSbPriceString = avgSbPrice.toFixed(2);
    }

    // prepare modal message
    let actionMsg = 'Markethunt plugin: ';
    if (bestSbSendEntryMatch !== null) {
        actionMsg += `Found a transfer of ${bestSbSendEntryMatch.sbSent.toLocaleString()} SB to ${partnerName}.` + 
            ` Buy price has been filled in for you.`;
    } else {
        actionMsg += 'No matching SB transfer found. Please fill in buy price manually.';
    }

    // open in new tab
    window.open(`https://${markethuntDomain}/portfolio.php?action=add_position` + 
        `&action_msg=${encodeURIComponent(actionMsg)}` +
        `&item_id=${targetItemId}` + 
        `&add_qty=${targetItemQty}` + 
        `&add_mark=${avgSbPriceString}` +
        `&add_mark_type=sb`,
        '_blank');
}

const journalObserverTarget = document.querySelector("#mousehuntContainer");
const journalObserver = new MutationObserver(function () {
    // Disconnect and reconnect later to prevent mutation loop
    journalObserver.disconnect();

    const journalContainer = journalObserverTarget.querySelector("#journalContainer");
    if (SettingsController.getEnablePortfolioButtons() && journalContainer) {
        // add portfolio buttons
        const supplyTransferJournalEntries = journalContainer.querySelectorAll("div.entry.supplytransferitem");
        addJournalButtons(supplyTransferJournalEntries);
    }

    // Reconnect observer once all mutations done
    journalObserver.observe(journalObserverTarget, {
        childList: true,
        subtree: true
    });
});

// Initial observe
journalObserver.observe(journalObserverTarget, {
    childList: true,
    subtree: true
});

const journalCssOverrides = `
.journalactions a {
    display: inline-block;
}
.journalactions a.actionportfolio {
    margin-right: 5px;
    background: url('${addPfolioBtnImgData}');
    width: 16px;
}
`;

/*******************************
 * 
 *  Import Portfolio  
 * 
 *******************************/

function addTouchPoint() {
    if ($('.invImport').length === 0) {
        const invPages = $('.inventory .torn_pages');
        //Inventory History Button
        const invImportElem = document.createElement('li');
        invImportElem.classList.add('crafting');
        invImportElem.classList.add('invImport');
        const invImportBtn = document.createElement('a');
        invImportBtn.href = "#";
        invImportBtn.innerText = "Export to Markethunt";
        invImportBtn.onclick = function () {
            onInvImportClick();
        };
        const icon = document.createElement("div");
        icon.className = "icon";
        invImportBtn.appendChild(icon);
        invImportElem.appendChild(invImportBtn);
        $(invImportElem).insertAfter(invPages);
    }
}

function submitInv() {
    if (!document.forms["import-form"].reportValidity()) {
        return;
    }

    const gold = Number($('.hud_gold').text().replaceAll(/[^\d]/g, ''));

    if (isNaN(gold)) {
        return;
    }

    const itemsToGet = ['weapon','base', 'trinket', 'bait', 'skin', 'crafting_item','convertible', 'potion', 'stat','collectible','map_piece','adventure']; //future proof this to allow for exclusions
    
    hg.utils.UserInventory.getItemsByClass(itemsToGet, true, function(data) {
        let importData = {
            itemsArray: [],
            inventoryGold: gold
        };
        data.forEach(function(arrayItem, index) {
            importData.itemsArray[index] = [arrayItem.item_id, arrayItem.quantity];
        });

        $('#import-data').val(JSON.stringify(importData));
        document.forms["import-form"].submit();
    })
}

function onInvImportClick(){
    $.dialog({
        title: 'Export inventory to Markethunt',
        content: `
        <form id="import-form" name="import-form" action="https://${markethuntDomain}/import_portfolio.php" method="post" target="_blank">
                <label for="import-portfolio-name">Portfolio name: <span style="color: red">*</span></label>
                <input type="text" id="import-portfolio-name" name="import-portfolio-name" required pattern=".+"/>
                <input type="hidden" id="import-data" name="import-data"/>
        </form>
        <div id="export-dialog-buttons" class="jconfirm-buttons" style="float: none; margin-top: 10px;"><button type="button" class="btn btn-primary">Export</button></div>`,
        boxWidth: '600px',
        useBootstrap: false,
        closeIcon: true,
        draggable: true,
        onOpen: function(){
            $('#import-portfolio-name').val('Portfolio ' + (new Date()).toISOString().substring(0, 10));
            this.$content.find('button').click(function(){
                submitInv();
            });
        }
    });
}

/*******************************
 * 
 *  Final setup and add css  
 * 
 *******************************/

$(document).ready(function() {
    GM_addStyle(GM_getResourceText("jq_confirm_css"));
    GM_addStyle(GM_getResourceText("jq_toast_css"));
    GM_addStyle(marketplaceCssOverrides);
    GM_addStyle(journalCssOverrides);
    GM_addStyle(materialSwitchCss);

    addTouchPoint();

    const supplyTransferJournalEntries = document.querySelectorAll("#journalContainer div.entry.supplytransferitem");
    addJournalButtons(supplyTransferJournalEntries);
});