YouTube Animated Thumbnails & Preview Videos

On hover, each thumbnail cycles though its 3 thumbnails. Additional feature to view those 3 thumbnails all at once (non-animated)

// ==UserScript==
// @name          YouTube Animated Thumbnails & Preview Videos
// @namespace     http://userscripts.org/users/23652
// @description   On hover, each thumbnail cycles though its 3 thumbnails. Additional feature to view those 3 thumbnails all at once (non-animated)
// @include       http://*.youtube.com/*
// @include       http://youtube.com/*
// @include       https://*.youtube.com/*
// @include       https://youtube.com/*
// @exclude       http://*youtube.com/my_videos_edit*
// @exclude       http://*youtube.com/my_subscribers*
// @exclude       https://*youtube.com/my_videos_edit*
// @exclude       https://*youtube.com/my_subscribers*
// @copyright     JoeSimmons
// @version       1.1.0
// @license       GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @require       https://greasyfork.org/scripts/1884-gm-config/code/GM_config.js?version=4836
// @require       https://greasyfork.org/scripts/1885-joesimmons-library/code/JoeSimmons'%20Library.js?version=4838
// @require       https://greasyfork.org/scripts/2817-jsl-ajax-plugin/code/JSL%20-%20AJAX%20plugin.js?version=7911
// @grant         GM_getValue
// @grant         GM_registerMenuCommand
// @grant         GM_setValue
// @grant         GM_xmlhttpRequest
// ==/UserScript==


