A Gelbooru Viewer

Adds a download button and makes thumbnails a little bigger. Make shure to add (.png, .jpeg, .jpg, etc) to the tampermonkey download whitelist

  1. // ==UserScript==
  2. // @name A Gelbooru Viewer
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.1
  5. // @description Adds a download button and makes thumbnails a little bigger. Make shure to add (.png, .jpeg, .jpg, etc) to the tampermonkey download whitelist
  6. // @author Anonymous
  7. // @match https://gelbooru.com/*page=post*s=list*
  8. // @grant GM_download
  9. // @grant GM_xmlhttpRequest
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14. // Your code here...
  15.  
  16. function setCustomStyles(){
  17. const customStyles = document.createElement("style");
  18. customStyles.innerHTML = "" +
  19. ".thumbnail-preview {" +
  20. "max-width: unset;"+
  21. "width: 30%;" +
  22. "min-height: 200px;" +
  23. "max-height: unset;" +
  24. "height: 500px;"+
  25. "}" +
  26. ".thumbnail-preview span {display: contents;}"+
  27. ".thumbnail-preview a {display: contents;}"+
  28. ".thumbnail-preview button {width: 100%; height: 25px; background-color: blue; color: white; text-decoration:none;border:none;}"+
  29. ".thumbnail-preview button[disabled] {background-color: gray; color: black;}"+
  30. ".thumbnail-preview button[data-done] {background-color: green;}"+
  31. ".thumbnail-preview button.secondary {background-color: gray;}"+
  32. ".thumbnail-preview img {"+
  33. "object-fit: contain;" +
  34. "width: 100%;" +
  35. "height: calc(100% - 50px);" +
  36. "} "+
  37. ".next-page-message-container {display:flex;align-items:center;width:100%;height:100px;margin:0;background-color:darkgray;} "+
  38. ".next-page-message-container p {width:fit-content;margin:auto;font-size:3em;}";
  39.  
  40. document.head.appendChild(customStyles);
  41. }
  42.  
  43. function parsePaginationInfo(doc){
  44. let currentPage,
  45. nextPage, nextPageUrl,
  46. previousPage,previousPageUrl;
  47.  
  48. const currentPageEl = doc.querySelector(".pagination b");
  49. if (currentPageEl){
  50. currentPage = currentPageEl.innerText;
  51. } else {
  52. console.debug("cant parse current page");
  53. }
  54.  
  55. const aList = Array.from(doc.querySelectorAll(".pagination a"));
  56.  
  57. for (let i=0; i < aList.length; i++){
  58. const a = aList[i];
  59. const altText = a.getAttribute("alt");
  60. if (!altText) { continue }
  61.  
  62. switch(altText){
  63. case "next": {
  64. nextPage = altText;
  65. nextPageUrl = a.href;
  66. break;
  67. }
  68. case "back": {
  69. previousPage = altText;
  70. previousPageUrl = a.href;
  71. break;
  72. }
  73. }
  74. }
  75.  
  76. const alertIfNotFound = (name, value) => {
  77. if (!value) { console.debug( name + " was not found"); }
  78. }
  79. alertIfNotFound("nextPage", nextPage);
  80. alertIfNotFound("previousPage", previousPage);
  81.  
  82. return {currentPage,
  83. nextPage, nextPageUrl,
  84. previousPage,previousPageUrl}
  85. }
  86.  
  87. function parseThumbPreview(el){
  88. const a = el.querySelector("a");
  89. const img = el.querySelector("img");
  90.  
  91. if (!a || !img){
  92. throw new Error("parse error " + el.outerHTML);
  93. }
  94.  
  95. const thumbUrl = img.src;
  96. const pageUrl = a.href;
  97.  
  98. if (!thumbUrl || !pageUrl) {
  99. throw new Error("parse error " + el.outerHTML);
  100. }
  101.  
  102. return {thumbUrl, pageUrl}
  103. }
  104.  
  105. function parseDetailPage(doc){
  106. let originalImageUrl = undefined;
  107. doc.querySelectorAll("#tag-list a").forEach(a=>{
  108. if (a.innerText === "Original image"){
  109. originalImageUrl = a.href;
  110. }
  111. });
  112.  
  113. if (!originalImageUrl) {
  114. throw new Error("parse error");
  115. }
  116.  
  117. return {originalImageUrl}
  118. }
  119.  
  120. async function getDetailPageDoc(url){
  121. const response = await fetch(url, {method:"GET"});
  122. if (!response.ok){ throw new Error("request error", response) }
  123. const text = await response.text();
  124. return new DOMParser().parseFromString(text,"text/html");
  125. }
  126.  
  127. async function downloadImage(url, saveAs=false){
  128. const name = url.substring(url.lastIndexOf("/")+1);
  129.  
  130. return new Promise((resolve, reject)=>{
  131. GM_download({
  132. url,
  133. name,
  134. saveAs,
  135. onload: (data)=>{
  136. console.debug("GM_download.onload", data);
  137. resolve();
  138. },
  139. onerror: (e)=>{
  140. console.debug("GM_download.onerror", e);
  141. reject(e);
  142. }
  143. });
  144. });
  145. }
  146.  
  147. async function downloadOriginalFromDetailPage(url){
  148. const doc = await getDetailPageDoc(url);
  149. console.debug("page fetched");
  150. const data = parseDetailPage(doc);
  151. console.debug("page parsed", data);
  152.  
  153. await downloadImage(data.originalImageUrl);
  154. console.debug("image downloaded");
  155. return;
  156.  
  157. }
  158.  
  159. // https://stackoverflow.com/questions/1038727/how-to-get-browser-width-using-javascript-code
  160. function getWidth() {
  161. return Math.max(
  162. document.body.scrollWidth,
  163. document.documentElement.scrollWidth,
  164. document.body.offsetWidth,
  165. document.documentElement.offsetWidth,
  166. document.documentElement.clientWidth
  167. );
  168. }
  169.  
  170. function getHeight() {
  171. return Math.max(
  172. document.body.scrollHeight,
  173. document.documentElement.scrollHeight,
  174. document.body.offsetHeight,
  175. document.documentElement.offsetHeight,
  176. document.documentElement.clientHeight
  177. );
  178. }
  179.  
  180. function openChildWindow(url,name="page",w=null,h=null) {
  181. // http://www.nigraphic.com/blog/java-script/how-open-new-window-popup-center-screen
  182.  
  183. if (!w) { w = getWidth() * 0.8; }
  184. if (!h) { h = getHeight() * 0.8; }
  185.  
  186. const dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left;
  187. const dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top;
  188.  
  189. const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
  190. const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height;
  191.  
  192. const left = ((width / 2) - (w / 2)) + dualScreenLeft;
  193. const top = ((height / 2) - (h / 2)) + dualScreenTop;
  194. const windowref = window.open(url, name, `width=${w},height=${h},top=${top},left=${left},resizable,location=no,toolbar=no,menubar=no,status=no,titlebar=0`);
  195. }
  196.  
  197.  
  198. function setOnScrollBottomHandler(handler){
  199. window.onscroll = function(e) {
  200. let pageHeight=document.documentElement.offsetHeight,
  201. windowHeight=window.innerHeight,
  202. scrollPosition=window.scrollY || window.pageYOffset || document.body.scrollTop + (document.documentElement && document.documentElement.scrollTop || 0);
  203.  
  204. if (pageHeight*0.98 <= (windowHeight+scrollPosition)) {
  205. console.debug('bottom reached');
  206. handler(e);
  207. }
  208. };
  209. }
  210.  
  211. function addButtons(){
  212. document.querySelectorAll(".thumbnail-preview").forEach(el=>{
  213. const item = parseThumbPreview(el);
  214.  
  215. const button = document.createElement("button");
  216. button.innerText = "download";
  217. button.addEventListener("click", async e=> {
  218. try{
  219. button.setAttribute("disabled","disabled");
  220. button.innerText = "downloading..."
  221. await downloadOriginalFromDetailPage(item.pageUrl);
  222. button.removeAttribute("disabled");
  223. button.dataset.done=true;
  224. button.innerText = "done";
  225. }
  226. catch(e) {
  227. button.innerText = "error";
  228. console.error(e);
  229. }
  230.  
  231. });
  232.  
  233. const openPageButton = document.createElement("button");
  234. openPageButton.classList.add("secondary");
  235. openPageButton.innerText = "open details";
  236. openPageButton.onclick = (e) => {
  237. e.preventDefault();
  238. openChildWindow(item.pageUrl, "Detail");
  239. }
  240.  
  241. el.appendChild(button);
  242. el.appendChild(openPageButton);
  243. });
  244. }
  245.  
  246. function addLoadOnScrollBottom(){
  247. const pagination = parsePaginationInfo(document);
  248.  
  249. if (pagination.nextPage) {
  250. const nextPageMessageContainer = document.createElement("footer");
  251. const nextPageMessageEl = document.createElement("p");
  252. nextPageMessageEl.innerText = "loading next page";
  253. nextPageMessageContainer.classList.add("next-page-message-container");
  254. nextPageMessageContainer.appendChild(nextPageMessageEl);
  255. document.body.appendChild(nextPageMessageContainer);
  256.  
  257. // Hide lasts br to show next page message sooner
  258. const containerPushBr = Array.from(document.querySelectorAll(".contain-push br"));
  259. let count = 0, limit=6;
  260. for (let i=containerPushBr.length - 1;i >= 0 && count < limit ;i--){
  261. containerPushBr[i].style.display = "none";
  262. count++;
  263. }
  264.  
  265. setOnScrollBottomHandler(e=>{window.location.replace(pagination.nextPageUrl)})
  266. }
  267. }
  268.  
  269. function init(){
  270. setCustomStyles();
  271. addButtons();
  272. addLoadOnScrollBottom();
  273. }
  274.  
  275. init();
  276.  
  277. })();