// ==UserScript==
// @name BDSMLR - clickable links to original high-res images and display timestamps
// @namespace bdsmlr_linkify
// @version 4.2.2
// @license GNU AGPLv3
// @description This script modifies image posts to link directly to their high-resolution version. The link is available as soon as a box appears around an image. The color of the box indicates the image height. Secondly, the script displays the post timestamp in the upper-right corner.
// @author marp
// @homepageURL https://greasyfork.org/en/users/204542-marp
// @match https://bdsmlr.com/
// @match https://bdsmlr.com/dashboard*
// @match https://*.bdsmlr.com/
// @match https://*.bdsmlr.com/post/*
// @match https://bdsmlr.com/search/*
// @match https://*.bdsmlr.com/search/*
// @match https://bdsmlr.com/blog/*
// @match https://bdsmlr.com/originalblogposts/*
// @match https://bdsmlr.com/likes*
// @grant GM_xmlhttpRequest
// @connect bdsmlr.com
// @run-at document-start
// ==/UserScript==
// jshint esversion:8
//console.info("START href: ", window.location.href);
//------------------------------------------------------------
// FIRST PART OF SCRIPT #2 - function that gets called by event oberver that is registered as part of 1st part #1 (see very end of this script)
//------------------------------------------------------------
function createImageLinks(myDoc, myContext) {
// console.info("createImageLinks: ", myContext);
if (myDoc===null) myDoc = myContext;
if (myDoc===null) return;
if (myContext===null) myContext = myDoc;
var tmpstr;
var singlematch;
var matches, matches2;
var imageurl;
var imagesrc;
var imagesrcfixed;
var cdnmatches;
var cdnnumber;
// iterate over all posts within the supplied context
matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'postholder')]"
+ " | " +
"./descendant-or-self::div[contains(@class,'post_content')]"
+ " | " +
"./descendant-or-self::div[contains(@class,'commenttext')]"
, myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var i=0, el; (i<matches.snapshotLength); i++) {
el = matches.snapshotItem(i);
if (el) {
try {
// iterate over all images
// skip over items that already have a link to a "non-cdn" bdsmlr url or that are not bdsmlr links at all
matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" +
"//*[(self::a or self::div) and (@href='') or ((contains(@class,'magnify') or contains(@class,'image-link')) and (contains(@href,'https://cdn') or contains(@href,'https://ocdn')) and contains(@href,'.bdsmlr.com'))]/img" +
" | " +
".//div[contains(@class,'image_container') or contains(@class,'image_content')]//div[contains(@class,'textcontent')]" +
"//img[(contains(@src,'https://cdn') or contains(@src,'https://ocdn')) and contains(@src,'.bdsmlr.com')]" +
" | " +
"./descendant-or-self::div[contains(@class,'singlecomment') or contains(@class,'commenttext')]" +
"//img[(contains(@src,'https://cdn') or contains(@src,'https://ocdn')) and contains(@src,'.bdsmlr.com') and (contains(@class,'fr-dib') or contains(@class,'fr-fic'))]"
,
el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var j=0, image, imageparent; (j<matches2.snapshotLength); j++) {
image=matches2.snapshotItem(j);
if (image) {
imagesrc = image.src;
imageparent = image.parentNode;
imageurl = imageparent.getAttribute("href");
if (imageurl === null || imageurl.length < 5) {
imageurl = image.getAttribute("src");
// No idea why this is needed... DevTools inspector always shows a valid image src attribute... but at script execution time... apparently not... seems to be some bdsmlr JavaScript post-processing...
if (imageurl === null || imageurl.length < 5) {
imageurl = image.getAttribute("data-echo");
}
}
// CDNO08 seem to have been broken during the botched infrastructure upgrade in early 2021
// (after which the ownership of bdsmlr seems to have changed - and the new team doesn't know all the history and thus issues)
// CDNO08 now redirects to CDN12 - but the image is to be found on CDN08, instead?!
// Also, some URLs are HTTP - which do not seem to work (error as if image does not exist) - but HTTPS does work
// 2021-11-01: Same goes from cdno12 -> cdn12 (instead of only cdno08 -> cdn08)
// CAREFUL: This observation is just based on a few purly-by-chance discoveries - no idea if there are other locations besides CDN012/CDN08
// 2022-10-23: All old cdn server seem to have been decommissioned and now redirect to cdn012|cdn013|cdn101 - and there's sopme weird code that points towards ocdn012|ocdn013|ocdn101 - but that seems to do nothing
// With the decommissioning, mayn old images seems to have been lost for good, now :-(
// Thus, for now, I'm turning off the redirections below - except the http->https one
// 2023-06-15: CDN012 seems to be partially broken - but ocdn012 works as an alternative for the broken urls. But not for all urls in general -> need to test if src url is broken instead of generic replace.
imagesrcfixed = null;
if (imagesrc.toLowerCase().startsWith("http://")) {
imagesrcfixed = "https://" + imagesrc.substring(7);
} else {
imagesrcfixed = imagesrc;
}
if (imagesrcfixed.toLowerCase().startsWith("https://cdno08.bdsmlr.com/")) {
imagesrcfixed = "https://cdn08.bdsmlr.com/" + imagesrcfixed.substring(26);
}
if (imagesrcfixed.toLowerCase().startsWith("https://cdno12.bdsmlr.com/")) {
imagesrcfixed = "https://cdn12.bdsmlr.com/" + imagesrcfixed.substring(26);
}
/* if (imagesrcfixed.toLowerCase().startsWith("https://cdno012.bdsmlr.com/")) {
imagesrcfixed = "https://cdn012.bdsmlr.com/" + imagesrcfixed.substring(27);
} */
if (imagesrcfixed.toLowerCase().startsWith("https://cdno06.bdsmlr.com/")) {
imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(26);
}
if (imagesrcfixed.toLowerCase().startsWith("https://cdno010.bdsmlr.com/")) {
imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(27);
}
if (imagesrcfixed.toLowerCase().startsWith("https://cdno05.bdsmlr.com/")) {
imagesrcfixed = "https://cdn101.bdsmlr.com/" + imagesrcfixed.substring(26);
}
if (imagesrcfixed !== imagesrc) {
image.setAttribute("src", imagesrcfixed);
imagesrc = image.src;
}
if (imageurl && imageurl.length > 5) {
getBestImageUrlPromise(imagesrc, imageurl, image)
.then( (result) => {
if ( (result !== null) && (result.image !== null) && (result.url !== null) ) {
var linkelem;
var divelem;
// 2023-06-15: deal with the partially broken SRC urls - if broken -> use the determine full link as image src url, as well
checkUrlHeaderOnlyPromise(result.image.src).then( {/* image src url is working - nothing to do */} ,
// but if it fails, use the just determined image url, instead
(srcresult) => { result.image.setAttribute("src", result.url); });
// Sometimes, images in comments are weirdly structured in hierarchies with paragraph elements -> it's better to create the link as direct parent of the image itself
// same goes for the element that the colored border box can be added to - there is no appropriate div element - so we use the image itself
if ( (result.image.parentNode.nodeName.toLowerCase() == "p") ||
((result.image.previousSibling !== null) && (result.image.previousSibling.nodeName.toLowerCase() == "p")) ||
((result.image.nextSibling !== null) && (result.image.nextSibling.nodeName.toLowerCase() == "p")) ) {
linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image, result.url);
divelem = result.image;
} else {
linkelem = insertOrChangeLinkElement(result.image.ownerDocument, result.image.parentNode, result.url);
var divmatch = result.image.ownerDocument.evaluate("./ancestor::div[(contains(@class,'hide') or contains(@class,'earlycomments')) and ancestor::div[contains(@class,'post')]]",
linkelem, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
divelem = divmatch.singleNodeValue;
}
divelem.style = "border: 5px solid grey;";
divelem.setAttribute("title", getSizeText(result.size));
getImageDimensionsPromise(result.url, divelem, result.size)
.then( (result2) => {
result2.element.style = "border: 5px solid " + result2.color + ";";
result2.element.setAttribute("title", getSizeText(result2.size) + " - " + result2.width + " x " + result2.height);
});
}
});
}
}
}
// multi-image posts - unhide all images (instead of having to manually click on "show x more images"
matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" +
"/div[contains(@style,'display:none')]",
el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var j=0, node; (j<matches2.snapshotLength); j++) {
node=matches2.snapshotItem(j);
if (node) {
node.style.display = "initial";
}
}
// multi-image posts - hide the "show x more images" element
matches2 = myDoc.evaluate(".//div[contains(@class,'image_container') or contains(@class,'image_content')]" +
"/following-sibling::div[contains(@class,'viewAll')]",
el, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var j=0, node; (j<matches2.snapshotLength); j++) {
node=matches2.snapshotItem(j);
if (node) {
node.style.display = "none";
}
}
} catch (e) { console.warn("error: ", e); }
}
}
}
// try to find the timestamp info and display in upper right corner
function displayTimestamps(myDoc, myContext) {
//console.info("displayTimestamps: ", myContext);
if (myDoc===null) myDoc = myContext;
if (myDoc===null) return;
if (myContext===null) myContext = myDoc;
var matches;
var tmpstr;
var singlematch;
var postinfo;
var timestamp;
var newnode;
matches = myDoc.evaluate("./descendant-or-self::div[contains(@class,'feed') and @title]",
myContext, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for(var i=0, el; (i<matches.snapshotLength); i++) {
el = matches.snapshotItem(i);
if (el) {
try {
timestamp = el.getAttribute("title");
if (timestamp && timestamp.length>5 && timestamp.length<70) {
singlematch = myDoc.evaluate(".//div[contains(@class,'post_info')]",
el, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
postinfo = singlematch.singleNodeValue;
if (postinfo) {
newnode = myDoc.createElement("div");
newnode.setAttribute("style", "float:right; margin-right: 10px; padding-right: 10px;");
newnode.innerHTML = timestamp;
postinfo.appendChild(newnode);
}
}
} catch (e) { console.warn("error: ", e); }
}
}
}
function isOriginalImageURL(imageurl) {
if (imageurl === undefined || imageurl === null) {
return false;
}
var tmpstr = imageurl.toLowerCase();
if (tmpstr.includes("bdsmlr.com/")) {
var pos = tmpstr.lastIndexOf(".");
var pos2 = tmpstr.lastIndexOf("-og.");
if (pos > 0 && (pos2+3)==pos) {
return true;
}
}
return false;
}
function getOriginalImageURL(imageurl) {
if (imageurl === null) {
return imageurl;
}
var pos = imageurl.lastIndexOf(".");
var pos2 = imageurl.lastIndexOf("-og.");
if (pos > 0 && (pos2+3)!=pos) {
return imageurl.substring(0, pos) + "-og" + imageurl.substring(pos);
} else {
return imageurl;
}
}
function insertOrChangeLinkElement(myDoc, wrapElement, linkTarget) {
if (wrapElement.nodeName.toLowerCase() == "a") {
wrapElement.setAttribute("href", linkTarget);
wrapElement.setAttribute("target", "_blank");
return wrapElement;
} else {
var parentNode = wrapElement.parentNode;
var newNode = myDoc.createElement("a");
newNode.setAttribute("href", linkTarget);
newNode.setAttribute("target", "_blank");
parentNode.replaceChild(newNode, wrapElement);
newNode.appendChild(wrapElement);
return newNode;
}
}
function getSizeText(sizeInBytes) {
if (sizeInBytes === null) {
return "";
}
if (sizeInBytes >= 1048576) {
return (sizeInBytes / 1048576).toFixed(1) + " MB";
}
else if (sizeInBytes >= 1024) {
return (sizeInBytes / 1024).toFixed(0) + " KB";
}
else {
return sizeInBytes.toFixed(0) + " B";
}
}
// This ASYNC method returns a promise to retrieve the HTTP response header data for the supplied URL.
// It uses an "HTTP HEAD" request which does NOT download the response payload (to minimize network traffic)
async function checkUrlHeaderOnlyPromise(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'HEAD',
url: url,
headers: {
"referer": "bdsmlr.com"
},
onload: function(response) {
if ((response.readyState >= 2) && (response.status == 200)) {
var contentLength = -1;
var conlenstr = response.responseHeaders.split("\r\n").find(str => str.toLowerCase().startsWith("content-length: "));
if (conlenstr !== undefined) {
contentLength = parseInt(conlenstr.slice(16), 10);
if (isNaN(contentLength)) {
contentLength = -1;
}
}
resolve( { url: response.finalUrl, size: contentLength, origurl: url} );
} else {
reject( { url: url, size: -1, origurl: url} );
}
},
ontimeout: function(response) {
reject( { url: url, size: -1, origurl: url} );
},
onerror: function(response) {
reject( { url: url, size: -1, origurl: url} );
}
});
});
}
// This ASYNC method returns a promise to determine the url of the originally uploaded image (the one with a suffix of "-og" in the name)
// This requires "testing" a lot of URLs, i.e. it causes server traffic (especially if it is an image on the "old" CDN servers from BDSMLR's early times).
// To minimize network traffic, this method only requests the HTTP headers for all these URLs - the image itself nor error webpages (404) are not fully downloaded.
// If multiple "-og" variants are found, the one with the largest size (in bytes, not image dimensions!) is chosen.
// If the og version is smaller than the non-og version it still sticks with the og, if it is larger than 5kb (5kb to exclude error pages, the non-og is typically upscaled and worse quality than the og)
// "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise "then" function of the caller
// IMPORTANT DISCOVERY: For some(!) image servers, the path part (the part after hostname) is CASE-SENSITIVE! Urgh!!
async function getBestImageUrlPromise(imageurl, linkurl, imageelement) {
var urlsToCheckPromises = [];
var uniqueset = new Set(); // Use a Set to collect all URLs that are to be tested - this automatically eliminates dups that the URL selection logic below might create
var matches;
var imageurl_cdnnum;
var linkurl_cdnnum;
var imageurl_cdnnumstr;
var linkurl_cdnnumstr;
var imageurl_path;
var linkurl_path;
var bestImageUrl;
var bestImageSize;
var bestImageIsOG;
//console.info("getBestImageUrlPromise-Enter: ", "imageurl:" + imageurl + " linkurl:" + linkurl);
// get CDN image server number as int and as string
matches = imageurl.toLowerCase().match(/https?:\/\/o?cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\//i);
if (matches !== null && matches.length > 0) {
imageurl_cdnnumstr = matches[1];
imageurl_cdnnum = parseInt(matches[1], 10);
} else {
imageurl_cdnnum = NaN;
}
// get CDNO image server number as int and as string
matches = linkurl.toLowerCase().match(/https?:\/\/o?cdno?(0?[1-9][0-9]*)\.bdsmlr\.com\//i);
if (matches !== null && matches.length > 0) {
linkurl_cdnnumstr = matches[1];
linkurl_cdnnum = parseInt(matches[1], 10);
} else {
linkurl_cdnnum = NaN;
}
// get non-hostname part of the url, including leading "/" (NOTE: this regex fixes buggy urls with multiple "//" after the hostname)
matches = imageurl.match(/https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)/i);
if (matches !== null && matches.length > 0) {
imageurl_path = matches[1];
} else {
imageurl_path = null;
}
// get non-hostname part of the url, including leading "/" (NOTE: this regex fixes buggy urls with multiple "//" after the hostname)
matches = linkurl.match(/https?:\/\/[^.]*\.?bdsmlr\.com\/*(\/.+$)/i);
if (matches !== null && matches.length > 0) {
linkurl_path = matches[1];
} else {
linkurl_path = null;
}
// fetch promise for the image that is currently shown in the webpage (ALLOW redirection on this URL)
uniqueset.add(imageurl);
// image to which the current unmodified link in the webpage is pointing to (ALLOW redirection on this URL)
uniqueset.add(linkurl);
// append "-og" suffix to the original image and link urls (without modifying the hostname)
uniqueset.add(getOriginalImageURL(imageurl));
uniqueset.add(getOriginalImageURL(linkurl));
if (!isNaN(imageurl_cdnnum) && (imageurl_cdnnum <= 13 || imageurl_cdnnum == 101)) {
uniqueset.add(getOriginalImageURL("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://ocdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add("https://cdno" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path);
uniqueset.add("https://cdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path);
uniqueset.add("https://ocdn" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path);
uniqueset.add(getOriginalImageURL("https://cdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://ocdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path));
uniqueset.add("https://cdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path);
uniqueset.add("https://cdno0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path);
uniqueset.add("https://ocdn0" + imageurl_cdnnumstr + ".bdsmlr.com" + imageurl_path);
uniqueset.add(getOriginalImageURL("https://cdn012.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno012.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://ocdn012.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdn013.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno013.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdn101.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdno101.bdsmlr.com" + imageurl_path));
}
if (!isNaN(linkurl_cdnnum) && (linkurl_cdnnum <= 13 || linkurl_cdnnum == 101)) {
uniqueset.add(getOriginalImageURL("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://ocdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add("https://cdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path);
uniqueset.add("https://cdno" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path);
uniqueset.add("https://ocdn" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path);
uniqueset.add(getOriginalImageURL("https://cdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://ocdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path));
uniqueset.add("https://cdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path);
uniqueset.add("https://cdno0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path);
uniqueset.add("https://ocdn0" + linkurl_cdnnumstr + ".bdsmlr.com" + linkurl_path);
uniqueset.add(getOriginalImageURL("https://cdn012.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno012.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://ocdn012.bdsmlr.com" + imageurl_path));
uniqueset.add(getOriginalImageURL("https://cdn013.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno013.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdn101.bdsmlr.com" + linkurl_path));
uniqueset.add(getOriginalImageURL("https://cdno101.bdsmlr.com" + linkurl_path));
}
for (var str of uniqueset) {
urlsToCheckPromises.push(checkUrlHeaderOnlyPromise(str));
}
bestImageUrl = null;
bestImageSize = -1;
bestImageIsOG = false;
await Promise.allSettled(urlsToCheckPromises).
then( (results) => results.forEach((result) => {
// If this result is a better image than currently known - replace and use this one as next best known image
if (result.status == "fulfilled") {
if ( (isOriginalImageURL(result.value.url) && !bestImageIsOG && (result.value.size > 5120)) ||
( (result.value.size > bestImageSize) && (result.value.size > 5120) && (isOriginalImageURL(result.value.url) == bestImageIsOG) ) ) {
bestImageSize = result.value.size;
bestImageUrl = result.value.url;
bestImageIsOG = isOriginalImageURL(bestImageUrl);
}
}
}));
return {url: bestImageUrl, size: bestImageSize, isOG: bestImageIsOG, image: imageelement};
}
// This ASYNC method gets the natural dimensions of the supplied image
// This means the image needs to be downloaded fully, unfortunately!
// Thus, a delay is to be expected, except if the image is already cached
// Depending on the image height, the method suggests a "markup color" and then discards the downloaded image again (but does not invalidate cache).
// "imageelement" is only passed-through - it is a helper to supply the DOM context to the surrounding asynchronous promise then function of the caller
async function getImageDimensionsPromise(imageurl, divelement, imagesize) {
var image;
var imageH;
var imageW;
var color;
// sanity check - skip full download of image if it is larger than 20MB
if ( (imagesize !== null) && (imagesize > 20971520) ) {
return {url: imageurl, element: divelement, width: "unknown", height: "unknown", color: "Grey", size:imagesize};
}
image = new Image();
image.src = imageurl;
await image.decode().then(function() {
imageH = image.naturalHeight;
imageW = image.naturalWidth;
});
image.src = "data:,"; // clear the image now that we no longer need it
if (imageH >= 2160) {
color = "hsl(160, 100%, 70%)";
} else if (imageH >= 1080) {
color = "hsl(" + (120.0 + 40.0 * ((imageH - 1080.0) / 1080.0)) + ", 100%, " + (50.0 + 20.0 * ((imageH - 1080.0) / 1080.0)) + "%)";
// color = "Lime"; // #00FF00, HSL(120°, 100%, 50%)
} else if (imageH >=270 ) {
color = "hsl(" + (120.0 * ((imageH - 270.0) / 810.0)) + ", 100%, 50%)";
// color = "Red"; // #FF0000, HSL(0°, 100%, 50%)
} else if (imageH < 270 && imageH > 0 ) {
color = "hsl(0, 100%, " + (50.0 * (imageH / 270.0)) + "%)";
// color = "Red"; // #FF0000, HSL(0°, 100%, 50%)
} else {
color = "Grey";
}
return {url: imageurl, element: divelement, width: imageW, height: imageH, color: color, size:imagesize};
}
//------------------------------------------------------------
// FIRST PART OF SCRIPT #1 - initial statement and registration of event observer
//------------------------------------------------------------
// script runs NOT in the context of an image - i.e. dashboard, blog stream, individual post
// -> register event observer for future to-be-loaded posts (endless scrolling) and execute first part of script (createImageLinks & displayTimestamps) on already loaded posts
// undo badly coded attempts from bdsmlr to hide unwanted posts mentioning reblog.me
// -> their code breaks infinite scrolling and incorrectly hides legitimate posts
// this requires that this script is set to: @run-at document-start
var scriptnode = document.createElement("script");
scriptnode.setAttribute("type", "text/javascript");
scriptnode.textContent = "let byespam = function () {} ";
document.head.insertBefore(scriptnode, document.head.firstChild);
//console.info("scriptNodes INIT");
// with @run-at document-start, only the HEAD section will have been loaded
// -> register an observer to act as latter body nodes are being loaded
// -> namely, we wait for the embedded <script> node that handles endless scrolling...
// -> ...to modify it as to trigger much sooner so that the scrolling experience is smoother (trigger and complete loading BEFORE reaching the end of page)
var scriptobserver = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(addedNode) {
if (addedNode.nodeName == "SCRIPT" && addedNode.textContent.includes("$(document).height() - $(window).height())")) {
addedNode.textContent = addedNode.textContent.replaceAll("$(document).height() - $(window).height())", "$(document).height() - (5 * $(window).height()))");
scriptobserver.disconnect();
}
});
});
});
var scriptconfig = { attributes: false, childList: true, characterData: false, subtree: true };
scriptobserver.observe(document, scriptconfig);
// remainder of script requires @run-at document-end
document.addEventListener ("DOMContentLoaded", runAtDocumentEnd);
function runAtDocumentEnd() {
// 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);
displayTimestamps(mutation.target.ownerDocument, addedNode);
});
});
});
// configuration of the observer
// "theme1" is the class used by the feed root node for individual user's blog (xxxx.bdsmlr.com) -> seems unstable/temporary name -> might be changed by bdsmlr
var config = { attributes: false, childList: true, characterData: false, subtree: true };
// pass in the target node (<div> element contains all stream posts), as well as the observer options
var postsmatch = document.evaluate(".//div[contains(@class,'newsfeed')] | .//div[contains(@class,'theme1')]", 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)
createImageLinks(document, postsnode);
displayTimestamps(document, postsnode);
//start the observer for new nodes
observer.observe(postsnode, config);
// also observe the right sidebar blog stream on the dashboard
// pass in the target node, as well as the observer options (subtree has to be true here - target nodes are further down the hierarchy)
var config2 = { attributes: false, childList: true, characterData: false, subtree: true };
var sidepostsmatch = document.evaluate(".//div[@id='rightposts']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
var sidepostsnode = sidepostsmatch.singleNodeValue;
// sidebar does only exist on dashboard
if (sidepostsnode) {
//start the observer for overlays
observer.observe(sidepostsnode, config2);
}
}