Greasy Fork is available in English.

Gelbooru Overhaul

Various toggleable changes to Gelbooru such as enlarging the gallery, removing the sidebar, and more.

  1. // ==UserScript==
  2. // @name Gelbooru Overhaul
  3. // @namespace https://github.com/Enchoseon/gelbooru-overhaul-userscript/raw/main/gelbooru-overhaul.user.js
  4. // @version 0.7.2
  5. // @description Various toggleable changes to Gelbooru such as enlarging the gallery, removing the sidebar, and more.
  6. // @author Enchoseon
  7. // @include *gelbooru.com*
  8. // @run-at document-start
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_download
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. "use strict";
  16. // =============
  17. // Configuration
  18. // =============
  19. const config = {
  20. general: {
  21. amoled: true, // A very lazy Amoled theme
  22. autoDarkMode: true, // Apply Amoled theme if system in Dark mode, higher priority than 'amoled'
  23. autoDarkModeForceTime: false, // Ignore system theme and check time for dark mode
  24. autoDarkModeStartHour: 19, // Start and End time if ForceTime is enabled or system does not supports dark mode
  25. autoDarkModeEndHour: 7,
  26. sexySidebar: true, // Move the leftmost sidebar to the top-left of the screen next to the Gelbooru logo
  27. },
  28. post: {
  29. fitVertically: true, // Scale media to fit vertically in the screen
  30. center: true, // Center media
  31. },
  32. gallery: {
  33. removeTitle: true, // Removes the title attribute from thumbnails
  34. rightClickDownload: true, // Makes it so that when you right-click thumbnails you'll download their highest-resolution counterpart
  35. rightClickDownloadSaveAsPrompt: false, // Show the "Save As" File Explorer prompt when right-click downloading
  36. enlargeFlexbox: true, // Make the thumbnails in the gallery slightly larger & reduce the number of columns
  37. enlargeThumbnailsOnHover: true, // Make the thumbnails in the gallery increase in scale when you hover over them (best paired with gallery.higherResThumbnailsOnHover)
  38. higherResThumbnailsOnHover: true, // Make the thumbnails in the gallery higher-resolution when you hover over them
  39. advancedBlacklist: true, // Use the advanced blacklist that supports AND operators & // comments
  40. advancedBlacklistConfig: `
  41. // Humans
  42. realistic
  43. photo_(medium)
  44. // Extremely Niche Kinks
  45. egg_laying
  46. minigirl penis_hug
  47. // Shitty Artists
  48. shadman
  49. morrie
  50. `, // ^ This arbitrary blacklist is purely for demo purposes. For a larger blacklist, see blacklist.txt in the GitHub repository
  51. },
  52. download: {
  53. blockUnknownArtist: true, // Block the download of files without a tagged artist
  54. missingArtistText: "_unknown-artist", // Text that replaces where the artist name would usually be in images missing artist tags
  55. },
  56. };
  57. var css = "";
  58. // =======================================================
  59. // Higher-Resolution Preview When Hovering Over Thumbnails
  60. // Download Images in Gallery on Right-Click
  61. // Remove Title from Thumbnails
  62. // Advanced Blacklist
  63. // =======================================================
  64. if (config.gallery.higherResThumbnailsOnHover || config.gallery.rightClickDownload || config.gallery.removeTitle || config.gallery.advancedBlacklist) {
  65. document.addEventListener("DOMContentLoaded", function () {
  66. Object.values(document.querySelectorAll(".thumbnail-preview")).forEach((elem) => {
  67. var aElem = elem.querySelector("a");
  68. var imgElem = aElem.querySelector("img");
  69. if (config.gallery.higherResThumbnailsOnHover) { // Higher-Resolution Preview When Hovering Over Thumbnails
  70. imgElem.addEventListener("mouseenter", function() {
  71. convertThumbnail(imgElem, aElem, false);
  72. }, false);
  73. }
  74. if (config.gallery.rightClickDownload) { // Download Images in Gallery on Right-Click
  75. imgElem.addEventListener("contextmenu", (event) => {
  76. event.preventDefault();
  77. convertThumbnail(imgElem, aElem, true).then(function() {
  78. downloadImage(imgElem, aElem);
  79. });
  80. })
  81. }
  82. if (config.gallery.removeTitle) { // Remove Title from Thumbnails
  83. imgElem.title = "";
  84. }
  85. if (config.gallery.advancedBlacklist) { // Advanced Blacklist
  86. config.gallery.advancedBlacklistConfig.forEach((blacklistLine) => {
  87. if (blacklistLine.includes("&&")) { // AND statements
  88. var remove = true;
  89. blacklistLine = blacklistLine.split("&&");
  90. blacklistLine.forEach((andArg) => {
  91. if (!tagFound(andArg)) {
  92. remove = false;
  93. }
  94. });
  95. if (remove) {
  96. elem.remove();
  97. }
  98. } else if (tagFound(blacklistLine)) { // Simple & straightforward blacklisting
  99. elem.remove();
  100. }
  101. });
  102. function tagFound(query) { // Check if a tag is present in the imgElem
  103. var tags = imgElem.alt.split(",");
  104. tags = tags.map(tag => tag.trim())
  105. if (tags.includes(query)) {
  106. return true;
  107. }
  108. return false;
  109. }
  110. }
  111. });
  112. Object.values(document.querySelector(".mainBodyPadding").querySelectorAll("div")).reverse()[1].querySelectorAll("a").forEach((aElem) => {
  113. var imgElem = aElem.querySelector("img");
  114. if (config.gallery.higherResThumbnailsOnHover) { // Higher-Resolution Preview When Hovering Over Thumbnails
  115. imgElem.addEventListener("mouseenter", function() {
  116. convertThumbnail(imgElem, aElem, false);
  117. }, false);
  118. }
  119. if (config.gallery.rightClickDownload) { // Download Images in Gallery on Right-Click
  120. imgElem.addEventListener("contextmenu", (event) => {
  121. event.preventDefault();
  122. convertThumbnail(imgElem, aElem, true).then(function() {
  123. downloadImage(imgElem, aElem);
  124. });
  125. })
  126. }
  127. if (config.gallery.removeTitle) { // Remove Title from Thumbnails
  128. imgElem.title = "";
  129. }
  130. if (config.gallery.advancedBlacklist) { // Advanced Blacklist
  131. config.gallery.advancedBlacklistConfig.forEach((blacklistLine) => {
  132. if (blacklistLine.includes("&&")) { // AND statements
  133. var remove = true;
  134. blacklistLine = blacklistLine.split("&&");
  135. blacklistLine.forEach((andArg) => {
  136. if (!tagFound(andArg)) {
  137. remove = false;
  138. }
  139. });
  140. if (remove) {
  141. elem.remove();
  142. }
  143. } else if (tagFound(blacklistLine)) { // Simple & straightforward blacklisting
  144. elem.remove();
  145. }
  146. });
  147. function tagFound(query) { // Check if a tag is present in the imgElem
  148. var tags = imgElem.alt.split(",");
  149. tags = tags.map(tag => tag.trim())
  150. if (tags.includes(query)) {
  151. return true;
  152. }
  153. return false;
  154. }
  155. }
  156. });
  157. });
  158. }
  159. // =================================
  160. // Make Leftmost Sidebar Collapsable
  161. // =================================
  162. if (config.general.sexySidebar && window.location.search !== "") {
  163. document.addEventListener("DOMContentLoaded", function () {
  164. var div = document.createElement("div");
  165. div.id = "sidebar";
  166. div.innerHTML = document.querySelectorAll(".aside")[0].innerHTML;
  167. document.body.appendChild(div);
  168. });
  169. css += `
  170. .aside {
  171. grid-area: aside;
  172. display: none;
  173. }
  174. #container {
  175. grid-template-columns: 0px auto;
  176. }
  177. #sidebar {
  178. position: fixed;
  179. width: 4px;
  180. height: 100%;
  181. padding-top: 60px;
  182. overflow: hidden;
  183. background: red;
  184. top: 0;
  185. left: 0;
  186. transition: 142ms;
  187. z-index: 420690;
  188. }
  189. #sidebar:hover {
  190. position: fixed;
  191. width: 240px;
  192. height: 100%;
  193. padding-top: 0px;
  194. overflow-y: scroll;
  195. background: ${isDarkMode() ? 'black' : 'white'};
  196. opacity: 0.9;
  197. }
  198. `;
  199. }
  200. // =============================
  201. // Scale Media To Fit Vertically
  202. // =============================
  203. if (config.post.fitVertically) {
  204. css += `
  205. #image, #gelcomVideoPlayer {
  206. height: 90vh !important;
  207. width: auto !important;
  208. }
  209. `;
  210. // resize to fit horizontally on 'Click here to expand image.'
  211. document.addEventListener("DOMContentLoaded", function () {
  212. let resizeLink = document.querySelector("#resize-link").querySelector("a");
  213. let oldOnClick = resizeLink.onclick;
  214. resizeLink.onclick = function(event) {
  215. oldOnClick(event);
  216. Object.values(document.querySelectorAll("#image, #gelcomVideoPlayer")).forEach((elem) => {
  217. elem.style.cssText += `
  218. height: auto !important;
  219. width: 95vw !important;
  220. `;
  221. });
  222. };
  223. });
  224. }
  225. // ============
  226. // Center Media
  227. // ============
  228. if (config.post.center) {
  229. css += `
  230. .image-container {
  231. display: flex !important;
  232. justify-content: center;
  233. }
  234. `;
  235. }
  236. // ===========================
  237. // Enlarge Thumbnails On Hover
  238. // ===========================
  239. if (config.gallery.enlargeThumbnailsOnHover) {
  240. css += `
  241. .thumbnail-preview a img {
  242. transform: scale(1);
  243. transition: transform 169ms;
  244. }
  245. .thumbnail-preview a img:hover {
  246. transform: scale(2.42);
  247. transition-delay: 142ms;
  248. }
  249. .thumbnail-preview:hover {
  250. position: relative;
  251. z-index: 690;
  252. }
  253. .mainBodyPadding div a img {
  254. max-height: 10vw !important;
  255. transform: scale(1);
  256. transition: transform 169ms;
  257. }
  258. .mainBodyPadding div a img:hover {
  259. transform: scale(2.42);
  260. transition-delay: 142ms;
  261. position: relative;
  262. z-index: 690;
  263. }
  264. `;
  265. }
  266. // =======================
  267. // Enlarge Gallery Flexbox
  268. // =======================
  269. if (config.gallery.enlargeFlexbox) {
  270. css += `
  271. .thumbnail-preview {
  272. height: 21em;
  273. width: 20%;
  274. }
  275. .thumbnail-preview {
  276. transform: scale(1.42);
  277. }
  278. html, body {
  279. overflow-x: hidden;
  280. }
  281. .searchArea {
  282. z-index: 420;
  283. }
  284. #paginator {
  285. margin-top: 6.9em;
  286. }
  287. main {
  288. margin-top: 1.21em;
  289. }
  290. `;
  291. }
  292. // ===========================
  293. // Extremely Lazy Amoled Theme
  294. // ===========================
  295. if (isDarkMode()) {
  296. css += `
  297. body, #tags-search {
  298. color: white;
  299. }
  300. .note-body {
  301. color: black !important;
  302. }
  303. .aside, .searchList, header, .navSubmenu, #sidebar {
  304. filter: saturate(42%);
  305. }
  306. .thumbnail-preview a img {
  307. border-radius: 0.42em;
  308. }
  309. #container, header, .navSubmenu, body, .alert-info, footer, html, #tags-search {
  310. background-color: black !important;
  311. background: black !important;
  312. }
  313. .searchArea a, .commentBody, textarea, .ui-menu {
  314. filter: invert(1) saturate(42%);
  315. }
  316. .aside, .alert-info, #tags-search {
  317. border: unset;
  318. }
  319. `;
  320. }
  321. // ==========
  322. // Inject CSS
  323. // ==========
  324. (function() {
  325. var s = document.createElement("style");
  326. s.setAttribute("type", "text/css");
  327. s.appendChild(document.createTextNode(css));
  328. document.querySelector("head").appendChild(s);
  329. })();
  330. // =================
  331. // Process Blacklist
  332. // =================
  333. (function() {
  334. var blacklist = config.gallery.advancedBlacklistConfig.split(/\r?\n/);
  335. var output = [];
  336. blacklist.forEach((line) => { // Convert blacklist to array form
  337. line = line.trim();
  338. if (!line.startsWith("//") && line !== "") { // Ignore comments
  339. output.push(line.replace(/ /g, "&&") // Marker to tell the blacklist loop this is an AND statement
  340. .replace(/_/g, " ") // Format to be same as imgElem alt text
  341. .toLowerCase()
  342. );
  343. }
  344. });
  345. config.gallery.advancedBlacklistConfig = output;
  346. })();
  347. // ================
  348. // Utility Functions
  349. // ================
  350. // Get higher-resolution counterpart of a thumbnail
  351. function convertThumbnail(imgElem, aElem, highestQuality) {
  352. return new Promise(function(resolve, reject) {
  353. var gelDB = GM_getValue("gelDB", {});
  354. var index = hash(aElem.href);
  355. if (!gelDB[index] || (highestQuality && !gelDB[index].high) || (!gelDB[index].medium)) { // Request higher-resolution image (unless it's already indexed)
  356. var xobj = new XMLHttpRequest();
  357. xobj.open("GET", aElem.href, true);
  358. xobj.onreadystatechange = function() {
  359. if (xobj.readyState == 4 && xobj.status == "200") {
  360. const responseDocument = new DOMParser().parseFromString(xobj.responseText, "text/html");
  361. if (responseDocument.querySelector("#gelcomVideoPlayer")) { // Reject videos
  362. reject("Gelbooru Overhaul doesn't support videos in convertThumbnail() or downloadImage() yet.");
  363. if (highestQuality) {
  364. alert("Gelbooru Overhaul doesn't support videos in convertThumbnail() or downloadImage() yet.");
  365. }
  366. return;
  367. }
  368. gelDB[index] = {};
  369. gelDB[index].tags = convertTagElem(responseDocument.querySelector("#tag-list")); // Grab tags
  370. gelDB[index].medium = responseDocument.querySelector("#image").src; // Get medium-quality src
  371. gelDB[index].high = responseDocument.querySelectorAll("script:not([src])"); // Get highest-quality src
  372. gelDB[index].high = gelDB[index].high[gelDB[index].high.length - 1]
  373. .innerHTML
  374. .split(`image.attr('src','`)[1]
  375. .split(`');`)[0];
  376. GM_setValue("gelDB", gelDB);
  377. output();
  378. }
  379. };
  380. xobj.send(null);
  381. } else { // Skip the AJAX voodoo if it's already indexed. Added bonus of cache speed.
  382. output();
  383. }
  384. function output() {
  385. if (highestQuality) {
  386. imgElem.src = gelDB[index].high;
  387. } else {
  388. imgElem.src = gelDB[index].medium;
  389. }
  390. resolve();
  391. }
  392. });
  393. }
  394. // Convert tag list elem into a friendlier object
  395. function convertTagElem(tagElem) {
  396. var tagObj = {
  397. "artist": [],
  398. "character": [],
  399. "copyright": [],
  400. "metadata": [],
  401. "general": [],
  402. "deprecated": [],
  403. };
  404. Object.values(tagElem.querySelectorAll("li")).forEach((tag) => {
  405. if (tag.className.startsWith("tag-type-")) {
  406. var type = tag.className.replace("tag-type-", "");
  407. tag = tag.querySelector("span a")
  408. .href
  409. .replace("https://gelbooru.com/index.php?page=wiki&s=list&search=", "");
  410. tagObj[type].push(tag);
  411. }
  412. });
  413. return tagObj;
  414. }
  415. // Generate hash from string (https://stackoverflow.com/a/52171480)
  416. function hash(str, seed = 0) {
  417. let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
  418. for (let i = 0, ch; i < str.length; i++) {
  419. ch = str.charCodeAt(i);
  420. h1 = Math.imul(h1 ^ ch, 2654435761);
  421. h2 = Math.imul(h2 ^ ch, 1597334677);
  422. }
  423. h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
  424. h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
  425. return 4294967296 * (2097151 & h2) + (h1>>>0);
  426. };
  427. // Download image
  428. function downloadImage(imgElem, aElem) {
  429. var gelDB = GM_getValue("gelDB", {});
  430. var index = hash(aElem.href);
  431. var extension = imgElem.src.split(".").at(-1);
  432. var artist = gelDB[index].tags.artist.join(" ");
  433. if (config.download.blockUnknownArtist && artist === "") { // Don't download if blockUnknownArtist is enabled and artist tag is missing
  434. return;
  435. }
  436. GM_download({
  437. url: imgElem.src,
  438. name: formatFilename(artist, index, extension),
  439. saveAs: config.gallery.rightClickDownloadSaveAsPrompt,
  440. })
  441. }
  442. // Create the filename from the artist's name
  443. function formatFilename(artist, index, extension) {
  444. if (artist === "") {
  445. artist = config.download.missingArtistText;
  446. }
  447. const illegalRegex = /[\/\?<>\\:\*\|":]/g;
  448. artist = decodeURI(artist).replace(illegalRegex, "_") // Make filename-safe (https://stackoverflow.com/a/11101624)
  449. .replace(/_{2,}/g, "_") // and remove consecutive underscores
  450. .toLowerCase() + "_" + index + "." + extension;
  451. return artist;
  452. }
  453. // Check if dark mode should be applied
  454. function isDarkMode() {
  455. //if auto enabled
  456. if(config.general.autoDarkMode)
  457. {
  458. let hasMediaColorScheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme)').media !== 'not all');
  459.  
  460. if(config.general.autoDarkModeForceTime || !hasMediaColorScheme)
  461. {
  462. let hours = new Date().getHours();
  463. if(hours >= config.general.autoDarkModeStartHour || hours <= config.general.autoDarkModeEndHour)
  464. {
  465. return true;
  466. }
  467. else
  468. {
  469. return false;
  470. }
  471. }
  472. //system in dark mode
  473. if(window.matchMedia('(prefers-color-scheme: dark)').matches)
  474. {
  475. return true;
  476. }
  477. //system in light mode
  478. else
  479. {
  480. return false;
  481. }
  482. }
  483. //if permanent dark mode enabled
  484. else if(config.general.amoled)
  485. {
  486. return true;
  487. }
  488. else
  489. {
  490. return false;
  491. }
  492. }
  493. })();