Buyee Seller Filter

Add infinite scrolling and options for filtering sellers to the Buyee search results page

当前为 2025-04-01 提交的版本,查看 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Buyee Seller Filter
// @license      MIT
// @version      0.999
// @description  Add infinite scrolling and options for filtering sellers to the Buyee search results page
// @author       rhgg2
// @match        https://buyee.jp/item/search/*
// @icon         https://www.google.com/s2/favicons?domain=buyee.jp
// @namespace https://greasyfork.org/users/1243343
// ==/UserScript==

// stuff to handle loading stage of page

var notYetRun = true;

if ((document.readyState === 'complete') && notYetRun) {
    notYetRun = false;
    buyeeSellerFilter();
} else {
    window.addEventListener('load', () => { notYetRun = false; buyeeSellerFilter() } );
}

// sellers to hide; each seller to be hidden added as a key with value TRUE
var sellersBlacklist;
// items to hide; each item to be hidden added as a key with value
// a timestamp indicating when we last checked if the auction is  active
var itemsBlacklist;
// should hidden sellers/items actually be hidden?
var hideHidden;

// are we on the desktop site?
var isDesktop = (navigator.userAgent.match(/Android/i)
         || navigator.userAgent.match(/webOS/i)
         || navigator.userAgent.match(/iPhone/i)
         || navigator.userAgent.match(/iPad/i)
         || navigator.userAgent.match(/iPod/i)
         || navigator.userAgent.match(/BlackBerry/i)
         || navigator.userAgent.match(/Windows Phone/i)) ? false : true;

// save state to local storage
function serialiseData() {
   localStorage.hideHidden = JSON.stringify(hideHidden);
   localStorage.sellersBlacklist = JSON.stringify(sellersBlacklist);
   localStorage.itemsBlacklist = JSON.stringify(itemsBlacklist);
}

// load state from local storage
function unSerialiseData() {
    sellersBlacklist = ("sellersBlacklist" in localStorage) ? JSON.parse(localStorage.sellersBlacklist) : {};
    itemsBlacklist = ("itemsBlacklist" in localStorage) ? JSON.parse(localStorage.itemsBlacklist) : {};
    hideHidden = ("hideHidden" in localStorage) ? JSON.parse(localStorage.hideHidden) : true;
}

// fetch a URL and return a document containing it
function fetchURL(url) {
    return fetch(url)
    .then((response) => {
        return response.text()
    })
    .then((html) => {
        // Parse the text
        var parser = new DOMParser();
        var doc = parser.parseFromString(html, "text/html");
        return doc;
    });
}

// make a bullet node
function makeBullet() {
    let node = document.createElement("span");
    node.innerText = ' • ';
    node.classList.add('rg-node');
    node.style.width = '14px';
    node.style['text-align'] = 'center';
    if (!isDesktop) { node.classList.add('g-text'); }
    return node;
}

// create a node which shows/hides/demotes the given seller when clicked
function makeSellerStatusNode(seller,status) {
    let node = document.createElement("a");
    node.href = "javascript:void(0);";
    if (status == "Show") {
        node.onclick = (() => {
            delete sellersBlacklist[seller];
            serialiseData();
            processCardsIn(document);
        });
    } else {
        node.onclick = (() => {
            sellersBlacklist[seller] = true;
            serialiseData();
            processCardsIn(document);
        });
    }
    node.innerText = status;
    node.classList.add('rg-node');
    if (!isDesktop) { node.classList.add('g-text'); }
    return node;
}

// create a node which shows/hides the given item when clicked
function makeItemStatusNode(url,card,status) {
    let node = document.createElement("a");
    node.href = "javascript:void(0);";
    if (status == "Show") {
        node.onclick = (() => {
            delete itemsBlacklist[url];
            serialiseData();
            processCard(card);
        });
    } else {
        node.onclick = (() => {
            // use current timestamp so we can delete old listings later
            itemsBlacklist[url] = Date.now();
            serialiseData();
            processCard(card);
        });
    }
    node.innerText = status;
    node.classList.add('auctionSearchResult__statusItem');
    node.classList.add('rg-node');
    return node;
}

// make a "loading" node for the infinite scrolling
function makeLoadingNode() {
    let card = document.createElement("li");
    card.classList.add('itemCard');
    card.classList.add('rg-loading');

    let innerDiv = document.createElement("div");
    innerDiv.classList.add('imgLoading');
    innerDiv.style.height = '150px';

    card.appendChild(innerDiv);

    return card;
}

// Remove old items from items blacklist if the corresponding auction more than one week old.
function cleanItems() {
    var now = Date.now();
    Object.keys(itemsBlacklist).foreach( (url) => {
        if (now - itemsBlacklist[url] > 604800000) {
            delete itemsBlacklist[url];
        }
    });
    serialiseData();
}

