Gelbooru Visited and Type Highlighter

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

Ekde 2020/10/30. Vidu La ĝisdata versio.

// ==UserScript==
// @name         Gelbooru Visited and Type Highlighter
// @namespace    http://tampermonkey.net/
// @version      10.1.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
// @include      https://gelbooru.com/index.php*page=post*s=list*
// @include      https://gelbooru.com/index.php*page=post*s=view*
// @include      https://gelbooru.com/index.php*page=pool*s=show*
// @include      https://gelbooru.com/index.php*page=favorites*s=view*
// @include      https://gelbooru.com/index.php*page=tags*s=saved_search*
// @include      https://gelbooru.com/index.php*page=wiki*s=view*
// @include      https://gelbooru.com/index.php*page=account*s=profile*
// @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;

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

function binarySearch(items, value, first, last) {
    if (items.length === 0) {
        return -1;
    }
    const middle = (last + first) >> 1;
    if (last - first <= 1) {
        return (value < items[middle]) ? middle - 1 : middle;
    }
    if (value === items[middle]) {
        return middle;
    }
    if (value < items[middle]) {
        return binarySearch(items, value, first, middle);
    }
    return binarySearch(items, value, middle, last);
}

// Tests whether value is in items
function truthyBinarySearch(items, value) {
    if (items.length === 0 || value > items[items.length - 1] || value < items[0]) {
        return false;
    }
    if (value === items[0] || value === items[items.length - 1]) {
        return true;
    }

    const index = binarySearch(items, value, 0, items.length - 1);

    return (items[index] === value);
}

// Inserts value in items if not already present, returns whether insertion took place
function binaryInsertion(items, value) {
    if (items.length === 0 || value > items[items.length - 1]) {
        items.push(value);
        return true;
    }
    if (value < items[0]) {
        items.unshift(value);
        return true;
    }

    const index = binarySearch(items, value, 0, items.length - 1);

    if (items[index] !== value) {
        items.splice(index + 1, 0, value);
        return true;
    }
    return false;
}

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

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

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

    const name = cname + "=";
    const decodedCookie = decodeURIComponent(document.cookie);
    const ca = decodedCookie.split(';');
    for (let i = 0; i < ca.length; i++) {
        let 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
    const userID = getCookie('user_id');

    return userID ? parseInt(userID) : -1;
}

