Tumblr Dashboard - clickable links to images and display time-stamps

NEW Tumblr UI - READ DESCRIPTION. Linkifies all images in the tumblr dashboard and side-view streams. The script also displays the time-stamp of each post in the upper right corner.

当前为 2020-05-15 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Tumblr Dashboard - clickable links to images and display time-stamps
// @namespace    tumblr_dashboard_linkify
// @version      3.1
// @license      GNU AGPLv3
// @description  NEW Tumblr UI - READ DESCRIPTION. Linkifies all images in the tumblr dashboard and side-view streams. The script also displays the time-stamp of each post in the upper right corner.
// @author       marp
// @homepageURL  https://greasyfork.org/en/users/204542-marp
// @include      https://www.tumblr.com/dashboard
// @include      https://www.tumblr.com/dashboard/*
// @include      https://www.tumblr.com/likes
// @include      https://www.tumblr.com/likes/*
// @include      https://*.media.tumblr.com/*.png
// @include      https://*.media.tumblr.com/*.gif
// @include      https://*.media.tumblr.com/*.jpg
// @include      https://*.media.tumblr.com/*.webp
// @run-at document-end
// ==/UserScript==



function nsResolver(prefix) {
  if (prefix === 'svg') { 
    return 'http://www.w3.org/2000/svg';
  } else {
    return null;
  }
}


function doNothing_tumblr_dashboard_linkify(event) {
  e.preventDefault();
  return false;
}

function insertLinkElement(myDoc, wrapElement, linkTarget) {
  var newnode;
  var parentnode;
  
  newnode = myDoc.createElement("a");
  newnode.setAttribute("href", linkTarget);
  newnode.setAttribute("target", "_blank");
  newnode.addEventListener("click", doNothing_tumblr_dashboard_linkify, true);
  parentnode = wrapElement.parentNode;
  parentnode.replaceChild(newnode, wrapElement);
  newnode.appendChild(wrapElement);
}

function getHighResImageURL(imageElement) {
  var srcarray;
  var tmpstr;
  
  srcarray = imageElement.getAttribute("srcset").split(",");
  // QUICK AND DIRTY - assume largest image is the last in array... seems to be true for Tumblr... but might change...
  tmpstr = srcarray[srcarray.length-1].trim();
  return tmpstr.substring(0, tmpstr.indexOf(" "));
}



