Path Of Exile Trade Aggregator

Aggregates the number of listings per account name and displays a whisper button for each account name in the Path Of Exile trade site.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Path Of Exile Trade Aggregator
// @namespace   Violentmonkey Scripts
// @match       https://www.pathofexile.com/trade*
// @grant       none
// @version     1.0.2.1
// @author      CerikNguyen
// @license MIT
// @description Aggregates the number of listings per account name and displays a whisper button for each account name in the Path Of Exile trade site.
// ==/UserScript==

// ------------------------------------------------- helper functions -------------------------------------------------

// helper function to add an element to the DOM
function addElement(parent, elm) {
    const tmp = parent.querySelector(`#${elm.id}`);
    if (tmp) {
        tmp.remove();
    }
    parent.appendChild(elm);
}

function resetCountByListing(accountName, listingKey) {
    delete accountData[accountName][listingKey];
}

function resetCount(accountName) {
    delete accountData[accountName];
}

function resetAllCounts() {
    for (const accountName in accountData) {
        resetCount(accountName);
    }
    listings.clear();
}

function extractResultsDiv() {
    const results = document.body.querySelectorAll('.resultset');
    if (results.length === 0) {
        return null;
    }

    // Collect arrays of child nodes
    const childNodesArrays = Array.from(results).map(result => Array.from(result.childNodes));

    // Flatten the array of arrays into a single array
    const flattened = childNodesArrays.flat();

    return flattened;
}

function extractResults() {
    const res = extractResultsDiv();
    if (!res) {
        return [];
    }
    return res;
}

// Object to hold the counts and whisper button links of each account name
const accountData = {};

// Set to hold listing keys to avoid duplicates
const listings = new Set();

// Extract the logged-in user's account name, preventing aggregated search of own listings
const loggedInUserElement = document.querySelector('.loggedInStatus .profile-link a');
const loggedInUsername = loggedInUserElement ? loggedInUserElement.textContent : null;

// ------------------------------------------------- element initialization ------------------------------------------------
// styling css
const style = document.createElement('style');
style.id = 'aggregator-style';

style.innerHTML = `

#aggregator {
    position: fixed;
    top: 0;
    right: 0;
    background-color: rgba(0, 0, 0, 0.7);
    padding: 5px;
    z-index: 1000;
    transition: right 0.2s ease 0s;
}

#showAggregator {
    position: fixed;
    top: 50px;
    right: 0;
    z-index: 1001;
    transition: right 0.2s ease 0s;
}

/*
compatibility with Better Trading
*/

.bt-body > #aggregator,
.bt-body > #showAggregator {
    right: 400px; /* Adjust based on the width of the other extension */
    top: 100px;
}

.bt-is-collapsed > #aggregator,
.bt-is-collapsed > #showAggregator {
    right: 0; /* Move it back when the other extension is collapsed */
    top: 100px;
}

#results-table {
    border-spacing: 0 0.4em;
    border-collapse: separate;
}

.actions-cell {
    display: flex;
    justify-content: center;
    padding-left: 5px;
    padding-right: 5px;
}

.text-cell {
    text-align: center;
    padding-left: 5px;
    padding-right: 5px;
}

.action-button{
    margin-left: 2px;
    margin-right: 2px;
}

.thead-cell{
    padding: 5px;
}

tbody tr:nth-child(even) {
    background-color: rgba(50, 50, 50, 0.7);
}

#hide-about {
    margin: 5px;
}

.aboutDiv {
    position: absolute;
    top: 0;
    left: 0;
    background-color: rgba(0, 0, 0, 1);
    padding: 5px;
    z-index: 1000;
    display: none;
}

.hidden {
    display: none;
}

ul {
    display: block;
    list-style-type: disc;
    margin-block-start: 1em;
    margin-block-end: 1em;
    margin-inline-start: 0px;
    margin-inline-end: 0px;
    padding-inline-start: 40px;
}

`;

addElement(document.head, style);

//initializing  the main injecting div
const aggregator = document.createElement('div');
aggregator.id = 'aggregator';
aggregator.classList.add('aggregator', 'results', 'bt-body');

