Add infinite scrolling and options for filtering sellers to the Buyee search results page
当前为
// ==UserScript== // @name Buyee Seller Filter // @license MIT // @version 1.86 // @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 // @require https://unpkg.com/[email protected]/libs/lz-string.min.js // ==/UserScript== (() => { 'use strict' /*** utilities ***/ 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 } Document.prototype.on = Element.prototype.on = function (type, selectorOrHandler, handlerOrCapture, capture=false) { // direct binding: el.on('click', fn) if (typeof selectorOrHandler === 'function') { this.addEventListener(type, selectorOrHandler, handlerOrCapture) return this } // delegated binding: el.on('click', 'button', fn) const selector = selectorOrHandler const handler = handlerOrCapture this.addEventListener(type, ev => { const el = ev.target.closest(selector) el && this.contains(el) && handler.call(el, ev, el) }, capture) return this } function el(sel = 'div', props = {}, ...children) { const [, tag = 'div', id, cls = ''] = sel.match(/^([a-z]+)?(?:#([\w-]+))?(.*)?$/) || [] const e = document.createElement(tag) id && (e.id = id) cls && e.classList.add(...cls.split('.').filter(Boolean)) Object.entries(props).forEach(([k,v]) => { k === 'style' ? typeof v === 'string' ? e.style.cssText = v : Object.assign(e.style, v) : k === 'data' ? Object.assign(e.dataset, v) : k === 'text' ? e.textContent = v : k === 'html' ? e.innerHTML = v : /^on[A-Z0-9]/i.test(k) ? e[k] = v : e.setAttribute(k, v) }) children.flat().forEach(c => e.append(c instanceof Node ? c : document.createTextNode(c))) return e } function fetchURL(url, timeout = 10000) { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeout) return fetch(url, { signal: controller.signal }) .then(r => (clearTimeout(timer), r.ok) ? r.text() : Promise.reject({ type: 'http', status: r.status })) .then(html => new DOMParser().parseFromString(html, 'text/html')) .catch(err => { err.type ??= err.name === 'AbortError' ? 'timeout' : err instanceof TypeError ? 'network' : 'other' return Promise.reject(err) }) } const Dom = { hide: el => el && (el.style.display = 'none'), show: el => el && el.style.removeProperty('display'), hidden: el => getComputedStyle(el)?.display === 'none' } const waitFor = (sel, fn) => { const el = $(sel) if (el) return fn(el) new MutationObserver((o, obs) => { const el = $(sel) if (el) { obs.disconnect(); fn(el) } }).observe(document.body, { childList: true, subtree: true }); } /*** config ***/ const BFConfig = { SCRAPER_THREADS: 5, SCRAPER_DELAY: 100, SCROLL_DELAY: 100, HIDE_TIMEOUT: 3500 } /*** state ***/ const BFState = (() => { const isDesktop = !/android|webos|iphone|ipad|ipod|blackberry|windows phone/i.test(navigator.userAgent) const state = { sellers: {}, items: {}, hide: true } const stateUpd = { sellers: {}, items: {} } let initialised = false const compress = str => 'BFLZ:' + LZString.compressToUTF16(str) // eslint-disable-line no-undef const decompress = str => str?.startsWith('BFLZ:') ? LZString.decompressFromUTF16(str.slice(5)) : str // eslint-disable-line no-undef const safeParse = s => { if (s) try { return JSON.parse(decompress(s)) } catch { return undefined } else return undefined } const load = () => { ['sellers', 'items'].forEach(key => { const r1 = localStorage[`BF${key}`], r2 = localStorage[`BFU${key}`] if (!r1 && !r2) return const v1 = safeParse(r1) ?? {} const v2 = safeParse(r2) ?? {} const store = state[key] Object.assign(store, v1) Object.entries(v2).forEach(([k,v]) => { store[k] ??= {} Object.assign(store[k], v) }) delete localStorage[`BFU${key}`] }) state.hide = safeParse(localStorage.BFhide) ?? true const now = Date.now(), WEEK = 6048e5 const prune = (store, len) => Object.entries(store).forEach(([k,v]) => v.lastSeen && now - v.lastSeen > len && delete store[k]) prune(state.items, WEEK) prune(state.sellers, 24 * WEEK) ;['sellers', 'items'].forEach(k => (localStorage[`BF${k}`] = compress(JSON.stringify(state[k])))) initialised = true } const proxy = key => { const get = k => { return state[key][k] } const set = (k,v) => { state[key][k] = v stateUpd[key][k] = v localStorage[`BFU${key}`] = JSON.stringify(stateUpd[key]) } const assign = (k,v) => { state[key][k] ??= {} stateUpd[key][k] ??= {} Object.assign(state[key][k], v) Object.assign(stateUpd[key][k], v) localStorage[`BFU${key}`] = JSON.stringify(stateUpd[key]) } return { get, set, assign } } return new Proxy({ sellers: proxy('sellers'), items: proxy('items'), hide: true, isDesktop }, { get(obj, k) { if (!initialised) load() return obj[k] }, set(obj, k, v) { obj[k] = v if (k === 'hide') localStorage.BFhide = JSON.stringify(v) return true } }) })() /*** image filters ***/ const BFFilters = (() => { const filters = { hue: { txt:'Hue', step:1, min:-180, max:180, val:0, def:0, filt() { return `hue-rotate(${this.val}deg)` }}, sat: { txt:'Sat', step:2, min:0, max:300, val:100, def:100, filt() { return `saturate(${this.val}%)` }}, bri: { txt:'Lum', step:1, min:0, max:300, val:100, def:100, filt() { return `brightness(${this.val}%)`}}, con: { txt:'Con', step:2, min:0, max:300, val:100, def:100, filt() { return `contrast(${this.val}%)`}} } const act = (filt, sgn) => { const a = filters[filt] a.val = Math.max(a.min, Math.min(a.max, a.val + a.step * sgn)) } const reset = o => Object.entries(filters).forEach(([k,v]) => (v.val = (o?.[k] ?? v.def))) const style = (o = null) => { o && reset(o); return Object.values(filters).map(v => v.filt()).join(' ') } const values = () => Object.fromEntries(Object.entries(filters).map(([k,v]) => [k, v.val])) const changed = () => Object.fromEntries(Object.entries(filters).filter(([k,v]) => v.val !== v.def).map(([k,v]) => [k, v.val])) const names = () => Object.fromEntries(Object.entries(filters).map(([k,v]) => [k, v.txt])) return { act, style, reset, values, changed, names } })() /*** smartphoto enhancements ***/ const BFPhoto = (() => { const smartPhoto = new SmartPhoto(".js-smartPhoto", { showAnimation: false }) // eslint-disable-line no-undef const node = $('.smartphoto') const pending = [] let open = false let id, sellerid, seller const add = img => { img.classList.add('js-smartPhoto') open ? pending.push(img) : smartPhoto.addNewItem(img) } const addControls = overlay => { if ($('.sp-filters')) return const panel = el('div.sp-filters', { style: { opacity: '0', pointerEvents: 'none' }}, ...Object.entries(BFFilters.names()).map(([k, txt]) => el('div.sp-row', { style: { display: 'flex', alignItems: 'center', gap: '8px' }}, el('span', { text: `${txt}:`, style: 'min-width: 40px' }), el('span.sp-val', { data: { for: k }, style: 'min-width: 25px' }), el('button', { data: { action: k, step: '-1' }, text: '-', style: 'min-width: 40px; min-height: 40px' }), el('button', { data: { action: k, step: '+1' }, text: '+', style: 'min-width: 40px; min-height: 40px' }) )), el('button', { data: { action: 'reset' }, translate: 'no', text: 'Ø', style: 'min-height: 40px' }) ) const toggle = el('button.sp-toggle', { text: 'Adjust', translate: 'no', onclick: () => { const shown = panel.style.opacity === '1' panel.style.opacity = shown ? '0' : '1' panel.style.pointerEvents = shown ? 'none' : 'auto' } }) overlay.append(toggle,panel) const update = () => { const style = BFFilters.style() const sellerThumbs = Array.from($$('li.itemCard[data-id]')) .filter(card => BFState.items.get(card.dataset.id)?.seller === sellerid) .map(card => card.$('.g-thumbnail__outer img')).filter(Boolean) ;[...($('.smartphoto-list')?.$$('.smartphoto-img') ?? []), ...($('.smartphoto-nav')?.$$('a') ?? []), ...sellerThumbs ].forEach(img => (img.style.filter = style)) panel.$$('.sp-val').forEach(s => (s.textContent = BFFilters.values()[s.dataset.for])) } const act = (name, step) => { name === 'reset' ? BFFilters.reset() : BFFilters.act(name, step) update() } let holdTimer, activeBtn const startHoldEvs = (...evs) => evs.forEach(ev => panel.on(ev, 'button[data-action]', (e, btn) => { e.preventDefault() 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) }) ) const stopHoldEvs = (...evs) => evs.forEach(ev => panel.on(ev, () => { activeBtn && (activeBtn.style.transform = '') clearTimeout(holdTimer) clearInterval(holdTimer) holdTimer = activeBtn = null }) ) panel.on('bf-update', update) startHoldEvs('mousedown', 'touchstart') stopHoldEvs('mouseup', 'mouseleave', 'touchend', 'touchcancel') update() } const load = () => BFFilters.reset(seller?.filters) const save = () => seller && BFState.sellers.assign(sellerid, { filters: BFFilters.changed() }) document.on('pointerdown', 'a.js-smartPhoto', (e, a) => { open = true id = a.dataset.group sellerid = BFState.items.get(id)?.seller seller = sellerid && BFState.sellers.get(sellerid) load() }, true) smartPhoto.on('open', () => { id = smartPhoto.data.currentGroup sellerid = BFState.items.get(id)?.seller seller = sellerid && BFState.sellers.get(sellerid) load() $('.sp-filters')?.dispatchEvent(new CustomEvent('bf-update')) }) smartPhoto.on('close', () => { save() open = false pending.forEach(e => smartPhoto.addNewItem(e)) pending.length = 0 }) new MutationObserver(() => $('.sp-filters') || addControls(node)) .observe(node, { childList: true, subtree: true }) document.on('pointerdown', (e) => { const panel = $('.sp-filters') const toggle = $('.sp-toggle') panel && panel.style.opacity === '1' && !panel.contains(e.target) && !toggle.contains(e.target) && toggle.click() }) return { add } })() /*** countdown timers ***/ const BFCountdown = (() => { const countdowns = new Map() function timeUntil(dateStr) { const jstDate = new Date(dateStr + ' GMT+0900') const now = new Date() const diff = (jstDate - now)/1000 if (diff <= 0) return "Ended" const fmt = (n, t) => n > 0 ? `${n} ${t}${n > 1 ? 's' : ''}` : null if (diff < 1800) { const min = Math.floor((diff / 60) % 60) const sec = Math.floor(diff % 60) return [fmt(min, 'minute'), fmt(sec, 'second')].filter(Boolean).join(' ') } else { const days = Math.floor(diff / 86400) const hrs = Math.floor((diff / 3600) % 24) const min = Math.floor((diff / 60) % 60) return [fmt(days, "day"), fmt(hrs, "hour"), fmt(min, "minute")].filter(Boolean).join(' ') } } function add(date, el) { countdowns.set(el, date) el.$('.g-text').textContent = timeUntil(date) } function remove(el) { countdowns.delete(el) } function update() { countdowns.forEach((date, el) => (el.$('.g-text').textContent = timeUntil(date))) setTimeout(update, 1000) } update() return { add, remove } })() /*** card scraping ***/ const BFQueue = (callback) => { const processingQueue = [] const inFlightItems = new Set() const readItems = new Set() let activeCount = 0 function run() { if (!processingQueue.length || activeCount >= BFConfig.SCRAPER_THREADS) return const { id, url } = processingQueue.shift() activeCount++ fetchURL(url) .then(doc => { callback(id, doc) readItems.add(id) }) .catch(err => console.error('fetchURL failed for', url, err)) .finally(() => { inFlightItems.delete(id) setTimeout(() => { activeCount-- run() }, BFConfig.SCRAPER_DELAY) }) activeCount < BFConfig.SCRAPER_THREADS && run() } // force = true allows reprocessing of a card function add(item, force = false) { if (inFlightItems.has(item.id) || (readItems.has(item.id) && !force)) return processingQueue.push(item) inFlightItems.add(item.id) run() } return { add } } /*** handle watchlist ***/ const BFWatchlist = (() => { let watchlist = [] const apiCall = (url, body = '') => fetch(`https://buyee.jp/api/v1/${url}`, { credentials: 'include', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, method: 'POST', body }).then(r => r.json()) const has = id => watchlist.includes(id) const refresh = () => apiCall('watch_list/find') .then(data => (watchlist = data?.data?.list ?? [])) const add = id => apiCall('watch_list/add', `auctionId=${id}&buttonType=search&isSignupRedirect=false`) .then(() => watchlist.push(id)) const remove = id => apiCall('watch_list/remove', `auctionId=${id}`) .then(() => watchlist.splice(watchlist.indexOf(id), 1)) const toggle = id => has(id) ? remove(id) : add(id) const updateBtn = (btn, id) => { const active = has(id) btn.classList.toggle('is-active', active) btn.firstElementChild.classList.toggle('g-feather-star-active', active) btn.firstElementChild.classList.toggle('g-feather-star', !active) } return { refresh, add, remove, toggle, has, updateBtn } })() const BF = (() => { const BFCardScraper = BFQueue((id, doc) => { const sellernode = $x('//a[contains(@href,"search/customer")]',doc) const [, sellerid] = sellernode?.href?.match(/\/item\/search\/customer\/(.*)/) || [] const data = { hide: false, seller: sellerid } const bidBtn = $x('//button[contains(normalize-space(.), "Snipe")]', doc) const msg = doc.$('.messagebox, .message.error') if(sellerid && !BFState.sellers.get(sellerid)) { BFState.sellers.set(sellerid, { 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 // can I bid? bidBtn?.classList.contains('disable') && (data.blocked = (msg?.textContent.includes('manually reviewing')) ? 'P' : true) // P = pending // can I request a review? msg?.$('a')?.href?.includes('inquiry') && (data.review = msg.$('a').href) // 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) BFState.items.set(id, data) annotateCard($(`li.itemCard[data-id="${id}"]`)) }) function annotateCard(card) { const url = card.$('.itemCard__itemName a').href const id = card.dataset.id = url.match(/\/item\/jdirectitems\/auction\/([^?&]*)/)?.[1] || '' const item = BFState.items.get(id) const sellerid = item?.seller const seller = sellerid && BFState.sellers.get(sellerid) const infoList = card.$('.itemCard__infoList') const statusList = card.$('ul.auctionSearchResult__statusList') const scrape = (force = false) => BFCardScraper.add({ url, id }, force) const newInfoNode = (title, body) => el('li.itemCard__infoItem.bf-node', {}, el('span.g-title', { text: title }), el('span.g-text', ...(typeof body === 'string' ? [{ text : body }] : [{}, body]))) const resetCard = () => { card.$$('.bf-node').forEach(node => node.remove()) card.$$('li.itemCard__infoItem').forEach(node => node.textContent.trim() === '' && node.remove()) card.$('.g-thumbnail__outer a').href = "#" } const handleSellerInfo = () => { infoList.append(newInfoNode( 'Seller', el('a', { text: seller.name, href: seller.url, target: '_blank', rel: 'noopener noreferrer' }) )) } const handleTimeLeft = () => { const oldTime = infoList.firstElementChild const newTime = newInfoNode('Time Left','') newTime.classList.remove('bf-node') BFCountdown.remove(oldTime) oldTime.replaceWith(newTime) BFCountdown.add(item.endTime, newTime) } const handleWatchers = () => { card.$('.g-feather')?.parentNode .append(el('span.watchList__watchNum.bf-node', { text: item.watchNum, style: 'user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none' })) } const handleBlocked = () => { const node = item.review ? el('button.bf-node', { type: 'button', text: 'Unblock', style: { color: 'red', backgroundColor: 'pink', border: 'solid 1px red', fontSize: '100%' }, onclick: () => { window.open(item.review, '_blank') delete item.review scrape('force') } }) : item.blocked ? el('li.bf-node.auctionSearchResult__statusItem', { text: item.blocked === 'P' ? 'Pending' : 'Blocked', style: { color: 'red', backgroundColor: 'pink' } }) : null node && statusList.append(node) } const attachSmartPhoto = () => { const thumb = card.$('.g-thumbnail__outer a') if (thumb?.classList.contains('js-smartPhoto') || !item.images?.length) return thumb.href = item.images[0] thumb.dataset.group = id BFPhoto.add(thumb) seller.filters && (thumb.$('img').style.filter = BFFilters.style(seller.filters)) const hidden = el('div', { style: 'display:none' }) thumb.parentNode.append(hidden) item.images.slice(1).forEach( image => { const link = el('a', { href: image, data: { group: id }}) hidden.append(link) BFPhoto.add(link) }) } const handleShowHideBtns = () => { const btn = (v, txt, fn) => el('button.bf-node', { type: 'button', text: [v.hide ? 'Show' : 'Hide', txt].join(' '), onclick: e => fn.call(e.currentTarget, e), style: 'font-size: 100%' }) const hidden = seller.hide || item.hide const confirm = (node, fn) => { if (node.textContent.includes('Hide')) { const old = node.textContent node.textContent = 'Really?' setTimeout(() => (node.textContent = old), BFConfig.HIDE_TIMEOUT) } else fn() } if (hidden && BFState.hide) { Dom.hide(card) } else { Dom.show(card) card.style.opacity = hidden ? '0.9' : '' card.style['background-color'] = hidden ? '#ffbfbf' : '' infoList.append(btn(seller, 'seller', function () { confirm(this, () => { BFState.sellers.assign(sellerid, { hide: !seller.hide }); annotateCardsIn(document) }) })) if (!seller.hide || BFState.hide) { statusList.append(btn(item, '', function () { confirm(this, () => { BFState.items.assign(id, { hide: !item.hide }) annotateCard(card) }) })) } new Date(item.endTime + ' GMT+0900') < Date.now() && scrape() item.blocked && scrape() } } resetCard() if (!item) { scrape(); return } BFWatchlist.has(id) && scrape() BFState.items.assign(id, { lastSeen: Date.now() }) if (sellerid) BFState.sellers.assign(sellerid, { lastSeen: Date.now() }) handleSellerInfo() handleTimeLeft() handleWatchers() handleBlocked() attachSmartPhoto() handleShowHideBtns() } function annotateCardsIn(element) { element.$$('ul.auctionSearchResult li.itemCard:not(.rg-loading)').forEach(annotateCard) } const moveCards = (() => { const observer = new IntersectionObserver((entries, obs) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue target.src = target.dataset.src delete target.dataset.src target.style.background = '' obs.unobserve(target) } }) return function moveCards(src, tgt, loadingEl) { if (!src || !tgt) return const batch = el('div', { style: { opacity: 0, transform: 'translateY(30px)', transition: 'opacity 0.4s ease, transform 0.4s ease' }}) const imgs = [] let anyVisible = false src.$$('ul.auctionSearchResult li.itemCard:not(.rg-loading)').forEach(card => { const id = card.dataset.id if (tgt.$(`li.itemCard[data-id="${id}"]`)) return batch.append(card) anyVisible = anyVisible || !Dom.hidden(card) const btn = card.$('div.watchButton') btn && BFWatchlist.updateBtn(btn, id) const img = card.$('img.lazyLoadV2') if (img) { img.dataset.src ??= img.src img.style.background = 'url(https://cdn.buyee.jp/images/common/loading-spinner.gif) no-repeat 50%;' img.src = 'https://cdn.buyee.jp/images/common/spacer.gif' imgs.push(img) } }) tgt.append(batch) imgs.forEach(img => observer.observe(img)) if (anyVisible) { Dom.hide(loadingEl) requestAnimationFrame(() => requestAnimationFrame(() => { batch.style.opacity = 1; batch.style.transform = '' })) } } })() const handleInfiniteScroll = () => { const container = $('.g-main:not(.g-modal)') const resultsNode = container?.firstElementChild const loadingNode = 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' }) ) const KEY = 'BFSession' let currentURL = new URL(document.location) let currentPage = currentURL.searchParams.get('page') || 1 let naviOnScreen = false let isLoading = false let isLastPage = false const saveSession = () => { const state = { currentPage: isLoading ? Math.max(1, currentPage - 1) : currentPage, cards: resultsNode?.outerHTML, scrollY: window.scrollY } sessionStorage.setItem(KEY, JSON.stringify(state)) } const restoreSession = () => { if (performance.getEntriesByType('navigation')[0]?.type !== 'back_forward') { sessionStorage.removeItem(KEY) window.scrollTo(0, 0) return } const raw = sessionStorage.getItem(KEY) if (!raw) return false try { const state = JSON.parse(raw) if (state.currentPage) currentPage = state.currentPage setTimeout(() => window.scrollTo(0, state?.scrollY ?? 0), 50) const doc = new DOMParser().parseFromString(state.cards, 'text/html') if (doc) { resultsNode.innerHTML = '' annotateCardsIn(doc) moveCards(doc, resultsNode, loadingNode) } } catch (err) { console.error('[BFScrollCache] restore failed', err) } finally { sessionStorage.removeItem(KEY) } } const loadPageLoop = () => { if (isLoading) return if (!naviOnScreen || isLastPage) { naviOnScreen && Dom.hide(loadingNode) return } isLoading = true currentPage++ currentURL.searchParams.set('page', currentPage) resultsNode.append(loadingNode) loadingNode.$('.g-title').textContent = `Loading page ${currentPage}` Dom.show(loadingNode) fetchURL(currentURL) .then(doc => { annotateCardsIn(doc) moveCards(doc, resultsNode, loadingNode) isLoading = false isLastPage = BFState.isDesktop ? !doc.$('div.page_navi a:nth-last-child(2)') || doc.$('div.page_navi a:nth-last-child(2)').innerText !== '>' : !doc.$('li.page--arrow a span.last') setTimeout(loadPageLoop, BFConfig.SCROLL_DELAY) }) .catch(e => { isLoading = false currentPage-- e.type === 'http' && e.status === 404 ? Dom.hide(loadingNode) : setTimeout(loadPageLoop, BFConfig.SCROLL_DELAY) }) } const pageNavi = $(BFState.isDesktop ? 'div.page_navi' : 'ul.pagination') let lastY = 0 let isStopped = false let above = true window.addEventListener('scroll', () => { const rect = pageNavi.getBoundingClientRect() const stopY = rect.top - window.visualViewport.height const isDown = window.scrollY > lastY lastY = window.scrollY if (above && !isStopped && stopY < -50) { above = false // if (isDown && !isStopped && stopY > -125 && stopY < -50) { isStopped = true window.scrollTo({ top: window.scrollY + rect.top - window.visualViewport.height, behavior: 'smooth' }) setTimeout(() => { document.body.overflow = 'hidden' setTimeout(() => { document.body.overflow = 'auto' isStopped = false }, 25) }, 100) } else if (!above & stopY > 0) above = true }) const observer = new IntersectionObserver(entries => { entries.forEach(e => (naviOnScreen = e.isIntersecting)) naviOnScreen && loadPageLoop() }, { rootMargin: "0px 0px 540px 0px" }) restoreSession() window.addEventListener('beforeunload', saveSession) observer.observe(pageNavi) } function main() { const container = $('.g-main:not(.g-modal)') const resultsNode = container.firstElementChild const handleVisibilityChange = () => { document.on('visibilitychange', () => { if (!document.hidden) { const hide = BFState.hide // BFState.load() BFState.hide = hide // annotateCardsIn(document) } }) } const injectStyles = () => { const common = ` button.bf-node { border: 1px solid silver; padding: 3px 5px; border-radius: 3px; margin: 2px; font-family: Arial,"Hiragino Kaku Gothic ProN",Meiryo,"MS PGothic",sans-serif; } button.sp-toggle { position: absolute; top: 15px; left: 50%; transform: translateX(-50%); padding: 10px; height: 40px; cursor: pointer; z-index: 10000; background: rgba(0,0,0,0.5); color: white; border: none; border-radius: 6px; } div.sp-filters { position: absolute; top: 60px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 8px; padding: 10px; z-index: 9999; background: rgba(0,0,0,0.4); color: white; border-radius: 6px; user-select: none; font: 14px sans-serif; touch-action: manipulation; } ` const desktop = ` button.bf-node:hover { filter: brightness(90%); } ` const mobile = ` #goog-gt-tt, .goog-te-balloon-frame{display: none !important;} .goog-text-highlight { background: none !important; box-shadow: none !important;} ` document.head.append(el('style', { text: common + (BFState.isDesktop ? desktop : mobile) })) } const mergeResultsLists = () => { if (container.children.length > 1) { const second = container.children[1] second.$$('ul.auctionSearchResult li.itemCard') .forEach(card => resultsNode.append(card)) Dom.hide(second) } } const handleShowHideLink = () => { const showHideParent = BFState.isDesktop ? $('.result-num').parentNode : $('.result-num') showHideParent.append( el('span.result-num', { style: BFState.isDesktop ? 'left:20%' : 'display:inline' }, el('button#rg-show-hide-link', { type: 'button', text: (BFState.hide ? 'Show' : 'Hide') + ' hidden', onclick: function () { BFState.hide = !BFState.hide this.innerText = (BFState.hide ? 'Show' : 'Hide') + ' hidden' annotateCardsIn(document) }, style: 'display:inline-block; width:110px' }) ) ) } const handleWatchBtns = () => { document.on('click', 'div.watchButton', (e, btn) => { e.preventDefault() e.stopImmediatePropagation() const id = BFState.isDesktop ? btn.dataset.auctionId : btn.parentNode.parentNode.dataset.id const card = btn.closest("li.itemCard") const url = card.$('.itemCard__itemName a')?.href BFWatchlist.toggle(id).then(() => { BFWatchlist.updateBtn(btn,id) BFWatchlist.has(id) && BFCardScraper.add({ url, id }, 'force') }) }) } injectStyles() handleVisibilityChange() mergeResultsLists() handleShowHideLink() handleWatchBtns() handleInfiniteScroll() BFWatchlist.refresh().then(() => annotateCardsIn(document)) } waitFor('.g-main:not(.g-modal)', main) })() })()