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 24. 10. 2020. See the latest version.

// ==UserScript==
// @name         Gelbooru Visited and Type Highlighter
// @namespace    http://tampermonkey.net/
// @version      9.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 = false;

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

// 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 + '")');
}

// Check if link is in visited list
function markIfVisited(galleryLink, visitedIDs) {
    let linkURL = new URL(galleryLink.getAttribute('href'), window.location.href);
    let linkSearchParams = new URLSearchParams(linkURL.search);
    let id = linkSearchParams.get('id');

    if (visitedIDs.includes(id)) {
        galleryLink.classList.add('visited');
    }
}

// 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';

    // Create visited image database if doesn't exist
    if (localStorage.getItem('visitedIDs') === null) {
        localStorage.setItem('visitedIDs', JSON.stringify([]));
    }

    // Get list of visited images
    let visitedIDs = JSON.parse(localStorage.getItem('visitedIDs'));

    // Find out what kind of page we're on
    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);
                        if (visitedIDs.length > 0) {
                            markIfVisited(galleryLink, visitedIDs);
                        }
                    });
                }
            }
            // 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, 
				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,
                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),
                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);
            // Add to visited database
            let currentURLSearchParams = new URLSearchParams(url.search);
            let id = currentURLSearchParams.get('id');
            if (!visitedIDs.includes(id)) {
                visitedIDs.push(id);
                localStorage.setItem('visitedIDs', JSON.stringify(visitedIDs));
            }

            // Causes the visited highlighting to update immediately in the original page this was opened from (or any other page) if opened in new tab or window
            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 => markIfVisited(mltLink, visitedIDs));
            }

            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,
                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);
                    if (visitedIDs.length > 0) {
                        markIfVisited(galleryLink, visitedIDs);
                    }
                });
            }
        }
        // Apply borders
        let css = document.createElement('style');
        css.innerHTML = `
            div.thumbnail-container a img {
                outline: 3px solid ` + imgUnvistedColor + `;
            }
            div.thumbnail-container a:visited img,
            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 "],
            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 "]),
            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) {
            // Mark visited links
            if (visitedIDs.length > 0) {
                let galleryLinks = document.querySelectorAll('.thumb a');
                galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
            }
            css.innerHTML = css.innerHTML + `
                .thumb a img {
                    outline: 3px solid ` + imgUnvistedColor + `;
                }
                .thumb a:visited img,
                .thumb a.visited img {
                    outline-color: ` + imgVisitedColor + `;
                }
                .thumb a:visited img[title*=" webm"],
                .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"]),
                .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
        // Mark visited links
        if (visitedIDs.length > 0) {
            let galleryLinks = document.querySelectorAll('.container-fluid > .thumb a');
            galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
        }

        // 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,
            .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"],
            .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"]),
            .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);
    } else if (searchParams.get('s') === 'view' && searchParams.get('page') === 'wiki') { // Wiki entry page
        // Mark visited links
        if (visitedIDs.length > 0) {
            let galleryLinks = document.querySelectorAll('tr > td:nth-child(2) a[href*="s=view"]');
            galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
        }

        // Apply borders
        let css = document.createElement('style');
        css.innerHTML = `
            a .thumbnail-preview img {
                padding: 5px;
                outline: 3px solid ` + imgUnvistedColor + `;
            }
            a:visited .thumbnail-preview img,
            a.visited .thumbnail-preview img {
                outline-color: ` + imgVisitedColor + `;
            }
            a .thumbnail-preview img[alt*="animated_gif"],
            a .thumbnail-preview img[alt*="animated_png"],
            a .thumbnail-preview img[alt*="animated "]:not([alt*=" webm"]) {
                outline: 3px solid ` + gifUnvisitedColor + `;
            }
            a .thumbnail-preview img[alt*=" webm"] {
                outline: 5px solid ` + webmUnvistedColor + `;
                margin: 5px 7px;
            }
            a:visited .thumbnail-preview img[alt*=" webm"],
            a.visited .thumbnail-preview img[alt*=" webm"] {
                outline-color: ` + webmVisitedColor + `;
            }
            a:visited .thumbnail-preview img[alt*="animated_gif"],
            a:visited .thumbnail-preview img[alt*="animated_png"],
            a:visited .thumbnail-preview img[alt*="animated "]:not([alt*=" webm"]),
            a.visited .thumbnail-preview img[alt*="animated_gif"],
            a.visited .thumbnail-preview img[alt*="animated_png"],
            a.visited .thumbnail-preview img[alt*="animated "]:not([alt*=" webm"]) {
                outline-color: ` + gifVisitedColor + `;
            }
        `;
        document.head.appendChild(css);
    }
})();