Buyee Seller Filter

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

当前为 2025-10-17 提交的版本,查看 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Buyee Seller Filter
// @license      MIT
// @version      1.69
// @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==


/*** DOM utils ***/

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, maybeHandler, capture=false) {
  // direct binding: el.on('click', fn)
  if (typeof selectorOrHandler === 'function') {
    this.addEventListener(type, selectorOrHandler)
    return this
  }

  // delegated binding: el.on('click', 'button', fn)
  const selector = selectorOrHandler
  const handler = maybeHandler
  this.addEventListener(type, ev => {
    const el = ev.target.closest(selector)
    if (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)
  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 === 'dataset') Object.assign(e.dataset, 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
}

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'))
}

const Dom = {
  hide: el => { if (el) el.style.display = 'none' },
  show: el => { if (el) el.style.removeProperty('display') },
  hidden: el => getComputedStyle(el)?.display === 'none'
}


/*** state ***/

const BFState = {
  sellers: {},
  items: {},
  hide: true,
  savedKeys: ['sellers', 'items', 'hide'],

  save() {
    this.savedKeys.forEach(k =>
      (localStorage[`BF${k}`] = JSON.stringify(this[k])))
  },

  load() {
    this.savedKeys.forEach(k => {
      const v = localStorage[`BF${k}`]
      if (v) this[k] = JSON.parse(v)
    })
  },

  clean() {
    const now = Date.now()
    for (const url in this.items) {
      if (now - this.items[url].lastSeen > 604800000) delete this.items[url]
    }
    this.save()
  },

  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
}

/*** 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 style = () => Object.values(filters).map(v => v.filt()).join(' ')
  const reset = o => Object.entries(filters).forEach(([k,v]) => (v.val = (o && o[k]) ?? v.def))
  const values = () => Object.fromEntries(Object.entries(filters).map(([k,v]) => [k, v.val]))
  const changedValues = () => 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]))
  const styleOf = o => { reset(o); return style() }

  return { act, style, reset, values, changedValues, names, styleOf }
})()



/*** 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

  const add = img => {
    img.classList.add('js-smartPhoto')
    if (open) pending.push(img)
    else smartPhoto.addNewItem(img)
  }

  const addControls = overlay => {
    if ($('.sp-filters')) return

    const panel = el('div.sp-filters', {
      style: {
        position: 'absolute',
        top: '60px',
        left: '50%',
        transform: 'translateX(-50%)',
        display: 'flex',
        flexDirection: 'column',
        gap: '8px',
        zIndex: 9999,
        background: 'rgba(0,0,0,0.4)',
        padding: '10px',
        borderRadius: '6px',
        userSelect: 'none',
        color: 'white',
        font: '14px sans-serif',
        touchAction: 'manipulation',
        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,
            'data-step': '-1',
            text: '-',
            style: 'min-width: 40px; min-height: 40px'
          }),
          el('button', {
            'data-action' : k,
            'data-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',
      style: {
        position: 'absolute',
        top: '15px',
        left: '50%',
        transform: 'translateX(-50%)',
        background: 'rgba(0,0,0,0.5)',
        color: 'white',
        border: 'none',
        borderRadius: '6px',
        padding: '10px',
        height: '40px',
        cursor: 'pointer',
        zIndex: 10000
      },
      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 sellerid = BFState.items[id]?.seller
      const sellerThumbs = Array.from($$('li.itemCard[data-id]'))
                          .filter(card => BFState.items[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) => {
      if (name === 'reset') BFFilters.reset()
      else 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, () => {
        if (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 = () => {
    const sellerid = BFState.items[id]?.seller
    const seller = sellerid && BFState.sellers[sellerid]
    BFFilters.reset(seller?.filters)
  }

  const save = () => {
    const sellerid = BFState.items[id]?.seller
    const seller = BFState.sellers[sellerid]
    if (seller) {
      seller.filters = BFFilters.changedValues()
      BFState.save()
    }
  }

  document.on('pointerdown', 'a.js-smartPhoto', (e, a) => {
    open = true
    id = a.dataset.group
    load()
  }, true)

  smartPhoto.on('open', () => {
    const id = smartPhoto.data.currentGroup
    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')
    if (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() {
    for (const [el, date] of countdowns)
      el.$('.g-text').textContent = timeUntil(date)
    setTimeout(update, 1000)
  }

  update()

  return { add, remove }
})()

/*** card scraping ***/

const BFQueue = (callback) => {
  const MAX_CONCURRENT = 3
  const DELAY = 250

  const processingQueue = []
  const inFlightItems = new Set()
  const readItems = new Set()
  let activeCount = 0

  const sleep = ms => new Promise(r => setTimeout(r, ms))

  function run() {
    if (!processingQueue.length || activeCount >= MAX_CONCURRENT) 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)
      sleep(DELAY).then(() => {
        activeCount--
        run()
      })
    })
    activeCount < MAX_CONCURRENT && 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 }
}

