yande.re refine

Refining yande.re

As of 2020-03-10. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name           yande.re refine
// @namespace      https://greasyfork.org/scripts/397612-yande-re-refine
// @description    Refining yande.re
// @include        *://behoimi.org/*
// @include        *://www.behoimi.org/*
// @include        *://*.donmai.us/*
// @include        *://konachan.tld/*
// @include        *://yande.re/*
// @include        *://chan.sankakucomplex.com/*
// @version        2020.03.11.a
// @grant          none
// ==/UserScript==

//If true, each added page retains its paginator.  If false, elements are smoothly joined together.
var pageBreak = false

//Minimum amount of window left to scroll, maintained by loading more pages.
var scrollBuffer = 600

//Time (in ms) the script will wait for a response from the next page before attempting to fetch the page again.  If the script gets trapped in a loop trying to load the next page, increase this value.
var timeToFailure = 15000

var previewLocked = false

//============================================================================
//=========================Script initialization==============================
//============================================================================

var nextPage, mainTable, mainParent, timeout, iframe
const SUGGEST_WIDTH = 300
let previewIframe, previewImage, previewImageDiv, previewDialog
let imagesList = []
let currentImage = 0
let pending = ref(false, (v) => document.getElementById('loader').classList.toggle('hidden', !v))

injectGlobalStyle()
initialize()

function initialize() {
  //Stop if inside an iframe
  if (window != window.top || scrollBuffer == 0) return

  //Stop if no "table"
  mainTable = getMainTable(document)
  if (!mainTable) return

  injectStyle()
  initDOM()
  addImages(getImages())

  //Stop if no more pages
  nextPage = getNextPage(document)
  if (!nextPage) return

  //Hide the blacklist sidebar, since this script breaks the tag totals and post unhiding.
  var sidebar = document.getElementById('blacklisted-sidebar')
  if (sidebar) sidebar.style.display = 'none'

  //Other important variables:
  mainParent = mainTable.parentNode
  pending.value = false

  iframe = document.createElement('iframe')
  iframe.width = iframe.height = 0
  iframe.style.visibility = 'hidden'
  document.body.appendChild(iframe)

  //Slight delay so that Danbooru's initialize_edit_links() has time to hide all the edit boxes on the Comment index
  iframe.addEventListener(
    'load',
    function(e) {
      setTimeout(appendNewContent, 100)
    },
    false,
  )

  window.addEventListener('scroll', testScrollPosition, false)
  testScrollPosition()
}

//============================================================================
//============================Script functions================================
//============================================================================

//Some pages match multiple "tables", so order is important.
function getMainTable(source) {
  //Special case: Sankaku post index with Auto Paging enabled
  if (
    /sankaku/.test(location.host) &&
    /auto_page=1/.test(document.cookie) &&
    /^(post(\/|\/index\/?)?|\/)$/.test(location.pathname)
  )
    return null

  var xpath = [
    ".//div[@id='c-favorites']//div[@id='posts']", // Danbooru (/favorites)
    ".//div[@id='posts']/div", // Danbooru; don't want to fall through to the wrong xpath if no posts ("<article>") on first page.
    ".//div[@id='c-pools']//section/article/..", // Danbooru (/pools/####)

    ".//div[@id='a-index']/table[not(contains(@class,'search'))]", // Danbooru (/forum_topics, ...), take care that this doesn't catch comments containing tables
    ".//div[@id='a-index']", // Danbooru (/comments, ...)

    ".//table[contains(@class,'highlight')]", // large number of pages
    ".//div[@id='content']/div/div/div/div/span[@class='author']/../../../..", // Sankaku: note search
    ".//div[contains(@id,'comment-list')]/div/..", // comment index
    ".//*[not(contains(@id,'popular'))]/span[contains(@class,'thumb')]/a/../..", // post/index, pool/show, note/index
    ".//li/div/a[contains(@class,'thumb')]/../../..", // post/index, note/index
    ".//div[@id='content']//table/tbody/tr[@class='even']/../..", // user/index, wiki/history
    ".//div[@id='content']/div/table", // 3dbooru user records
    ".//div[@id='forum']", // forum/show
  ]

  for (var i = 0; i < xpath.length; i++) {
    getMainTable = (function(query) {
      return function(source) {
        var mTable = new XPathEvaluator().evaluate(
          query,
          source,
          null,
          XPathResult.FIRST_ORDERED_NODE_TYPE,
          null,
        ).singleNodeValue
        if (!mTable || !pageBreak) return mTable

        //Special case: Danbooru's /favorites lacks the extra DIV that /posts has, which causes issues with the paginator/page break.
        var xDiv = document.createElement('div')
        xDiv.style.overflow = 'hidden'
        mTable.parentNode.insertBefore(xDiv, mTable)
        xDiv.appendChild(mTable)
        return xDiv
      }
    })(xpath[i])

    var result = getMainTable(source)
    if (result) {
      //alert("UPW main table query: "+xpath[i]+"\n\n"+location.pathname);
      return result
    }
  }

  return null
}

function getNextPage(doc = document) {
  return (doc.querySelector('a.next_page') || {}).href
}

