Buyee Seller Filter

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

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

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

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

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

您需要先安装一个扩展,例如 篡改猴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)