- // ==UserScript==
- // @name Gelbooru Overhaul
- // @namespace https://github.com/Enchoseon/gelbooru-overhaul-userscript/raw/main/gelbooru-overhaul.user.js
- // @version 0.7.2
- // @description Various toggleable changes to Gelbooru such as enlarging the gallery, removing the sidebar, and more.
- // @author Enchoseon
- // @include *gelbooru.com*
- // @run-at document-start
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_download
- // ==/UserScript==
-
- (function() {
- "use strict";
- // =============
- // Configuration
- // =============
- const config = {
- general: {
- amoled: true, // A very lazy Amoled theme
- autoDarkMode: true, // Apply Amoled theme if system in Dark mode, higher priority than 'amoled'
- autoDarkModeForceTime: false, // Ignore system theme and check time for dark mode
- autoDarkModeStartHour: 19, // Start and End time if ForceTime is enabled or system does not supports dark mode
- autoDarkModeEndHour: 7,
- sexySidebar: true, // Move the leftmost sidebar to the top-left of the screen next to the Gelbooru logo
- },
- post: {
- fitVertically: true, // Scale media to fit vertically in the screen
- center: true, // Center media
- },
- gallery: {
- removeTitle: true, // Removes the title attribute from thumbnails
- rightClickDownload: true, // Makes it so that when you right-click thumbnails you'll download their highest-resolution counterpart
- rightClickDownloadSaveAsPrompt: false, // Show the "Save As" File Explorer prompt when right-click downloading
- enlargeFlexbox: true, // Make the thumbnails in the gallery slightly larger & reduce the number of columns
- enlargeThumbnailsOnHover: true, // Make the thumbnails in the gallery increase in scale when you hover over them (best paired with gallery.higherResThumbnailsOnHover)
- higherResThumbnailsOnHover: true, // Make the thumbnails in the gallery higher-resolution when you hover over them
- advancedBlacklist: true, // Use the advanced blacklist that supports AND operators & // comments
- advancedBlacklistConfig: `
- // Humans
- realistic
- photo_(medium)
- // Extremely Niche Kinks
- egg_laying
- minigirl penis_hug
- // Shitty Artists
- shadman
- morrie
- `, // ^ This arbitrary blacklist is purely for demo purposes. For a larger blacklist, see blacklist.txt in the GitHub repository
- },
- download: {
- blockUnknownArtist: true, // Block the download of files without a tagged artist
- missingArtistText: "_unknown-artist", // Text that replaces where the artist name would usually be in images missing artist tags
- },
- };
- var css = "";
- // =======================================================
- // Higher-Resolution Preview When Hovering Over Thumbnails
- // Download Images in Gallery on Right-Click
- // Remove Title from Thumbnails
- // Advanced Blacklist
- // =======================================================
- if (config.gallery.higherResThumbnailsOnHover || config.gallery.rightClickDownload || config.gallery.removeTitle || config.gallery.advancedBlacklist) {
- document.addEventListener("DOMContentLoaded", function () {
- Object.values(document.querySelectorAll(".thumbnail-preview")).forEach((elem) => {
- var aElem = elem.querySelector("a");
- var imgElem = aElem.querySelector("img");
- if (config.gallery.higherResThumbnailsOnHover) { // Higher-Resolution Preview When Hovering Over Thumbnails
- imgElem.addEventListener("mouseenter", function() {
- convertThumbnail(imgElem, aElem, false);
- }, false);
- }
- if (config.gallery.rightClickDownload) { // Download Images in Gallery on Right-Click
- imgElem.addEventListener("contextmenu", (event) => {
- event.preventDefault();
- convertThumbnail(imgElem, aElem, true).then(function() {
- downloadImage(imgElem, aElem);
- });
- })
- }
- if (config.gallery.removeTitle) { // Remove Title from Thumbnails
- imgElem.title = "";
- }
- if (config.gallery.advancedBlacklist) { // Advanced Blacklist
- config.gallery.advancedBlacklistConfig.forEach((blacklistLine) => {
- if (blacklistLine.includes("&&")) { // AND statements
- var remove = true;
- blacklistLine = blacklistLine.split("&&");
- blacklistLine.forEach((andArg) => {
- if (!tagFound(andArg)) {
- remove = false;
- }
- });
- if (remove) {
- elem.remove();
- }
- } else if (tagFound(blacklistLine)) { // Simple & straightforward blacklisting
- elem.remove();
- }
- });
- function tagFound(query) { // Check if a tag is present in the imgElem
- var tags = imgElem.alt.split(",");
- tags = tags.map(tag => tag.trim())
- if (tags.includes(query)) {
- return true;
- }
- return false;
- }
- }
- });
- Object.values(document.querySelector(".mainBodyPadding").querySelectorAll("div")).reverse()[1].querySelectorAll("a").forEach((aElem) => {
- var imgElem = aElem.querySelector("img");
- if (config.gallery.higherResThumbnailsOnHover) { // Higher-Resolution Preview When Hovering Over Thumbnails
- imgElem.addEventListener("mouseenter", function() {
- convertThumbnail(imgElem, aElem, false);
- }, false);
- }
- if (config.gallery.rightClickDownload) { // Download Images in Gallery on Right-Click
- imgElem.addEventListener("contextmenu", (event) => {
- event.preventDefault();
- convertThumbnail(imgElem, aElem, true).then(function() {
- downloadImage(imgElem, aElem);
- });
- })
- }
- if (config.gallery.removeTitle) { // Remove Title from Thumbnails
- imgElem.title = "";
- }
- if (config.gallery.advancedBlacklist) { // Advanced Blacklist
- config.gallery.advancedBlacklistConfig.forEach((blacklistLine) => {
- if (blacklistLine.includes("&&")) { // AND statements
- var remove = true;
- blacklistLine = blacklistLine.split("&&");
- blacklistLine.forEach((andArg) => {
- if (!tagFound(andArg)) {
- remove = false;
- }
- });
- if (remove) {
- elem.remove();
- }
- } else if (tagFound(blacklistLine)) { // Simple & straightforward blacklisting
- elem.remove();
- }
- });
- function tagFound(query) { // Check if a tag is present in the imgElem
- var tags = imgElem.alt.split(",");
- tags = tags.map(tag => tag.trim())
- if (tags.includes(query)) {
- return true;
- }
- return false;
- }
- }
- });
- });
- }
- // =================================
- // Make Leftmost Sidebar Collapsable
- // =================================
- if (config.general.sexySidebar && window.location.search !== "") {
- document.addEventListener("DOMContentLoaded", function () {
- var div = document.createElement("div");
- div.id = "sidebar";
- div.innerHTML = document.querySelectorAll(".aside")[0].innerHTML;
- document.body.appendChild(div);
- });
- css += `
- .aside {
- grid-area: aside;
- display: none;
- }
- #container {
- grid-template-columns: 0px auto;
- }
- #sidebar {
- position: fixed;
- width: 4px;
- height: 100%;
- padding-top: 60px;
- overflow: hidden;
- background: red;
- top: 0;
- left: 0;
- transition: 142ms;
- z-index: 420690;
- }
- #sidebar:hover {
- position: fixed;
- width: 240px;
- height: 100%;
- padding-top: 0px;
- overflow-y: scroll;
- background: ${isDarkMode() ? 'black' : 'white'};
- opacity: 0.9;
- }
- `;
- }
- // =============================
- // Scale Media To Fit Vertically
- // =============================
- if (config.post.fitVertically) {
- css += `
- #image, #gelcomVideoPlayer {
- height: 90vh !important;
- width: auto !important;
- }
- `;
- // resize to fit horizontally on 'Click here to expand image.'
- document.addEventListener("DOMContentLoaded", function () {
- let resizeLink = document.querySelector("#resize-link").querySelector("a");
- let oldOnClick = resizeLink.onclick;
- resizeLink.onclick = function(event) {
- oldOnClick(event);
- Object.values(document.querySelectorAll("#image, #gelcomVideoPlayer")).forEach((elem) => {
- elem.style.cssText += `
- height: auto !important;
- width: 95vw !important;
- `;
- });
- };
- });
- }
- // ============
- // Center Media
- // ============
- if (config.post.center) {
- css += `
- .image-container {
- display: flex !important;
- justify-content: center;
- }
- `;
- }
- // ===========================
- // Enlarge Thumbnails On Hover
- // ===========================
- if (config.gallery.enlargeThumbnailsOnHover) {
- css += `
- .thumbnail-preview a img {
- transform: scale(1);
- transition: transform 169ms;
- }
- .thumbnail-preview a img:hover {
- transform: scale(2.42);
- transition-delay: 142ms;
- }
- .thumbnail-preview:hover {
- position: relative;
- z-index: 690;
- }
- .mainBodyPadding div a img {
- max-height: 10vw !important;
- transform: scale(1);
- transition: transform 169ms;
- }
- .mainBodyPadding div a img:hover {
- transform: scale(2.42);
- transition-delay: 142ms;
- position: relative;
- z-index: 690;
- }
- `;
- }
- // =======================
- // Enlarge Gallery Flexbox
- // =======================
- if (config.gallery.enlargeFlexbox) {
- css += `
- .thumbnail-preview {
- height: 21em;
- width: 20%;
- }
- .thumbnail-preview {
- transform: scale(1.42);
- }
- html, body {
- overflow-x: hidden;
- }
- .searchArea {
- z-index: 420;
- }
- #paginator {
- margin-top: 6.9em;
- }
- main {
- margin-top: 1.21em;
- }
- `;
- }
- // ===========================
- // Extremely Lazy Amoled Theme
- // ===========================
- if (isDarkMode()) {
- css += `
- body, #tags-search {
- color: white;
- }
- .note-body {
- color: black !important;
- }
- .aside, .searchList, header, .navSubmenu, #sidebar {
- filter: saturate(42%);
- }
- .thumbnail-preview a img {
- border-radius: 0.42em;
- }
- #container, header, .navSubmenu, body, .alert-info, footer, html, #tags-search {
- background-color: black !important;
- background: black !important;
- }
- .searchArea a, .commentBody, textarea, .ui-menu {
- filter: invert(1) saturate(42%);
- }
- .aside, .alert-info, #tags-search {
- border: unset;
- }
- `;
- }
- // ==========
- // Inject CSS
- // ==========
- (function() {
- var s = document.createElement("style");
- s.setAttribute("type", "text/css");
- s.appendChild(document.createTextNode(css));
- document.querySelector("head").appendChild(s);
- })();
- // =================
- // Process Blacklist
- // =================
- (function() {
- var blacklist = config.gallery.advancedBlacklistConfig.split(/\r?\n/);
- var output = [];
- blacklist.forEach((line) => { // Convert blacklist to array form
- line = line.trim();
- if (!line.startsWith("//") && line !== "") { // Ignore comments
- output.push(line.replace(/ /g, "&&") // Marker to tell the blacklist loop this is an AND statement
- .replace(/_/g, " ") // Format to be same as imgElem alt text
- .toLowerCase()
- );
- }
- });
- config.gallery.advancedBlacklistConfig = output;
- })();
- // ================
- // Utility Functions
- // ================
- // Get higher-resolution counterpart of a thumbnail
- function convertThumbnail(imgElem, aElem, highestQuality) {
- return new Promise(function(resolve, reject) {
- var gelDB = GM_getValue("gelDB", {});
- var index = hash(aElem.href);
- if (!gelDB[index] || (highestQuality && !gelDB[index].high) || (!gelDB[index].medium)) { // Request higher-resolution image (unless it's already indexed)
- var xobj = new XMLHttpRequest();
- xobj.open("GET", aElem.href, true);
- xobj.onreadystatechange = function() {
- if (xobj.readyState == 4 && xobj.status == "200") {
- const responseDocument = new DOMParser().parseFromString(xobj.responseText, "text/html");
- if (responseDocument.querySelector("#gelcomVideoPlayer")) { // Reject videos
- reject("Gelbooru Overhaul doesn't support videos in convertThumbnail() or downloadImage() yet.");
- if (highestQuality) {
- alert("Gelbooru Overhaul doesn't support videos in convertThumbnail() or downloadImage() yet.");
- }
- return;
- }
- gelDB[index] = {};
- gelDB[index].tags = convertTagElem(responseDocument.querySelector("#tag-list")); // Grab tags
- gelDB[index].medium = responseDocument.querySelector("#image").src; // Get medium-quality src
- gelDB[index].high = responseDocument.querySelectorAll("script:not([src])"); // Get highest-quality src
- gelDB[index].high = gelDB[index].high[gelDB[index].high.length - 1]
- .innerHTML
- .split(`image.attr('src','`)[1]
- .split(`');`)[0];
- GM_setValue("gelDB", gelDB);
- output();
- }
- };
- xobj.send(null);
- } else { // Skip the AJAX voodoo if it's already indexed. Added bonus of cache speed.
- output();
- }
- function output() {
- if (highestQuality) {
- imgElem.src = gelDB[index].high;
- } else {
- imgElem.src = gelDB[index].medium;
- }
- resolve();
- }
- });
- }
- // Convert tag list elem into a friendlier object
- function convertTagElem(tagElem) {
- var tagObj = {
- "artist": [],
- "character": [],
- "copyright": [],
- "metadata": [],
- "general": [],
- "deprecated": [],
- };
- Object.values(tagElem.querySelectorAll("li")).forEach((tag) => {
- if (tag.className.startsWith("tag-type-")) {
- var type = tag.className.replace("tag-type-", "");
- tag = tag.querySelector("span a")
- .href
- .replace("https://gelbooru.com/index.php?page=wiki&s=list&search=", "");
- tagObj[type].push(tag);
- }
- });
- return tagObj;
- }
- // Generate hash from string (https://stackoverflow.com/a/52171480)
- function hash(str, seed = 0) {
- let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
- for (let i = 0, ch; i < str.length; i++) {
- ch = str.charCodeAt(i);
- h1 = Math.imul(h1 ^ ch, 2654435761);
- h2 = Math.imul(h2 ^ ch, 1597334677);
- }
- h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909);
- h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909);
- return 4294967296 * (2097151 & h2) + (h1>>>0);
- };
- // Download image
- function downloadImage(imgElem, aElem) {
- var gelDB = GM_getValue("gelDB", {});
- var index = hash(aElem.href);
- var extension = imgElem.src.split(".").at(-1);
- var artist = gelDB[index].tags.artist.join(" ");
- if (config.download.blockUnknownArtist && artist === "") { // Don't download if blockUnknownArtist is enabled and artist tag is missing
- return;
- }
- GM_download({
- url: imgElem.src,
- name: formatFilename(artist, index, extension),
- saveAs: config.gallery.rightClickDownloadSaveAsPrompt,
- })
- }
- // Create the filename from the artist's name
- function formatFilename(artist, index, extension) {
- if (artist === "") {
- artist = config.download.missingArtistText;
- }
- const illegalRegex = /[\/\?<>\\:\*\|":]/g;
- artist = decodeURI(artist).replace(illegalRegex, "_") // Make filename-safe (https://stackoverflow.com/a/11101624)
- .replace(/_{2,}/g, "_") // and remove consecutive underscores
- .toLowerCase() + "_" + index + "." + extension;
- return artist;
- }
- // Check if dark mode should be applied
- function isDarkMode() {
- //if auto enabled
- if(config.general.autoDarkMode)
- {
- let hasMediaColorScheme = (window.matchMedia && window.matchMedia('(prefers-color-scheme)').media !== 'not all');
-
- if(config.general.autoDarkModeForceTime || !hasMediaColorScheme)
- {
- let hours = new Date().getHours();
- if(hours >= config.general.autoDarkModeStartHour || hours <= config.general.autoDarkModeEndHour)
- {
- return true;
- }
- else
- {
- return false;
- }
- }
- //system in dark mode
- if(window.matchMedia('(prefers-color-scheme: dark)').matches)
- {
- return true;
- }
- //system in light mode
- else
- {
- return false;
- }
- }
- //if permanent dark mode enabled
- else if(config.general.amoled)
- {
- return true;
- }
- else
- {
- return false;
- }
- }
- })();