您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
All image posts in Twitter Home, other blog streams and single post views link to the high-res "orig" version. Thumbnail images in the stream are modified to display uncropped.
当前为
// ==UserScript== // @name Twitter - clickable links to images and show uncropped thumbnails // @namespace twitter_linkify // @version 3.1 // @license GNU AGPLv3 // @description All image posts in Twitter Home, other blog streams and single post views link to the high-res "orig" version. 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 // ==/UserScript== function 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") || !(myStyle.includes("absolute")) ) ) { myNode.setAttribute("style", "position: absolute; top: 0px; bottom: 0px; left: 0px; right: 0px"); } } function adjustSingleBackgroundSize(myNode) { var myStyle = myNode.getAttribute("style"); if ( (myStyle !== null) && ( !(myStyle.includes("contain")) ) ) { myNode.style.backgroundSize = "contain"; } } function 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' or @data-testid='tweetPhoto']", singlelink, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); var singlenode = singlematch.singleNodeValue; if (singlenode !== null) { adjustSingleMargin(singlenode); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { 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) { adjustSingleBackgroundSize(singlenode) var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { 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); var imagenode = singlematch.singleNodeValue; if (imagenode !== null) { var imgurl = new URL(imagenode.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/')]", imagenode, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); singlenode = singlematch.singleNodeValue; if (singlenode !== null) { singlenode.href = imgurl.href; } } } } } function processImages(myDoc, myContext) { //console.info("processImages-0 ", 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) { 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<matches.snapshotLength); i++) { el=matches.snapshotItem(i); createSingleImageLink(myDoc, el); } } } } var blurStyles = null; // some styles are added on-demand, but once we get the style for the image blurring, we stop updating the list and use this cache for performance reasons var blurStylesStop = false; function processBlurring(myDoc, myContext) { if (myContext.nodeType === Node.ELEMENT_NODE) { if (!blurStylesStop) { // Find all CSS that implement blurring - example match: ".r-yfv4eo { filter: blur(30px); }" // Keep the style names of these matches in an array // NOTE: This code assumes that all these CSS have selectors without element types, i.e. ".r-yfv4eo" instead of "div.r-yfv4eo" blurStyles = Array.from(myDoc.styleSheets).flatMap(ss => Array.from(ss.cssRules).filter(css => css instanceof CSSStyleRule && css.cssText.indexOf('blur(')>=0)).map(css => css.selectorText.substring(1)); } var matches; var pos; for (const bs of blurStyles) { matches = myDoc.evaluate("./descendant-or-self::div[contains(@class, '"+bs+"')]", myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i<matches.snapshotLength); i++) { el=matches.snapshotItem(i); el.className = el.className.replace(bs, ''); //remove the blurring // remove the overlay with the info text and button to show/ide (assumption: it is always the next sibling element) if (el.nextSibling !== null) { el.nextSibling.remove(); blurStylesStop = true; // found and used the correct blurring style - stop searching and rebuilding the style list (performance) } } } } } function observeArticles(myDoc, myContext) { if (myContext.nodeType === Node.ELEMENT_NODE) { var singlematch; var matches; matches=myDoc.evaluate("./descendant-or-self::article[./ancestor::section/ancestor::div[@data-testid='primaryColumn']/ancestor::main]", myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for(var i=0, el; (i<matches.snapshotLength); i++) { el=matches.snapshotItem(i); processImages(myDoc, el); processBlurring(myDoc, el); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { mutation.addedNodes.forEach(function(addedNode) { processImages(mutation.target.ownerDocument, addedNode); processBlurring(mutation.target.ownerDocument, addedNode); }); }); }); var config = { attributes: false, childList: true, characterData: false, subtree: true }; observer.observe(el, config); } } } function insertLinkElement(myDoc, wrapElement, linkTarget, downloadName) { var newnode; var parentnode; newnode = myDoc.createElement("a"); newnode.setAttribute("href", linkTarget); newnode.setAttribute("target", "_blank"); newnode.setAttribute("download", downloadName); parentnode = wrapElement.parentNode; parentnode.replaceChild(newnode, wrapElement); newnode.appendChild(wrapElement); } function getCleanImageURL(imageurl) { var pos = imageurl.toLowerCase().lastIndexOf(":"); var pos2 = imageurl.toLowerCase().indexOf("/"); if (pos >= 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) { // 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) { 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) observeArticles(document, reactrootnode); //start the observer for new nodes observer.observe(reactrootnode, config); } }