/* CHANGELOG -------------------------------------------------------------------------------------------

1.1.0 (4/8/2014)
    - fixed thumbnail previews sometimes not displaying the right video's thumbnail

1.0.94 (2/25/2014)
    - fixed problem with thumbnails appearing under the masthead

1.0.93 (11/18/2013)
    - removed fading, too inconsistent & laggy

1.0.92 (10/8/2013)
    - re-write of the entire script for readability and efficiency
    - added some minor fade effects
    - instead of making the video black when hovering over a thumbnail,
        the script will now keep showing the video
    - previews open in the opposite corner of where your mouse is
    - more reliable HQ image getting
    - only the hovered over thumbnail is animated, not all the images now

1.0.91
    - Added a new option that allows animated thumbnails only on hovered images

1.0.90
    - Moved the options button to inside the YouTube dropdown menu

1.0.89
    - Added compatibility for Opera & Chrome

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


(function () {
    'use strict';

    var rVi = /[a-z0-9-]+\.ytimg\.com\/vi\/([^\/]+)\//i,
        rYtimgUrl = /[a-z0-9-]+\.ytimg\.com\/vi\//i,
        rWhichImage = /(vi\/[a-z0-9-_]+\/)(1|3|mqdefault)(\.jpg)/i,
        x = 0, y = 0,
        isHoverEnabled, thumbSize, isAnimationEnabled, animationSpeed,
        menulist, __preview_timeout, __animation_interval, stillOnThumb;

    var preview = {
        // clears the timeout and hides the hover preview
        hide : function () {
            window.clearInterval(__preview_timeout);
            JSL('#hover_img').hide().find('img').attribute('src', '');
        },
        show : function () {
            // sets the timeout to show the hover preview
            __preview_timeout = window.setTimeout(function () {
                JSL('#hover_img').show('block');
            }, 400);
        }
    };

    function getNextImage(currentImage) {
        var imagesInOrder = ['1', 'mqdefault', '3'],
            indexOfNextImage = imagesInOrder.indexOf(currentImage) + 1;

        if ( indexOfNextImage > (imagesInOrder.length - 1) ) {
            // if it's on the last one, go back to the first
            return imagesInOrder[0];
        } else {
            // if it's not on the last one, show the next one
            return imagesInOrder[indexOfNextImage];
        }
    }

    function handleHoverLogic(urlIdPrefix) {
        var box = JSL('#hover_img'),
            hori, vert;

        preview.hide();

        // set the previews for this thumbnail
        JSL('#hover_img_1').attribute('src', urlIdPrefix + '1.jpg');
        JSL('#hover_img_2').attribute('src', urlIdPrefix + 'mqdefault.jpg');
        JSL('#hover_img_3').attribute('src', urlIdPrefix + '3.jpg');

        // check for a higher quality preview while we
        // wait for the image to display
        JSL.ajax([
            urlIdPrefix + 'maxresdefault.jpg',
            urlIdPrefix + 'hqdefault.jpg'
        ], {
            method : 'HEAD',
            onload : function (resp) {
                var img2 = JSL('#hover_img_2');

                // check for a 200 (OK) status
                // & that we're showing the correct thumbnail
                if ( resp.status === 200 && resp.url.getMatch(rVi, 1) === img2.attribute('src').getMatch(rVi, 1) ) {
                    img2.attribute('src', resp.url);
                    JSL.ajaxClear();
                }
            }
        });

        vert = !(y < (window.innerHeight / 2) ) ? 'top' : 'bottom'; // should the preview be on the top or bottom
        hori = !(x < ( (window.innerWidth - 15) / 2) ) ? 'left' : 'right'; // should the preview be on the left or right

        // set the vertical align style property of each of the 3 preview images
        JSL('#hover_img img').css('vertical-align', vert);

        // reset the position of the hover box
        box.css('top', 'auto').css('right', 'auto').css('bottom', 'auto').css('left', 'auto');

        // set the corner it will appear in
        box.css(vert, '0').css(hori, '0');

        // set a delay for the previews to show
        preview.show();
    }

    function handleAnimationLogic(elem) {
        __animation_interval = JSL.setInterval(function () {
            var currentImage = elem.attribute('src').getMatch(rWhichImage, 2),
                nextImage = getNextImage(currentImage);

            elem.attribute( 'src', elem.attribute('src').replace(rWhichImage, '$1' + nextImage + '$3') );
        }, animationSpeed);
    }

    function show(event) {
        var elem = JSL(event.target),
            id = elem.attribute('src').getMatch(rVi, 1),
            urlIdPrefix = 'http://' + elem.attribute('src').getMatch(rYtimgUrl) + id + '/';

        // filter out non-thumbnails
        if ( elem.is('img') && elem.isnt('#hover_img, img[id^="hover_img_"]') && urlIdPrefix.getMatch(rVi) ) {
            if (isHoverEnabled) {
                handleHoverLogic(urlIdPrefix);
            }
            
            if (isAnimationEnabled) {
                handleAnimationLogic(elem);
            }
        }
    }

    function hide(event) {
        var elem = JSL(event.target),
            id = elem.attribute('src').getMatch(rVi, 1),
            urlIdPrefix = 'http://' + elem.attribute('src').getMatch(rYtimgUrl) + id + '/';

        // clear the last HQ thumbnail request
        JSL.ajaxClear();

        // don't hide the preview if the mouse goes over it
        if ( elem.isnt('#hover_img, img[id^="hover_img_"]') ) {
            preview.hide();
        }

        // stop animating the current thumbnail
        if ( elem.is('img') && elem.isnt('#hover_img, img[id^="hover_img_"]') && elem.attribute('src').getMatch(rWhichImage) ) {
            JSL.clearInterval(__animation_interval); // stop the animation
            elem.attribute( 'src', elem.attribute('src').replace(rWhichImage, '$1mqdefault$3') ); // reset the thumbnail
        }
    }

    function trackMouse(event) {
        x = event.pageX - window.pageXOffset;
        y = event.pageY - window.pageYOffset;
    }

    function GM_config_open() {
        GM_config.open();
    }

    // Make sure the page is not in a frame
    if (window.self !== window.top) { return; }

    // String.prototype.getMatch by JoeSimmons
    // e.g., 'foobar'.getMatch(/foo(bar)/, 1) ==> 'bar'
    Object.defineProperty(String.prototype, 'getMatch', {
        value : function (regex, index) {
            var match = this.match(regex) || ['', '', '', '', '', '', '', '', '', ''];

            if (typeof index === 'number' && index > -1) {
                return match[index];
            }

            return match[0];
        }
    });

    GM_config.init('YouTube Animated Thumbnails Options', {
        hoverimages : {
            section : ['Hover Options'],
            label : 'Enable Hover Images?',
            type : 'checkbox',
            'default' : true,
            title : 'Hovering over a thumbnail shows 3 preview images.'
        },
        thumbSize : {
            label : 'Hover Thumbnail Size',
            type : 'select',
            options : {
                '90' : 'Small',
                '216' : 'Medium',
                '432' : 'Large'
            },
            'default' : '432',
            'title' : 'Choose the size of the hovering thumbnails'
        },
        animatedthumbnails : {
            section : ['Animated Thumbs Options'],
            label : 'Enable Animated Thumbnails?',
            type : 'checkbox',
            'default' : true,
            title : 'Thumbnails cycle through the 3 images while hovering.'
        },
        animationspeed : {
            label : 'Animation (Cycle) Speed',
            type : 'select',
            options : {
                '1200' : 'Super Slow',
                '800' : 'Slow',
                '600' : 'Medium',
                '400' : 'Fast',
                '200' : 'Super Fast'
            },
            'default' : '600',
            title : 'Set the speed of the cycled images'
        }
    });

    JSL.runAt('interactive', function () {
        isHoverEnabled = GM_config.get('hoverimages') === true;
        thumbSize = GM_config.get('thumbSize');
        isAnimationEnabled = GM_config.get('animatedthumbnails') === true;
        animationSpeed = parseInt(GM_config.get('animationspeed'), 10);

        JSL.addStyle('' +
            '#hover_img { ' +
                'position: fixed; ' +
                'z-index: 999999999999; ' +
                'background: #000000; ' +
                'border: 1px solid #000000; ' +
                'outline: 1px solid #CCCCCC; ' +
            '}' +
            '#hover_img img { ' +
                'max-height: ' + thumbSize + 'px; ' +
            '}' +

            // fix for images showing under the youtube player
            '#hover_img img { ' +
                'background-color: #000000; ' +
            '}' +
            '#yt_at_ops { ' +
                'display: inline-block; ' +
                'padding: 6px; ' +
            '}' +
            '#GM_config { ' +
                'z-index: 999999999999 !important; ' + // 999,999,999,999
            '}' +
        '');

        JSL(document.body).append('' +
            '<div id="hover_img" style="display: none;">' +
                '<img src="" alt="" id="hover_img_1" />' +
                '<img src="" alt="" id="hover_img_2" />' +
                '<img src="" alt="" id="hover_img_3" />' +
            '</div>' +
        '');
        
        // Add a user script command for the options menu
        if (typeof GM_registerMenuCommand === 'function') {
            GM_registerMenuCommand('YouTube Animated Thumbnails Options', GM_config.open);
        }

        // Add an options button to the page
        JSL('#masthead-expanded-menu-list, #footer-main').append('' +
            '<li id="yt_at_ops" class="masthead-expanded-menu-item">' +
                '<a href="javascript: void(0);" class="yt-uix-sessionlink">Animated Thumbnails Options</a>' +
            '</li>' +
        '');
        JSL('#yt_at_ops a').addEvent('click', GM_config.open);

        if (isHoverEnabled) {
            JSL.addEvent(window, 'mousemove', trackMouse);
            JSL.addEvent(window, 'click', preview.hide);
        }
        JSL.addEvent(window, 'mouseover', show);
        JSL.addEvent(window, 'mouseout', hide);
    });

})();