Gelbooru Overhaul

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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