// ==UserScript== // @name Twitter - clickable links to images and show uncropped thumbnails // @namespace twitter_linkify // @version 2.1.0 // @license GNU AGPLv3 // @description Supports new Twitter UI. Linkifies all image posts in Twitter Home, other blog streams and single post views. These links point to the "orig" version of the image. Thumbnail images in the stream are modified to display uncropped. // @author marp // @homepageURL https://greasyfork.org/en/users/204542-marp // @include https://twitter.com/ // @include https://twitter.com/* // @include https://pbs.twimg.com/media/* // @exclude https://twitter.com/settings // @exclude https://twitter.com/settings/* // @run-at document-end // @downloadURL none // ==/UserScript== function new_adjustSingleMargin(myNode) { // I SHOULD remove only margin-... values - but there never seems to be anything else - so go easy way and remove ALL style values var myStyle = myNode.getAttribute("style"); if ((myStyle !== null) && (myStyle.includes("margin"))) { myNode.setAttribute("style", ""); } } function new_adjustSingleBackgroundSize(myNode) { var myStyle = myNode.getAttribute("style"); if ((myStyle !== null) && !(myStyle.includes("contain"))) { myNode.style.backgroundSize = "contain"; } } function new_createSingleImageLink(myDoc, myContext) { if (myContext.nodeType === Node.ELEMENT_NODE) { var singlematch; var singlelink; singlematch=myDoc.evaluate("./ancestor-or-self::a[contains(@href,'/photo/') and ancestor::article]", myContext, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); singlelink = singlematch.singleNodeValue; if (singlelink !== null) { // persistently remove "margin-..." styles (they "de-center" the images) singlematch=myDoc.evaluate(".//div[@aria-label='Image']", singlelink, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var singlenode = singlematch.singleNodeValue; if (singlenode !== null) { new_adjustSingleMargin(singlenode); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { new_adjustSingleMargin(mutation.target); }); }); var config = { attributes: true, attributeFilter: [ "style" ], attributeOldValue: false, childList: false, characterData: false, subtree: false }; observer.observe(singlenode, config); } // persistently change image zoom from "cover" to "contain" - this ensures that the full thumbnail is visible singlematch=myDoc.evaluate(".//div[contains(@style,'background-image')]", singlelink, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); singlenode = singlematch.singleNodeValue; if (singlenode !== null) { new_adjustSingleBackgroundSize(singlenode) var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { new_adjustSingleBackgroundSize(mutation.target); }); }); var config = { attributes: true, attributeFilter: [ "style" ], attributeOldValue: false, childList: false, characterData: false, subtree: false }; observer.observe(singlenode, config); } // change the link to point to the "orig" version of the image directly singlematch=myDoc.evaluate(".//img[contains(@src,'https://pbs.twimg.com/media/') and contains(@src,'name=')]", singlelink, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); singlenode = singlematch.singleNodeValue; if (singlenode !== null) { var imgurl = new URL(singlenode.getAttribute("src")); var params = new URLSearchParams(imgurl.search.substring(1)); params.set("name", "orig"); imgurl.search = "?" + params.toString(); singlematch=myDoc.evaluate("./ancestor-or-self::a[contains(@href,'/photo/')]", singlenode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); singlenode = singlematch.singleNodeValue; if (singlenode !== null) { singlenode.href = imgurl.href; } } } } } function new_processImages(myDoc, myContext) { if (myContext.nodeType === Node.ELEMENT_NODE) { var singlematch=myDoc.evaluate("./ancestor-or-self::a[contains(@href,'/photo/')]", myContext, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var singlenode=singlematch.singleNodeValue; if (singlenode !== null) { new_createSingleImageLink(myDoc, singlenode); // applies if the added node is descendant or equal to a single image link } else { // this assumes that the added node CONTAINS image link(s), i.e. is an ancestor of image(s) var matches=myDoc.evaluate("./descendant-or-self::a[contains(@href,'/photo/')]", myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i= 0 && pos > pos2) { return imageurl.substring(0, pos); } else { return imageurl; } } function getFilename(imageurl) { return getCleanImageURL(imageurl).substring(imageurl.toLowerCase().lastIndexOf("/")+1); } // Two very different actions depending on if this is on twitter.com or twing.com if (window.location.href.includes('pbs.twimg.com/media')){ var params = new URLSearchParams(document.location.search.substring(1)); if (params.has("name")) { if (params.get("name") !== "orig" ) { // new Twitter UI - no need anymore to modify the SaveAs filename params.set("name", "orig"); document.location.search = "?" + params.toString(); } } else { // old Twitter UI - insert link with attrib "download" to modify the SaveAs filename (need to click the image) var image = document.querySelector('img'); var url = image.src; insertLinkElement(document, image, getCleanImageURL(url)+":orig", getFilename(url)); } } else { var reactrootmatch = document.evaluate("//div[@id='react-root']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var reactrootnode = reactrootmatch.singleNodeValue; if (reactrootnode !== null) { //new Twitter UI // create an observer instance and iterate through each individual new node var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(addedNode) { new_observeArticles(mutation.target.ownerDocument, addedNode); }); }); }); // configuration of the observer var config = { attributes: false, childList: true, characterData: false, subtree: true }; //process already loaded nodes (the initial posts before scrolling down for the first time) new_observeArticles(document, reactrootnode); //start the observer for new nodes observer.observe(reactrootnode, config); } else { // old Twitter UI // create an observer instance and iterate through each individual new node var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(addedNode) { old_createImageLinks(mutation.target.ownerDocument, addedNode); }); }); }); // configuration of the observer // NOTE: subtree is false as the wanted nodes are direct children of
    -> notable performance improvement var config = { attributes: false, childList: true, characterData: false, subtree: false }; // pass in the target node (
      element contains all stream posts), as well as the observer options var postsmatch = document.evaluate("//ol[@id='stream-items-id']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var postsnode = postsmatch.singleNodeValue; //process already loaded nodes (the initial posts before scrolling down for the first time) old_createImageLinks(document, postsnode); //start the observer for new nodes observer.observe(postsnode, config); // also observe the overlay node - this is the node used when opening an individsual post as overlay // NOTE: subtree is true here as the wanted nodes are ancestors of the node used as observer root var config2 = { attributes: false, childList: true, characterData: false, subtree: true }; // pass in the target node, as well as the observer options var overlaymatch = document.evaluate("//div[contains(@class,'PermalinkOverlay-content')]", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var overlaynode = overlaymatch.singleNodeValue; //start the observer for overlays observer.observe(overlaynode, config2); } }