Gelbooru Overhaul

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
        }
    }
})();