TweetDeck Image Assistant

Download/Share Images Faster

目前为 2017-05-20 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name TweetDeck Image Assistant
  3. // @namespace http://ejew.in/
  4. // @version 1.0
  5. // @description Download/Share Images Faster
  6. // @author EntranceJew
  7. // @match https://tweetdeck.twitter.com/*
  8. // @require https://cdn.rawgit.com/eligrey/FileSaver.js/5ed507ef8aa53d8ecfea96d96bc7214cd2476fd2/FileSaver.min.js
  9. // @require https://cdn.rawgit.com/kamranahmedse/jquery-toast-plugin/1105577ed71ef368f8aa3d96295857643dca43d7/dist/jquery.toast.min.js
  10. // @noframes
  11. // @resource toastCSS https://cdn.rawgit.com/kamranahmedse/jquery-toast-plugin/1105577ed71ef368f8aa3d96295857643dca43d7/dist/jquery.toast.min.css
  12. // @grant GM_addStyle
  13. // @grant GM_getResourceText
  14. // ==/UserScript==
  15. /*
  16. 1.0 - fixed issue with logic in setting default image resolution
  17. 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
  18. 0.8 - added t.co link unmasking
  19. 0.7 - apparently getting gif sources works most reliably inside callbacks
  20. 0.6 - hotfix to prevent redundant page reloading with stream-media seek methods
  21. 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
  22. 0.4 - changed download icon, added copy links button, videos now don't flash their preview, videos no longer close your draft tweets panel
  23. 0.3 - gif support wasn't that hard
  24. 0.2 - removed debug prints, updated mimes, added video download link, instant-spice now grabs videos
  25. 0.1 - initial version
  26. */
  27.  
  28. (function() {
  29. 'use strict';
  30.  
  31. GM_addStyle( GM_getResourceText("toastCSS") );
  32.  
  33. var toast_prototype = {
  34. text: "Don't forget to star the repository if you like it.", // Text that is to be shown in the toast
  35. heading: 'Note', // Optional heading to be shown on the toast
  36. icon: 'success', // Type of toast icon
  37. showHideTransition: 'slide', // fade, slide or plain
  38. allowToastClose: true, // Boolean value true or false
  39. hideAfter: 1000, // false to make it sticky or number representing the miliseconds as time after which toast needs to be hidden
  40. 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
  41. 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
  42.  
  43.  
  44.  
  45. textAlign: 'left', // Text alignment i.e. left, right or center
  46. loader: true, // Whether to show loader or not. True by default
  47. loaderBg: '#9EC600', // Background color of the toast loader
  48. beforeShow: function () {}, // will be triggered before the toast is shown
  49. afterShown: function () {}, // will be triggered after the toat has been shown
  50. beforeHide: function () {}, // will be triggered before the toast gets hidden
  51. afterHidden: function () {} // will be triggered after the toast has been hidden
  52. };
  53.  
  54. function toast( heading, text, icon ){
  55. return $.toast(jQuery.extend(true, toast_prototype, {
  56. heading: heading,
  57. text: text,
  58. icon: icon
  59. }));
  60. }
  61.  
  62. var toolbar_size = 6;
  63. var tool_icon_width = (1 / toolbar_size) * 100;
  64.  
  65. GM_addStyle( ".tweet-detail-action-item, .without-tweet-drag-handles .tweet-detail-action-item { width: " + tool_icon_width + "% !important; }" );
  66.  
  67. var tool_icon = '<li class="tweet-action-item pull-left margin-r--13 margin-l--1">';
  68. tool_icon += '<a class="js-show-tip tweet-action position-rel" href="#" rel="download" title="" data-original-title="Download">';
  69. tool_icon += '<i class="icon icon-attachment icon-attachment-toggle txt-center"></i> <span class="is-vishidden"> Download </span>';
  70. tool_icon += '</a> </li>';
  71.  
  72. var link_icon = '<li class="tweet-action-item clipboard pull-left margin-r--13 margin-l--1">';
  73. link_icon += '<a class="js-show-tip tweet-action position-rel" href="#" rel="hotlink" title="" data-original-title="Hotlink">';
  74. link_icon += '<i class="icon icon-link icon-link-toggle txt-center"></i> <span class="is-vishidden"> Hotlink </span>';
  75. link_icon += '</a> </li>';
  76.  
  77. var mime_db = {
  78. jpeg: "image/jpeg",
  79. jpg: "image/jpeg",
  80. gif: "image/gif",
  81. webp: "image/webp",
  82. mp4: "video/mp4",
  83. m3u8: "application/x-mpegURL",
  84. undefined: "text/plain"
  85. };
  86.  
  87. function clipboard_data( text ){
  88. var tc = $('.compose-text-container .js-compose-text');
  89. var orig = tc.val();
  90. var active = document.activeElement;
  91. tc.val( text );
  92. tc[0].focus();
  93. tc[0].setSelectionRange( 0, text.length );
  94. document.execCommand("copy");
  95. tc.val( orig );
  96. active.focus();
  97. toast("Copied <em>" + text.split(/\r*\n/).length + "</em> Lines!", text, "info");
  98. }
  99.  
  100. // http://stackoverflow.com/a/2091331
  101. function getQueryVariable(str, variable) {
  102. var query = str.substring(1);
  103. var vars = query.split('&');
  104. for (var i = 0; i < vars.length; i++) {
  105. var pair = vars[i].split('=');
  106. if (decodeURIComponent(pair[0]) == variable) {
  107. return decodeURIComponent(pair[1]);
  108. }
  109. }
  110. console.log('Query variable %s not found', variable);
  111. }
  112.  
  113. function detect_mime(url){
  114. return mime_db[ /(?:\.([^.]+))?$/.exec(url)[1] ];
  115. }
  116.  
  117. function get_img_data( url, on_load ) {
  118. var xhr = new XMLHttpRequest();
  119. xhr.open("GET", url);
  120. xhr.responseType = "blob";
  121. xhr.onload = on_load;
  122. xhr.send();
  123. }
  124.  
  125. function download_now( url ){
  126. if( url.length ){
  127. get_img_data( url, function( e ){
  128. var img_name = url.substring( url.lastIndexOf('/')+1 );
  129. var the_blob = new Blob([this.response], {type: detect_mime(url)});
  130. var save_file_name = img_name.replace(/:orig$/, "");
  131. saveAs( the_blob, save_file_name );
  132. if( save_file_name.endsWith('mp4') ){
  133. toast("Downloaded <em>1</em> Video!", save_file_name, "info");
  134. }
  135. });
  136. }
  137. }
  138.  
  139. function nice_url( url, replacement ){
  140. if( replacement === "" ){
  141. // whatever
  142. } else if( !replacement ){
  143. replacement = ":orig";
  144. }
  145. var bg = url;
  146. bg = bg.replace('url(','').replace(')','').replace(/\"/gi, "");
  147. bg = bg.replace(/:thumb$/, replacement);
  148. bg = bg.replace(/:small$/, replacement);
  149. bg = bg.replace(/:medium$/, replacement);
  150. bg = bg.replace(/:large$/, replacement);
  151. return bg;
  152. }
  153.  
  154. // danger: this could potentially lockup if the element isn't guaranteed to appear.
  155. function lock_find( selector, context ){
  156. var results = $( selector, context );
  157. while( !results.length ){
  158. results = $( selector, context );
  159. }
  160. return results;
  161. }
  162.  
  163. // we have to do literal jungle japes in order to get to the follow button from here
  164. // strap in
  165. function follow_tweet( selector ){
  166. selector.find('ul.tweet-actions i.icon-more').click();
  167. var column_owner = selector.parents('.column-panel').find('h1.column-title span.attribution').text();
  168. var more = lock_find('.js-dropdown.dropdown-menu a[data-action="followOrUnfollow"]', selector);
  169. more.parent('li.is-selectable').addClass('is-selected');
  170. more.click();
  171. var follow_container = lock_find('div.js-modal-panel');
  172. var column_owner_follow = null;
  173.  
  174. // entrancejew only follows from his third account
  175. // entrancejew also refuses to implement settings yet
  176. if( column_owner == "@EntranceJew" ){
  177. column_owner_follow = lock_find('div.js-follow-from:nth-child(3)', follow_container);
  178. } else {
  179. follow_container.find('.js-from-username').each(function(){
  180. var this_name = $( this ).text();
  181. if( this_name.includes( column_owner ) ){
  182. column_owner_follow = $( this ).parent('.js-follow-from');
  183. }
  184. });
  185. }
  186.  
  187. var follow_button = null;
  188. var follow_seeker = setInterval(function(){
  189. follow_button = column_owner_follow.find('.js-action-follow[class*=" s-"]');
  190. if( follow_button.length ){
  191. if( follow_button.hasClass('s-not-following') ){
  192. var user_to_follow = $('.mdl-header-title a[rel="user"]').text();
  193. follow_button.find('button').click();
  194. toast("Followed <em>1</em> Users!", user_to_follow, "info");
  195. } else if( !follow_button.hasClass('s-following') ){
  196. var attrs = follow_button.attr('class');
  197. toast("I'm Confused!", "What is a <em>" + attrs + "</em>?", "error");
  198. }
  199. follow_container.find('.icon-close').click();
  200. clearInterval(follow_seeker);
  201. }
  202. },50);
  203. }
  204.  
  205. setInterval(function(){
  206. $('.stream-item:not([data-ejew])').each(function(){
  207. var grand_dad = $( this );
  208.  
  209. /*
  210. // for appending to the dropdown menu if we wanted that
  211. var tool_bar = grand_dad.find('.js-dropdown-content > ul');
  212. tool_bar.prepend('<li class="is-selectable"><a href="#" data-action="ejew">Spice it up</a></li>');
  213. */
  214.  
  215. // find all the images and store their links in data
  216. var sources = [];
  217. var media_type = 'idk';
  218. if( grand_dad.find('.is-video').length ){
  219. media_type = 'video';
  220. sources.push( function( e ){
  221. var anchor = grand_dad.find('.js-media-image-link');
  222. var o_target = anchor.attr('target');
  223. var o_src = anchor.attr('src');
  224. anchor.attr('target', '');
  225. anchor.attr('src', '#');
  226. anchor.click();
  227.  
  228. var embeds = lock_find('.js-embeditem');
  229.  
  230. var vid_url = '';
  231. embeds.each(function(){
  232. var iframe_src = $( this ).find( 'iframe' ).attr('src');
  233. if( iframe_src ){
  234. vid_url = getQueryVariable( iframe_src, 'video_url' );
  235. }
  236. $('.mdl-dismiss .icon-close').click();
  237. });
  238.  
  239. anchor.attr('target', o_target);
  240. anchor.attr('src', o_src);
  241.  
  242. if( vid_url.length ){
  243. return vid_url;
  244. }
  245. });
  246. } else if( grand_dad.find('.is-gif').length ){
  247. media_type = 'gif';
  248. sources.push( function(){
  249. return grand_dad.find('video.js-media-gif').attr('src');
  250. });
  251. } else {
  252. grand_dad.find('.js-media-image-link, .js-media .media-image').each( function(i, el){
  253. sources.push( nice_url( $( el ).css('background-image') ) );
  254. });
  255. if( sources.length ){
  256. media_type = 'image';
  257. }
  258. }
  259. var orig_link = grand_dad.find("a.txt-small.no-wrap[rel=\"url\"]");
  260. orig_link.on('click', function(e){
  261. if( e.ctrlKey ){
  262. e.preventDefault();
  263. clipboard_data( $( this ).attr("href") );
  264. }
  265. });
  266. grand_dad.data('ejew-sources', sources);
  267. grand_dad.data('direct-url', orig_link.attr("href"));
  268.  
  269. // enhance stock buttons with auto-follow
  270. grand_dad.find('.icon-retweet').on('click', function(e){
  271. if( e.ctrlKey ){
  272. follow_tweet( grand_dad );
  273. }
  274. });
  275. grand_dad.find('.icon-favorite').on('click', function(e){
  276. if( e.ctrlKey ){
  277. follow_tweet( grand_dad );
  278. }
  279. });
  280.  
  281. // add more buttons
  282. var new_link = $( link_icon );
  283. new_link.on('click', function(e){
  284. var sources = grand_dad.data('ejew-sources');
  285. for( var i = 0; i < sources.length; i++ ){
  286. if( typeof( sources[i] ) != "string" ){
  287. sources[i] = sources[i]( this );
  288. }
  289. }
  290.  
  291. var the_url = grand_dad.data('direct-url');
  292. if( e.ctrlKey && sources.length > 1){
  293. sources[0] = the_url;
  294. }
  295.  
  296. if( sources.length ){
  297. clipboard_data( sources.join("\n") );
  298. } else {
  299. clipboard_data( the_url );
  300. }
  301. });
  302.  
  303. // make an instance of the toolbar button
  304. var new_tool = $( tool_icon );
  305. new_tool.on('click', function(e){
  306. var sources = grand_dad.data('ejew-sources');
  307. for( var i = 0; i < sources.length; i++ ){
  308. var source = sources[i];
  309. if( typeof( source ) != "string" ){
  310. source = source( this );
  311. }
  312. download_now( source );
  313. }
  314.  
  315. if( sources.length > 1 || !sources[0].endsWith("mp4") ){
  316. toast("Downloaded <em>" + sources.length + "</em> Images!", sources.join("\n"), "info");
  317. }
  318.  
  319. if( e.ctrlKey ){
  320. follow_tweet( grand_dad );
  321. }
  322. });
  323.  
  324. // attach
  325. var attachment_point = grand_dad.find('ul.tweet-actions > li:nth-last-child(2)');
  326. attachment_point.before( new_tool );
  327. attachment_point.before( new_link );
  328.  
  329. // prevent loading up this element again
  330. grand_dad.attr('data-ejew', 'in');
  331. });
  332.  
  333. // unmask t.co links
  334. var links_to_unmask = $('a[href^="https://t.co/"][data-full-url]');
  335. links_to_unmask.each(function(){
  336. $( this ).attr('href', $( this ).data('full-url') );
  337. });
  338. if( links_to_unmask.length > 0 ){
  339. toast("Unmasked <em>" + links_to_unmask.length + "</em> Links!", "<em>That's a lot!</em>", "info");
  340. }
  341.  
  342. // make it so that you can copy image source from previews
  343. $('img.media-img:not([data-ejew])').each(function(){
  344. $( this ).attr('src', nice_url( $( this ).attr('src'), "" ) );
  345. $( this ).attr('data-ejew', 'in');
  346. });
  347.  
  348. // provide a download source link in zoomable previews for videos
  349. $('.js-embeditem:not([data-ejew])').each(function(){
  350. var iframe_src = $( this ).find( 'iframe' ).attr('src');
  351. if( iframe_src ){
  352. var vid_url = getQueryVariable( iframe_src, 'video_url' );
  353. var dl_link = $( '<a href="#">Download Source</a>' );
  354. dl_link.on('click', function(){
  355. download_now( vid_url );
  356. });
  357. $(".med-origlink").after( dl_link );
  358. }
  359. $( this ).attr('data-ejew', 'in');
  360. });
  361. }, 300);
  362. })();