Greasy Fork is available in English.

Splinterlands Enhanced Trading

Add filters to splinterlands market

// ==UserScript==
// @name         Splinterlands Enhanced Trading
// @namespace    http://tampermonkey.net/
// @version      1
// @description  Add filters to splinterlands market
// @author       Cullen#1432
// @match        https://splinterlands.com/*
// @icon         https://www.google.com/s2/favicons?domain=splinterlands.com
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';
    // Your code here...
    GM_addStyle(`
    .header.enhanced-trading br {
        content: "";
        display: block;
        margin: 12px;
    }

    .enhanced-trading .filter-form {
        width: 100%;
        margin: 0px !important;
        text-align: center;
    }

    #btn_refresh {
        -moz-transition: all .1s ease-in;
        -o-transition: all .1s ease-in;
        -webkit-transition: all .1s ease-in;
        transition: all .1s ease-in;
        margin-left: 20px;
    }

    #btn_refresh.awaiting {
        background-color: #3c763d;
    }

    #btn_refresh.awaiting:hover {
        background-color: #53a054;
    }

    #btn_refresh.refreshing {
        background-color: #a94442;
    }
    `)
    let isSortingTable = false;
    // Setting up the dropdowns
    function setupDropDowns() {
        // Grab the first row so we can scrap the different columns
        let trItem = document.querySelector('.card-list-container tbody tr');
        // Holds our dropdown data that we're about to build
        let dropDownBuilder = [];
        // Grab the columns
        trItem.querySelectorAll('td').forEach( tdItem => {
            // Save the column label
            let tdLabel = tdItem.classList.value;
            if(tdLabel === 'check' || tdLabel === 'cooldown'){
                return;
            }
            //Build our collection of all unique values in that column
            let targetTdItemCollection = [];
            document.querySelectorAll('.card-list-container tbody tr td.'+tdLabel).forEach(targetTdItem => {
                let targetTdItemText = targetTdItem.innerHTML;
                if(targetTdItemCollection.indexOf(targetTdItemText) === -1) {
                    targetTdItemCollection.push(targetTdItemText);
                }
            });
            // Sort collection
            let sortedCollection = targetTdItemCollection;
            if(tdLabel === 'lvl') {
                sortedCollection = targetTdItemCollection.sort((x,y) => {
                    return x.substring(2) > y.substring(2) ? 1 : -1
                });
            }
            switch(tdLabel) {
                case 'lvl':
                    sortedCollection = targetTdItemCollection.sort((x,y) => {
                        return x.substring(2) - y.substring(2);
                    });
                    break;
                case 'bcx':
                    sortedCollection = targetTdItemCollection.sort((x,y) => {
                        return x - y;
                    });
                    break;
                case 'price-bcx':
                case 'price':
                    sortedCollection = targetTdItemCollection.sort((x,y) => {
                        return x.replace(',', '').replace('$', '') - y.replace(',', '').replace('$', '');
                    });
                    break;
                default:
                    sortedCollection = targetTdItemCollection.sort((x,y) => {
                        return x > y ? 1 : -1;
                    });
                    break;
            }
            // Push the label + collection to the builder array
            dropDownBuilder.push({'name':tdLabel, 'items':sortedCollection})
        });

        let headerContainer = document.createElement('div');
        headerContainer.className = 'header enhanced-trading';

        let filterContainer = document.createElement('div');
        filterContainer.className = 'filter-form';

        let verticalCenterContainer = document.createElement('div');
        verticalCenterContainer.className = 'vertical-center';

        dropDownBuilder.forEach((dropdownItemCollection, i) => {
            if(i === 4) {
                verticalCenterContainer.appendChild(document.createElement('br'));
            }
            // Creating and adding the dropdown label
            let dropDownItemCollectionLabel = document.createElement('span');
            dropDownItemCollectionLabel.innerHTML = dropdownItemCollection.name + ' ';
            dropDownItemCollectionLabel.id = dropdownItemCollection.name+'_label';
            verticalCenterContainer.appendChild(dropDownItemCollectionLabel);
            // Creating the actual dropdown container
            let dropdownItemCollectionContainer = document.createElement('select');
            dropdownItemCollectionContainer.id = dropdownItemCollection.name+'_sort';
            // Adding option items to dropdown container
            dropdownItemCollection.items.forEach((dropdownItem, j) => {
                if(j === 0) {
                    let dropdownItemContainerAll = document.createElement('option');
                    dropdownItemContainerAll.value = 'All';
                    dropdownItemContainerAll.innerHTML = 'All';
                    dropdownItemCollectionContainer.appendChild(dropdownItemContainerAll);
                }
                let dropdownItemContainer = document.createElement('option');
                dropdownItemContainer.value = dropdownItem;
                dropdownItemContainer.innerHTML = dropdownItem;
                dropdownItemCollectionContainer.appendChild(dropdownItemContainer);
            });
            // Add event listeners
            dropdownItemCollectionContainer.addEventListener('change',(e) => {
                hideRowsFor(e.target.id.replace('_sort', ''), e.target.value);
            })
            // Adding the dropdown container to the ui
            verticalCenterContainer.appendChild(dropdownItemCollectionContainer);
        });
        // Boring adding-all-ui-containers part
        filterContainer.appendChild(verticalCenterContainer);
        headerContainer.appendChild(filterContainer);
        document.querySelector('.header').after(headerContainer);
    }
    function hideRowsFor(row, val) {
        // Reset the other selectors
        document.querySelectorAll('.enhanced-trading select').forEach(dropdown => {
            if(row+'_sort' !== dropdown.id) {
                dropdown.selectedIndex = 0;
            }
        })
        isSortingTable = true;
        // Show them all
        document.querySelectorAll('.card-list-container tbody tr[style*="display: none"]').forEach(trItem => {
            trItem.style.display = 'table-row';
        })
        // We're done here if they selected All
        if(val === 'All') {
            isSortingTable = false;
            return;
        }
        // Hide the ones we hate
        document.querySelectorAll('.card-list-container tbody tr').forEach(trItem => {
            if (trItem.querySelector('.'+row).innerHTML != val) {
                trItem.style.display = 'none';
            }
        })
        // Give observers a chance to finish processing mutations before telling them we're done table sorting
        setTimeout(() => {isSortingTable = false}, 1);
    }
    function setupRefresh() {
        let refreshButton = document.createElement('button');
        refreshButton.id = 'btn_refresh';
        refreshButton.className = 'new-button awaiting'
        refreshButton.innerHTML = 'REFRESH';
        refreshButton.addEventListener('click', e => {
            let tab = document.querySelector('.tournament-header-buttons .selected').id;
            if(['tab_market', 'tab_rentals'].includes(tab)) {
                document.querySelector('#btn_refresh').innerHTML = 'REFRESHING...';
                document.querySelector('#btn_refresh').className = 'new-button refreshing';
                document.querySelector('.scroll-container table').style.opacity = 0.4;
                SM.Api('/market/for_' + (tab === 'tab_market' ? 'sale' : 'rent') + '_by_card', {
                    card_detail_id: id,
                    gold: gold,
                    edition: edition
                }, response => {
                    response.forEach(r => r.bcx = SM.GetCardBCX(r));
                    var data = {
                        items: response.sort((a, b) => parseFloat(a.buy_price) / a.bcx - parseFloat(b.buy_price) / b.bcx),
                        id: id,
                        gold: gold,
                        edition: edition
                    };
                    SM._for_sale_by_card = data;
                    let htmlData = SM.ShowComponent('/cards/' + (tab === 'tab_market' ? 'market' : 'rental') + '_details', data);
                    let htmlContainer = document.createElement('div');
                    htmlContainer.innerHTML = htmlData.trim();
                    let htmlTable = htmlContainer.querySelector('.card-list.noselect');
                    document.querySelector('.card-list.noselect').innerHTML = htmlTable.innerHTML;
                    document.querySelector('#btn_refresh').innerHTML = 'REFRESH';
                    document.querySelector('#btn_refresh').className = 'new-button awaiting'
                    document.querySelector('.scroll-container table').style.opacity = 1;
                    triggerSort()
                });
            }
        });
        document.querySelector('.buttons .filter-form').appendChild(refreshButton);
    }
    function triggerSort() {
        var order = $('#market_sort').val();
		var items = SM._for_sale_by_card.items;
        let tab = document.querySelector('.tournament-header-buttons .selected').id;
		switch(order) {
			case 'price':
				items.sort((a, b) => parseFloat(a.buy_price) - parseFloat(b.buy_price));
				break;
			case 'price_desc':
				items.sort((a, b) => parseFloat(b.buy_price) - parseFloat(a.buy_price));
				break;
			case 'price_bcx':
				items.sort((a, b) => parseFloat(a.buy_price) / a.bcx - parseFloat(b.buy_price) / b.bcx);
				break;
			case 'price_bcx_desc':
				items.sort((a, b) => parseFloat(b.buy_price) / b.bcx - parseFloat(a.buy_price) / a.bcx);
				break;
			case 'bcx':
				items.sort((a, b) => a.bcx - b.bcx);
				break;
			case 'bcx_desc':
				items.sort((a, b) => b.bcx - a.bcx);
				break;
		}
        // Sadly this line only uses the first 1000 results sorted, despite SM._for_sale_by_card.items sometimes showing more
        // Ideally you wanna disect SM.ShowComponent which runs a function called render. toString the render() and maybe remake that call without the 1k limit so filters can be applied BEFORE that
        // Can't just filter on SM._for_sale_by_card.items because that doesn't include card level (I think? unless xp does that)
		$('.card-list').html(SM.ShowComponent('/cards/' + (tab === 'tab_market' ? 'market' : 'rental') + '_details_list', items));
        init();
    }
    let tableObserver = null;
    let uhoh = 0;
    function setupTableListener() {
        tableObserver?.disconnect();
        const tableTargetNode = document.querySelector('.card-list.noselect');
        const tableConfig = { attributes: true, childList: true, subtree: true };
        const tableCallback = function(mutationList, observer){
            if(!isSortingTable) {
                document.querySelectorAll('.enhanced-trading select').forEach(dropdown => {
                    if(dropdown.selectedIndex !== 0) {
                        let row = dropdown.id.replace('_sort', '')
                        let val = dropdown.options[dropdown.selectedIndex].value;
                        hideRowsFor(row, val);
                    }
                })
            }
        }
        tableObserver = new MutationObserver(tableCallback);
        tableObserver.observe(tableTargetNode, tableConfig);
    }
    //Main handlers, sets up most things
    const initTargetNode = document.body;
    const initConfig = { attributes: true, childList: true, subtree: true };
    const initCallback = function(mutationList, observer){
        const isUILoaded = !!document.querySelector('.scroll-container');
        const isEnhancedTradingLoaded = !!document.querySelector('.enhanced-trading');
        if (isUILoaded && !isEnhancedTradingLoaded) {
            setupDropDowns();
            setupRefresh();
            setupTableListener();
	    }
    };
    const initObserver = new MutationObserver(initCallback);
	initObserver.observe(initTargetNode, initConfig);
})();