您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Refining yande.re
// ==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/*2 // @include *://yande.re/* // @include *://chan.sankakucomplex.com/* // @version 2021.08.28.a // @grant none // ==/UserScript== // You can found alternatives in https://gist.github.com/jimmywarting/ac1be6ea0297c16c477e17f8fbe51347 const CORS_PROXY = 'https://cors-anywhere.herokuapp.com/' const CORS_ENABLED = false const CACHE_ENABLED = true const CACHE_ONLY_LIKE = true const STORAGE_KEY = 'yandere-refine-liked' const INDEXED_DB_NAME = 'yandere-refine-cache' const SUGGEST_WIDTH = 300 // Minimum amount of window left to scroll, maintained by loading more pages. const 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. const timeToFailure = 15000 //= =========================================================================== //= ========================Script initialization============================== //= =========================================================================== let nextPage, mainTable, mainParent, timeout, iframe let previewIframe, previewImage, previewImageDiv, previewDialog const imagesList = [] let currentImage = 0 const pending = ref(false, v => document.getElementById('loader').classList.toggle('hidden', !v), ) const likedList = readLikeList() const viewingFavorites = isViewingFavorites() let db const memcache = {} 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. const 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', (e) => { setTimeout(appendNewContent, 100) }, false, ) window.addEventListener('scroll', testScrollPosition, false) testScrollPosition() if (CACHE_ENABLED) { const script = document.createElement('script') script.src = 'https://unpkg.com/dexie@latest/dist/dexie.js' script.onload = () => { // eslint-disable-next-line no-undef db = new Dexie(INDEXED_DB_NAME) db.version(1).stores({ images: 'id, blob', }) } document.head.appendChild(script) } } //= =========================================================================== //= ===========================Script functions================================ //= =========================================================================== function awaitImage(img) { if (img.completed) return return new Promise((resolve) => { img.onload = () => { resolve(img) img.onload = undefined } }) } async function setCache(id, img) { if (!db) return false console.log(`Caching ${id}`) const canvas = document.createElement('canvas') canvas.width = img.naturalWidth canvas.height = img.naturalHeight const ctx = canvas.getContext('2d') ctx.drawImage(img, 0, 0) const blob = await new Promise((resolve) => { canvas.toBlob(resolve) }) await db.images.put({ id, blob }) return true } async function getCache(id) { if (memcache[id]) return URL.createObjectURL(memcache[id]) if (!db) return const data = await db.images.get(id) if (data && data.blob) { console.log(`Loading cache of ${id}`) memcache[id] = data.blob return URL.createObjectURL(data.blob) } } async function removeCache(id) { if (!db) return await db.images.delete(id) } // 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 const 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 (let i = 0; i < xpath.length; i++) { // eslint-disable-next-line no-func-assign getMainTable = (function(query) { return function(source) { const mTable = new XPathEvaluator().evaluate( query, source, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null, ).singleNodeValue if (!mTable) return mTable // Special case: Danbooru's /favorites lacks the extra DIV that /posts has, which causes issues with the paginator/page break. const xDiv = document.createElement('div') xDiv.style.overflow = 'hidden' mTable.parentNode.insertBefore(xDiv, mTable) xDiv.appendChild(mTable) return xDiv } })(xpath[i]) const 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(() => { 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.includes(iframe.contentDocument.location.href)) { setTimeout(() => { pending.value = false }, 1000) return } const 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; } #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: 250px !important; transform: translate(-246px, 0); 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; } div#paginator a { border: none; } #comments { max-width: unset !important; width: unset !important; padding: 20px; } .avatar { border-radius: 1000px; } form textarea { color: white; background: inherit; padding: 10px 5px; } .comment .content { text-align: left; } ` 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: 3px; right: 3px; content: url('https://api.iconify.design/mdi:cards-heart.svg?color=%23f37e92&height=20'); 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; } .preview-dialog { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background: rgba(0,0,0,0.7); z-index: 100; } .preview-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; } .preview-dialog .image-host { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; overflow: auto; text-align: center; } .preview-dialog .image-host img { margin: auto; } .preview-dialog .image-host img.loading { filter: blur(3px); height: 100vh; } .preview-dialog .image-host.full { overflow: hidden } .preview-dialog .image-host.full img { max-width: 100vw; max-height: 100vh; } ` document.body.appendChild(s) } function initPreviewIframe() { previewDialog = document.createElement('div') previewDialog.addClassName('preview-dialog hidden') previewDialog.onclick = (e) => { if (e.target === previewDialog) previewDialog.classList.toggle('hidden', true) } window.onkeydown = (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') previewImageDiv.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 || (li.querySelector('a.smallimg') || {}).href const id = page.split('/').slice(-1)[0] const resText = (li.querySelector('.directlink-res') || {}).textContent let res if (resText && resText.includes('x')) { const [height, width] = resText.split(' x ').map(i => +i) res = { height, width, radio: width / height } } if (viewingFavorites) setLiked(id, true) const liked = isLiked(id) 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.textContent = 'Loading...' list.appendChild(loader) } function addImages(images) { const gallery = document.getElementById('gallery') const RADIO = Math.round(window.innerWidth / SUGGEST_WIDTH) images.forEach((info, i) => { const 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 thumb.classList.toggle('liked', info.liked) 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 getProxiedUrl(url) { if (!CORS_ENABLED) return url return CORS_PROXY + url } async function openImage(idx, type = 'image') { currentImage = idx const img = imagesList[idx] const { page, large, id, thumb } = img if (!previewIframe) initPreviewIframe() if (!large) type = 'page' previewDialog.classList.toggle('hidden', false) previewImage.dataset.id = id if (type === 'image') { previewImage.crossOrigin = null const cache = await getCache(id) if (cache) { previewImage.src = cache // show image previewIframe.classList.toggle('hidden', true) previewImageDiv.classList.toggle('hidden', false) } else { // show image previewIframe.classList.toggle('hidden', true) previewImageDiv.classList.toggle('hidden', false) // thumbnail previewImage.classList.toggle('loading', true) previewImage.src = thumb await awaitImage(previewImage) // full image if (CORS_ENABLED) previewImage.crossOrigin = 'Anonymous' const url = getProxiedUrl(large) previewImage.src = url await awaitImage(previewImage) previewImage.classList.toggle('loading', false) // image changed if (previewImage.dataset.id !== id) return // cache if (!CACHE_ONLY_LIKE || isLiked(id)) await setCache(id, previewImage) } } else { previewIframe.src = page previewIframe.classList.toggle('hidden', false) previewImageDiv.classList.toggle('hidden', true) await awaitImage(previewIframe) if ( previewIframe.contentWindow.location.href !== page && !previewIframe.contentWindow.location.pathname.startsWith('/post/show/') ) { location.href = previewIframe.contentWindow.location.href previewDialog.classList.toggle('hidden', true) previewIframe.onload = null } } } function getFavoritesLike() { return document.querySelector('.user .submenu li:nth-child(3) a').href } function isViewingFavorites() { const fav = getFavoritesLike() if (!fav) return false const a = (new URL(fav).searchParams.get('tags') || '') .toLowerCase() .split(' ') .sort() const b = (new URL(location.href).searchParams.get('tags') || '') .toLowerCase() .split(' ') .sort() return a[0] && a[0] === b[0] && a[1] === b[1] } async function vote(id, score) { const 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, }) await rawResponse.json() } function readLikeList() { return Object.fromEntries( (localStorage.getItem(STORAGE_KEY) || '').split(',').map(i => [i, true]), ) } function isLiked(id) { return !!likedList[id] } function setLiked(id, v) { likedList[id] = v localStorage.setItem( STORAGE_KEY, Object.entries(likedList) .map(([i, v]) => (v ? i : null)) .filter(i => i) .join(','), ) } function like(idx) { const image = imagesList[idx] vote(image.id, 3) image.liked = true setLiked(image.id, true) image.dom.classList.toggle('liked', image.liked) } function unlike(idx) { const image = imagesList[idx] vote(image.id, 2) image.liked = false setLiked(image.id, false) image.dom.classList.toggle('liked', image.liked) removeCache(image.id) }