Gelbooru Overhaul

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

// ==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;
        }
    }
})();