const BFCardScraper = BFQueue((id, doc) => {
  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 BFState.sellers))
    BFState.sellers[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)

  BFState.items[id] = data
  BFState.save()
  annotateCard(document.$(`li.itemCard[data-id="${id}"]`))
})

/*** 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 }

})()

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[id]
  const seller = item && BFState.sellers[item.seller]
  const infoList = card.$('.itemCard__infoList')
  const statusList = card.$('ul.auctionSearchResult__statusList')

  const scrape = () => BFCardScraper.add({ url, id })

  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 = () => {
    const star = card.$('.g-feather').parentNode
    star.append(el('span.watchList__watchNum.bf-node', { text: item.watchNum }))
  }

  const attachSmartPhoto = () => {
    if (!item.images?.length) return
    const thumb = card.$('.g-thumbnail__outer a')
    if (thumb.classList.contains('js-smartPhoto')) return

    thumb.href = item.images[0]
    thumb.dataset.group = id
    BFPhoto.add(thumb)

    if (seller.filters) thumb.$('img').style.filter = BFFilters.styleOf(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, dataset: { 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: () => { v.hide = !v.hide; BFState.save(); fn() }
      })
    const hidden = seller.hide || item.hide

    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', () => annotateCardsIn(document)))
      if (!seller.hide || BFState.hide) statusList.append(btn(item, '', () => annotateCard(card)))
      if (new Date(item.endTime + ' GMT+0900') < Date.now()) scrape()
    }
  }

  resetCard()
  if (!item) { scrape(); return }
  if (BFWatchlist.has(id)) scrape()
  item.lastSeen = Date.now()

  handleSellerInfo()
  handleTimeLeft()
  handleWatchers()
  attachSmartPhoto()
  handleShowHideBtns()
}

function annotateCardsIn(element) {
  element.$$('ul.auctionSearchResult li.itemCard:not(.rg-loading)').forEach(annotateCard)
  BFState.save()
}

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

    src.$$('ul.auctionSearchResult li.itemCard').forEach(card => {
      const id = card.dataset.id
      if (tgt.$(`li.itemCard[data-id="${id}"]`)) return

      tgt.append(card)

      if (!(Dom.hidden(card)) && !(Dom.hidden(loadingEl))) Dom.hide(loadingEl)

      const img = card.$('img.lazyLoadV2')
      if (img) observer.observe(img)

      const btn = card.$('div.watchButton')
      if (btn) BFWatchlist.updateBtn(btn, id)
    })
  }
})()

function buyeeSellerFilter () {
  const container = document.$('.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 hideTranslate = () => {
    if (!BFState.isDesktop) {
      document.head.append(el('style', { text: `
#goog-gt-tt, .goog-te-balloon-frame{display: none !important;}
.goog-text-highlight { background: none !important; box-shadow: none !important;}
      `}))
    }
  }

  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)
      ? document.$('.result-num').parentNode
      : document.$('.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
            BFState.save()
            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)
      BFWatchlist.updateBtn(btn, id)
      if (BFWatchlist.has(id)) BFCardScraper.add({ url, id }, 'force')
    })
  }

  const handleInfiniteScroll = () => {
    let currentURL = new URL(document.location)
    let currentPage = document
    let naviOnScreen = false

    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 nextPageURL = url => {
      url.searchParams.set('page', 1 + (+url.searchParams.get('page') || 1))
      return url
    }

    const lastPage = doc => BFState.isDesktop
      ? doc.$('div.page_navi a:nth-last-child(2)')?.innerText !== '>'
      : doc.$('li.page--arrow a span.last') === null

    const loadPageLoop = () => {
      if (!naviOnScreen || lastPage(currentPage)) {
        if (naviOnScreen) Dom.hide(loadingNode)
        return
      }

      currentURL = nextPageURL(currentURL)
      resultsNode.append(loadingNode)
      loadingNode.$('.g-title').innerText = 'Loading page ' + currentURL.searchParams.get('page')
      Dom.show(loadingNode)

      fetchURL(currentURL)
      .then(page => {
        currentPage = page
        annotateCardsIn(currentPage)
        moveCards(currentPage, resultsNode, loadingNode)
      })
      .catch(e => console.error('Failed to load page: ', e))
      .finally(() => setTimeout(loadPageLoop, 100))
    }

    const observer = new IntersectionObserver(entries => {
      entries.forEach(e => (naviOnScreen = e.isIntersecting))
      if (naviOnScreen) loadPageLoop()
    }, { rootMargin: "0px 0px 540px 0px" })

    observer.observe($(BFState.isDesktop ? 'div.page_navi' : 'ul.pagination'))
  }

  BFState.load()
  hideTranslate()
  handleVisibilityChange()
  mergeResultsLists()
  handleShowHideLink()
  handleWatchBtns()
  handleInfiniteScroll()

  BFWatchlist.refresh()
  .then(() => annotateCardsIn(document))
  .then(() => BFState.clean())
}

// stuff to handle loading stage of page

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

waitFor('.g-main:not(.g-modal)', buyeeSellerFilter)