您需要先安装一个扩展,例如 篡改猴、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 2.3.2
- // @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 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") || !(myStyle.includes("static")) ) ) {
- myNode.setAttribute("style", "position: static");
- }
- }
- 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);
- 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 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<matches.snapshotLength); i++) {
- el=matches.snapshotItem(i);
- new_createSingleImageLink(myDoc, el);
- }
- }
- }
- }
- function new_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);
- new_processImages(myDoc, el);
- var observer = new MutationObserver(function(mutations) {
- mutations.forEach(function(mutation) {
- mutation.addedNodes.forEach(function(addedNode) {
- new_processImages(mutation.target.ownerDocument, addedNode);
- });
- });
- });
- var config = { attributes: false, childList: true, characterData: false, subtree: true };
- observer.observe(el, config);
- }
- }
- }
- // CODE for OLD Twitter UI - fnctionality not tested anymore -> let me know if something is broken
- function old_createImageLinks(myDoc, myContext) {
- //console.info("createImageLinks: ", myContext);
- if (myDoc===null) myDoc= myContext;
- if (myDoc===null) return;
- if (myContext===null) myContext= myDoc;
- var matches;
- var tmpstr;
- matches=myDoc.evaluate(".//div[contains(@class,'AdaptiveMedia-photoContainer')]/img",
- myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
- for(var i=0, el; (i<matches.snapshotLength); i++) {
- el=matches.snapshotItem(i);
- if (el) {
- try {
- tmpstr=getCleanImageURL(el.getAttribute("src"), false);
- // only need ":small" variant for stream/thumbnail display (save bandwidth and increase performance)
- el.setAttribute("src", tmpstr+":small");
- // create correct link to ":orig" image - best way to access is by opening in new tab via "middle-click")
- insertLinkElement(myDoc, el, tmpstr+":orig", getFilename(tmpstr));
- // try to scale the thumbnail image so that it displays fully and uncropped within the available space
- // This does not do a real aspect ratio calc but uses a "trick" by analysing how Twitter was positioning the cropped image
- tmpstr=el.getAttribute("style");
- if (tmpstr !== null) {
- if (tmpstr.toLowerCase().includes("top") ) {
- el.setAttribute("style", "height: 100%; width: auto");
- }
- else if (tmpstr.includes("left")) {
- el.setAttribute("style", "height: auto; width: 100%");
- }
- }
- } catch (e) { console.warn("error: ", e); }
- }
- }
- }
- 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) {
- //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 <ol id="posts"> -> notable performance improvement
- var config = { attributes: false, childList: true, characterData: false, subtree: false };
- // pass in the target node (<ol> 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);
- }
- }