const aggregatorInnerHTML = `
    <button id="hide" class="btn btn-default">Hide</button>
    <button id="clear-all" class="btn btn-default">Clear All</button>
    <button id="refresh" class="btn btn-default">Refresh</button>
    <button id="about" class="btn btn-default">About</button>
    <div class="table-responsive" style="margin: 5px">
        <table id="results-table" class="table">
            <thead>
                <tr>
                    <th class="thead-cell">Account Name</th>
                    <th class="thead-cell">Amount Listed</th>
                    <th class="thead-cell">Count</th>
                    <th class="thead-cell">Total</th>
                    <th class="thead-cell">Actions</th>
                </tr>
            </thead>
            <tbody id="results-list">
            </tbody>
        </table>
    </div>
`;

addElement(document.body, aggregator);

const aboutDiv = document.createElement('div');
aboutDiv.id = 'aboutDiv';
aboutDiv.classList.add('aboutDiv');
aboutDiv.innerHTML = `

    <button id="hide-about" class="btn btn-default">Close</button>
    <h3> Path Of Exile Trade Aggregator </h3>
    <br/>
    <span> This extension aggregates the all listings under the same account name and displays a whisper button for each account name in the Path Of Exile trade site. </span>
    <br/>
    <br/>
    <span> Changelog: </span>
    <br/>
    <ul>
        <li> Minor UI tweaks </li>
        <li> "Whisper" now becomes "Whispered" after clicking </li>
        <li> Alternate row color for better readability </li>
        <li> Added an about section </li>
        <li> Added a Kofi link for donation. Thank you for your support! </li>
    </ul>
    <br/>
    <a href='https://ko-fi.com/H2H4WPVOX' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>

`;

aboutDiv.querySelector('#hide-about').addEventListener('click', () => {
    aboutDiv.style.display = 'none';
});

const showButton = document.createElement('button');
showButton.id = 'showAggregator';
showButton.classList.add('btn', 'btn-default');
showButton.textContent = 'Show Aggregator';

showButton.addEventListener('click', () => {
    aggregator.classList.remove('hidden');
    showButton.classList.add('hidden');
    localStorage.setItem('aggregatorState', 'open'); // Update localStorage
});

addElement(document.body, showButton);

function initAggregator() {
    aggregator.innerHTML = aggregatorInnerHTML;

    addElement(aggregator, aboutDiv);

    aggregator.querySelector('#about').addEventListener('click', () => {
        aboutDiv.style.display = 'block';
    });

    // Check localStorage for the aggregator's state
    const aggregatorState = localStorage.getItem('aggregatorState');

    if (aggregatorState === 'closed') {
        aggregator.classList.add('hidden');
        showButton.classList.remove('hidden');
    } else {
        // By default or if the state is 'open', the aggregator is visible
        aggregator.classList.remove('hidden');
        showButton.classList.add('hidden');
    }

    document.getElementById('hide').addEventListener('click', () => {
        aggregator.classList.add('hidden');
        showButton.classList.remove('hidden');
        localStorage.setItem('aggregatorState', 'closed');
    });

    document.getElementById('clear-all').addEventListener('click', () => {
        resetAllCounts();
        updateAggregator();
    });

    document.getElementById('refresh').addEventListener('click', () => {
        // Reset the counts and reprocess the nodes to get the latest counts
        resetAllCounts();
        const results = extractResults();
        // console.log(results);
        processNodes(results);
        updateAggregator();
    });

    // Initial check in case the page has already loaded
    processNodes(extractResults());
    updateAggregator();
}