// process changes to a results card; this may be called to refresh the page on a state change,
// so be sure to account for the previous changes
function processCard(card) {

    // set up seller field; delete any existing hide/demote/show links
    let sellerNode;
    if (isDesktop) {
        sellerNode = card.querySelector("span.auctionSearchResult__seller");
    } else {
        sellerNode = card.querySelector("ul.itemCard__infoList li:nth-child(2) span.g-text")
    }
    let seller = sellerNode.querySelector("a").innerText.trim();

    card.querySelectorAll(".rg-node").forEach(node => { node.parentNode.removeChild(node); });

    let statusNode = card.querySelector("ul.auctionSearchResult__statusList");
    let url = card.querySelector('.itemCard__itemName a').href.match(/https:\/\/buyee.jp\/item\/jdirectitems\/auction\/(.*)\?.*/);
    if (url) { url = url[1] }

    if (seller in sellersBlacklist) {
        if (hideHidden) {
            // hide the card
            card.style.display = 'none';
            card.style.removeProperty('opacity');
            card.style.removeProperty('background-color');
        } else {
            // show with red background
            card.style.opacity = '0.9';
            card.style['background-color'] = '#ffbfbf';
            card.style.removeProperty('display');

            // add show link
            if (!isDesktop) { sellerNode.parentNode.appendChild(makeBullet()); }
            sellerNode.parentNode.appendChild(makeSellerStatusNode(seller,'Show'));
        }
    } else if (url in itemsBlacklist) {

        // update timestamp to prevent item from being cleaned up for next seven days
        itemsBlacklist[url] = Date.now();

        if (hideHidden) {
            // hide the card
            card.style.display = 'none';
            card.style.removeProperty('opacity');
            card.style.removeProperty('background-color');
        } else {
            // show with red background
            card.style.opacity = '0.9';
            card.style['background-color'] = '#ffbfbf';
            card.style.removeProperty('display');

            // add show/hide links
            if (!isDesktop) { sellerNode.parentNode.appendChild(makeBullet()); }
            sellerNode.parentNode.appendChild(makeSellerStatusNode(seller,'Hide'));
            statusNode.appendChild(makeItemStatusNode(url,card,'Show'));
        }
    } else {
        // unhide card
        card.style.removeProperty('opacity');
        card.style.removeProperty('background-color');
        card.style.removeProperty('order');
        card.style.removeProperty('display');

        // add hide links
        if (!isDesktop) { sellerNode.parentNode.appendChild(makeBullet()); }
        sellerNode.parentNode.appendChild(makeSellerStatusNode(seller,'Hide'));
        statusNode.appendChild(makeItemStatusNode(url,card,'Hide'));
    }
}

// process changes to all results cards in a given element
function processCardsIn(element) {
    element.querySelectorAll("ul.auctionSearchResult li.itemCard:not(.rg-loading)").forEach(card => {
        processCard(card);
    });

    // some itemsBlacklist timestamps may have been updated, so serialise
    serialiseData();
}

