// ==UserScript==
// @name Tumblr Dashboard - clickable links to images and display time-stamps
// @namespace tumblr_dashboard_linkify
// @version 3.3
// @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 http://greasyfork.icu/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//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]"
+ " | " +
"self::div/img[@role='img' and @srcset and @sizes and parent::div/parent::figure/ancestor::button[@aria-label]/ancestor::article]",
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//header[@role='banner']" +
" | " +
"./ancestor-or-self::article/descendant::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);
console.info("matched-element HTML: ", el.outerHTML);
// find the grandchild span that contains the "More Options" Ellipsis button
// make sure that this span is the last child (if it is not then this script might have run already)
singlematch = myDoc.evaluate("((./div/span)[last()])[.//button//svg:svg]",
el, nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
singlenode = singlematch.singleNodeValue;
console.info("matched-element: ", singlenode);
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;
console.info("matched-element: ", singlenode);
if (singlenode) {
permalinknode = singlenode;
// get the class attrib of the "more options" span - we'll create a second div with same class
tmpstr = moreoptionsnode.getAttribute("class");
// create new node with the timestamp text
newnode = myDoc.createElement("span");
newnode.setAttribute("class", tmpstr);
newnode.setAttribute("style", "font-size: 85%; padding-left: 1em");
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 the blog title (if available)
if (sib===null) {
singlenode.parentNode.parentNode.setAttribute("style", "padding: 0px;");
sib = singlenode.parentNode.previousElementSibling; //this is the 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");
}
//remove 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 + "'))] ) ]"+
"and "+
"not( ./descendant-or-self::img[@role='img'] ) ]",
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;
if (rootnode) {
//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 };
// Tumblr 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);
console.info("Initialization done");
}