您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add infinite scrolling and options for filtering sellers to the Buyee search results page
// ==UserScript== // @name Buyee Seller Filter // @license MIT // @version 1.0 // @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://greasyforks.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() } ); } // highlight interval for newly listed items; highlighted in green when new, slowly // fading to white over the number of hours specified below const newlyListedHighlightTime = 12; // 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; // list of item that have already been seen; items are added with a timestamp indicating // when last seen var alreadySeenList; // should hidden sellers/items actually be hidden? var hideHidden; // should new items be highlighted? var highlightNew; // 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.highlightNew = JSON.stringify(highlightNew); localStorage.sellersBlacklist = JSON.stringify(sellersBlacklist); localStorage.itemsBlacklist = JSON.stringify(itemsBlacklist); localStorage.alreadySeenList = JSON.stringify(alreadySeenList); } // load state from local storage function unSerialiseData() { sellersBlacklist = ("sellersBlacklist" in localStorage) ? JSON.parse(localStorage.sellersBlacklist) : {}; itemsBlacklist = ("itemsBlacklist" in localStorage) ? JSON.parse(localStorage.itemsBlacklist) : {}; alreadySeenList = ("alreadySeenList" in localStorage) ? JSON.parse(localStorage.alreadySeenList) : {}; hideHidden = ("hideHidden" in localStorage) ? JSON.parse(localStorage.hideHidden) : true; highlightNew = ("highlightNew" in localStorage) ? JSON.parse(localStorage.highlightNew) : 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; } // RGB to hex const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => { const hex = x.toString(16) return hex.length === 1 ? '0' + hex : hex }).join('') // 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 and already-seen list 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]; } }); Object.keys(alreadySeenList).foreach( (url) => { if (now - alreadySeenList[url] > 604800000) { delete alreadySeenList[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 item is not already seen, add it to the already seen list if (!(url in alreadySeenList)) { alreadySeenList[url] = Date.now(); } 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'); // if new within the last few hours, colour background green, slowly fading to white // "few" here is defined by newlyListedHighlightTime var timeSinceFirstSeen = Date.now() - alreadySeenList[url]; if (highlightNew && (timeSinceFirstSeen < (newlyListedHighlightTime * 3600000))) { var redBlueIntensity = 191 + Math.floor((64 * timeSinceFirstSeen) / (3600000 * newlyListedHighlightTime)); card.style['background-color'] = rgbToHex(redBlueIntensity,255,redBlueIntensity); } // 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); }); optionsLink.style.display = 'inline-block'; optionsLink.style.width = '110px'; // make link to highlight new results let highlightLink = document.createElement("a"); highlightLink.href = "javascript:void(0);"; highlightLink.id = "rg-highlight-new-link"; highlightLink.innerText = highlightNew ? "Don't highlight new" : "Highlight new"; highlightLink.onclick = (function() { highlightNew = !highlightNew; serialiseData(); highlightLink.innerText = highlightNew ? "Don't highlight new" : "Highlight new"; processCardsIn(document); }); highlightLink.style.display = 'inline-block'; highlightLink.style.width = '150px'; // put link in the search options bar let optionsNode = document.createElement("span"); optionsNode.classList.add('result-num'); if (isDesktop) { optionsNode.style.left = '20%'; } optionsNode.appendChild(optionsLink); optionsNode.appendChild(highlightLink); 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(); }