function createImageLinks(myDoc, myContext) {

  if (myDoc===null) myDoc= myContext;
  if (myDoc===null) return;
  if (myContext===null) myContext= myDoc;
  
  var matches;
  var tmpstr;
  
  // the img might be added as part of a whole pos (first expr) - or just the img or the div/img, in which case we need to check if the image is part of the correct hierarchy (second expr)
  matches=myDoc.evaluate(".//article[@aria-label]//button[@aria-label]//figure/div/img[@role='img' and @srcset and @sizes]"
                         + " | " +
                         "self::img[@role='img' and @srcset and @sizes and parent::div/parent::figure/ancestor::button[@aria-label]/ancestor::article[@aria-label]]"
                         + " | " +
                         "self::div/img[@role='img' and @srcset and @sizes and parent::div/parent::figure/ancestor::button[@aria-label]/ancestor::article[@aria-label]]",
                         myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  for(var i=0, el; (i<matches.snapshotLength); i++) {
    el=matches.snapshotItem(i);
    if (el) {
      try {
//        console.info("matched-element: ", el);
        tmpstr=getHighResImageURL(el);
        insertLinkElement(myDoc, el.parentNode, tmpstr);
      } catch (e) { console.warn("error: ", e); }
    }
  }
}


function displayDateTime(myDoc, myContext) {
  if (myDoc===null) myDoc= myContext;
  if (myDoc===null) return;
  if (myContext===null) myContext= myDoc;
  
  var matches;
  var singlematch;
  var singlenode;
  var moreoptionsnode;
  var permalinknode;
  var tmpstr;
  var newnode;
  var pos;
  
  matches=myDoc.evaluate(".//article[@aria-label]//header[@role='banner']", 
                         myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  for(var i=0, el; (i<matches.snapshotLength); i++) {
    el=matches.snapshotItem(i);
    if (el) {
      try {
//console.info("matched-element: ", el);
        // find the grandchild div that contains the "More Options" Ellipsis button
        // make sure that this div is the last child (if it is not then this script might have run already)
        singlematch = myDoc.evaluate("((./div/div)[last()])[.//button//svg:svg]", 
                                     el, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
        singlenode = singlematch.singleNodeValue; 
        if (singlenode) {
          moreoptionsnode = singlenode;
          // find the permalink element - this has the timestamp as the title attribute (mouseover popup)
          singlematch = myDoc.evaluate("./a[@role='button' and @target='_blank' and @title]", 
                                       el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
          singlenode = singlematch.singleNodeValue; 
          if (singlenode) {
            permalinknode = singlenode;
            // get the class attrib of the "more options" div - we'll create a second div with same class
            tmpstr = moreoptionsnode.getAttribute("class");
            // create new node with the timestamp text
            newnode = myDoc.createElement("div");
            newnode.setAttribute("class", tmpstr);
            newnode.setAttribute("style", "font-size: 85%");
            newnode.setAttribute("displaytimestampscript", "1"); // flag that this node was added my this script
            tmpstr = permalinknode.getAttribute("title");
            pos = tmpstr.lastIndexOf(" - ");
            if (pos >= 0) {
              tmpstr = tmpstr.substring(pos+3);
            }
            newnode.textContent = tmpstr;
            moreoptionsnode.parentNode.appendChild(newnode);
          } 
        }
      } catch (e) { console.warn("error: ", e); }
    }
  }
}



function removeImageHtmlCrap(myDoc, myContext) {
  if (myDoc===null) myDoc= myContext;
  if (myDoc===null) return;
  if (myContext===null) myContext= myDoc;
  
	var imgurl_full;
  var imgurl_match;
  var partialurl;
  var singlematch;
  var singlenode;
  var sib;
  var vsize;
  
  imgurl_full = window.location.href;
  // this part of the URL isd the same for all available sizes of the image
  partialurl = imgurl_full.match(/https?:\/\/[^/]+\.tumblr\.com\/[^/]+\//i); 
  if (partialurl) {
    imgurl_match = partialurl[0];
  
    singlematch = myDoc.evaluate("./descendant-or-self::img[contains(@srcset,'" + imgurl_match + "') or contains(@src,'" + imgurl_match + "')]",
                                 myContext, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
    singlenode = singlematch.singleNodeValue; 
    if (singlenode) {
      // modify the image to use the largest available size varient (which is equal to the page URL!)
      // change several styles so that the image fits into the available viewport space
      singlenode.parentNode.setAttribute("style", "padding: 0px;");
      sib = singlenode.previousElementSibling; //this is thr blog title (if available)
      if (sib) {
        vsize = sib.clientHeight;
      } else {
        vsize = 0;
      }
      if (singlenode.hasAttribute("srcset")) {
        singlenode.removeAttribute("srcset");
        singlenode.removeAttribute("sizes");
      }
		  singlenode.setAttribute("src", imgurl_full);
		  singlenode.setAttribute("style", "max-width: 99vw; max-height: calc(99vh - " + vsize +"px);");
      singlenode.removeAttribute("class");
    }
    
    //removce all DIVs that are unrelated to the image as well as to the blog title (which appears right above the image)
    matches = myDoc.evaluate("./descendant-or-self::div[not( ./descendant-or-self::img["+ 
                                                               "(contains(@srcset,'" + imgurl_match + "') or contains(@src,'" + imgurl_match + "'))] )"+
                                                       "and "+
                                                       "not( ./ancestor-or-self::div/child::img["+
                                                               "(contains(@srcset,'" + imgurl_match + "') or contains(@src,'" + imgurl_match + "'))] ) ]", 
                             myContext, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for(var i=0, el; (i<matches.snapshotLength); i++) {
      el=matches.snapshotItem(i);
      if (el) {
        try {
          el.remove();
        } catch (e) { console.warn("error: ", e); }
      }
    }

  }
}



if ( window.location.href.includes('.media.tumblr.com/') ) {
	// special part of script - acting only on direct image URLs to remove the HTML-crap injected by Tumblr

  // 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) {
        removeImageHtmlCrap(mutation.target.ownerDocument, addedNode.parentNode);
      });
    });    
  });

  // configuration of the observer
  var config = { attributes: false, childList: true, characterData: false, subtree: true };

  // new twitter UI has few stable IDs - need to start very high with "root" node
  var singlematch = document.evaluate("//body[@id='tumblr']/div[@id='root']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  //console.info("singlematch: ", singlematch);
  var rootnode = singlematch.singleNodeValue;

  //start the observer for new nodes
  observer.observe(rootnode, config);

  //process already loaded nodes (the initial posts before scrolling down for the first time)
  removeImageHtmlCrap(document, rootnode);

  
} else { // this is "normal" part of script - acting on anything except direct image URLs


  // 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) {
        createImageLinks(mutation.target.ownerDocument, addedNode);
        displayDateTime(mutation.target.ownerDocument, addedNode);
      });
      mutation.removedNodes.forEach(function(removedNode) {
        // TUMBLR - OH NO YOU DON'T !!
        if (removedNode.hasAttribute("displaytimestampscript")) {
          mutation.target.insertBefore(removedNode, mutation.nextSibling);
        }
      });
    });    
  });

  // configuration of the observer
  var config = { attributes: false, childList: true, characterData: false, subtree: true };

  // new twitter UI has few stable IDs - need to start very high with "root" node
  var singlematch = document.evaluate("//body[@id='tumblr']/div[@id='root']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  //console.info("singlematch: ", singlematch);
  var rootnode = singlematch.singleNodeValue;

  //start the observer for new nodes
  observer.observe(rootnode, config);

  //process already loaded nodes (the initial posts before scrolling down for the first time)
  createImageLinks(document, rootnode);
  displayDateTime(document, rootnode);

}