TweetDeck Image Assistant

Download/Share Images Faster

目前為 2017-05-20 提交的版本,檢視 最新版本

// ==UserScript==
// @name         TweetDeck Image Assistant
// @namespace    http://ejew.in/
// @version      0.9
// @description  Download/Share Images Faster
// @author       EntranceJew
// @match        https://tweetdeck.twitter.com/*
// @require      https://cdn.rawgit.com/eligrey/FileSaver.js/5ed507ef8aa53d8ecfea96d96bc7214cd2476fd2/FileSaver.min.js
// @require      https://cdn.rawgit.com/kamranahmedse/jquery-toast-plugin/1105577ed71ef368f8aa3d96295857643dca43d7/dist/jquery.toast.min.js
// @noframes
// @resource    toastCSS https://cdn.rawgit.com/kamranahmedse/jquery-toast-plugin/1105577ed71ef368f8aa3d96295857643dca43d7/dist/jquery.toast.min.css
// @grant       GM_addStyle
// @grant       GM_getResourceText
// ==/UserScript==
/*
0.9 - toast notifications, better clipboard access methods, better image sources, ctrl+click like/rt/download to follow from column owner, fixed errors in previewer
0.8 - added t.co link unmasking
0.7 - apparently getting gif sources works most reliably inside callbacks
0.6 - hotfix to prevent redundant page reloading with stream-media seek methods
0.5 - video links no longer destroy links, ctrl+click the timestamp to copy the tweet link, ctrl+click the link icon to prepare multi-image tweets for discord
0.4 - changed download icon, added copy links button, videos now don't flash their preview, videos no longer close your draft tweets panel
0.3 - gif support wasn't that hard
0.2 - removed debug prints, updated mimes, added video download link, instant-spice now grabs videos
0.1 - initial version
*/

