- // ==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.10b
- // @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, pending, timeout, iframe
-
- let previewIframe, previewImage, previewImageDiv, previewDialog
-
- let imagesList = []
- let currentImage = 0
-
- if (typeof customF != 'undefined') customF()
-
- 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()
- processItems()
-
- //Stop if no paginator
- var paginator = getPaginator(document)
- if (!paginator) return
-
- //Stop if no more pages
- nextPage = getNextPage(paginator)
- 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:
- scrollBuffer += window.innerHeight
- mainParent = mainTable.parentNode
- pending = 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,
- )
-
- //Stop if empty page
- if (
- /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(
- mainTable.innerHTML,
- )
- )
- return
-
- //Add copy of paginator to the top
- mainParent.insertBefore(paginator.cloneNode(true), mainParent.firstChild)
-
- if (!pageBreak) paginator.style.display = 'none'
- //Hide bottom paginator
- else {
- //Reposition bottom paginator and add horizontal break
- mainTable.parentNode.insertBefore(
- document.createElement('hr'),
- mainTable.nextSibling,
- )
- mainTable.parentNode.insertBefore(paginator, mainTable.nextSibling)
- }
-
- //Listen for scroll events
- 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 getPaginator(source) {
- var pager = new XPathEvaluator().evaluate(
- "descendant-or-self::div[@id='paginator' or @class='paginator' or @id='paginater']",
- source,
- null,
- XPathResult.FIRST_ORDERED_NODE_TYPE,
- null,
- ).singleNodeValue
-
- // Need clear:none to prevent the 2nd page from being pushed to below the sidebar on the Post index... but we don't want this when viewing a specific pool,
- // because then the paginator is shoved to the right of the last images on a page. Other sites have issues with clear:none as well, like //yande.re/post.
- if (
- pager &&
- location.host.indexOf('donmai.') >= 0 &&
- document.getElementById('sidebar')
- )
- pager.style.clear = 'none'
-
- return pager
- }
-
- function getNextPage(source) {
- let page = getPaginator(source)
- if (page)
- page = new XPathEvaluator().evaluate(
- ".//a[@alt='next' or @rel='next' or contains(text(),'>') or contains(text(),'Next')]",
- page,
- null,
- XPathResult.FIRST_ORDERED_NODE_TYPE,
- null,
- ).singleNodeValue
-
- return page && page.href
- }
-
- function testScrollPosition() {
- if (!nextPage) testScrollPosition = function() {}
- //Take the max of the two heights for browser compatibility
- else if (
- !pending &&
- window.pageYOffset + scrollBuffer >
- Math.max(
- document.documentElement.scrollHeight,
- document.documentElement.offsetHeight,
- )
- ) {
- pending = true
- timeout = setTimeout(function() {
- pending = 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 = false
- }, 1000)
- return
- }
-
- //Copy content from retrived page to current page, but leave off certain headers, labels, etc...
- var sourcePaginator = document.adoptNode(getPaginator(iframe.contentDocument))
- var nextElem,
- deleteMe,
- source = document.adoptNode(getMainTable(iframe.contentDocument))
-
- if (
- /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(
- source.innerHTML,
- )
- )
- nextPage = null
- else {
- nextPage = getNextPage(sourcePaginator)
-
- if (pageBreak) mainParent.appendChild(source)
- else {
- //Hide elements separating one table from the next (h1 is used for user names on comment index)
- var rems = source.querySelectorAll('h2, h3, h4, thead, tfood')
- for (var i = 0; i < rems.length; i++) rems[i].style.display = 'none'
-
- //Move contents of next table into current one
- var fragment = document.createDocumentFragment()
- while ((nextElem = source.firstChild)) fragment.appendChild(nextElem)
- mainTable.appendChild(fragment)
- }
- }
-
- //Add the paginator at the bottom if needed.
- if (!nextPage || pageBreak) mainParent.appendChild(sourcePaginator)
- if (pageBreak && nextPage)
- mainParent.appendChild(document.createElement('hr'))
-
- //Clear the pending request marker and check position again
- processItems()
- pending = 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; width: 200px !important; transform: translateX(-210px); 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; }
- `
- document.body.appendChild(s)
- }
-
- function injectStyle() {
- const s = document.createElement('style')
- s.innerHTML = `
- a.directlink { display: none !important; }
- ul#post-list-posts { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; }
- ul#post-list-posts li { width: auto !important; margin: auto; padding: 5px; transition: .3s ease-out; overflow: visible; }
- ul#post-list-posts li img, ul#post-list-posts li .inner { width: auto !important; height: auto !important; }
- ul#post-list-posts li img { border-radius: 5px; }
- ul#post-list-posts li a { transition: .3s ease-out; }
- ul#post-list-posts li a.liked img { border: 2px solid pink; }
- ul#post-list-posts li:hover { transform: scale(1.3); z-index: 1; }
- ul#post-list-posts li:hover a { opacity: 1; box-shadow: 6px 6px 20px 0px rgba(50, 50, 50, 0.75); }
- .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.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 processItems() {
- document
- .querySelectorAll(
- 'ul#post-list-posts li:not([_init]), ul#post-list-posts li:not([_init])',
- )
- .forEach((li) => {
- li.setAttribute('_init', true)
- const a = li.querySelector('a.thumb')
- const url = a.href
- a.href = 'javascript:;'
-
- if (!url) return
-
- const large = (li.querySelector('a.largeimg') || {}).href
- const id = url.split('/').slice(-1)[0]
- const image = { url, large, id, a }
-
- let index = imagesList.length
- imagesList.push(image)
-
- let lastClicked = -Infinity
- let timer = null
- a.onclick = (e) => {
- e.preventDefault()
- // double click
- if (Date.now() - lastClicked < 300) {
- if (image.liked) unlike(index)
- else like(index)
-
- clearTimeout(timer)
- // click
- } else {
- lastClicked = +Date.now()
- timer = setTimeout(() => openImage(index), 400)
- }
- return false
- }
- })
- }
-
- function openImage(idx, type = 'image') {
- currentImage = idx
- const { url, large, id } = imagesList[idx]
- console.log({ url, large, id })
- if (!previewIframe) initPreviewIframe()
-
- if (!large) type = 'page'
-
- if (type === 'image') {
- previewImage.src = large
- previewIframe.classList.toggle('hidden', true)
- previewImageDiv.classList.toggle('hidden', false)
- } else {
- previewIframe.onload = () => {
- if (previewIframe.contentWindow.location.href !== url) {
- location.href = previewIframe.contentWindow.location.href
- previewDialog.classList.toggle('hidden', true)
- previewIframe.onload = null
- }
- }
- previewIframe.src = url
- 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.a.classList.toggle('liked', image.liked)
- }
-
- function unlike(idx) {
- let image = imagesList[idx]
- vote(image.id, 2)
- image.liked = false
- image.a.classList.toggle('liked', image.liked)
- }