// ==UserScript==
// @name Buyee Seller Filter
// @license MIT
// @version 1.61
// @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
// @grant none
// @require https://unpkg.com/[email protected]/js/smartphoto.min.js
// ==/UserScript==
/* eslint-disable no-alert, no-console */
Document.prototype.$ = Element.prototype.$ = function(sel) { return this.querySelector(sel) }
Document.prototype.$$ = Element.prototype.$$ = function(sel) { return this.querySelectorAll(sel) }
const $ = Element.prototype.$.bind(document)
const $$ = Element.prototype.$$.bind(document)
function $x(path, ctx = document) {
return ctx.evaluate(path, ctx, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
}
//function el(tag, cls = '', style = '', text) {
// const n = document.createElement(tag)
// if (cls) n.className = cls
// if (style) n.style.cssText = style
// if (text) n.textContent = text
// return n
//}
function el(sel = 'div', props = {}, ...children) {
const [, tag = 'div', id, cls = ''] = sel.match(/^([a-z]+)?(?:#([\w-]+))?(.*)?$/) || []
const e = document.createElement(tag)
if (id) e.id = id
if (cls) e.classList.add(...cls.split('.').filter(Boolean))
for (const [k, v] of Object.entries(props)) {
if (k === 'style') typeof v === 'string' ? e.style.cssText = v : Object.assign(e.style, v)
else if (k === 'text') e.textContent = v
else if (k === 'html') e.innerHTML = v
else if (/^on[A-Z0-9]/i.test(k)) e[k] = v
else e.setAttribute(k, v)
}
for (const c of children.flat()) e.append(c instanceof Node ? c : document.createTextNode(c))
return e
}
// stored state
let sellersData = {}; // metadata associated to sellers
let itemsData = {}; // metadata associated to items
let hideHidden = true; // should hidden items actually be hidden?
// page state
let buyeeWatchlist = []
let smartPhoto = new SmartPhoto(".js-smartPhoto", { showAnimation: false });
let smartPhotoOpen = false;
let pendingSmartPhotoItems = [];
let smartPhotoNode = $('.smartphoto');
// buyee watchlist
// are we on the desktop site?
const 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
function serialiseData() {
localStorage.sellersData = JSON.stringify(sellersData)
localStorage.hideHidden = JSON.stringify(hideHidden)
localStorage.itemsData = JSON.stringify(itemsData)
}
function unSerialiseData() {
sellersData = localStorage.sellersData ? JSON.parse(localStorage.sellersData) : {}
hideHidden = localStorage.hideHidden ? JSON.parse(localStorage.hideHidden) : true
itemsData = localStorage.itemsData ? JSON.parse(localStorage.itemsData) : {}
}
function fetchURL(url) {
return fetch(url)
.then(r => r.ok ? r.text() : Promise.reject(`HTTP ${r.status}`))
.then(html => new DOMParser().parseFromString(html, 'text/html'))
}
// create a node which shows/hides the given seller when clicked
function makeStatusToggle(data, caption, callback) {
return el('button.rg-node', {
type: 'button',
text: caption,
onclick: () => {
console.log(data)
data.hide = !data.hide
serialiseData()
callback()
}
})
}
function makeSellerStatusNode(seller,caption) {
return makeStatusToggle(sellersData[seller], caption, () => processCardsIn(document))
}
function makeItemStatusNode(item,card,caption) {
return makeStatusToggle(itemsData[item], caption, () => processCard(card))
}
// const node = el('button', isDesktop ? 'rg-node' : 'rg-node g-text', '', status)
// node.type = 'button'
// node.onclick = () => {
// const s = sellersData[seller]
// s.hide = !s.hide
// serialiseData()
// processCardsIn(document)
// }
// return node
// }
// create a node which shows/hides the given item when clicked
// function makeItemStatusNode(item,card,status) {
// const node = el('button', 'rg-node auctionSearchResult__statusItem', '', status)
// node.type = 'button'
// node.onclick = () => {
// const s = itemsData[item]
// s.hide = !s.hide
// serialiseData()
// processCard(card)
// }
// return node
// }
// clear previous info nodes on a card
function clearInfoNodes(card) {
const list = card.$('.itemCard__infoList')
while (list.childElementCount > 2) list.lastElementChild.remove()
card.$('.watchList__watchNum')?.remove()
}
function addSellerNode(card, seller) {
const list = card.$('.itemCard__infoList')
const node = list.firstElementChild.cloneNode(true)
node.$('.g-title').textContent = 'Seller'
const text = node.$('.g-text')
text.textContent = ''
text.append(el('a', {
text: seller.name,
href: seller.url,
target: '_blank',
rel : 'noopener noreferrer'
}))
list.append(node)
return node
}
// add number of watches to item card
function addWatchersNode(card, count) {
const star = card.$('.g-feather').parentNode
star.append(el('span.watchList__watchNum', { text: count }))
}
// make a "loading" node for the infinite scrolling
function makeLoadingNode() {
return el('li.itemCard.rg-loading', {},
el('div.imgLoading', { style: 'height: 50px' }),
el('div.g-title', { text: 'Loading page 2', style: 'text-align: center; height: 50px' })
)
}
// convert time to time left
function timeUntil(dateStr) {
const jstDate = new Date(dateStr + " GMT+0900");
const now = new Date();
let diffSec = Math.floor((jstDate - now)/1000);
if (diffSec <= 0) return "Ended";
const diffDays = Math.floor(diffSec / (60 * 60 * 24));
diffSec -= diffDays * 60 * 60 * 24;
const diffHours = Math.floor(diffSec / (60 * 60));
diffSec -= diffHours * 60 * 60;
const diffMinutes = Math.floor(diffSec / 60);
diffSec -= diffMinutes * 60;
const fmt = (n, label) => n > 0 ? `${n} ${label}${n > 1 ? "s" : ""}` : null;
let parts;
const totalMinutes = diffDays * 24 * 60 + diffHours * 60 + diffMinutes;
if (totalMinutes < 30) {
// Show minutes and seconds
parts = [fmt(diffMinutes, "minute"), fmt(diffSec, "second")].filter(Boolean);
} else {
// Show days, hours, minutes
parts = [fmt(diffDays, "day"), fmt(diffHours, "hour"), fmt(diffMinutes, "minute")].filter(Boolean);
}
return parts.join(" ");
}
// Remove old items from items list if not seen for > 1 week.
function cleanItems() {
const now = Date.now()
for (const url in itemsData) {
if (now - itemsData[url].lastSeen > 604800000) delete itemsData[url]
}
serialiseData()
}
// countdown timers
const countdowns = [];
// Add a countdown
function addCountdown(date, el) {
countdowns.push({ date, el })
el.textContent = timeUntil(date)
}
// update all countdowns
function updateCountdowns() {
const now = new Date()
for (const cd of countdowns) {
cd.el.textContent = timeUntil(cd.date)
}
setTimeout(updateCountdowns, 1000)
}
updateCountdowns()
// queue of cards to process
const MAX_CONCURRENT = 3;
const DELAY = 250;
const processingQueue = [];
let activeCount = 0;
const inFlightItems = new Set();
const readItems = new Set();
function sleep(ms) {
return new Promise(r => setTimeout(r, ms))
}
function loadCard() {
if (!processingQueue.length || activeCount >= MAX_CONCURRENT) return
const { card, url, item } = processingQueue.shift()
activeCount++
fetchURL(url)
.then(doc => {
fillCardData(card, doc, item)
processCard(card)
readItems.add(item)
})
.catch(err => console.error('fetchURL failed for', url, err))
.finally(() => {
inFlightItems.delete(item)
sleep(DELAY).then(() => {
activeCount--
loadCard()
})
})
activeCount < MAX_CONCURRENT && loadCard()
}
// add card to processing queue
function addCardToQueue(card, url, item)
{
if (inFlightItems.has(item)) return
processingQueue.push({ card, url, item })
inFlightItems.add(item)
loadCard()
}
// extract data from card page
function fillCardData(card, doc, item)
{
const sellernode = $x('//a[contains(@href,"search/customer")]',doc)
const [, seller] = sellernode?.href?.match(/\/item\/search\/customer\/(.*)/) || []
const data = { hide: false, seller };
if (seller && !(seller in sellersData)) {
sellersData[seller] = {
name: sellernode.innerText.trim(),
url: sellernode.href,
hide: false
}
}
// get end time
data.endTime = $x('//li[.//*[contains(text(),"Closing Time")]]',doc)
?.$('span:last-child')
?.textContent
// get number of watchers
data.watchNum =
Array.from(doc.$$('script'))
.map(s => s.textContent.match(/buyee\.TOTAL_WATCH_COUNT\s*=\s*(\d+);/))
.find(Boolean)?.[1]
?? doc.$('.watchButton__watchNum')?.textContent
?? 0
// get image links
data.images = Array.from(doc.$$('.js-smartPhoto')).map(node => node.href)
itemsData[item] = data
serialiseData()
}
// patch SmartPhoto's internal click to track "opening in progress"
document.addEventListener('click', e => {
const node = e.target.closest('a.js-smartPhoto')
if (node) smartPhotoOpen = true
})
smartPhotoNode.addEventListener('close', () => {
smartPhotoOpen = false
for (const item in pendingSmartPhotoItems) smartPhoto.addNewItem(item)
pendingSmartPhotoItems = []
})
function addSmartPhotoItem(node) {
node.classList.add('js-smartPhoto')
if (smartPhotoOpen) pendingSmartPhotoItems.push(node)
else smartPhoto.addNewItem(node)
}
const actions = {
hue: { txt:'H', step:1, min:-180, max:180, val:0, def:0},
sat: { txt:'S', step:2, min:0, max:300, val:100, def:100 },
bri: { txt:'V', step:1, min:0, max:300, val:100, def:100 }
}
const spObserver = new MutationObserver(() => smartPhotoNode?.$('.sp-filters') || addControls(smartPhotoNode))
spObserver.observe(smartPhotoNode, { childList: true, subtree: true })
smartPhotoNode.addEventListener('open', () => Object.values(actions).forEach(a => (a.val = a.def)))
function addControls(overlay) {
if (overlay.$('.sp-filters')) return
const toggle = el('button.sp-toggle', {
text: 'Adjust',
style: {
position: 'absolute',
top: '15px',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(0,0,0,0.4)',
color: 'white',
border: 'none',
borderRadius: '10%',
padding: '5px',
height: '30px',
cursor: 'pointer',
zIndex: 10000
},
onclick: () => {
const shown = panel.style.opacity === '1'
panel.style.opacity = shown ? '0' : '1'
panel.style.pointerEvents = shown ? 'none' : 'auto'
}
})
const panel = el('div.sp-filters', {
style: {
position: 'absolute',
top: '50px',
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
flexDirection: 'column',
gap: '8px',
zIndex: 9999,
background: 'rgba(0,0,0,0.4)',
padding: '6px 10px',
borderRadius: '8px',
userSelect: 'none',
color: 'white',
font: '14px sans-serif',
touchAction: 'manipulation',
opacity: 0,
pointerEvents: 'none'
}},
...Object.entries(actions).map(([k, a]) =>
el('div.sp-row', { style: {
display: 'flex',
alignItems: 'center',
gap: '8px'
}},
el('button', {
'data-action' : k,
'data-step': -a.step,
translate: 'no',
text: a.txt + '-',
style: 'min-width: 42px'}),
el('span.sp-val', {
'data-for' : k,
text: a.val,
style: 'min-width: 25px; text-align: center'
}),
el('button', {
'data-action' : k,
'data-step': a.step,
translate: 'no',
text: a.txt + '+',
style: 'min-width: 42px'
})
)),
el('button', {
'data-action' : 'reset',
translate: 'no',
text: 'Ø',
style: 'min-width: 42px'
})
)
overlay.append(toggle,panel)
const update = () => {
const img = $('.smartphoto-list .current .smartphoto-img')
if (img) img.style.filter = `hue-rotate(${actions.hue.val}deg) saturate(${actions.sat.val}%) brightness(${actions.bri.val}%)`
panel.$$('.sp-val').forEach(s => (s.textContent = actions[s.dataset.for].val))
}
const act = (name, step) => {
if (name === 'reset') Object.values(actions).forEach(a => (a.val = a.def))
else {
const a = actions[name]
a.val = Math.max(a.min, Math.min(a.max, a.val + step))
}
update()
}
// --- Hold-to-repeat setup ---
let holdTimer, activeBtn
function startHold(btn) {
const {action, step} = btn.dataset
activeBtn = btn
btn.style.transform = 'scale(0.92)'
act(action, +step)
holdTimer = setTimeout(() => {
holdTimer = setInterval(() => act(action, +step), 30)
}, 150)
}
function stopHold() {
activeBtn?.style && (activeBtn.style.transform = '')
clearTimeout(holdTimer)
clearInterval(holdTimer)
holdTimer = activeBtn = null
}
;['mousedown', 'touchstart'].forEach( ev =>
panel.addEventListener(ev, e => {
const btn = e.target.closest('button[data-action]')
if (btn) {
e.preventDefault()
startHold(btn)
}
})
)
;['mouseup', 'mouseleave', 'touchend', 'touchcancel']
.forEach( ev => panel.addEventListener(ev, stopHold))
update()
}
// 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) {
// find url
const url = card.$('.itemCard__itemName a').href;
const [, item] = url.match(/\/item\/jdirectitems\/auction\/([^?&]*)/) || []
card.$('.g-thumbnail__outer a').href = "#"
if (!(item in itemsData)) {
addCardToQueue(card, url, item)
return
}
// refresh any cards on the watchlist
if (buyeeWatchlist.includes(item) && !readItems.has(item)) addCardToQueue(card, url, item)
const itemData = itemsData[item]
const seller = itemData.seller
const sellerData = sellersData[seller]
// update last seen
itemData.lastSeen = Date.now()
// clear old cruft
card.$$('.rg-node').forEach(node => node.remove())
clearInfoNodes(card)
let statusNode = card.$('ul.auctionSearchResult__statusList')
let sellerNode = addSellerNode(card, sellerData)
let timeLeftNode = card.querySelector(".itemCard__infoList").firstElementChild
timeLeftNode.querySelector(".g-title").innerText = "Time Left"
timeLeftNode.querySelector(".g-text").classList.remove("g-text--attention")
addCountdown(itemData.endTime, timeLeftNode.querySelector(".g-text"))
addWatchersNode(card, itemData.watchNum);
// link images
if (itemData.images.length > 0) {
let thumbnailNode = card.querySelector(".g-thumbnail__outer a");
if (!(thumbnailNode.classList.contains("js-smartPhoto"))) {
thumbnailNode.href = itemData.images[0];
thumbnailNode.dataset.group = item;
addSmartPhotoItem(thumbnailNode);
let imageDiv = document.createElement("div");
thumbnailNode.parentNode.append(imageDiv);
imageDiv.style.display = "none";
itemData.images.slice(1).forEach( image => {
let imgNode = document.createElement("a");
imgNode.href = image;
imgNode.dataset.group = item;
imageDiv.append(imgNode);
addSmartPhotoItem(imgNode);
});
}
}
const hideCard = (() => {
card.style.display = 'none';
card.style.removeProperty('opacity');
card.style.removeProperty('background-color');
});
const showHiddenCard = (() => {
card.style.opacity = '0.9';
card.style['background-color'] = '#ffbfbf';
card.style.removeProperty('display');
});
const showCard = (() => {
card.style.removeProperty('opacity');
card.style.removeProperty('background-color');
card.style.removeProperty('order');
card.style.removeProperty('display');
});
if (sellerData.hide) {
if (hideHidden) {
hideCard();
} else {
showHiddenCard();
sellerNode.parentNode.append(makeSellerStatusNode(seller,'Show seller'));
}
} else if (itemData.hide) {
if (hideHidden) {
hideCard();
} else {
showHiddenCard();
sellerNode.parentNode.append(makeSellerStatusNode(seller,'Hide seller'));
statusNode.append(makeItemStatusNode(item,card,'Show'));
}
} else {
showCard();
sellerNode.parentNode.append(makeSellerStatusNode(seller,'Hide seller'));
statusNode.append(makeItemStatusNode(item,card,'Hide'));
// refresh if auction ended
if (timeUntil(itemData.endTime) === 'Ended' && !readItems.has(item)) {
addCardToQueue(card, url, item);
}
}
}
// process changes to all results cards in a given element
function processCardsIn(element) {
element.querySelectorAll("ul.auctionSearchResult li.itemCard:not(.rg-loading)").forEach(processCard);
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 => {
// move the card
elementTo.append(card);
// if we moved a visible card, hide the loading element
if (!movedVisibleCard && card.style.display != "none") {
movedVisibleCard = true;
loadingElement.style.display = "none";
}
// add to lazy loading observer
let imgNode = card.querySelector('img.lazyLoadV2');
if (imgNode) observer.observe(imgNode);
// initialise favourite star
let watchButton = card.querySelector('div.watchButton');
let id = isDesktop ? watchButton.dataset.auctionId : watchButton.parentNode.parentNode.dataset.id;
if (buyeeWatchlist.includes(id)) {
watchButton.classList.add("is-active");
watchButton.firstElementChild.classList.remove("g-feather-star");
watchButton.firstElementChild.classList.add("g-feather-star-active");
} else {
watchButton.classList.remove("is-active");
watchButton.firstElementChild.classList.remove("g-feather-star-active");
watchButton.firstElementChild.classList.add("g-feather-star");
}
});
}
// find the URL for the next page of search results after the given one
function nextPageURL(url) {
const 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 a span.last");
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.append(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.append(card);
});
container.children[1].style.display = "none";
}
// make link to show or hide hidden results
let optionsLink = document.createElement("button");
optionsLink.type = "button";
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';
// put link in the search options bar
let optionsNode = document.createElement("span");
optionsNode.classList.add('result-num');
if (isDesktop) optionsNode.style.left = '20%';
optionsNode.append(optionsLink);
if (isDesktop) {
document.querySelector(".result-num").parentNode.append(optionsNode);
} else {
optionsNode.style.display = 'inline';
document.querySelector(".result-num").append(optionsNode);
}
// refresh watchlist
// perform initial processing of cards
// clean up old items
fetch("https://buyee.jp/api/v1/watch_list/find", {
credentials: "include",
headers: { "X-Requested-With": "XMLHttpRequest" }
})
.then(response => response.json())
.then(data => (buyeeWatchlist = data.data.list))
.then(() => processCardsIn(document))
.then(cleanItems)
// handle favourite stars
document.addEventListener("click", e => {
const watchButton = e.target.closest("div.watchButton")
if (!watchButton) return
e.preventDefault()
e.stopImmediatePropagation()
const id = isDesktop ? watchButton.dataset.auctionId : watchButton.parentNode.parentNode.dataset.id
const card = watchButton.closest("li.itemCard")
const url = card.querySelector('.itemCard__itemName a').href
const [, item] = url.match(/\/item\/jdirectitems\/auction\/(.*)(\?.*)?/) || []
if (watchButton.classList.contains('is-active')) {
fetch("https://buyee.jp/api/v1/watch_list/remove", {
"credentials": "include",
"headers": {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
"method": "POST",
"body": `auctionId=${id}`})
.then(() => {
buyeeWatchlist.splice(buyeeWatchlist.indexOf(id), 1)
watchButton.classList.remove('is-active')
watchButton.firstElementChild.classList.remove('g-feather-star-active')
watchButton.firstElementChild.classList.add('g-feather-star')
});
} else {
fetch("https://buyee.jp/api/v1/watch_list/add", {
"credentials": "include",
"headers": {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
"method": "POST",
"body": `auctionId=${id}&buttonType=search&isSignupRedirect=false`})
.then(() => {
buyeeWatchlist.push(id)
watchButton.classList.add('is-active')
watchButton.firstElementChild.classList.remove('g-feather-star')
watchButton.firstElementChild.classList.add('g-feather-star-active')
})
addCardToQueue(card,url,item)
}
})
// image lazy loader
const imageObserver = new IntersectionObserver((entries, observer) => {
for (const entry of entries) {
if (!entry.isIntersecting) return
const target = entry.target
target.src = target.dataset.src
delete target.dataset.src
target.style.background = ''
observer.unobserve(target)
}
})
// load subsequent pages of results if navi bar on screen
let currentURL = document.location
let currentPage = document
let naviOnScreen = false
const loadingNode = makeLoadingNode()
function loadPageLoop() {
setTimeout(() => {
if (naviOnScreen && notLastPage(currentPage)) {
currentURL = nextPageURL(currentURL)
// display loading node
resultsNode.append(loadingNode)
loadingNode.$('.g-title').innerText = 'Loading page ' + currentURL.searchParams.get('page')
loadingNode.style.removeProperty('display')
// get next page of results
fetchURL(currentURL)
.then(page => {
currentPage = page
processCardsIn(currentPage)
moveCards(currentPage,resultsNode, loadingNode, imageObserver)
loadPageLoop()
})
}
else if (naviOnScreen) loadingNode.style.display = 'none'
}, 100)
}
// 540 px bottom margin so that next page loads a bit before navi bar appears on screen
const loadObserver = new IntersectionObserver(entries => {
entries.map(e => (naviOnScreen = e.isIntersecting))
if (naviOnScreen) loadPageLoop()
}, { rootMargin: "0px 0px 540px 0px" })
loadObserver.observe($(isDesktop ? 'div.page_navi' : 'ul.pagination'))
}
// stuff to handle loading stage of page
if ($('.g-main:not(.g-modal)')) buyeeSellerFilter()
else {
const startupObserver = new MutationObserver(() => {
if ($('.g-main:not(.g-modal)')) {
startupObserver.disconnect()
buyeeSellerFilter()
}
})
startupObserver.observe(document.body, { childList: true, subtree: true });
}