// ==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();