(function() {
    'use strict';

    GM_addStyle( GM_getResourceText("toastCSS") );

    var toast_prototype = {
        text: "Don't forget to star the repository if you like it.", // Text that is to be shown in the toast
        heading: 'Note', // Optional heading to be shown on the toast
        icon: 'success', // Type of toast icon
        showHideTransition: 'slide', // fade, slide or plain
        allowToastClose: true, // Boolean value true or false
        hideAfter: 1000, // false to make it sticky or number representing the miliseconds as time after which toast needs to be hidden
        stack: 32, // false if there should be only one toast at a time or a number representing the maximum number of toasts to be shown at a time
        position: 'bottom-left', // bottom-left or bottom-right or bottom-center or top-left or top-right or top-center or mid-center or an object representing the left, right, top, bottom values



        textAlign: 'left',  // Text alignment i.e. left, right or center
        loader: true,  // Whether to show loader or not. True by default
        loaderBg: '#9EC600',  // Background color of the toast loader
        beforeShow: function () {}, // will be triggered before the toast is shown
        afterShown: function () {}, // will be triggered after the toat has been shown
        beforeHide: function () {}, // will be triggered before the toast gets hidden
        afterHidden: function () {}  // will be triggered after the toast has been hidden
    };

    function toast( heading, text, icon ){
        return $.toast(jQuery.extend(true, toast_prototype, {
            heading: heading,
            text: text,
            icon: icon
        }));
    }

    var toolbar_size = 6;
    var tool_icon_width = (1 / toolbar_size) * 100;

    GM_addStyle( ".tweet-detail-action-item, .without-tweet-drag-handles .tweet-detail-action-item { width: " + tool_icon_width + "% !important; }" );

    var tool_icon = '<li class="tweet-action-item pull-left margin-r--13 margin-l--1">';
    tool_icon += '<a class="js-show-tip tweet-action position-rel" href="#" rel="download" title="" data-original-title="Download">';
    tool_icon += '<i class="icon icon-attachment icon-attachment-toggle txt-center"></i> <span class="is-vishidden"> Download </span>';
    tool_icon += '</a> </li>';

    var link_icon = '<li class="tweet-action-item clipboard pull-left margin-r--13 margin-l--1">';
    link_icon += '<a class="js-show-tip tweet-action position-rel" href="#" rel="hotlink" title="" data-original-title="Hotlink">';
    link_icon += '<i class="icon icon-link icon-link-toggle txt-center"></i> <span class="is-vishidden"> Hotlink </span>';
    link_icon += '</a> </li>';

    var mime_db = {
        jpeg: "image/jpeg",
        jpg: "image/jpeg",
        gif: "image/gif",
        webp: "image/webp",
        mp4: "video/mp4",
        m3u8: "application/x-mpegURL",
        undefined: "text/plain"
    };

    function clipboard_data( text ){
        var tc = $('.compose-text-container .js-compose-text');
        var orig = tc.val();
        var active = document.activeElement;
        tc.val( text );
        tc[0].focus();
        tc[0].setSelectionRange( 0, text.length );
        document.execCommand("copy");
        tc.val( orig );
        active.focus();
        toast("Copied <em>" + text.split(/\r*\n/).length + "</em> Lines!", text, "info");
    }

    // http://stackoverflow.com/a/2091331
    function getQueryVariable(str, variable) {
        var query = str.substring(1);
        var vars = query.split('&');
        for (var i = 0; i < vars.length; i++) {
            var pair = vars[i].split('=');
            if (decodeURIComponent(pair[0]) == variable) {
                return decodeURIComponent(pair[1]);
            }
        }
        console.log('Query variable %s not found', variable);
    }

    function detect_mime(url){
        return mime_db[ /(?:\.([^.]+))?$/.exec(url)[1] ];
    }

    function get_img_data( url, on_load ) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.responseType = "blob";
        xhr.onload = on_load;
        xhr.send();
    }

    function download_now( url ){
        if( url.length ){
            get_img_data( url, function( e ){
                var img_name = url.substring( url.lastIndexOf('/')+1 );
                var the_blob = new Blob([this.response], {type: detect_mime(url)});
                var save_file_name = img_name.replace(/:orig$/, "");
                saveAs( the_blob, save_file_name );
                if( save_file_name.endsWith('mp4') ){
                    toast("Downloaded <em>1</em> Video!", save_file_name, "info");
                }
            });
        }
    }

    function nice_url( url, replacement ){
        if( !replacement || replacement !== "" ){
            replacement = ":orig";
        }
        var bg = url;
        bg = bg.replace('url(','').replace(')','').replace(/\"/gi, "");
        bg = bg.replace(/:thumb$/, replacement);
        bg = bg.replace(/:small$/, replacement);
        bg = bg.replace(/:medium$/, replacement);
        bg = bg.replace(/:large$/, replacement);
        return bg;
    }

    // danger: this could potentially lockup if the element isn't guaranteed to appear.
    function lock_find( selector, context ){
        var results = $( selector, context );
        while( !results.length ){
            results = $( selector, context );
        }
        return results;
    }

    // we have to do literal jungle japes in order to get to the follow button from here
    // strap in
    function follow_tweet( selector ){
        selector.find('ul.tweet-actions i.icon-more').click();
        var column_owner = selector.parents('.column-panel').find('h1.column-title span.attribution').text();
        var more = lock_find('.js-dropdown.dropdown-menu a[data-action="followOrUnfollow"]', selector);
        more.parent('li.is-selectable').addClass('is-selected');
        more.click();
        var follow_container = lock_find('div.js-modal-panel');
        var column_owner_follow = null;

        // entrancejew only follows from his third account
        // entrancejew also refuses to implement settings yet
        if( column_owner == "@EntranceJew" ){
            column_owner_follow = lock_find('div.js-follow-from:nth-child(3)', follow_container);
        } else {
            follow_container.find('.js-from-username').each(function(){
                var this_name = $( this ).text();
                if( this_name.includes( column_owner ) ){
                    column_owner_follow = $( this ).parent('.js-follow-from');
                }
            });
        }

        var follow_button = null;
        var follow_seeker = setInterval(function(){
            follow_button = column_owner_follow.find('.js-action-follow[class*=" s-"]');
            if( follow_button.length ){
                if( follow_button.hasClass('s-not-following') ){
                    var user_to_follow = $('.mdl-header-title a[rel="user"]').text();
                    follow_button.find('button').click();
                    toast("Followed <em>1</em> Users!", user_to_follow, "info");
                } else if( !follow_button.hasClass('s-following') ){
                    var attrs = follow_button.attr('class');
                    toast("I'm Confused!", "What is a <em>" + attrs + "</em>?", "error");
                }
                follow_container.find('.icon-close').click();
                clearInterval(follow_seeker);
            }
        },50);
    }

    setInterval(function(){
        $('.stream-item:not([data-ejew])').each(function(){
            var grand_dad = $( this );

            /*
            // for appending to the dropdown menu if we wanted that
            var tool_bar = grand_dad.find('.js-dropdown-content > ul');
            tool_bar.prepend('<li class="is-selectable"><a href="#" data-action="ejew">Spice it up</a></li>');
            */

            // find all the images and store their links in data
            var sources = [];
            var media_type = 'idk';
            if( grand_dad.find('.is-video').length ){
                media_type = 'video';
                sources.push( function( e ){
                    var anchor = grand_dad.find('.js-media-image-link');
                    var o_target = anchor.attr('target');
                    var o_src = anchor.attr('src');
                    anchor.attr('target', '');
                    anchor.attr('src', '#');
                    anchor.click();

                    var embeds = lock_find('.js-embeditem');

                    var vid_url = '';
                    embeds.each(function(){
                        var iframe_src = $( this ).find( 'iframe' ).attr('src');
                        if( iframe_src ){
                            vid_url = getQueryVariable( iframe_src, 'video_url' );
                        }
                        $('.mdl-dismiss .icon-close').click();
                    });

                    anchor.attr('target', o_target);
                    anchor.attr('src', o_src);

                    if( vid_url.length ){
                        return vid_url;
                    }
                });
            } else if( grand_dad.find('.is-gif').length ){
                media_type = 'gif';
                sources.push( function(){
                    return grand_dad.find('video.js-media-gif').attr('src');
                });
            } else {
                grand_dad.find('.js-media-image-link, .js-media .media-image').each( function(i, el){
                    sources.push( nice_url( $( el ).css('background-image') ) );
                });
                if( sources.length ){
                    media_type = 'image';
                }
            }
            var orig_link = grand_dad.find("a.txt-small.no-wrap[rel=\"url\"]");
            orig_link.on('click', function(e){
                if( e.ctrlKey ){
                    e.preventDefault();
                    clipboard_data( $( this ).attr("href") );
                }
            });
            grand_dad.data('ejew-sources', sources);
            grand_dad.data('direct-url', orig_link.attr("href"));

            // enhance stock buttons with auto-follow
            grand_dad.find('.icon-retweet').on('click', function(e){
                if( e.ctrlKey ){
                    follow_tweet( grand_dad );
                }
            });
            grand_dad.find('.icon-favorite').on('click', function(e){
                if( e.ctrlKey ){
                    follow_tweet( grand_dad );
                }
            });

            // add more buttons
            var new_link = $( link_icon );
            new_link.on('click', function(e){
                var sources = grand_dad.data('ejew-sources');
                for( var i = 0; i < sources.length; i++ ){
                    if( typeof( sources[i] ) != "string" ){
                        sources[i] = sources[i]( this );
                    }
                }

                var the_url = grand_dad.data('direct-url');
                if( e.ctrlKey && sources.length > 1){
                    sources[0] = the_url;
                }

                if( sources.length ){
                    clipboard_data( sources.join("\n") );
                } else {
                    clipboard_data( the_url );
                }
            });

            // make an instance of the toolbar button
            var new_tool = $( tool_icon );
            new_tool.on('click', function(e){
                var sources = grand_dad.data('ejew-sources');
                for( var i = 0; i < sources.length; i++ ){
                    var source = sources[i];
                    if( typeof( source ) != "string" ){
                        source = source( this );
                    }
                    download_now( source );
                }

                if( sources.length > 1 || !sources[0].endsWith("mp4") ){
                    toast("Downloaded <em>" + sources.length + "</em> Images!", sources.join("\n"), "info");
                }

                if( e.ctrlKey ){
                    follow_tweet( grand_dad );
                }
            });

            // attach
            var attachment_point = grand_dad.find('ul.tweet-actions > li:nth-last-child(2)');
            attachment_point.before( new_tool );
            attachment_point.before( new_link );

            // prevent loading up this element again
            grand_dad.attr('data-ejew', 'in');
        });

        // unmask t.co links
        var links_to_unmask = $('a[href^="https://t.co/"][data-full-url]');
        links_to_unmask.each(function(){
            $( this ).attr('href', $( this ).data('full-url') );
        });
        if( links_to_unmask.length > 0 ){
            toast("Unmasked <em>" + links_to_unmask.length + "</em> Links!", "<em>That's a lot!</em>", "info");
        }

        // make it so that you can copy image source from previews
        $('img.media-img:not([data-ejew])').each(function(){
            $( this ).attr('src', nice_url( $( this ).attr('src'), "" ) );
            $( this ).attr('boners', 'farts');
            $( this ).attr('data-ejew', 'in');
        });

        // provide a download source link in zoomable previews for videos
        $('.js-embeditem:not([data-ejew])').each(function(){
            var iframe_src = $( this ).find( 'iframe' ).attr('src');
            if( iframe_src ){
                var vid_url = getQueryVariable( iframe_src, 'video_url' );
                var dl_link = $( '<a href="#">Download Source</a>' );
                dl_link.on('click', function(){
                    download_now( vid_url );
                });
                $(".med-origlink").after( dl_link );
            }
            $( this ).attr('data-ejew', 'in');
        });
    }, 300);
})();