Gelbooru Animation Highlighter

Make webm highlighting work in many more places and mark animated gif as well.

// ==UserScript==
// @name         Gelbooru Animation Highlighter
// @namespace    http://tampermonkey.net/
// @version      14.13.2
// @description  Make webm highlighting work in many more places and mark animated gif as well.
// @author       Xerodusk
// @homepage     https://greasyfork.org/en/users/460331-xerodusk
// @license      GPL-3.0-or-later
// @match        https://gelbooru.com/index.php*page=post*s=list*
// @match        https://gelbooru.com/index.php*page=post*s=view*
// @match        https://gelbooru.com/index.php*page=pool*s=show*
// @match        https://gelbooru.com/index.php*page=favorites*s=view*
// @match        https://gelbooru.com/index.php*page=tags*s=saved_search*
// @match        https://gelbooru.com/index.php*page=wiki*s=view*
// @match        https://gelbooru.com/index.php*page=account*s=profile*
// @match        https://gelbooru.com/index.php*page=comment*s=list*
// @grant        none
// @icon         https://gelbooru.com/favicon.png
// ==/UserScript==
/* jshint esversion: 6 */

/*   configuration   */

// You can modify these from the "Highlighter Settings" item on the right side of the header on any page this runs on.

// Highlight colors
// Values can be hexadecimal, rgb, hsl, color name, or whatever CSS color definitions your browser supports
const webmColor = localStorage.getItem('webmUnvisitedColor') || '#1565C0'; // Color for WebMs
const gifColor = localStorage.getItem('gifUnvisitedColor') || '#FFD600'; // Color for animated gifs/pngs

// Whether to use the experimental workaround to display gif/webm highlighting on "More Like This" links on image pages
// NOTICE:   This is experimental and is not guaranteed to be perfect
const displayMoreLikeThisAnimatedTypes = JSON.parse(localStorage.getItem('displayMoreLikeThisAnimatedTypes') || 'true');

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

// Messy workaround for finding the type of "More Like This" links on image pages
function getAnimatedType(galleryLinks) {
    const event = new Event('tagsretrieve');

    let retrieved = galleryLinks.length;

    function checkImage(link) {
        const linkURL = new URL(link.href, window.location.href).href;

        fetch(linkURL).then(response => response.text()).then((responseText) => {
            const parser = new DOMParser();

            const htmlDoc = parser.parseFromString(responseText, 'text/html');

            const sideLinks = htmlDoc.querySelectorAll('ul#tag-list > li:not([class^="tag-type"]) > a');

            // Get tags for possible later use
            const tagsEditField = htmlDoc.querySelector('textarea#tags.tagBox[name="tags"]');
            const tags = tagsEditField.value;
            // Get rating for possible later use
            const ratingEditField = htmlDoc.querySelector('input[name="rating"][checked]');
            const ratingAbbrv = ratingEditField.value;
            let rating = '';
            switch (ratingAbbrv) {
                case 'e': rating = 'explicit'; break;
                case 'q': rating = 'questionable'; break;
                case 's': rating = 'sensitive'; break;
                case 'g': rating = 'general'; break;
                default: break;
            }
            // Get score for possible later use
            const linkSearchParams = new URLSearchParams(new URL(linkURL).search);
            const linkID = linkSearchParams.get('id');
            const score = htmlDoc.getElementById('psc' + linkID).textContent;

            const image = link.querySelector('img');

            image.dataset.tags = tags + '  score:' + score + ' rating:' + rating;

            retrieved--;
            if (retrieved === 0) {
                window.dispatchEvent(event);
            }

            for (let i = 0, len = sideLinks.length; i < len; i++) {
                if (sideLinks[i].textContent === 'Original image') {
                    const imageURL = sideLinks[i].href;

                    if (imageURL.endsWith('.webm')) {
                        if (image) {
                            image.classList.add('webm');
                        }
                    } else if (imageURL.endsWith('.gif') || tags.split(' ').includes('animated')) {
                        if (image) {
                            image.classList.add('gif');
                        }
                    }

                    break;
                }
            }
        });
    }

    galleryLinks.forEach(galleryLink => checkImage(galleryLink));
}

