Pixiv Infinite Scroll/Download Links

Adds infinite scroll and inline expansion on the search page and artists' works pages. For manga mode a two-step expansion is used.

当前为 2014-07-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Pixiv Infinite Scroll/Download Links
  3. // @description Adds infinite scroll and inline expansion on the search page and artists' works pages. For manga mode a two-step expansion is used.
  4. // @namespace https://github.com/an-electric-sheep/userscripts
  5. // @match *://www.pixiv.net/search*
  6. // @match *://www.pixiv.net/member_illust*
  7. // @version 0.3.1
  8. // @grant none
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12. "use strict";
  13.  
  14. var Maybe = function (wrapped) {
  15. if (typeof this !== "object" || Object.getPrototypeOf(this) !== Maybe.prototype) {
  16. var o = Object.create(Maybe.prototype);
  17. o.constructor.apply(o, arguments);
  18. return o;
  19. }
  20. this.wrapped = wrapped;
  21. }
  22.  
  23. Maybe.prototype.isEmpty = function(){return null == this.wrapped}
  24. Maybe.prototype.orElse = function(other){return this.isEmpty() ? Maybe(other) : this}
  25. Maybe.prototype.apply = function(f){if(!this.isEmpty()){f.apply(null, [this.wrapped].concat(Array.slice(arguments, 1)))};return this;}
  26. Maybe.prototype.map = function(f){return this.isEmpty() ? this : Maybe(f.apply(null, [this.wrapped].concat(Array.slice(arguments,1))));}
  27. Maybe.prototype.get = function(){return this.wrapped;}
  28.  
  29. var paginator;
  30. var loading = false;
  31.  
  32. var imgContainerSelector = "._image-items, .image-items, .display_works > ul";
  33.  
  34. document.addEventListener("DOMContentLoaded", function() {
  35. for(var e of document.querySelectorAll("iframe, .ad-printservice, .popular-introduction")){e.remove()}
  36. var sheet = document.querySelector("head").appendChild(document.createElement("style")).sheet;
  37. [
  38. // global
  39. "#wrapper {width: unset;}",
  40. // search page
  41. ".layout-body {width: 85vw;}",
  42. // member page
  43. ".layout-a {width: unset;}",
  44. ".layout-a .layout-column-2 {width: calc(100vw - 190px);}",
  45. // member works list
  46. ".display_works {width: unset;}",
  47. ".display_works .image-item {float: none; }",
  48. // search and member works list
  49. "._image-items, .image-items, .display_works > ul {display: flex;flex-wrap: wrap;}",
  50. ".image-item img {padding: 0px; border: none;}",
  51. ".inline-expandable {cursor: pointer;}",
  52. ".image-item.expanded {width: 100%; height: unset;}",
  53. ".image-item.expanded img {max-width: -moz-available;}",
  54. ".manga-item {background-color: #f3f3f3 !important;}",
  55. ".image-item img.manga-medium {max-width: 156px; max-height: 230px; cursor: pointer;}",
  56. // animated content inlined in the search page
  57. ".exploded-animation-scroller {overflow-x: auto; width: 100%; margin: 5px 0px; box-shadow: 0px 0px 4px 1px #444;}",
  58. ".exploded-animation {display: flex; width: -moz-fit-content; }",
  59. ".exploded-animation img {margin-left: 5px;}",
  60. ".control-elements {display: flex; justify-content: space-around;align-items: center;}",
  61. ".control-elements > * {position: relative;}",
  62. ].forEach(r => sheet.insertRule(r,0))
  63. paginator = Maybe(document.querySelectorAll(".pager-container")).map(paginators => paginators[paginators.length-1]).get();
  64. window.addEventListener("scroll", isNextNeeded)
  65. window.addEventListener("resize", isNextNeeded)
  66. for(var e of document.querySelectorAll(".image-item")){customizeImageItem(e)}
  67. isNextNeeded();
  68. })
  69.  
  70.  
  71.  
  72. function inViewport (el) {
  73.  
  74. var rect = el.getBoundingClientRect();
  75.  
  76. return (
  77. rect.top >= 0 &&
  78. rect.left >= 0 &&
  79. rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  80. rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  81. );
  82. }
  83.  
  84. function mangaItemExpand() {
  85. this.removeEventListener("click", mangaItemExpand)
  86. var container = this.parentElement;
  87. var newImg = document.createElement("img");
  88. // just try to load the big image, this may fail for some older images, just expand in that case
  89. newImg.src = this.src.replace(/(_p\d+)/, "_big$1")
  90. newImg.addEventListener("load", () => {container.replaceChild(newImg, this);container.classList.add("expanded")})
  91. newImg.addEventListener("error", () => container.classList.add("expanded"))
  92. newImg.className = "manga"
  93. }
  94.  
  95.  
  96. function insertMangaItems(parentItem,url) {
  97. var req = new XMLHttpRequest
  98. req.open("get", url)
  99. req.onload = function() {
  100. var rsp = this.responseXML
  101. var nextItem = parentItem.nextSibling
  102. for(var e of rsp.querySelectorAll(".item-container")) {
  103. let mediumImg = e.querySelector(".image")
  104. let bigUrl = e.querySelector(".full-size-container").href
  105. let item = document.createElement("li")
  106. item.className = "image-item manga-item"
  107. let img = document.createElement("img")
  108. img.src = mediumImg.dataset.src
  109. img.className = "manga-medium"
  110. img.addEventListener("click", mangaItemExpand)
  111. item.appendChild(img)
  112. parentItem.parentNode.insertBefore(item, nextItem)
  113. }
  114. }
  115. req.responseType = "document"
  116. req.send()
  117. }
  118.  
  119.  
  120. function insertAnimationItems(container, mediumDoc) {
  121. var script = mediumDoc.querySelector("#wrapper script")
  122. // it's not a strong sandbox. it just avoids the loaded script writing to the main window
  123. var sandbox = document.createElement("iframe")
  124. sandbox.src = window.location.href
  125. sandbox.seamless = true
  126. sandbox.setAttribute("srcdoc", "<!DOCTYPE html><html><head><script async src='https://cdn.jsdelivr.net/jszip/2.2.2/jszip.min.js'></script><script>window.pixiv = {context: {}}</script><script>"+ script.firstChild.data +"</script></head></html>")
  127. sandbox.onload = () => {
  128. var sandboxWindow = sandbox.contentWindow
  129. var illustData = sandboxWindow.pixiv.context.ugokuIllustFullscreenData
  130. var req = new sandboxWindow.XMLHttpRequest
  131. req.open("get", illustData.src)
  132. req.responseType = "arraybuffer"
  133. req.onload = function () {
  134. var controlElements = document.createElement("div")
  135. controlElements.className = "control-elements"
  136. var oldElements = document.createElement("div")
  137. while(container.hasChildNodes()){oldElements.appendChild(container.firstChild)}
  138. controlElements.appendChild(oldElements)
  139. container.appendChild(controlElements)
  140. var downloadInfo = document.createElement("div")
  141. var buffer = this.response
  142. var zip = new sandboxWindow.JSZip(buffer)
  143. var downloadLink = document.createElement("a")
  144. downloadLink.className = "animation-download"
  145. downloadLink.innerHTML = downloadLink.download = sandboxWindow.pixiv.context.illustId + ".zip"
  146. downloadInfo.appendChild(document.createTextNode("Download: "))
  147. downloadInfo.appendChild(downloadLink)
  148. downloadInfo.appendChild(document.createElement("br"))
  149. downloadInfo.appendChild(document.createTextNode("pixiv2webm and pixiv2gif available "))
  150. downloadInfo.appendChild(Maybe(document.createElement("a")).apply(e => {e.href = "https://github.com/an-electric-sheep/userscripts"; e.innerHTML = "on github"}).get())
  151. controlElements.appendChild(downloadInfo)
  152. var scrollContainer = document.createElement("div")
  153. var explodedAnimation = document.createElement("div")
  154. scrollContainer.className = "exploded-animation-scroller"
  155. explodedAnimation.className = "exploded-animation"
  156. scrollContainer.appendChild(explodedAnimation)
  157. container.appendChild(scrollContainer)
  158. var timingInformation = []
  159.  
  160. for(var name in zip.files){
  161. let file = zip.file(name)
  162. let img = document.createElement("img")
  163. let imgBuf = file.asArrayBuffer()
  164. let imgBlob = new Blob([imgBuf])
  165. img.src = URL.createObjectURL(imgBlob)
  166. timingInformation.push(file.name +"\t"+ illustData.frames.find((e) => e.file == name).delay)
  167. explodedAnimation.appendChild(img)
  168. }
  169. container.classList.add("expanded")
  170. zip.file("frame_delays.txt", timingInformation.join("\n"))
  171. downloadLink.href = URL.createObjectURL(zip.generate({type: "blob"}))
  172. sandbox.remove();
  173. }
  174. req.send()
  175. }
  176. document.body.appendChild(sandbox)
  177.  
  178. }
  179.  
  180. function listItemExpand() {
  181. var container = this.parentNode
  182. var mediumLink = container.querySelector("a.work").href
  183. var req = new XMLHttpRequest
  184. req.open("get", mediumLink)
  185. req.onload = function() {
  186. var rsp = this.responseXML;
  187. if(rsp.querySelector("._ugoku-illust-player-container")) {
  188. insertAnimationItems(container, rsp)
  189. }
  190. Maybe(rsp.querySelector('.works_display a[href*="mode"]')).apply((modeLink) => {
  191. var modeLinkUrl = modeLink.href
  192. var mediumSrc = modeLink.querySelector("img").src
  193. var mode = modeLinkUrl.match(/mode=(.+?)&/)[1]
  194. if(mode == "big") {
  195. var img = container.querySelector("img")
  196. img.src = mediumSrc.replace("_m.", ".");
  197. container.classList.add("expanded")
  198. }
  199. if(mode == "manga"){
  200. insertMangaItems(container, modeLinkUrl)
  201. }
  202. })
  203. }
  204. req.responseType = "document"
  205. req.send()
  206. this.removeEventListener("click", listItemExpand)
  207. }
  208.  
  209. const greasedImageItems = new WeakMap;
  210.  
  211. function customizeImageItem(e) {
  212. if(greasedImageItems.has(e))
  213. return;
  214. greasedImageItems.set(e, true);
  215. var workLink = e.querySelector("a.work")
  216.  
  217. var img = workLink.querySelector("img")
  218. img.classList.add("inline-expandable")
  219. img.dataset.thumbSrc = img.src
  220. e.insertBefore(img, workLink)
  221. img.addEventListener("click", listItemExpand)
  222. }
  223.  
  224. function loadNext() {
  225. if(loading)
  226. return;
  227. loading = true;
  228. var nextLink = paginator.querySelector("a[rel=next]")
  229. if(nextLink) {
  230. var req = new XMLHttpRequest();
  231. req.open("get", nextLink.href)
  232. req.onload = function() {
  233. var rsp = this.responseXML;
  234. var container = document.querySelector(imgContainerSelector)
  235. for(var e of rsp.querySelectorAll(".image-item")){
  236. var imageItem = document.importNode(e, true)
  237. container.appendChild(imageItem)
  238. customizeImageItem(imageItem)
  239. }
  240. while(paginator.hasChildNodes())
  241. paginator.firstChild.remove()
  242. for(var e of rsp.querySelector(".pager-container").childNodes){paginator.appendChild(document.importNode(e, true) )}
  243. loading = false;
  244. isNextNeeded();
  245. }
  246. req.responseType = "document"
  247. req.send()
  248. }
  249. }
  250.  
  251. function isNextNeeded() {
  252. if(loading)
  253. return;
  254.  
  255. if(paginator && inViewport(document.querySelector(".image-item:last-child"))) {
  256. loadNext();
  257. }
  258. }