// move all results cards in a given element to the given element
// as soon as one visible card is moved, hide loadingElement
// add cards to observer for lazy loading
function moveCards(elementFrom, elementTo, loadingElement, observer) {
    var movedVisibleCard = (loadingElement === undefined) ? true : false;
    elementFrom.querySelectorAll("ul.auctionSearchResult li.itemCard").forEach(card => {
        // add to lazy loading observer
        observer.observe(card.querySelector('img.lazyLoadV2'));
        // make the seller link work properly on desktop (a hack)
        if (isDesktop) {
            var newURL = new URL(document.location);
            var sellerLink = card.querySelector("span.auctionSearchResult__seller a");
            var sellerText = sellerLink.getAttribute('data-bind').match(/click: search\.bind\(\$data, \{ seller: \'(.*)\' \}\)/)[1];
            newURL.pathname = newURL.pathname + '/seller/' + encodeURIComponent(sellerText.replaceAll("/", "%2F"));
            sellerLink.href = newURL.toString();
        }
        // move the card
        elementTo.appendChild(card);
        // if we moved a visible card, hide the loading element
        if (!movedVisibleCard && card.style.display != "none") {
            movedVisibleCard = true;
            loadingElement.style.display = "none";
        }
    });
}

// find the URL for the next page of search results after the given one
function nextPageURL(url) {
    var newURL = new URL(url);
    var currentPage = newURL.searchParams.get('page') ?? '1';
    newURL.searchParams.delete('page');
    newURL.searchParams.append('page', parseInt(currentPage) + 1);
    return newURL;
}

// check that the given HTML document is not the last page of results
function notLastPage(doc) {
    if (isDesktop) {
        let button = doc.querySelector("div.page_navi a:nth-last-child(2)");
        return (button && button.innerText === ">");
    } else {
        let button = doc.querySelector("li.page--arrow:nth-last-child(2)");
        return (button != null);
    }
}

// the main function
function buyeeSellerFilter () {

    // initial load of data
    unSerialiseData();

    // reload data when tab regains focus
    document.addEventListener("visibilitychange", () => {
        if (!document.hidden) {
            // don't change hideHidden, so save its value first
            let hideHiddenSaved = hideHidden;
            unSerialiseData();
            hideHidden = hideHiddenSaved;
            processCardsIn(document);
        }
    });

    if (!isDesktop) {
        // disable the google translate popup (annoying with show/hide buttons)
        var style = `
#goog-gt-tt, .goog-te-balloon-frame{display: none !important;}
.goog-text-highlight { background: none !important; box-shadow: none !important;}
`;
        var styleSheet = document.createElement("style");
        styleSheet.innerText = style;
        document.head.appendChild(styleSheet);
    }

    let container = document.querySelector('.g-main:not(.g-modal)');
    let resultsNode = container.children[0];

    // sometimes the results are broken into two lists of ten; if so, merge them.
    if (container.children.length > 1) {
        container.children[1].querySelectorAll("ul.auctionSearchResult li.itemCard").forEach(card => {
            resultsNode.appendChild(card);
        });
        container.children[1].style.display = "none";
    }

    // make link to show or hide hidden results
    let optionsLink = document.createElement("a");
    optionsLink.href = "javascript:void(0);";
    optionsLink.id = "rg-show-hide-link";
    optionsLink.innerText = hideHidden ? "Show hidden" : "Hide hidden";
    optionsLink.onclick = (function() {
        hideHidden = !hideHidden;
        serialiseData();
        optionsLink.innerText = hideHidden ? "Show hidden" : "Hide hidden";
        processCardsIn(document);
    });

    // put link in the search options bar
    let optionsNode = document.createElement("span");
    optionsNode.classList.add('result-num');
    if (isDesktop) {
        optionsNode.style.left = '25%';
    } else {
        optionsNode.appendChild(makeBullet());
    }
    optionsNode.appendChild(optionsLink);

    if (isDesktop) {
        document.querySelector(".result-num").parentNode.appendChild(optionsNode);
    } else {
        optionsNode.style.display = 'inline';
        document.querySelector(".result-num").appendChild(optionsNode);
    }

    // perform initial processing of cards
    processCardsIn(document);

    // image lazy loader
    const imageObserver = new IntersectionObserver(loadImage);

    function loadImage(entries, observer) {
        entries.forEach(entry => {
            if (!entry.isIntersecting) {
                return;
            }

            const target = entry.target;
            target.src = target.getAttribute('data-src');
            target.removeAttribute('data-src');
            target.style.background = '';

            observer.unobserve(target);
        });
    }

    // load subsequent pages of results if navi bar on screen
    var currentURL = document.location;
    var currentPage = document;
    var naviOnScreen = false;

    const loadingNode = makeLoadingNode();

    function loadPageLoop() {
        // code to add further pages of results; loop over pages,
        // with a minimum 50ms delay between to avoid rampaging
        // robot alerts (unlikely as the loads are pretty slow)
        // stop if the navi bar is no longer on screen (so don't load infinitely)
        setTimeout(() => {
            if (naviOnScreen && notLastPage(currentPage)) {
                // display loading node
                resultsNode.appendChild(loadingNode);
                loadingNode.style.removeProperty('display');

                // get next page of results
                currentURL = nextPageURL(currentURL);
                fetchURL(currentURL)
                .then((page) => {
                    currentPage = page;
                    processCardsIn(currentPage);
                    moveCards(currentPage,resultsNode, loadingNode, imageObserver);
                    loadPageLoop();
                });
            } else if (naviOnScreen) {
                // finished loading pages, hide loading node
                loadingNode.style.display = 'none';
            }
        }, 100);
    }

    // function to handle navi bar appearing/disappearing from screen
    function handleIntersection(entries) {
        entries.map((entry) => {
            naviOnScreen = entry.isIntersecting
        });
        if (naviOnScreen) { loadPageLoop(); }
    }

    // 540 px bottom margin so that next page loads a bit before navi bar appears on screen
    const loadObserver = new IntersectionObserver(handleIntersection, { rootMargin: "0px 0px 540px 0px" });

    if (isDesktop) {
        loadObserver.observe(document.querySelector("div.page_navi"));
    } else {
        loadObserver.observe(document.querySelector("ul.pagination"));
    }

    // clean up old blacklisted items
    cleanItems();
}