// ==UserScript==
// @name mangaupdates Cover Preview
// @namespace szMangaupdatesCoverPreview
// @include https://www.mangaupdates.com/*
// @include http://www.mangaupdates.com/*
// @version 1.2.2
// @description Previews covers in mangaupdates.com when hovering over hyperlinks that lead to series pages.
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @require http://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js
// @license http://creativecommons.org/licenses/by-nc-sa/4.0/
// @run-at document-end
// ==/UserScript==
const MAXCACHEAGE = 7 * 24 * 60 * 60 * 1000; // Max Age before Cached data gets overridden with current data. Max Age is 7 days in milliseconds //days * h * min * sec * ms
var SELECTOR1 = 'tr a'; //index/group/readinglist pages
var SELECTOR2 = ''; //individual serie pages recommendation titles
const PREDIFINEDNATIVTITLE = "Series Info"; //in case native title is used to display something different
const INDIVIDUALPAGETEST = /series\.html\?id\=[0-9]*$/;
var STYLESHEETHIJACKFORBACKGROUND = "";
var STYLESHEETHIJACKFORTITLE = "";
const DEFAULTTITLEBACKGROUNDCOLOR = '#aac'; //if no hijack available use plain color
const DEFAULTBACKGROUNDCOLOR ='#ccc'; //if no hijack available use plain color
const IMAGELINKCONTAINER = 'sContainer'; //serieseditimg
const IMAGELINKCONTAINERANONYM = ''; //sContainer
const CONTAINERNUMBER = 1;
const IMAGEBLOCKER = "www.mangaupdates.com/images/stat_increase.gif, www.mangaupdates.com/images/stat_decrease.gif"; //tested with string.match(). no need for prefixed http https in url. Can even be just the file name
const HASERROR = true;
const RE = /\s*,\s*/; //Regex for split and remove empty spaces
const IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE);
function concatSelector()
{
var result;
if(SELECTOR1)
result = SELECTOR1;
if(SELECTOR2)
{
if(SELECTOR1 != "") //in case selector1 is missing
result += ', ';
result += SELECTOR2;
}
return result;
}
//get value from key. Decide if timestamp is older than MAXCACHEAGE than look for new image
function GM_getCachedValue(key)
{
var currentTime = Date.now();
var keyCache = key+'cachedTime';
if(GM_getValue(keyCache)) //has previous timestamp for key
{
var measuredTimedifference = currentTime-parseInt(GM_getValue(keyCache));
if(measuredTimedifference < MAXCACHEAGE )
{
if(GM_getValue(key))
{
return GM_getValue(key); //when time difference smaller MAXCACHEAGE and value available return value
}
else
GM_deleteValue(keyCache); //if no value available delete previous key with timestamp
}
else
{ //when time difference bigger than MAXCACHEAGE delete timestamp and value;
GM_deleteValue(keyCache);
GM_deleteValue(key);
}
}
else //when no timestamp available return false
return false;
}
//set value and currenttime for key
function GM_setCachedValue(key, value)
{
var currentTime = Date.now(); // current datetime in milliseconds
GM_setValue(key, value);
GM_setValue(key+'cachedTime', currentTime);
}
var onHover = false;
var currentTitelHover;
// popupPositioning function
jQuery.fn.popupPos = function (event, element, style) {
var offsetToBottomBorderY = 5; //offset to bottom border
var offsetToRightBorderX = 5; //offset to right border
var X,Y;
var hoveredSelectedPosX,hoveredSelectedPosY;
var distanceToBottom, distanceToRight;
//Initialising variables (multiple usages)
var scrollTop = $(window).scrollTop();
var scrollLeft = $(window).scrollLeft();
var elementPopup = $(this);
var elementParentOffset = element.parents().offset();
var elementParentOuterHeight = element.parents().outerHeight();
var elementParentOuterWidth = element.parents().outerWidth();
var elementOffset = element.offset();
var elementOuterHeight = element.outerHeight();
var elementOuterWidth = element.outerWidth();
if(style==1) //index: position next to parent table cell (SELECTOR1)
{
hoveredSelectedPosX = elementParentOffset.left + elementParentOuterWidth; //link position + tablecell width; + elementOuterWidth;;
hoveredSelectedPosY = elementParentOffset.top + elementParentOuterHeight; //link position + tablecell height; + elementOuterHeight;
}
else if(style==2) //recommendations: position next to link height and parent (SELECTOR2) width
{
hoveredSelectedPosX = elementParentOffset.left + elementParentOuterWidth; //elementOffset.left + elementOuterWidth;
hoveredSelectedPosY = elementOffset.top + elementOuterHeight;
}
X = hoveredSelectedPosX;
Y = hoveredSelectedPosY;
// Distance to the right
distanceToRight = $(window).width() - (X + offsetToRightBorderX - scrollLeft);
// Tooltip too close to the right?
if(distanceToRight < elementPopup.outerWidth() + offsetToRightBorderX)
X += distanceToRight - elementPopup.outerWidth();
// Distance to the bottom
distanceToBottom = $(window).height() - (Y + offsetToBottomBorderY - scrollTop);
// Tooltip too close to the bottom?
if(distanceToBottom < elementPopup.outerHeight() + offsetToBottomBorderY) //(offsetToBottomBorderY + elementPopup.outerHeight())
Y += distanceToBottom - elementPopup.outerHeight();
//console.log("Distance to the bottom " + distanceToBottom+" elementPopupHeight " +elementPopup.outerHeight()+ "\nDistance to the right " + distanceToRight+ " elementPopupouterWidth " +elementPopup.outerWidth());
//Tooltip over top border?
//if(Y + offsetToBottomBorderY < scrollTop) Y = scrollTop + offsetToBottomBorderY;
//if(X + offsetToRightBorderX < scrollLeft) X = scrollLeft + offsetToRightBorderX;
this.css('top', Y + 'px');
this.css('left', X + 'px');
console.log("final popup position "+X+' # '+Y);
return this;
};
var style=1;
//.wpb_wrapper a = title links on individual seriepage
$(SELECTOR2).mouseenter(function (e) {
style=2;
});
//td a = links in table cells (index and group page)
$(SELECTOR1).mouseenter(function (e) {
style=1;
});
$(window).on("load", function () {
function styleSheetContainsClass(f)
{
var localDomainCheck = '^http://'+document.domain;
//console.log("Domain check with: " + localDomainCheck);
var hasStyle = false;
var stylename = '.'+f;
var fullStyleSheets = document.styleSheets;
// console.log("start styleSheetContainsClass " + stylename);
if(fullStyleSheets)
{
for (var i = 0; i < fullStyleSheets.length-1; i++){
//console.log("loop fullStyleSheets " + stylename); //hier im bereich firefox bug?
var styleSheet = fullStyleSheets[i];
//var stylehref = ""+styleSheet.href;
//console.log("style test : " + styleSheet.href);
//if(styleSheet != null)
{
if(styleSheet.href != null) //https://gold.xitu.io/entry/586c67c4ac502e12d631836b "However since FF 3.5 (or thereabouts) you don't have access to cssRules collection when the file is hosted on a different domain" -> Access error for Firefox based browser. script error not continuing
if(styleSheet.href.match(localDomainCheck) )
{
if(styleSheet.cssRules)
{
for(var rulePos = 0; rulePos < styleSheet.cssRules.length-1; rulePos++)
{
if(styleSheet.cssRules[rulePos] !== undefined)
{
// console.log("styleSheet.cssRules[rulePos] "+ stylename);
if(styleSheet.cssRules[rulePos].selectorText)
{
if(styleSheet.cssRules[rulePos].selectorText == stylename)
{
console.log('styleSheet class has been found - style: ' + stylename);
hasStyle = true; //break;
return hasStyle;
}
}// else console.log("undefined styleSheet.cssRules[rulePos] "+ stylename);
}
// else console.log("loop undefined styleSheet.cssRules[rulePos] "+ stylename);
}
} //else console.log("undefined styleSheet.cssRules "+ stylename);
}
//console.log("stylesheet url " + styleSheet.href);
} //else console.log("undefined styleSheet "+ stylename);
if(hasStyle) break;
}
} //else console.log("undefined fullStyleSheets "+ stylename);
console.log("styleSheet class has not been found - style: " + stylename);
return hasStyle;
}
//if(STYLESHEETHIJACKFORBACKGROUND !="")
if (!styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUND) )
STYLESHEETHIJACKFORBACKGROUND = "";
//if(STYLESHEETHIJACKFORTITLE !="")
if (!styleSheetContainsClass(STYLESHEETHIJACKFORTITLE) )
STYLESHEETHIJACKFORTITLE = "";
$('body').append('<div ID="popover" '+stylesheetForBackground()+'></div>');
$('#popover').css('position', 'absolute');
$('#popover').css('z-index', '10');
$('#popover').css('box-shadow', '0px 0px 5px #7A7A7A');
//circle spinner from http://codepen.io/Beaugust/pen/DByiE
//add additional stylesheet for "@keyframe spin" into head after document finishes loading
//@keyframes spin is used for die loading spinner
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = 'body {}';
document.getElementsByTagName('head')[0].appendChild(style);
this.stylesheet = document.styleSheets[document.styleSheets.length-1];
try {
this.stylesheet.insertRule(`
@keyframes spin {
100% {
transform: rotate(360deg);
}
}`, this.stylesheet.cssRules.length);
} catch (e) {alert('error');}
});
function stylesheetForTitle()
{
if(STYLESHEETHIJACKFORTITLE != "")
return 'class="' + STYLESHEETHIJACKFORTITLE + '" style="display:inline-block;width:100%;text-align:center !important"';
else
return 'style="background-color:' + DEFAULTTITLEBACKGROUNDCOLOR+';display:inline-block;width:100%;text-align:center !important"';
}
function stylesheetForBackground()
{
if(STYLESHEETHIJACKFORBACKGROUND != "")
return 'class="'+ STYLESHEETHIJACKFORBACKGROUND+'" style="display:flex !important;flex-direction: column; align-items:center;pointer-events:none; width:auto; height:auto; max-width:500px; max-height:500px;"';
else
return 'style="background-color:'+DEFAULTBACKGROUNDCOLOR+';display:flex !important;flex-direction: column; align-items:center;pointer-events:none; width:auto; height:auto; max-width:500px; max-height:500px;"';
}
//when selected link is entered load imageurl and write popover content
$(concatSelector()).mouseenter(function (e) {
var element = $(this);
var Href = element.attr('href');
if (Href.search(INDIVIDUALPAGETEST) != - 1) //only trigger for links that point to serie pages
{
function inBlocklist(link)
{
if(IMAGEBLOCKERARRAY)
if(IMAGEBLOCKERARRAY.length > 0)
for(var i=0; i < IMAGEBLOCKERARRAY.length; i++)
if(IMAGEBLOCKERARRAY[i] != "")
if( link.match(IMAGEBLOCKERARRAY[i]) )
return true;
return false;
}
function refreshPopover(title, link)
{
/* clear popup
* append title and image into popup
* when img loading is finished reposition (popupPos) to element/border
*/
if(currentTitelHover == title) //popup only gets refreshed when currentTitelHover == title
{
$('#popover').empty();
if( inBlocklist(link) )
{
$('#popover').append('<div '+stylesheetForTitle()+'>'+title+'</div>Blocked Image / No Cover Image / Unwanted Image');
$('#popover').popupPos(e, element, style);
}
else
{
$('#popover').append('<div '+stylesheetForTitle()+'>'+title+'</div><img src="' + link + '" style="margin:5px; width:auto; height:400px !important; max-width:100%; max-height:100%; align-items: stretch;align-self: stretch;object-fit: contain;"></img>');
$('#popover img').on('load', function () {
//console.log(Href + "onload is executed"); // for testing purposes
if(onHover) //is mouse still hovering over same title after loading finishes?
$('#popover').popupPos(e, element, style);
});
}
}
}
//popup loading spinner
function showPopupLoadingSpinner(title, error = false)
{
$('#popover').empty();
if(error)
$('#popover').append('<div '+stylesheetForTitle()+'>'+title+'</div><div style="position: relative;width:150px; height:150px;color:#fff;display: flex; justify-content: center; flex-direction: column; text-align: center;">iamgelink getElementsByClassName is invalid</div>');
else
$('#popover').append('<div '+stylesheetForTitle()+'>'+title+'</div><div style="position: relative;width:150px; height:150px;color:#fff;display: flex; justify-content: center; flex-direction: column; text-align: center;">Getting image url<div style="z-index: -100;position:absolute;top:0;left:0;background-color:#000; box-sizing: border-box; width: 150px; height: 150px; border-radius: 100%; border: 10px solid rgba(255, 255, 255, 0.2); border-top-color: #FFF; animation: spin 1s infinite linear;"></div></div>');
$('#popover').popupPos(e, element, style);
}
//async wait until image is loaded before refreshing popup
function ajaxLoadImageUrlAndShowPopup(url, title)
{
$.ajax({
url: Href,
type: "GET",
dataType: 'text',
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function (data) {
if (!data && data == " ") {
console.log('response to ' + Href + ' was empty');
showPopupLoadingSpinner(serieTitle,1);
}
else
{
//vvv get imageurl link vvv
var htmlData = $('<div>').html(data)[0];
var imagelinkLoggedIn = htmlData.getElementsByClassName(IMAGELINKCONTAINER)[CONTAINERNUMBER];
var imagelink;
if(imagelinkLoggedIn) //need to consider different img classes for logged in and anonymous users
imagelink = imagelinkLoggedIn.getElementsByTagName('img')[0].src;
else
imagelink = htmlData.getElementsByClassName(IMAGELINKCONTAINERANONYM)[CONTAINERNUMBER].getElementsByTagName('img')[0].src;
//console.log("imagelink: " + imagelink);
// ^^^ get imageurl link ^^^
refreshPopover(serieTitle, imagelink); //popup only gets refreshed when currentTitelHover == serieTitle
// cache info
GM_setCachedValue(Href, imagelink); //cache imageurl link
console.log(Href + " url has been found and is written to temporary cache.\n"+ imagelink + ' successfully cached.'); // for testing purposes
}
}
});
}
onHover = true;
$('#popover').show();
var shortSerieTitle = element.text(); //get linkname
//move native title to custom data attribute. Suppress nativ title
if(!element.attr('datatitle'))
{
element.attr('datatitle', element.attr('title'));
element.removeAttr('title');
}
var serieTitle = element.attr('datatitle'); //try to get nativ title if available from datatitle
if(!serieTitle) //has no set nativ long title -> use (available shortend) linkname
serieTitle = shortSerieTitle;
else //no need to run check if it is already shortSerieTitle
if(serieTitle.match(PREDIFINEDNATIVTITLE)) //catch on individual serie page nativ title begins with "Recommended by" x people -> use linkname
serieTitle = shortSerieTitle;
currentTitelHover = serieTitle; //mark which titel is currently hovered
var retrievedImgLink = GM_getCachedValue(Href); //was imageurl cached?
if (retrievedImgLink)
{
refreshPopover(serieTitle, retrievedImgLink); //popup only gets refreshed when currentTitelHover == serieTitle
console.log(retrievedImgLink +' on the page '+ Href + " has been found and retrieved from the cache."); // for testing purposes
}
else
{
showPopupLoadingSpinner(serieTitle);
ajaxLoadImageUrlAndShowPopup(Href, serieTitle);
}
}
});
//hide and empty popup when mouse is not over title
$(concatSelector()).mouseleave(function () { //close popup when mouse leaves titlelink
//$('#popover').empty();
$('#popover').hide();
onHover = false;
});
$(document).mouseleave(function () { //force close when mouse is outside window and previous mouseleave does not get called
$('#popover').hide();
onHover = false;
});
$(window).blur(function () { //chrome fix -> force close when mouse is outside window alt + tab
$('#popover').hide();
onHover = false;
});