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-04-05 提交的版本,查看 最新版本

// ==UserScript==
// @name         Tumblr Dashboard - clickable links to images and display time-stamps
// @namespace    tumblr_dashboard_linkify
// @version      2.0.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/*
// @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;
  
  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
            newnode.textContent = permalinknode.getAttribute("title");
            moreoptionsnode.parentNode.appendChild(newnode);
          } 
        }
      } catch (e) { console.warn("error: ", e); }
    }
  }
}




// 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);