Add infinite scrolling and options for filtering sellers to the Buyee search results page
当前为
// ==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();
}