function testScrollPosition() {
  if (!nextPage)
     return

  //Take the max of the two heights for browser compatibility
  if (
    !pending.value &&
    window.pageYOffset + window.innerHeight + scrollBuffer >
      Math.max(
        document.documentElement.scrollHeight,
        document.documentElement.offsetHeight,
      )
  ) {
    console.log('loading ' + nextPage)
    pending.value = true
    timeout = setTimeout(function() {
      pending.value = false
      testScrollPosition()
    }, timeToFailure)
    iframe.contentDocument.location.replace(nextPage)
  }
}

function appendNewContent() {
  //Make sure page is correct.  Using 'indexOf' instead of '!=' because links like "https://danbooru.donmai.us/pools?page=2&search%5Border%5D=" become "https://danbooru.donmai.us/pools?page=2" in the iframe href.
  clearTimeout(timeout)
  if (nextPage.indexOf(iframe.contentDocument.location.href) < 0) {
    setTimeout(function() {
      pending.value = false
    }, 1000)
    return
  }

  let images = getImages(iframe.contentDocument)
  addImages(images)


  if (!images.length)
    nextPage = null
  else {
    nextPage = getNextPage(iframe.contentDocument)
  }

  if (nextPage) {
      history.pushState({}, iframe.contentDocument, nextPage)
  }
  else {
    // TODO: end of pages
    console.log('End of pages')
  }


  pending.value = false
  testScrollPosition()
}

function injectGlobalStyle() {
  const s = document.createElement('style')
  s.innerHTML = `
body { padding: 0; }
#title { display: none !important; }
#header { margin: 0 !important; text-align: center; }
#header ul { float: none !important; display: inline-block;}
#content > div:first-child > div.sidebar { position: fixed; left: 0; top: 0; bottom: 0; overflow: auto !important; z-index: 2; width: 200px !important; transform: translate(-200px, calc(-100vh + 30px)); border-bottom-right-radius: 30px; background: #171717dd; transition: all .2s ease-out; float: none !important; padding: 15px; }
#content > div:first-child > div.sidebar:hover { transform: translateX(0); }
div.content { width: 100vw; text-align: center; float: none }
div.footer { clear: both !important; }
`
  document.body.appendChild(s)
}

function injectStyle() {
  const s = document.createElement('style')
  s.innerHTML = `
#gallery .row { width: 100vw; white-space: nowrap; height: var(--image-height); --image-height: 300px; }
#gallery .row .thumb { position: relative; display: inline-block; transition: .2s ease-out; overflow: hidden; }
#gallery .row .thumb.liked::after { position: absolute; top: 10px; right: 10px; content: url('https://api.iconify.design/mdi:cards-heart.svg?color=%23f37e92&height=24'); vertical-align: -0.125em; }
#gallery .row .thumb:first-child { transform-origin: left; }
#gallery .row .thumb:last-child { transform-origin: right; }
#gallery .row .thumb img { height: var(--image-height); }
#gallery .row:hover { z-index: 1; }
#gallery .row .thumb:hover { transform: scale(1.3); z-index: 1; opacity: 1; box-shadow: 8px 8px 100px 10px rgba(0, 0, 0, 0.8); border-radius: 5px;  }

#loader { padding: 10px; text-align: center; }

.hidden { display: none !important; }
.preivew-dialog { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background: rgba(0,0,0,0.7); z-index: 100; }
.preivew-dialog iframe { position: absolute; height: 90vh; width: 80vw; top: 50%; left: 50%; transform: translate(-50%, -50%); background: grey; border: none; border-radius: 5px; overflow: hidden; }
.preivew-dialog .image-host { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; overflow: auto; text-align: center; }
.preivew-dialog .image-host img { margin: auto; }
.preivew-dialog .image-host img.loading { filter: blur(3px); height: 100vh; }
.preivew-dialog .image-host.full { overflow: hidden }
.preivew-dialog .image-host.full img { max-width: 100vw; max-height: 100vh; }
`
  document.body.appendChild(s)
}

function initPreviewIframe() {
  previewDialog = document.createElement('div')
  previewDialog.addClassName('preivew-dialog hidden')
  previewDialog.onclick = (e) => {
    if (e.target === previewDialog)
      previewDialog.classList.toggle('hidden', true)
  }
  window.onkeydown = (e) => {
    console.log(e)
    if (!previewDialog.classList.contains('hidden')) {
      if (e.key === 'ArrowLeft') {
        currentImage = Math.max(0, currentImage - 1)
        openImage(currentImage)
        e.preventDefault()
      }
      if (e.key === 'ArrowRight') {
        currentImage = Math.min(imagesList.length - 1, currentImage + 1)
        openImage(currentImage)
        e.preventDefault()
      }
      if (e.key === 'Escape') {
        previewDialog.classList.toggle('hidden', true)
        e.preventDefault()
      }
      if (e.key === 'Tab') {
        openImage(currentImage, 'page')
        e.preventDefault()
      }
      if (e.code === 'Space') {
        previewImageDiv.classList.toggle('full')
        e.preventDefault()
      }
      if (e.code === 'KeyL') {
        like(currentImage, 3)
        e.preventDefault()
      }
      if (e.code === 'KeyU') {
        unlike(currentImage, 2)
        e.preventDefault()
      }
    }
  }

  previewIframe = document.createElement('iframe')
  previewImageDiv = document.createElement('div')
  previewImageDiv.className = 'image-host full'
  previewImage = document.createElement('img')

  previewImage.onclick = (e) => {
    previewDialog.classList.toggle('hidden', true)
  }

  previewDialog.appendChild(previewIframe)
  previewImageDiv.appendChild(previewImage)
  previewDialog.appendChild(previewImageDiv)
  document.body.appendChild(previewDialog)
}

