yande.re refine

Refining yande.re

Устаревшая версия за 10.03.2020. Перейдите к последней версии.

  1. // ==UserScript==
  2. // @name yande.re refine
  3. // @namespace https://greasyfork.org/scripts/397612-yande-re-refine
  4. // @description Refining yande.re
  5. // @include *://behoimi.org/*
  6. // @include *://www.behoimi.org/*
  7. // @include *://*.donmai.us/*
  8. // @include *://konachan.tld/*
  9. // @include *://yande.re/*
  10. // @include *://chan.sankakucomplex.com/*
  11. // @version 2020.03.10b
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. //If true, each added page retains its paginator. If false, elements are smoothly joined together.
  16. var pageBreak = false
  17.  
  18. //Minimum amount of window left to scroll, maintained by loading more pages.
  19. var scrollBuffer = 600
  20.  
  21. //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.
  22. var timeToFailure = 15000
  23.  
  24. var previewLocked = false
  25.  
  26. //============================================================================
  27. //=========================Script initialization==============================
  28. //============================================================================
  29.  
  30. var nextPage, mainTable, mainParent, pending, timeout, iframe
  31.  
  32. let previewIframe, previewImage, previewImageDiv, previewDialog
  33.  
  34. let imagesList = []
  35. let currentImage = 0
  36.  
  37. if (typeof customF != 'undefined') customF()
  38.  
  39. injectGlobalStyle()
  40. initialize()
  41.  
  42. function initialize() {
  43. //Stop if inside an iframe
  44. if (window != window.top || scrollBuffer == 0) return
  45.  
  46. //Stop if no "table"
  47. mainTable = getMainTable(document)
  48. if (!mainTable) return
  49.  
  50. injectStyle()
  51. processItems()
  52.  
  53. //Stop if no paginator
  54. var paginator = getPaginator(document)
  55. if (!paginator) return
  56.  
  57. //Stop if no more pages
  58. nextPage = getNextPage(paginator)
  59. if (!nextPage) return
  60.  
  61. //Hide the blacklist sidebar, since this script breaks the tag totals and post unhiding.
  62. var sidebar = document.getElementById('blacklisted-sidebar')
  63. if (sidebar) sidebar.style.display = 'none'
  64.  
  65. //Other important variables:
  66. scrollBuffer += window.innerHeight
  67. mainParent = mainTable.parentNode
  68. pending = false
  69.  
  70. iframe = document.createElement('iframe')
  71. iframe.width = iframe.height = 0
  72. iframe.style.visibility = 'hidden'
  73. document.body.appendChild(iframe)
  74.  
  75. //Slight delay so that Danbooru's initialize_edit_links() has time to hide all the edit boxes on the Comment index
  76. iframe.addEventListener(
  77. 'load',
  78. function(e) {
  79. setTimeout(appendNewContent, 100)
  80. },
  81. false,
  82. )
  83.  
  84. //Stop if empty page
  85. if (
  86. /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(
  87. mainTable.innerHTML,
  88. )
  89. )
  90. return
  91.  
  92. //Add copy of paginator to the top
  93. mainParent.insertBefore(paginator.cloneNode(true), mainParent.firstChild)
  94.  
  95. if (!pageBreak) paginator.style.display = 'none'
  96. //Hide bottom paginator
  97. else {
  98. //Reposition bottom paginator and add horizontal break
  99. mainTable.parentNode.insertBefore(
  100. document.createElement('hr'),
  101. mainTable.nextSibling,
  102. )
  103. mainTable.parentNode.insertBefore(paginator, mainTable.nextSibling)
  104. }
  105.  
  106. //Listen for scroll events
  107. window.addEventListener('scroll', testScrollPosition, false)
  108. testScrollPosition()
  109. }
  110.  
  111. //============================================================================
  112. //============================Script functions================================
  113. //============================================================================
  114.  
  115. //Some pages match multiple "tables", so order is important.
  116. function getMainTable(source) {
  117. //Special case: Sankaku post index with Auto Paging enabled
  118. if (
  119. /sankaku/.test(location.host) &&
  120. /auto_page=1/.test(document.cookie) &&
  121. /^(post(\/|\/index\/?)?|\/)$/.test(location.pathname)
  122. )
  123. return null
  124.  
  125. var xpath = [
  126. ".//div[@id='c-favorites']//div[@id='posts']", // Danbooru (/favorites)
  127. ".//div[@id='posts']/div", // Danbooru; don't want to fall through to the wrong xpath if no posts ("<article>") on first page.
  128. ".//div[@id='c-pools']//section/article/..", // Danbooru (/pools/####)
  129.  
  130. ".//div[@id='a-index']/table[not(contains(@class,'search'))]", // Danbooru (/forum_topics, ...), take care that this doesn't catch comments containing tables
  131. ".//div[@id='a-index']", // Danbooru (/comments, ...)
  132.  
  133. ".//table[contains(@class,'highlight')]", // large number of pages
  134. ".//div[@id='content']/div/div/div/div/span[@class='author']/../../../..", // Sankaku: note search
  135. ".//div[contains(@id,'comment-list')]/div/..", // comment index
  136. ".//*[not(contains(@id,'popular'))]/span[contains(@class,'thumb')]/a/../..", // post/index, pool/show, note/index
  137. ".//li/div/a[contains(@class,'thumb')]/../../..", // post/index, note/index
  138. ".//div[@id='content']//table/tbody/tr[@class='even']/../..", // user/index, wiki/history
  139. ".//div[@id='content']/div/table", // 3dbooru user records
  140. ".//div[@id='forum']", // forum/show
  141. ]
  142.  
  143. for (var i = 0; i < xpath.length; i++) {
  144. getMainTable = (function(query) {
  145. return function(source) {
  146. var mTable = new XPathEvaluator().evaluate(
  147. query,
  148. source,
  149. null,
  150. XPathResult.FIRST_ORDERED_NODE_TYPE,
  151. null,
  152. ).singleNodeValue
  153. if (!mTable || !pageBreak) return mTable
  154.  
  155. //Special case: Danbooru's /favorites lacks the extra DIV that /posts has, which causes issues with the paginator/page break.
  156. var xDiv = document.createElement('div')
  157. xDiv.style.overflow = 'hidden'
  158. mTable.parentNode.insertBefore(xDiv, mTable)
  159. xDiv.appendChild(mTable)
  160. return xDiv
  161. }
  162. })(xpath[i])
  163.  
  164. var result = getMainTable(source)
  165. if (result) {
  166. //alert("UPW main table query: "+xpath[i]+"\n\n"+location.pathname);
  167. return result
  168. }
  169. }
  170.  
  171. return null
  172. }
  173.  
  174. function getPaginator(source) {
  175. var pager = new XPathEvaluator().evaluate(
  176. "descendant-or-self::div[@id='paginator' or @class='paginator' or @id='paginater']",
  177. source,
  178. null,
  179. XPathResult.FIRST_ORDERED_NODE_TYPE,
  180. null,
  181. ).singleNodeValue
  182.  
  183. // 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,
  184. // 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.
  185. if (
  186. pager &&
  187. location.host.indexOf('donmai.') >= 0 &&
  188. document.getElementById('sidebar')
  189. )
  190. pager.style.clear = 'none'
  191.  
  192. return pager
  193. }
  194.  
  195. function getNextPage(source) {
  196. let page = getPaginator(source)
  197. if (page)
  198. page = new XPathEvaluator().evaluate(
  199. ".//a[@alt='next' or @rel='next' or contains(text(),'>') or contains(text(),'Next')]",
  200. page,
  201. null,
  202. XPathResult.FIRST_ORDERED_NODE_TYPE,
  203. null,
  204. ).singleNodeValue
  205.  
  206. return page && page.href
  207. }
  208.  
  209. function testScrollPosition() {
  210. if (!nextPage) testScrollPosition = function() {}
  211. //Take the max of the two heights for browser compatibility
  212. else if (
  213. !pending &&
  214. window.pageYOffset + scrollBuffer >
  215. Math.max(
  216. document.documentElement.scrollHeight,
  217. document.documentElement.offsetHeight,
  218. )
  219. ) {
  220. pending = true
  221. timeout = setTimeout(function() {
  222. pending = false
  223. testScrollPosition()
  224. }, timeToFailure)
  225. iframe.contentDocument.location.replace(nextPage)
  226. }
  227. }
  228.  
  229. function appendNewContent() {
  230. //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.
  231. clearTimeout(timeout)
  232. if (nextPage.indexOf(iframe.contentDocument.location.href) < 0) {
  233. setTimeout(function() {
  234. pending = false
  235. }, 1000)
  236. return
  237. }
  238.  
  239. //Copy content from retrived page to current page, but leave off certain headers, labels, etc...
  240. var sourcePaginator = document.adoptNode(getPaginator(iframe.contentDocument))
  241. var nextElem,
  242. deleteMe,
  243. source = document.adoptNode(getMainTable(iframe.contentDocument))
  244.  
  245. if (
  246. /<p>(Nothing to display.|Nobody here but us chickens!)<.p>/.test(
  247. source.innerHTML,
  248. )
  249. )
  250. nextPage = null
  251. else {
  252. nextPage = getNextPage(sourcePaginator)
  253.  
  254. if (pageBreak) mainParent.appendChild(source)
  255. else {
  256. //Hide elements separating one table from the next (h1 is used for user names on comment index)
  257. var rems = source.querySelectorAll('h2, h3, h4, thead, tfood')
  258. for (var i = 0; i < rems.length; i++) rems[i].style.display = 'none'
  259.  
  260. //Move contents of next table into current one
  261. var fragment = document.createDocumentFragment()
  262. while ((nextElem = source.firstChild)) fragment.appendChild(nextElem)
  263. mainTable.appendChild(fragment)
  264. }
  265. }
  266.  
  267. //Add the paginator at the bottom if needed.
  268. if (!nextPage || pageBreak) mainParent.appendChild(sourcePaginator)
  269. if (pageBreak && nextPage)
  270. mainParent.appendChild(document.createElement('hr'))
  271.  
  272. //Clear the pending request marker and check position again
  273. processItems()
  274. pending = false
  275. testScrollPosition()
  276. }
  277.  
  278. function injectGlobalStyle() {
  279. const s = document.createElement('style')
  280. s.innerHTML = `
  281. body { padding: 0; }
  282. #title { display: none !important; }
  283. #header { margin: 0 !important; text-align: center; }
  284. #header ul { float: none !important; display: inline-block;}
  285. #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; }
  286. #content > div:first-child > div.sidebar:hover { transform: translateX(0); }
  287. div.content { width: 100vw; text-align: center; }
  288. `
  289. document.body.appendChild(s)
  290. }
  291.  
  292. function injectStyle() {
  293. const s = document.createElement('style')
  294. s.innerHTML = `
  295. a.directlink { display: none !important; }
  296. ul#post-list-posts { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; }
  297. ul#post-list-posts li { width: auto !important; margin: auto; padding: 5px; transition: .3s ease-out; overflow: visible; }
  298. ul#post-list-posts li img, ul#post-list-posts li .inner { width: auto !important; height: auto !important; }
  299. ul#post-list-posts li img { border-radius: 5px; }
  300. ul#post-list-posts li a { transition: .3s ease-out; }
  301. ul#post-list-posts li a.liked img { border: 2px solid pink; }
  302. ul#post-list-posts li:hover { transform: scale(1.3); z-index: 1; }
  303. ul#post-list-posts li:hover a { opacity: 1; box-shadow: 6px 6px 20px 0px rgba(50, 50, 50, 0.75); }
  304. .hidden { display: none !important; }
  305. .preivew-dialog { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; background: rgba(0,0,0,0.7); z-index: 100; }
  306. .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; }
  307. .preivew-dialog .image-host { position: fixed; top: 0; left: 0; height: 100vh; width: 100vw; overflow: auto; text-align: center; }
  308. .preivew-dialog .image-host img { margin: auto; }
  309. .preivew-dialog .image-host.full { overflow: hidden }
  310. .preivew-dialog .image-host.full img { max-width: 100vw; max-height: 100vh; }
  311. `
  312. document.body.appendChild(s)
  313. }
  314.  
  315. function initPreviewIframe() {
  316. previewDialog = document.createElement('div')
  317. previewDialog.addClassName('preivew-dialog hidden')
  318. previewDialog.onclick = (e) => {
  319. if (e.target === previewDialog)
  320. previewDialog.classList.toggle('hidden', true)
  321. }
  322. window.onkeydown = (e) => {
  323. console.log(e)
  324. if (!previewDialog.classList.contains('hidden')) {
  325. if (e.key === 'ArrowLeft') {
  326. currentImage = Math.max(0, currentImage - 1)
  327. openImage(currentImage)
  328. e.preventDefault()
  329. }
  330. if (e.key === 'ArrowRight') {
  331. currentImage = Math.min(imagesList.length - 1, currentImage + 1)
  332. openImage(currentImage)
  333. e.preventDefault()
  334. }
  335. if (e.key === 'Escape') {
  336. previewDialog.classList.toggle('hidden', true)
  337. e.preventDefault()
  338. }
  339. if (e.key === 'Tab') {
  340. openImage(currentImage, 'page')
  341. e.preventDefault()
  342. }
  343. if (e.code === 'Space') {
  344. previewImageDiv.classList.toggle('full')
  345. e.preventDefault()
  346. }
  347. if (e.code === 'KeyL') {
  348. like(currentImage, 3)
  349. e.preventDefault()
  350. }
  351. if (e.code === 'KeyU') {
  352. unlike(currentImage, 2)
  353. e.preventDefault()
  354. }
  355. }
  356. }
  357.  
  358. previewIframe = document.createElement('iframe')
  359. previewImageDiv = document.createElement('div')
  360. previewImageDiv.className = 'image-host full'
  361. previewImage = document.createElement('img')
  362.  
  363. previewImage.onclick = (e) => {
  364. previewDialog.classList.toggle('hidden', true)
  365. }
  366.  
  367. previewDialog.appendChild(previewIframe)
  368. previewImageDiv.appendChild(previewImage)
  369. previewDialog.appendChild(previewImageDiv)
  370. document.body.appendChild(previewDialog)
  371. }
  372.  
  373. function processItems() {
  374. document
  375. .querySelectorAll(
  376. 'ul#post-list-posts li:not([_init]), ul#post-list-posts li:not([_init])',
  377. )
  378. .forEach((li) => {
  379. li.setAttribute('_init', true)
  380. const a = li.querySelector('a.thumb')
  381. const url = a.href
  382. a.href = 'javascript:;'
  383.  
  384. if (!url) return
  385.  
  386. const large = (li.querySelector('a.largeimg') || {}).href
  387. const id = url.split('/').slice(-1)[0]
  388. const image = { url, large, id, a }
  389.  
  390. let index = imagesList.length
  391. imagesList.push(image)
  392.  
  393. let lastClicked = -Infinity
  394. let timer = null
  395. a.onclick = (e) => {
  396. e.preventDefault()
  397. // double click
  398. if (Date.now() - lastClicked < 300) {
  399. if (image.liked) unlike(index)
  400. else like(index)
  401.  
  402. clearTimeout(timer)
  403. // click
  404. } else {
  405. lastClicked = +Date.now()
  406. timer = setTimeout(() => openImage(index), 400)
  407. }
  408. return false
  409. }
  410. })
  411. }
  412.  
  413. function openImage(idx, type = 'image') {
  414. currentImage = idx
  415. const { url, large, id } = imagesList[idx]
  416. console.log({ url, large, id })
  417. if (!previewIframe) initPreviewIframe()
  418.  
  419. if (!large) type = 'page'
  420.  
  421. if (type === 'image') {
  422. previewImage.src = large
  423. previewIframe.classList.toggle('hidden', true)
  424. previewImageDiv.classList.toggle('hidden', false)
  425. } else {
  426. previewIframe.onload = () => {
  427. if (previewIframe.contentWindow.location.href !== url) {
  428. location.href = previewIframe.contentWindow.location.href
  429. previewDialog.classList.toggle('hidden', true)
  430. previewIframe.onload = null
  431. }
  432. }
  433. previewIframe.src = url
  434. previewIframe.classList.toggle('hidden', false)
  435. previewImageDiv.classList.toggle('hidden', true)
  436. }
  437. previewDialog.classList.toggle('hidden', false)
  438. }
  439.  
  440. async function vote(id, score) {
  441. let body = new FormData()
  442. body.append('id', id)
  443. body.append('score', score)
  444. const rawResponse = await fetch('https://yande.re/post/vote.json', {
  445. method: 'POST',
  446. headers: {
  447. 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').attributes
  448. .content.value,
  449. },
  450. body,
  451. })
  452. const content = await rawResponse.json()
  453.  
  454. console.log(content)
  455. }
  456.  
  457. function like(idx) {
  458. let image = imagesList[idx]
  459. vote(image.id, 3)
  460. image.liked = true
  461. image.a.classList.toggle('liked', image.liked)
  462. }
  463.  
  464. function unlike(idx) {
  465. let image = imagesList[idx]
  466. vote(image.id, 2)
  467. image.liked = false
  468. image.a.classList.toggle('liked', image.liked)
  469. }