您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download/Share Images Faster
当前为
// ==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); })();