Gelbooru Visited and Type Highlighter

Marks previously visited images on Gelbooru search pages, and extends animation highlighting from webm to also include gifs.

As of 2020-07-25. See the latest version.

// ==UserScript==
// @name         Gelbooru Visited and Type Highlighter
// @namespace    http://tampermonkey.net/
// @version      7.0.0
// @description  Marks previously visited images on Gelbooru search pages, and extends animation highlighting from webm to also include gifs.
// @author       Xerodusk
// @homepage     https://greasyfork.org/en/users/460331-xerodusk
// @match        https://gelbooru.com/index.php*
// @grant        none
// @icon         https://gelbooru.com/favicon.png
// ==/UserScript==
/* jshint esversion: 6 */

/*   configuration   */

// Highlight colors
// Values can be hexadecimal, rgb, rgba, hsl, hsla, color name, or whatever CSS color definitions your browser supports
const imgUnvistedColor = '#E1F5FE'; // Color for unvisted images
const imgVisitedColor = '#2E7D32'; // Color for visited images
const webmUnvistedColor = '#1565C0'; // Color for unvisted WebMs
const webmVisitedColor = '#C62828'; // Color for visited WebMs
const gifUnvisitedColor = '#FFD600'; // Color for unvisited animated gifs/pngs
const gifVisitedColor = '#6A1B9A'; // Color for visited animated gifs/pngs

// Whether to display visited/unvisited highlighting for your own favorites
// If false: Will only show visited/unvisited on other users' favorites pages
//           Animated GIF/WebM type highlighting will always be shown on all favorites
// If true:  Will also show visited/unvisited on your own favorites page
const displayCurrentUserFavoritesVisited = true;

/*-------------------*/

// Convert any image link URL to a single, standard form
function normalizeURL(linkURL) {
    'use strict';

    // Remove "tags" and "pool" attributes so the link being used for this
    // is constant for the same image across all searches it is included in
    let searchParams = new URLSearchParams(linkURL.search);
    searchParams.forEach((value, key) => {
        if (!['page', 's', 'id'].includes(key)) {
            searchParams.delete(key);
        }
    });
    let newLinkURL = new URL(linkURL);
    newLinkURL.search = searchParams.toString();
    return newLinkURL;
}

// Convert link formation so it is consistent across all pages
function normalizeLink(galleryLink) {
    'use strict';

    // Convert to absolute URL from whichever random form of relative URL they decided to use on this specific page with no real consistency
    let linkURL = new URL(galleryLink.getAttribute('href'), window.location.href);
    // Fix for Gelbooru's broken implementation of tags containing apostrophes under many navigational situations
    if (linkURL.href.includes(''')) {
        linkURL.href = linkURL.href.replace(''', '%27');
    }
    // Set click to go to the original url to preserve next/previous feature
    galleryLink.setAttribute('onmousedown', 'this.setAttribute("href", "' + linkURL + '")');
    galleryLink.setAttribute('onmouseup', 'this.setAttribute("href", "' + linkURL + '")');
    galleryLink.setAttribute('onkeydown', 'if(event.keyCode == 13) { this.setAttribute("href", "' + linkURL + '") }');
    // Convert URL to be the one URL for one image
    let newLinkURL = normalizeURL(linkURL);
    galleryLink.setAttribute('href', newLinkURL);
    galleryLink.setAttribute('onfocusout', 'this.setAttribute("href", "' + newLinkURL + '")');
}

