Tweetdeck Image Saver

Script to download tweet images in one click.

// ==UserScript==
// @name         Tweetdeck Image Saver
// @namespace    https://twitter.com/aloneunix
// @version      1.1
// @description  Script to download tweet images in one click.
// @author       aloneunix
// @include      https://tweetdeck.twitter.com/*
// @grant        GM_download
// ==/UserScript==

(function() {
    var TweetType = {
        Column: 0,
        Detail: 1,
        Modal: 2
    };
    function GetOrigUrl(url) {
        var u = new URL(url);
        u.searchParams = new URLSearchParams(url.search);
        u.searchParams.set("format", u.pathname.substring(u.pathname.lastIndexOf(".")+1)); // because sometimes png images have jpg format
        u.searchParams.set("name", "orig");
        return u
    }
    var TweetTools = (function () {
        function TweetTools(tweet, type) {
            var self = this;
            this.tweet = tweet;
            this.type = type;
            this.GatherData();
            var dlButton = this.CreateDLButton();
            dlButton.onclick = function () {
                var urls = self.GetImages();
                if (urls.length == 1) {
                    var u = GetOrigUrl(urls[0]);
                    GM_download({url: u.href, name: self.GetFilename(u)});
                } else {
                    urls.forEach(function(url, i) {
                        var u = GetOrigUrl(url);
                        GM_download({url: u.href, name: self.GetFilename(u, i)});
                    });
                }
            };
        }
        TweetTools.prototype.GetImages = function () {
            var urls = [];
            if (this.type == TweetType.Modal) {
                urls.push(this.tweet.querySelector(".js-media-image-link img").src);
            } else if (this.type == TweetType.Column || this.type == TweetType.Detail) {
                var imgLinks = this.tweet.getElementsByClassName("js-media-image-link");
                if (imgLinks.length == 1) {
                    if (this.type == TweetType.Column)
                        urls.push(imgLinks[0].style.backgroundImage.slice(4, -1).replace(/"/g, ""));
                    if (this.type == TweetType.Detail)
                        urls.push(imgLinks[0].firstElementChild.src);
                } else {
                    Array.from(imgLinks).forEach(function(url) {
                        urls.push(url.style.backgroundImage.slice(4, -1).replace(/"/g, ""));
                    });
                }
            }
            return urls;
        };
        TweetTools.prototype.GatherData = function () {
            this.username = this.tweet.querySelector("span.username").innerHTML.slice(1);
            if (this.type == TweetType.Modal) {
                this.tweet_id = this.tweet.querySelector("time.tweet-timestamp a").href.split("status/")[1];
            } else {
                this.tweet_id = this.tweet.getAttribute("data-tweet-id");
            }
            var ts;
            if (this.type == TweetType.Detail) {
                ts = this.tweet.querySelector(".margin-tl a").text.split(" · ")[1];
            } else {
                ts = parseInt(this.tweet.getElementsByClassName("tweet-timestamp")[0].getAttribute("data-time"));
            }
            ts = new Date(ts);
            this.ts = ts.getFullYear() + '-' +
                ('0'+ (ts.getMonth()+1)).slice(-2) + '-' +
                ('0'+ ts.getDate()).slice(-2); // I don't want to live on this planet anymore
        };
        TweetTools.prototype.CreateDLButton = function () {
            var actions = this.tweet.querySelector("footer ul");
            var dlIcon = document.createElement("i");
            dlIcon.className = "icon icon-share txt-right";
            var iconContainer = document.createElement("a");
            iconContainer.href = "#";
            iconContainer.appendChild(dlIcon);
            var dlButton = document.createElement("li");
            if (this.type == TweetType.Detail) {
                iconContainer.className = "tweet-detail-action";
                dlButton.className = "tweet-detail-action-item position-rel";
                Array.from(actions.children).forEach(function (action) {
                    action.style.width = "20%";
                });
                dlButton.style.width = "20%";
            } else {
                iconContainer.className = "tweet-action";
                dlButton.className = "tweet-action-item position-rel pull-left margin-r--13";
            }
            dlButton.appendChild(iconContainer);
            actions.appendChild(dlButton);
            this.tweet.className += " has-tis-tools";
            return dlButton;
        };
        TweetTools.prototype.GetFilename = function(url, index) {
            var extension = url.searchParams.get("format");
            var _index = index !== undefined ? "_"+(index+1) : "";
            return `${this.username}_${this.ts}_${this.tweet_id}${_index}.${extension}`;
        };
        return TweetTools;
    }());
    var columnDetailObserver = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            if (mutation.type == "childList") {
                var tweet = mutation.target.querySelector(".js-tweet-detail article:not(.has-tis-tools)");
                if (tweet)
                    new TweetTools(tweet, TweetType.Detail);
                var replies = mutation.target.querySelectorAll(".tweet-detail-reply article:not(.has-tis-tools)");
                if (replies.length !== 0) {
                    replies = Array.from(replies).filter(function(tweet) {
                        return tweet.querySelector(".js-media .js-media-image-link") && !tweet.querySelector(".quoted-tweet, .video-overlay");
                    });
                    replies.forEach(function (reply) {
                        new TweetTools(reply, TweetType.Column);
                    });
                }
            }
        });
    });
    var columnObserver = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            if (mutation.type == "childList") {
                var tweets = document.querySelectorAll("article.stream-item.is-actionable:not(.has-tis-tools)");
                tweets = Array.from(tweets).filter(function(tweet) {
                    return tweet.querySelector(".js-media .js-media-image-link") && !tweet.querySelector(".quoted-tweet, .video-overlay");
                });
                tweets.forEach(function(tweet) {
                    new TweetTools(tweet, TweetType.Column);
                });
            }
        });
    });
    var modalObserver = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            if (mutation.type == "childList") {
                var modal = document.querySelector("#open-modal .js-modal-panel");
                if (!modal) {
                    return;
                }
                var tweet = modal.querySelector(".med-tweet .item-box");
                if (tweet) {
                    new TweetTools(modal, TweetType.Modal);
                }
            }
        });
    });
    function initModalObserver() {
        var modalPanel = document.querySelector("#open-modal");
        modalObserver.observe(modalPanel, {
            childList: true
        });
    }
    function initColumnObserver() {
        var columns = document.querySelectorAll("#container section.column");
        Array.from(columns).forEach(function(column) {
            columnObserver.observe(column.querySelector("div.chirp-container"), {
                childList: true
            });
            columnDetailObserver.observe(column.querySelector("div.column-detail"), {
                childList: true, subtree: true
            });
        });
    }
    function initAppObserver() {
        var app = document.querySelector("div.application");
        new MutationObserver(function(mutations) {
            var appColumns = app.querySelector(".app-content .app-columns");
            if (appColumns.children.length !== 0) {
                this.disconnect();
                initColumnObserver();
                initModalObserver();
            }
        }).observe(app, {
            childList: true,
        });
    }
    initAppObserver();
})();