Tweetdeck Image Saver

Script to download tweet images in one click.

  1. // ==UserScript==
  2. // @name Tweetdeck Image Saver
  3. // @namespace https://twitter.com/aloneunix
  4. // @version 1.1
  5. // @description Script to download tweet images in one click.
  6. // @author aloneunix
  7. // @include https://tweetdeck.twitter.com/*
  8. // @grant GM_download
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. var TweetType = {
  13. Column: 0,
  14. Detail: 1,
  15. Modal: 2
  16. };
  17. function GetOrigUrl(url) {
  18. var u = new URL(url);
  19. u.searchParams = new URLSearchParams(url.search);
  20. u.searchParams.set("format", u.pathname.substring(u.pathname.lastIndexOf(".")+1)); // because sometimes png images have jpg format
  21. u.searchParams.set("name", "orig");
  22. return u
  23. }
  24. var TweetTools = (function () {
  25. function TweetTools(tweet, type) {
  26. var self = this;
  27. this.tweet = tweet;
  28. this.type = type;
  29. this.GatherData();
  30. var dlButton = this.CreateDLButton();
  31. dlButton.onclick = function () {
  32. var urls = self.GetImages();
  33. if (urls.length == 1) {
  34. var u = GetOrigUrl(urls[0]);
  35. GM_download({url: u.href, name: self.GetFilename(u)});
  36. } else {
  37. urls.forEach(function(url, i) {
  38. var u = GetOrigUrl(url);
  39. GM_download({url: u.href, name: self.GetFilename(u, i)});
  40. });
  41. }
  42. };
  43. }
  44. TweetTools.prototype.GetImages = function () {
  45. var urls = [];
  46. if (this.type == TweetType.Modal) {
  47. urls.push(this.tweet.querySelector(".js-media-image-link img").src);
  48. } else if (this.type == TweetType.Column || this.type == TweetType.Detail) {
  49. var imgLinks = this.tweet.getElementsByClassName("js-media-image-link");
  50. if (imgLinks.length == 1) {
  51. if (this.type == TweetType.Column)
  52. urls.push(imgLinks[0].style.backgroundImage.slice(4, -1).replace(/"/g, ""));
  53. if (this.type == TweetType.Detail)
  54. urls.push(imgLinks[0].firstElementChild.src);
  55. } else {
  56. Array.from(imgLinks).forEach(function(url) {
  57. urls.push(url.style.backgroundImage.slice(4, -1).replace(/"/g, ""));
  58. });
  59. }
  60. }
  61. return urls;
  62. };
  63. TweetTools.prototype.GatherData = function () {
  64. this.username = this.tweet.querySelector("span.username").innerHTML.slice(1);
  65. if (this.type == TweetType.Modal) {
  66. this.tweet_id = this.tweet.querySelector("time.tweet-timestamp a").href.split("status/")[1];
  67. } else {
  68. this.tweet_id = this.tweet.getAttribute("data-tweet-id");
  69. }
  70. var ts;
  71. if (this.type == TweetType.Detail) {
  72. ts = this.tweet.querySelector(".margin-tl a").text.split(" · ")[1];
  73. } else {
  74. ts = parseInt(this.tweet.getElementsByClassName("tweet-timestamp")[0].getAttribute("data-time"));
  75. }
  76. ts = new Date(ts);
  77. this.ts = ts.getFullYear() + '-' +
  78. ('0'+ (ts.getMonth()+1)).slice(-2) + '-' +
  79. ('0'+ ts.getDate()).slice(-2); // I don't want to live on this planet anymore
  80. };
  81. TweetTools.prototype.CreateDLButton = function () {
  82. var actions = this.tweet.querySelector("footer ul");
  83. var dlIcon = document.createElement("i");
  84. dlIcon.className = "icon icon-share txt-right";
  85. var iconContainer = document.createElement("a");
  86. iconContainer.href = "#";
  87. iconContainer.appendChild(dlIcon);
  88. var dlButton = document.createElement("li");
  89. if (this.type == TweetType.Detail) {
  90. iconContainer.className = "tweet-detail-action";
  91. dlButton.className = "tweet-detail-action-item position-rel";
  92. Array.from(actions.children).forEach(function (action) {
  93. action.style.width = "20%";
  94. });
  95. dlButton.style.width = "20%";
  96. } else {
  97. iconContainer.className = "tweet-action";
  98. dlButton.className = "tweet-action-item position-rel pull-left margin-r--13";
  99. }
  100. dlButton.appendChild(iconContainer);
  101. actions.appendChild(dlButton);
  102. this.tweet.className += " has-tis-tools";
  103. return dlButton;
  104. };
  105. TweetTools.prototype.GetFilename = function(url, index) {
  106. var extension = url.searchParams.get("format");
  107. var _index = index !== undefined ? "_"+(index+1) : "";
  108. return `${this.username}_${this.ts}_${this.tweet_id}${_index}.${extension}`;
  109. };
  110. return TweetTools;
  111. }());
  112. var columnDetailObserver = new MutationObserver(function (mutations) {
  113. mutations.forEach(function (mutation) {
  114. if (mutation.type == "childList") {
  115. var tweet = mutation.target.querySelector(".js-tweet-detail article:not(.has-tis-tools)");
  116. if (tweet)
  117. new TweetTools(tweet, TweetType.Detail);
  118. var replies = mutation.target.querySelectorAll(".tweet-detail-reply article:not(.has-tis-tools)");
  119. if (replies.length !== 0) {
  120. replies = Array.from(replies).filter(function(tweet) {
  121. return tweet.querySelector(".js-media .js-media-image-link") && !tweet.querySelector(".quoted-tweet, .video-overlay");
  122. });
  123. replies.forEach(function (reply) {
  124. new TweetTools(reply, TweetType.Column);
  125. });
  126. }
  127. }
  128. });
  129. });
  130. var columnObserver = new MutationObserver(function (mutations) {
  131. mutations.forEach(function (mutation) {
  132. if (mutation.type == "childList") {
  133. var tweets = document.querySelectorAll("article.stream-item.is-actionable:not(.has-tis-tools)");
  134. tweets = Array.from(tweets).filter(function(tweet) {
  135. return tweet.querySelector(".js-media .js-media-image-link") && !tweet.querySelector(".quoted-tweet, .video-overlay");
  136. });
  137. tweets.forEach(function(tweet) {
  138. new TweetTools(tweet, TweetType.Column);
  139. });
  140. }
  141. });
  142. });
  143. var modalObserver = new MutationObserver(function (mutations) {
  144. mutations.forEach(function (mutation) {
  145. if (mutation.type == "childList") {
  146. var modal = document.querySelector("#open-modal .js-modal-panel");
  147. if (!modal) {
  148. return;
  149. }
  150. var tweet = modal.querySelector(".med-tweet .item-box");
  151. if (tweet) {
  152. new TweetTools(modal, TweetType.Modal);
  153. }
  154. }
  155. });
  156. });
  157. function initModalObserver() {
  158. var modalPanel = document.querySelector("#open-modal");
  159. modalObserver.observe(modalPanel, {
  160. childList: true
  161. });
  162. }
  163. function initColumnObserver() {
  164. var columns = document.querySelectorAll("#container section.column");
  165. Array.from(columns).forEach(function(column) {
  166. columnObserver.observe(column.querySelector("div.chirp-container"), {
  167. childList: true
  168. });
  169. columnDetailObserver.observe(column.querySelector("div.column-detail"), {
  170. childList: true, subtree: true
  171. });
  172. });
  173. }
  174. function initAppObserver() {
  175. var app = document.querySelector("div.application");
  176. new MutationObserver(function(mutations) {
  177. var appColumns = app.querySelector(".app-content .app-columns");
  178. if (appColumns.children.length !== 0) {
  179. this.disconnect();
  180. initColumnObserver();
  181. initModalObserver();
  182. }
  183. }).observe(app, {
  184. childList: true,
  185. });
  186. }
  187. initAppObserver();
  188. })();