// Create interface for settings
function createSettingsInterface() {
    'use strict';

    // Get subheader
    const header = document.getElementById('myTopnav') || document.querySelector('#navbar ul') || document.querySelector('.header .center .flat-list');
    if (!header) {
        return;
    }

    // Create button
    const openSettingsButtonContainer = document.createElement('li');
    openSettingsButtonContainer.classList.add('open-highlighter-dialog');
    const openSettingsButton = document.createElement('a');
    openSettingsButton.appendChild(document.createTextNode('Highlighter Settings'));
    openSettingsButton.setAttribute('role', 'button');
    openSettingsButton.href = 'javascript:void(0)';
    openSettingsButton.onclick = () => dialog.classList.add('open');
    openSettingsButtonContainer.appendChild(openSettingsButton);

    // Create dialog
    const dialog = document.createElement('div');
    dialog.id = 'settings-dialog';
    dialog.classList.add('highlighter-dialog');
    const dialogHeader = document.createElement('h3');
    dialogHeader.classList.add('highlighter-dialog-header');
    dialogHeader.appendChild(document.createTextNode('Type Highlighter Settings'));
    dialog.appendChild(dialogHeader);

    const webmColorSetting = document.createElement('div');
    webmColorSetting.classList.add('highlighter-setting-single');
    const webmColorInput = document.createElement('input');
    webmColorInput.id = 'webm-input';
    webmColorInput.setAttribute('name', 'webm-input');
    webmColorInput.setAttribute('type', 'color');
    webmColorInput.value = webmColor;
    const webmColorLabel = document.createElement('label');
    webmColorLabel.classList.add('highlighter-dialog-text');
    webmColorLabel.setAttribute('for', 'webm-input');
    webmColorLabel.appendChild(document.createTextNode('WebM Color'));
    webmColorSetting.appendChild(webmColorInput);
    webmColorSetting.appendChild(webmColorLabel);

    const gifColorSetting = document.createElement('div');
    gifColorSetting.classList.add('highlighter-setting-single');
    const gifColorInput = document.createElement('input');
    gifColorInput.id = 'gif-input';
    gifColorInput.setAttribute('name', 'gif-input');
    gifColorInput.setAttribute('type', 'color');
    gifColorInput.value = gifColor;
    const gifColorLabel = document.createElement('label');
    gifColorLabel.classList.add('highlighter-dialog-text');
    gifColorLabel.setAttribute('for', 'gif-input');
    gifColorLabel.appendChild(document.createTextNode('GIF Color'));
    gifColorSetting.appendChild(gifColorInput);
    gifColorSetting.appendChild(gifColorLabel);

    // Create the buttons
    const dialogButtons = document.createElement('div');
    dialogButtons.classList.add('highlighter-dialog-buttons');
    const applyButton = document.createElement('button');
    const defaultButton = document.createElement('button');
    const closeButton = document.createElement('button');
    applyButton.appendChild(document.createTextNode('Save'));
    applyButton.onclick = () => {
        localStorage.setItem('webmUnvisitedColor', webmColorInput.value);
        localStorage.setItem('gifUnvisitedColor', gifColorInput.value);
        if (confirm('The page must be reloaded for changes to take effect.\n\nReload now?')) {
            location.reload();
        }
        dialog.classList.remove('open');
    };
    defaultButton.appendChild(document.createTextNode('Restore Defaults'));
    defaultButton.onclick = () => {
        localStorage.removeItem('webmUnvisitedColor');
        localStorage.removeItem('gifUnvisitedColor');
        if (confirm('The page must be reloaded for changes to take effect.\n\nReload now?')) {
            location.reload();
        }
        dialog.classList.remove('open');
    };
    closeButton.appendChild(document.createTextNode('Cancel'));
    closeButton.onclick = () => dialog.classList.remove('open');
    dialogButtons.appendChild(applyButton);
    dialogButtons.appendChild(defaultButton);
    dialogButtons.appendChild(closeButton);

    dialog.appendChild(dialogHeader);
    dialog.appendChild(webmColorSetting);
    dialog.appendChild(gifColorSetting);
    dialog.appendChild(dialogButtons);

    // Attach button to header
    header.appendChild(openSettingsButtonContainer);
    // Attach dialog to page
    document.body.appendChild(dialog);

    // Style everything
    const css = document.createElement('style');
    css.appendChild(document.createTextNode(`
        div.center {
            padding: 0 3em;
            margin: 0;
        }
        .header .center {
            box-sizing: border-box;
            width: 100%;
        }
        h2.siteName {
            height: 48px;
            width: 100px;
            display: inline-flex;
            align-items: center;
        }
        ul.flat-list {
            width: calc(100% - 117px);
        }
        #navbar ul.navbar-nav {
            width: calc(100% - 75px);
        }
        .open-highlighter-dialog {
            display: block;
        }
        @media (min-width: 1225px) {
            .open-highlighter-dialog {
                float: right !important;
            }
        }
        .highlighter-dialog {
            position: fixed;
            top: 0;
            right: -400px;
            z-index: 1003;
            background-color: white;
            box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
            width: 400px;
            max-width: 90vw;
            max-height: 90vh;
            padding: 12px;
            font-size: 12px;
            line-height: 1.42857143;
            box-sizing: border-box;
            transition: right 0.2s cubic-bezier(0,0,0.3,1);
        }
        .highlighter-dialog.open {
            right: 0;
            transition: right 0.25s cubic-bezier(0,0,0.3,1);
        }
        .highlighter-dialog * {
            box-sizing: border-box;
            font-family: verdana, sans-serif;
            line-height: inherit;
        }
        .highlighter-dialog-header {
            all: revert;
        }
        .highlighter-setting-single * {
            cursor: pointer;
        }
        .highlighter-setting-single input {
            padding: revert;
            margin: revert;
        }
        .highlighter-setting-single .highlighter-dialog-text {
            padding-left: 6px;
        }
        .highlighter-setting-single abbr {
            border: none;
            cursor: help;
            color: white;
            font-size: 14px;
        }
        .highlighter-setting-single abbr:after {
            content: '?';
            display: inline-block;
            font-family: sans-serif;
            font-weight: bold;
            text-align: center;
            font-size: 0.8em;
            line-height: 0.8em;
            border-radius: 50%;
            margin-left: 6px;
            padding: 0.13em 0.2em 0.09em 0.2em;
            color: inherit;
            border: 1px solid;
            text-decoration: none;
            background-color: rgb(33, 131, 252);
        }
        .highlighter-setting-single abbr sup {
            cursor: help;
        }
        .highlighter-setting-boolean {
            margin: 8px;
        }
        .highlighter-setting-boolean input[type='checkbox'] {
            appearance: revert;
        }
        .highlighter-dialog-text {
            display: inline-block;
            max-width: 100%;
            margin-bottom: 5px;
            white-space: unset;
        }
        .highlighter-dialog-buttons {
            padding-top: 6px;
        }
        .highlighter-dialog-buttons button {
            margin-right: 6px;
            cursor: pointer;
            font-size: inherit;
            padding: revert;
            appearance: revert;
        }
    `));
    document.head.appendChild(css);
}