function ref(v, handler) {
    let value = v
  return new Proxy({}, {
     get(obj, prop) {
         return value
     },
     set(obj, prop, v){
         if (value !== v) {
             value = v
             handler(value)
         }
     }
  })
}

function getImages(doc = document) {
    const result = Array.from(doc
    .querySelectorAll('ul#post-list-posts > li'))
    .map(li => {
        const page = (li.querySelector('a.thumb') || {}).href
        const thumb = (li.querySelector('a.thumb img') || {}).src
        const large = (li.querySelector('a.largeimg') || {}).href
        const id = page.split('/').slice(-1)[0]
        const resText = (li.querySelector('.directlink-res') || {}).innerText
        let res = undefined
        if (resText && resText.includes('x')) {
            let [height, width] = resText.split(' x ').map(i=>+i)
            res = { height, width, radio: width / height }
        }
        let liked = false

        return { page, thumb, large, id, res, liked }
    })
    doc.getElementById('post-list-posts').remove()
    return result
}

function initDOM() {
    const list = document.getElementById('post-list')
    const gallery = document.createElement('div')
    gallery.id = 'gallery'
    list.appendChild(gallery)
    const loader = document.createElement('div')
    loader.id = 'loader'
    loader.innerText = 'Loading...'
    list.appendChild(loader)
}


function addImages(images) {
    const gallery = document.getElementById('gallery')
    const RADIO = Math.round(window.innerWidth / SUGGEST_WIDTH)
    images.forEach((info, i) => {
        let idx = imagesList.length + i
        let row = gallery.querySelector('.row:last-child:not(.full)')
        if (!row) {
            row = document.createElement('div')
            row.className = 'row'
            gallery.appendChild(row)
        }
        row.dataset.width = +(row.dataset.width || 0) + (1/info.res.radio)

        if (+row.dataset.width >= RADIO) {
            row.classList.toggle('full', true)
            row.style = `--image-height: calc(100vw / ${row.dataset.width})`
        }

        const thumb = document.createElement('div')
        thumb.className = 'thumb'
        row.appendChild(thumb)

        const img = document.createElement('img')
        img.src = info.thumb
        thumb.appendChild(img)
        info.dom = thumb

        let lastClicked = -Infinity
        let timer = null
        img.onclick = (e) => {
            e.preventDefault()
            // double click
            if (Date.now() - lastClicked < 300) {
                if (info.liked)
                    unlike(idx)
                else
                    like(idx)

                clearTimeout(timer)
                // click
            } else {
                lastClicked = +Date.now()
                timer = setTimeout(() => openImage(idx), 400)
            }
            return false
        }
    })
    imagesList.push(...images)
}

function openImage(idx, type = 'image') {
  currentImage = idx
  const img = imagesList[idx]
  const { page, large, id, thumb } = img
  if (!previewIframe) initPreviewIframe()

  if (!large) type = 'page'

  if (type === 'image') {
    previewImage.classList.toggle('loading', true)
    previewImage.src = thumb
    previewImage.onload = ()=> {
        previewImage.src = large
        previewImage.onload = ()=> {
            previewImage.classList.toggle('loading', false)
            previewImage.onload = null
        }
    }
    previewIframe.classList.toggle('hidden', true)
    previewImageDiv.classList.toggle('hidden', false)
  } else {
    previewIframe.onload = () => {
      if (previewIframe.contentWindow.location.href !== page) {
        location.href = previewIframe.contentWindow.location.href
        previewDialog.classList.toggle('hidden', true)
        previewIframe.onload = null
      }
    }
    previewIframe.src = page
    previewIframe.classList.toggle('hidden', false)
    previewImageDiv.classList.toggle('hidden', true)
  }
  previewDialog.classList.toggle('hidden', false)
}

async function vote(id, score) {
  let body = new FormData()
  body.append('id', id)
  body.append('score', score)
  const rawResponse = await fetch('https://yande.re/post/vote.json', {
    method: 'POST',
    headers: {
      'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').attributes
        .content.value,
    },
    body,
  })
  const content = await rawResponse.json()

  console.log(content)
}

function like(idx) {
  let image = imagesList[idx]
  vote(image.id, 3)
  image.liked = true
  image.dom.classList.toggle('liked', image.liked)
}

function unlike(idx) {
  let image = imagesList[idx]
  vote(image.id, 2)
  image.liked = false
  image.dom.classList.toggle('liked', image.liked)
}