- // ==UserScript==
- // @name TweetDeck Image Assistant
- // @namespace http://ejew.in/
- // @version 1.1
- // @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==
- /*
- 1.1 - performance fixes, ctrl+hotlink doesn't destroy the first image's media link anymore, better button insertion, auto-follow lockfind updated, buttons don't appear in DMs anymore
- 1.0 - fixed issue with logic in setting default image resolution
- 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
- */
-
- /* # todo:
- * move code into bettertweetdeck
- * make it so that toasts batch by purpose, extending notification times
- * better support for tweet-details view (sometimes causing lockouts?)
- * better support for modal big preview tweets
- * stack quote-tweets
- * do not count quoted media as a media source
- * context menu item insertion
- * context sensitive actions
- * custom action icons / styles
- * update action button tooltips when modifier keys are pressed
- * action to follow all explicitly mentioned users in a tweet (not in the conversation)
- * better tweetdeck integration
- */
-
- (function() {
- 'use strict';
-
- GM_addStyle( GM_getResourceText("toastCSS") );
-
- var settings = {
- toast: {
- hideAfter: 1000,
- }
- };
-
- 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: settings.toast.hideAfter, // 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;
- }
-
- .jq-toast-single {
- word-break: break-all;
- }
- `);
-
- function add_action_style( name, details ){
- var style = `
- .tweet-action:hover .icon-` + details.icon_name +`,
- .tweet-detail-action:hover .icon-` + details.icon_name +`,
- .dm-action:hover .icon-` + details.icon_name +`,
- .tweet-action:focus .icon-` + details.icon_name +`,
- .tweet-detail-action:focus .icon-` + details.icon_name +`,
- .dm-action:focus .icon-` + details.icon_name +`,
- .tweet-action:active .icon-` + details.icon_name +`,
- .tweet-detail-action:active .icon-` + details.icon_name +`,
- .dm-action:active .icon-` + details.icon_name +`,
- .tweet-action.is-selected .icon-` + details.icon_name +`,
- .is-selected.tweet-detail-action .icon-` + details.icon_name +`,
- .is-selected.dm-action .icon-` + details.icon_name +` {
- color: ` + details.color + `;
- }`;
- GM_addStyle( style );
- }
-
- //.icon-link:before{ content: "\f088"; }
- var action_properties = {
- 'download': {
- 'name': 'Download',
- 'tool_tip': 'Download',
- 'rel': 'download',
- 'icon_name': 'attachment',
- 'before_content': "\f088",
- 'color': '#D400E0',
- 'on': {
- 'click': [
- ['download_url', ['media_link']],
- 'media_next',
- ['download_url', ['media_link']],
- 'media_next',
- ['download_url', ['media_link']],
- 'media_next',
- ['download_url', ['media_link']],
- 'media_next',
- ],
- 'ctrl+click': [
- 'default',
- 'follow'
- ]
- },
- },
- 'hotlink': {
- 'name': 'Hotlink',
- 'tool_tip': 'Hotlink',
- 'rel': 'hotlink',
- 'icon_name': 'link',
- 'before_content': "\f098",
- 'color': '#00498A',
- 'on': {
- 'click': [
- 'clipboard_open',
- ['clipboard_push', ['media_link']],
- 'media_next',
- ['clipboard_push', ['media_link']],
- 'media_next',
- ['clipboard_push', ['media_link']],
- 'media_next',
- ['clipboard_push', ['media_link']],
- 'media_next',
- 'clipboard_copy'
- ],
- 'ctrl+click': [
- 'clipboard_open',
- ['clipboard_push', ['tweet_link']],
- 'media_next',
- 'media_next',
- ['clipboard_push', ['media_link']],
- 'media_next',
- ['clipboard_push', ['media_link']],
- 'media_next',
- ['clipboard_push', ['media_link']],
- 'media_next',
- 'clipboard_copy'
- ],
- },
- },
- };
-
- for (var name in action_properties) {
- add_action_style(name, action_properties[name]);
- }
-
- var possible_locations = {
- 'single': {
- 'context': '.tweet-detail-wrapper > article.stream-item',
- //'toolbar': 'tweet-detail-actions',
- //'item': 'tweet-detail-action-item',
- 'filters': [
- ":not('.feature-customtimelines')",
- ":last"
- ],
- 'images': [
- 'img.media-img',
- ],
- },
- 'normal': {
- 'context': 'article.stream-item',
- //'toolbar': 'tweet-actions',
- //'item': 'tweet-action-item',
- 'filters': [
- ":not('.feature-customtimelines')",
- ":last"
- ]
- },
- 'modal': {
- 'context': 'div#open-modal div.item-box',
- //'toolbar':
- //
- 'filters': []
- }
- };
-
- function add_to_toolbar( context = 'html', action = action_properties.download, location = possible_locations.normal ){
- // var slugged = action.name.replace(/\s/g, '').toLowerCase();
-
- // clone reply
- var x = $( context ).find( "ul[class*=-actions] > li:first" ).clone();
-
- // set the attributes
- x.find('a')
- .removeClass('js-reply-action')
- .addClass('js-'+action.rel+'-action')
- .addClass('js-show-tip')
- .attr('rel', action.rel )
- .attr('title', action.tool_tip )
- .data('original-title', action.tool_tip );
-
- x.find('i')
- .removeClass('icon-reply')
- .addClass('icon-'+action.icon_name)
- .addClass('icon-'+action.icon_name+'-toggle');
-
- x.find('span.is-vishidden')
- .text( action.tool_tip );
-
- // plant it back at the end
- var z = $( context ).find( "ul[class*=-actions] > li" );
- location.filters.forEach(function(element){
- z = $( z ).filter( element );
- });
- z = $( z ).prev();
- $( x ).insertAfter( $(z) );
-
- return $( x );
- }
-
- 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.replace(/\r*\n/, "<br>"), "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 is_empty( str ){
- return !(str && typeof(str) == "string" && str.trim().length !== 0 && str !== "none");
- }
-
- function unique(array) {
- return $.grep(array, function(el, index) {
- return index === $.inArray(el, array);
- });
- }
-
- function unpack_sources( packed_sources, context ){
- var unpacked = [];
- for( var str in packed_sources ){
- var src = packed_sources[str];
- if( typeof(src) === "function" ){
- src = src( context );
- }
- if( !is_empty( src ) ){
- unpacked.push( nice_url( src ) );
- }
- }
- return unique(unpacked);
- }
-
- function download_now( url, prefix = "twitter_" ){
- 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, prefix + 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 === "" ){
- // whatever
- } else if( !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[class*=-actions] > li:not(.feature-customtimelines) i.icon-more" ).filter(":last").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-context');
- 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(){
- // process all toolbars and tweet locations
- for(var key in possible_locations){
- var location = possible_locations[key];
- $( location.context + ':not([data-ejew])').each(function(){
- var grand_dad = $( this );
-
- // 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 {
- var patterns = [
- '.js-media-image-link',
- 'a.med-link img.media-img',
- '.js-media .media-image'
- ];
- patterns.forEach(function( pattern ){
- grand_dad.find( pattern ).each( function(i, el){
- var src = $( el ).css('background-image');
- if( is_empty( src ) ){
- src = $( el ).attr('src');
- }
- if( !is_empty( src ) ){
- sources.push( src );
- }
- });
- });
- 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
- add_to_toolbar( grand_dad, action_properties.download ).on('click', function(e){
- var sauce = grand_dad.data('ejew-sources').slice();
- console.log( sauce );
- var sources = unpack_sources( sauce , this );
- console.log( sources );
-
- for( var i = 0; i < sources.length; i++ ){
- download_now( sources[i] );
- }
-
- if( sources.length && !sources[0].endsWith("mp4") ){
- toast("Downloaded <em>" + sources.length + "</em> Images!", sources.join("\n<br>"), "info");
- }
-
- if( e.ctrlKey ){
- follow_tweet( grand_dad );
- }
- });
- add_to_toolbar( grand_dad, action_properties.hotlink ).on('click', function(e){
- var sauce = grand_dad.data('ejew-sources').slice();
- console.log( sauce );
- var sources = unpack_sources( sauce , this );
- console.log( sources );
-
- var the_url = grand_dad.data('direct-url');
- if( e.ctrlKey && sources.length ){
- sources[0] = the_url;
- }
-
- if( sources.length ){
- clipboard_data( sources.join("\n") );
- } else {
- clipboard_data( the_url );
- }
- });
-
- // prevent loading up this element again
- grand_dad.attr('data-ejew', 'in');
- });
- }
-
- // 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('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');
- });
-
- // 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");
- }
- }, 300);
- })();