(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

            window.typeHighlighterInstalled = true;

            // "More Like This" results, currently in beta, could break, but this code should hypothetically never break the page from this end
            const mltLinks = document.querySelectorAll('.mainBodyPadding > div > a[href*="page=post&s=view"]');
            if (mltLinks) {
                if (displayMoreLikeThisAnimatedTypes) {
                    getAnimatedType(mltLinks);
                }
            }

            const css = document.createElement('style');
            css.appendChild(document.createTextNode(`
                a[href*="page=post&s=view"] img.webm:not(.gif) {
                    padding: 3px;
                    border: 3px solid ` + webmColor + ` !important;
                }
                a[href*="page=post&s=view"] img.gif {
                    padding: 3px;
                    border: 3px solid ` + gifColor + `;
                }
            `));
            document.head.appendChild(css);
        } else if (s === 'list') { // Search page
            // Apply borders
            const css = document.createElement('style');
            css.appendChild(document.createTextNode(`
                article.thumbnail-preview {
                    background-color: transparent;
                    width: auto;
                    margin: 20px 10px 0 10px;
                }
                article.thumbnail-preview a img.webm {
                    box-sizing: content-box;
                    padding: 3px;
                    border: 3px solid ` + webmColor + ` !important;
                }
                article.thumbnail-preview a img[title*="animated_gif"],
                article.thumbnail-preview a img[title*="animated_png"],
                article.thumbnail-preview a img[title*="animated "]:not(.webm) {
                    box-sizing: content-box;
                    padding: 3px;
                    outline: 3px solid ` + gifColor + `;
                }
            `));
            document.head.appendChild(css);
        }
    } else if (page === 'pool' && s === 'show') { // Pool page
        // Apply borders
        const css = document.createElement('style');
        css.appendChild(document.createTextNode(`
            .thumbnail-preview {
                padding: 3px;
            }
            div.thumbnail-container a img[title*=" video "] {
                outline: 3px solid ` + webmColor + ` !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*=" video "]) {
                outline: 3px solid ` + gifColor + `;
            }
        `));
        document.head.appendChild(css);
    } else if (page === 'favorites' && s === 'view') { // Favorites page
        // Apply type borders
        const css = document.createElement('style');
        css.appendChild(document.createTextNode(`
            .thumb {
                margin: 5px;
            }
            .thumb a img[title*="animated_gif"],
            .thumb a img[title*="animated_png"],
            .thumb a img[title*="animated "] {
                padding: 3px;
                margin: 5px 0 3px 0;
                outline: 3px solid ` + gifColor + `;
            }
            .thumb a img[title*=" video "],
            .thumb a img[title$=" video"] {
                padding: 3px;
                margin: 5px 0 3px 0;
                outline: 3px solid ` + webmColor + `;
            }
        `));
        document.head.appendChild(css);
    } else if (page === 'tags' && s === 'saved_search') { // Saved Searches page
        // Apply borders
        const css = document.createElement('style');
        css.appendChild(document.createTextNode(`
            .thumbnail-container > .thumbnail-preview a img {
                padding: 3px;
            }
            .thumbnail-container > .thumbnail-preview a img[alt*="animated_gif"],
            .thumbnail-container > .thumbnail-preview a img[alt*="animated_png"],
            .thumbnail-container > .thumbnail-preview a img[alt*="animated "] {
                outline: 3px solid ` + gifColor + `;
            }
            .thumbnail-container > .thumbnail-preview a img[alt*=" video "],
            .thumbnail-container > .thumbnail-preview a img[alt$=" video"] {
                outline: 3px solid ` + webmColor + `;
            }
        `));
        document.head.appendChild(css);
    } else if (page === 'wiki' && !(s === 'list')) { // Wiki entry page
        // Apply borders
        const css = document.createElement('style');
        css.appendChild(document.createTextNode(`
            a .thumbnail-preview img[alt*="animated_gif"],
            a .thumbnail-preview img[alt*="animated_png"],
            a .thumbnail-preview img[alt*="animated "] {
                padding: 3px;
                outline: 3px solid ` + gifColor + `;
            }
            a .thumbnail-preview img[alt*=" video "],
            a .thumbnail-preview img[alt$=" video"] {
                padding: 3px;
                outline: 3px solid ` + webmColor + `;
                margin: 5px 7px;
            }
        `));
        document.head.appendChild(css);
    } else if (page === 'account' && s === 'profile') { // Profile page
        // Apply borders
        const css = document.createElement('style');
        css.appendChild(document.createTextNode(`
            .profileThumbnailPadding {
                max-width: none !important;
            }
            .profileStatisticsBoxContainer {
                z-index: auto;
            }
            a[href*="s=view"] img {
                padding: 3px;
                max-height: 190px;
                width: auto !important;
                max-width: 150px;
                object-fit: scale-down;
            }
            a[href*="s=view"] img[alt*="animated_gif"],
            a[href*="s=view"] img[alt*="animated_png"],
            a[href*="s=view"] img[alt*="animated "] {
                outline: 3px solid ` + gifColor + `;
            }
            a[href*="s=view"] img[alt*=" video "] {
                outline: 3px solid ` + webmColor + `;
            }
        `));
        document.head.appendChild(css);
    } else if (page === 'comment' && s === 'list') { // Comments page
        // Apply borders
        const css = document.createElement('style');
        css.appendChild(document.createTextNode(`
            a[href*="s=view"] img {
                position: relative;
                top: -3px;
                left: -3px;
                padding: 3px;
            }
            a[href*="s=view"] img.flagged {
                background-color: #FF0000 !important;
                border: none !important;
            }
            a[href*="s=view"] img[title*="animated_gif"],
            a[href*="s=view"] img[title*="animated_png"],
            a[href*="s=view"] img[title*="animated "] {
                outline: 3px solid ` + gifColor + `;
            }
            a[href*="s=view"] img[title*=" video "],
            a[href*="s=view"] img[title$=" video"] {
                outline: 3px solid ` + webmColor + `;
            }
        `));
        document.head.appendChild(css);
    }

    createSettingsInterface();
})();