Buyee Seller Filter

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

当前为 2024-02-15 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Buyee Seller Filter
// @license      MIT
// @version      0.98
// @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 firstRun = 1;
var currentURL = window.location;

(function() {
  'use strict';
  (new MutationObserver(pageObserve)).observe(document, {
    childList: true,
    subtree: true
  });
})();

// observe for page to actually load
function pageObserve(changes, observer) {
  if (currentURL != window.location) {
    currentURL = window.location;
    firstRun = 1;
  }
  if (firstRun == 1 && document.querySelector("ul.auctionSearchResult")) {
      firstRun = 0;
      buyeeSellerFilter();
  }
} */

if (document.readyState === 'complete') {
    buyeeSellerFilter();
} else {
    window.addEventListener('load', () => 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 has ended
function cleanItems() {
    Object.keys(itemsBlacklist).forEach(url => {
        // is auction one week old?
        if (Date.now() - itemsBlacklist[url] > 604800000) {
            // update time checked
            itemsBlacklist[url] = Date.now();
            fetchURL('https://buyee.jp/item/yahoo/auction/' + url + '/')
                .then(page => {
                if (page.querySelector(".inbox") && page.querySelector(".inbox").innerText === 'This auction has ended.') {
                    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\/yahoo\/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) {
        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);
    });
}

// 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');
    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';
            }
        }, 50);
    }

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