// ==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)
}