// Function to update the floating div with the latest counts and whisper buttons
function updateAggregator() {
    const resultsList = document.getElementById('results-list');

    if (!resultsList) {
        initAggregator();
        return;
    }

    resultsList.innerHTML = ''; // Clear previous results

    // Calculate total listings per account
    const accountsTotalListings = Object.entries(accountData).map(([account, listings]) => {
        const totalListings = Object.values(listings).reduce((sum, { count }) => sum + count, 0);
        return { account, totalListings, listings };
    });

    // Sort accounts by total listings and keep only the top 10
    const topAccounts = accountsTotalListings.sort((a, b) => b.totalListings - a.totalListings).slice(0, 10);


    // Iterate and display sorted accounts
    topAccounts.forEach(({ account, listings }) => {
        Object.entries(listings).forEach(([listingKey, data]) => {
            const row = document.createElement('tr');

            const accountCell = document.createElement('td');
            accountCell.classList.add('text-cell');
            accountCell.textContent = account;

            const amountListedCell = document.createElement('td');
            amountListedCell.classList.add('text-cell');
            amountListedCell.textContent = listingKey;

            const countCell = document.createElement('td');
            countCell.classList.add('text-cell');
            countCell.textContent = data.count;

            const totalCell = document.createElement('td');
            totalCell.classList.add('text-cell');
            listingPrice = Number.parseFloat(listingKey.split(" ")[0]);
            //get currency as the rest of the string
            listingCurrency = listingKey.split(" ").slice(1).join(" ");
            totalCell.textContent = `${listingPrice * data.count} ${listingCurrency}`;


            const actionsCell = document.createElement('td');
            actionsCell.classList.add('actions-cell');

            const whisperButton = document.createElement('button');
            whisperButton.classList.add('btn', 'btn-xs', 'btn-default', 'action-button');
            whisperButton.textContent = 'Whisper';
            whisperButton.addEventListener('click', () => {
                row.classList.add('whispered');
                whisperButton.textContent = 'Whispered';
                data.whisperButton.click();
            });

            const resetButton = document.createElement('button');
            resetButton.classList.add('btn', 'btn-xs', 'btn-default', 'action-button');
            resetButton.textContent = 'Clear';
            resetButton.addEventListener('click', () => {
                delete accountData[account][listingKey];
                updateAggregator();
            });
            actionsCell.appendChild(whisperButton);
            actionsCell.appendChild(resetButton);

            row.appendChild(accountCell);
            row.appendChild(amountListedCell);
            row.appendChild(countCell);
            row.appendChild(totalCell);
            row.appendChild(actionsCell);

            resultsList.appendChild(row);
        });
    });
}

// Flag to check if the aggregator has been updated
var updated = false;

// Function to process added nodes
function processNodes(addedNodes) {
    addedNodes.forEach(node => {
        if (node.parentElement && !node.parentElement.classList.contains('resultset')) return;

        if (listings.has(node)) {
            return; // Skip nodes that have already been processed
        }

        listings.add(node);

        const profileLink = node.querySelector ? node.querySelector('span.profile-link a') : null;
        const whisperButton = node.querySelector ? node.querySelector('button.direct-btn') : null;
        const priceField = node.querySelector ? node.querySelector('div.price span[data-field="price"]') : null;
        if (!priceField) return;
        const priceSpan = priceField.querySelector ? priceField.childNodes[3] : null;
        const currencySpan = priceField.querySelector ? priceField.childNodes[5] : null;
        const errorSpan = node.querySelector ? node.querySelector('span.error') : null;

        if (errorSpan) {
            return;
        }

        if (profileLink && whisperButton && priceSpan && currencySpan) {
            const accountName = profileLink.textContent.trim();
            if (accountName === loggedInUsername) return;

            const quantity = priceSpan.textContent.trim();
            const currencyType = currencySpan.textContent.trim();
            const listingKey = `${quantity} ${currencyType}`;

            if (!accountData[accountName]) {
                accountData[accountName] = {};
            }

            if (!accountData[accountName][listingKey]) {
                // console.log("Adding new listing for " + accountName + " " + listingKey);
                accountData[accountName][listingKey] = { count: 1, whisperButton: whisperButton };
            } else {
                // console.log("Incrementing count for " + accountName + " " + listingKey);
                accountData[accountName][listingKey].count += 1;
            }

            updated = true;
        }

    });
}

// observer to watch for changes in the DOM
const observer = new MutationObserver(mutations => {
    updated = false;
    mutations.forEach(mutation => {
        processNodes(mutation.addedNodes);
    });
    if (updated) {
        updateAggregator();
    }
});

// Configuration of the observer:
const config = { childList: true, subtree: true };

// Start observing the body for changes
observer.observe(document.body, config);

initAggregator();