(function() {
    'use strict';

    // Find out what kind of page we're on
    const searchParams = new URLSearchParams(window.location.search);

    if (!searchParams.has('page') || !searchParams.has('s')) {
        return false;
    }

    const page = searchParams.get('page');
    const s = searchParams.get('s');

    if (page === 'post') {
        if (s === 'view') { // Image page
            // Get id of current image
            const url = new URL(window.location);
            const currentURLSearchParams = new URLSearchParams(url.search);
            const id = parseInt(currentURLSearchParams.get('id'));
            // Get list of visited images
            let visitedIDs;

            function updateVisitedIDs(event) {
                visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
                if (binaryInsertion(visitedIDs, id)) {
                    localStorage.setItem('visitedIDs', JSON.stringify(visitedIDs));
                } else {
                    window.removeEventListener('storage', updateVisitedIDs);
                }
            }
            window.addEventListener('storage', updateVisitedIDs); // Update changes if another image being loaded in a different window changes the list before this one
            updateVisitedIDs();

            // "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
            const mltContainer = document.getElementsByClassName('contain-push')[0];
            // Get all image thumbnail links
            const mltLinks = mltContainer.querySelectorAll('#right-col > div > div > a');
            if (!!mltLinks) {
                mltLinks.forEach(mltLink => markIfVisited(mltLink, visitedIDs));
            }

            const 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 (s === 'list') { // Search page
            // Get list of visited images
            const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
            if (visitedIDs.length > 0) {
                // Get search results area
                const galleryContainer = document.querySelector('.thumbnail-container');
                if (!!galleryContainer) {
                    // Get all image thumbnail links
                    const galleryLinks = galleryContainer.querySelectorAll('div.thumbnail-preview a');
                    if (!!galleryLinks) {
                        galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
                    }
                }
            }

            // Apply borders
            const 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 (page === 'pool' && s === 'show') { // Pool page
        // Get list of visited images
        const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
        if (visitedIDs.length > 0) {
            // Get image thumbnails area
            const galleryContainer = document.querySelector('.thumbnail-container');
            if (!!galleryContainer) {
                // Get all image thumbnail links
                const galleryLinks = galleryContainer.querySelectorAll('span a');
                if (!!galleryLinks) {
                    galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
                }
            }
        }
        // Apply borders
        const 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 (page === 'favorites' && s === 'view') { // Favorites page
        // Apply borders
        const 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 + `;
            }
        `;

        const userID = displayCurrentUserFavoritesVisited ? -1 : getUserID();
        if (searchParams.has('id') && parseInt(searchParams.get('id')) != userID) {
            // Get list of visited images
            const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
            // Mark visited links
            if (visitedIDs.length > 0) {
                const galleryLinks = document.querySelectorAll('.thumb a[href*="page=post"]');
                galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
            }
            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 (page === 'tags' && s === 'saved_search') { // Saved Searches page
        // Get list of visited images
        const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
        // Mark visited links
        if (visitedIDs.length > 0) {
            const galleryLinks = document.querySelectorAll('.container-fluid > .thumb a');
            galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
        }

        // Apply borders
        const 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 (page === 'wiki' && s === 'view') { // Wiki entry page
        // Get list of visited images
        const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
        // Mark visited links
        if (visitedIDs.length > 0) {
            const galleryLinks = document.querySelectorAll('tr > td:nth-child(2) a[href*="s=view"]');
            galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
        }

        // Apply borders
        const 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);
    } else if (page === 'account' && s === 'profile') { // Profile page
        // Get list of visited images
        const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
        // Mark visited links
        if (visitedIDs.length > 0) {
            const galleryLinks = document.querySelectorAll('a[href*="s=view"]');
            galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
        }

        // Apply borders
        const css = document.createElement('style');
        css.innerHTML = `
            .profileThumbnailPadding {
                max-width: none !important;
            }
            #statistics > span:last-child {
                display: none;
            }
            a[href*="s=view"] img {
                padding: 5px;
                outline: 3px solid ` + imgUnvistedColor + `;
                max-height: 190px;
                object-fit: scale-down;
            }
            a[href*="s=view"]:visited img,
            a[href*="s=view"].visited img {
                outline-color: ` + imgVisitedColor + `;
            }
            a[href*="s=view"] img[alt*="animated_gif"],
            a[href*="s=view"] img[alt*="animated_png"],
            a[href*="s=view"] img[alt*="animated "]:not([alt*=" webm"]) {
                outline: 3px solid ` + gifUnvisitedColor + `;
            }
            a[href*="s=view"] img[alt*=" webm"] {
                outline: 5px solid ` + webmUnvistedColor + `;
                margin: 5px 7px;
            }
            a[href*="s=view"]:visited img[alt*=" webm"],
            a[href*="s=view"].visited img[alt*=" webm"] {
                outline-color: ` + webmVisitedColor + `;
            }
            a[href*="s=view"]:visited img[alt*="animated_gif"],
            a[href*="s=view"]:visited img[alt*="animated_png"],
            a[href*="s=view"]:visited img[alt*="animated "]:not([alt*=" webm"]),
            a[href*="s=view"].visited img[alt*="animated_gif"],
            a[href*="s=view"].visited img[alt*="animated_png"],
            a[href*="s=view"].visited img[alt*="animated "]:not([alt*=" webm"]) {
                outline-color: ` + gifVisitedColor + `;
            }
        `;
        document.head.appendChild(css);
    }
})();