// Get cookie by name
// From https://www.w3schools.com/js/js_cookies.asp
function getCookie(cname) {
    'use strict';

    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for (var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

// Get current user's user ID, if exists
function getUserID() {
    'use strict';

    // Get user ID from cookie
    let userID = getCookie('user_id');

    return userID ? userID : -1;
}

(function() {
    'use strict';

    let searchParams = new URLSearchParams(window.location.search);

    if (!searchParams.has('page') || !searchParams.has('s')) {
        return false;
    }
    if (searchParams.get('page') === 'post') {
        if (searchParams.get('s') == 'list') { // Search page
            // Get search results area
            let galleryContainer = document.querySelector('.thumbnail-container');
            if (!!galleryContainer) {
                // Get all image thumbnail links
                let galleryLinks = galleryContainer.querySelectorAll('div.thumbnail-preview a');
                if (!!galleryLinks) {
                    galleryLinks.forEach(galleryLink => normalizeLink(galleryLink));
                }
            }
            // Apply borders
            let css = document.createElement('style');
            css.innerHTML = `
                div.thumbnail-preview {
                    background-color: transparent;
                }
                div.thumbnail-preview a img.thumbnail-preview:not(.webm) {
                    outline: 3px solid ` + imgUnvistedColor + `;
                }
                div.thumbnail-preview a:visited img.thumbnail-preview {
                    outline-color: ` + imgVisitedColor + `;
                }
                div.thumbnail-preview a img.thumbnail-preview.webm {
                    border-color: ` + webmUnvistedColor + ` !important;
                }
                div.thumbnail-preview a:visited img.thumbnail-preview.webm {
                    border-color: ` + webmVisitedColor + ` !important;
                }
                div.thumbnail-preview a img.thumbnail-preview[title*="animated_gif"],
                div.thumbnail-preview a img.thumbnail-preview[title*="animated_png"],
                div.thumbnail-preview a img.thumbnail-preview[title*="animated "]:not(.webm) {
                    outline-color: ` + gifUnvisitedColor + `;
                }
                div.thumbnail-preview a:visited img.thumbnail-preview[title*="animated_gif"],
                div.thumbnail-preview a:visited img.thumbnail-preview[title*="animated_png"],
                div.thumbnail-preview a:visited img.thumbnail-preview[title*="animated "]:not(.webm) {
                    outline-color: ` + gifVisitedColor + `;
                }
                div.thumbnail-preview a:focus img.thumbnail-preview:not(.webm) {
                    outline-color: #FFA726 !important;
                }
                div.thumbnail-preview a:focus img.thumbnail-preview.webm {
                    border-color: #FFA726 !important;
                }
            `;
            document.head.appendChild(css);
        } else if (searchParams.get('s') === 'view') { // Image page
            // Add URL without the "tags" attribute to the history
            // so as to make it match what the search results pages are modified for
            // (and avoid breaking back button functionality while we're at it)
            let url = new URL(window.location);
            url = normalizeURL(url);
            window.history.replaceState({}, '', '/' + url.href.substring(url.href.indexOf('/') + 1));
            // "More Like This" results, currently in beta, could break, but this code should hypothetically never break the page from this end
            // Unfortunately, type highlighting is impossible with their current implementation, but visited highlighting is still possible
            /*let mltContainer = document.getElementById('post-view');
            // Get all image thumbnail links
            let mltLinks = mltContainer.querySelectorAll('#right-col > div > div > a');
            if (!!mltLinks) {
                mltLinks.forEach(mltLink => normalizeLink(mltLink));
            }*/ // In case this becomes necessary later, what with this being a beta feature that could change rapidly and all
            let css = document.createElement('style');
            css.innerHTML = css.innerHTML + `
                a img.mltThumbs {
                    padding: 5px;
                    margin: 5px !important;
                    outline: 3px solid ` + imgUnvistedColor + `;
                }
                a:visited img.mltThumbs {
                    outline-color: ` + imgVisitedColor + `;
                }
            `;
            document.head.appendChild(css);
        }
    } else if (searchParams.get('s') === 'show' && searchParams.get('page') === 'pool') { // Pool page
        // Get image thumbnails area
        let galleryContainer = document.querySelector('.thumbnail-container');
        if (!!galleryContainer) {
            // Get all image thumbnail links
            let galleryLinks = galleryContainer.querySelectorAll('span a');
            if (!!galleryLinks) {
                galleryLinks.forEach(galleryLink => normalizeLink(galleryLink));
            }
        }
        // Apply borders
        let css = document.createElement('style');
        css.innerHTML = `
            div.thumbnail-container a img {
                outline: 3px solid ` + imgUnvistedColor + `;
            }
            div.thumbnail-container a:visited img {
                outline-color: ` + imgVisitedColor + `;
            }
            div.thumbnail-container a img[title*=" webm "] {
                outline: 5px solid ` + webmUnvistedColor + ` !important;
            }
            div.thumbnail-container a:visited img[title*=" webm "] {
                outline-color: ` + webmVisitedColor + ` !important;
            }
            div.thumbnail-container a img[title*="animated_gif"],
            div.thumbnail-container a img[title*="animated_png"],
            div.thumbnail-container a img[title*="animated "]:not([title*=" webm "]) {
                outline-color: ` + gifUnvisitedColor + `;
            }
            div.thumbnail-container a:visited img[title*="animated_gif"],
            div.thumbnail-container a:visited img[title*="animated_png"],
            div.thumbnail-container a:visited img[title*="animated "]:not([title*=" webm "]) {
                outline-color: ` + gifVisitedColor + `;
            }
            div.thumbnail-container a:focus img {
                outline-color: #FFA726 !important;
            }
        `;
        document.head.appendChild(css);
    } else if (searchParams.get('s') === 'view' && searchParams.get('page') === 'favorites') { // Favorites page
        // Apply borders
        let css = document.createElement('style');
        css.innerHTML = `
            .thumb {
                margin: 5px;
            }
            .thumb a img {
                padding: 5px;
                margin: 5px;
            }
            .thumb a img[title*="animated_gif"],
            .thumb a img[title*="animated_png"],
            .thumb a img[title*="animated "]:not([title*=" webm"]) {
                outline: 3px solid ` + gifUnvisitedColor + `;
            }
            .thumb a img[title*=" webm"] {
                outline: 5px solid ` + webmUnvistedColor + `;
            }
        `;

        let userID = displayCurrentUserFavoritesVisited ? -1 : getUserID();
        if (searchParams.has('id') && searchParams.get('id') != userID) {
            css.innerHTML = css.innerHTML + `
                .thumb a img {
                    outline: 3px solid ` + imgUnvistedColor + `;
                }
                .thumb a:visited img {
                    outline-color: ` + imgVisitedColor + `;
                }
                .thumb a:visited img[title*=" webm"] {
                    outline-color: ` + webmVisitedColor + `;
                }
                .thumb a:visited img[title*="animated_gif"],
                .thumb a:visited img[title*="animated_png"],
                .thumb a:visited img[title*="animated "]:not([title*=" webm"]) {
                    outline-color: ` + gifVisitedColor + `;
                }
            `;
        }
        document.head.appendChild(css);
    } else if (searchParams.get('s') === 'saved_search' && searchParams.get('page') === 'tags') { // Saved Searches page
        // Apply borders
        let css = document.createElement('style');
        css.innerHTML = `
            .container-fluid > .thumb a .thumbnail-preview {
                outline: 3px solid ` + imgUnvistedColor + `;
            }
            .container-fluid > .thumb a:visited .thumbnail-preview {
                outline-color: ` + imgVisitedColor + `;
            }
            .container-fluid > .thumb a .thumbnail-preview[alt*="animated_gif"],
            .container-fluid > .thumb a .thumbnail-preview[alt*="animated_png"],
            .container-fluid > .thumb a .thumbnail-preview[alt*="animated "]:not([alt*=" webm"]) {
                outline: 3px solid ` + gifUnvisitedColor + `;
            }
            .container-fluid > .thumb a .thumbnail-preview[alt*=" webm"] {
                outline: 5px solid ` + webmUnvistedColor + `;
                margin: 5px 7px;
            }
            .container-fluid > .thumb a:visited .thumbnail-preview[alt*=" webm"] {
                outline-color: ` + webmVisitedColor + `;
            }
            .container-fluid > .thumb a:visited .thumbnail-preview[alt*="animated_gif"],
            .container-fluid > .thumb a:visited .thumbnail-preview[alt*="animated_png"],
            .container-fluid > .thumb a:visited .thumbnail-preview[alt*="animated "]:not([alt*=" webm"]) {
                outline-color: ` + gifVisitedColor + `;
            }
        `;
        document.head.appendChild(css);
    }
})();