// ==UserScript==
// https://gf.qytechs.cn/scripts/26439-novelupdates-cover-preview/
// @name novelupdates Cover Preview
// @namespace somethingthatshouldnotclashwithotherscripts
// @include https://www.novelupdates.com/*
// @include http://www.novelupdates.com/*
// @include https://forum.novelupdates.com/*
// @include http://forum.novelupdates.com/*
// @version 1.5.4
// @description Previews covers in novelupdates.com when hovering over hyperlinks that lead to novel pages.
// @inject-into content
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @run-at document-end
// @license http://creativecommons.org/licenses/by-nc-sa/4.0/
// ==/UserScript==
const MAXCACHEAGE = 7 * 24 * 60 * 60 * 1000; // Max Age before Cached data gets overridden with current data. Max Age is 3 day in milliseconds //days * h * min * sec * ms
let STYLESHEETHIJACKFORBACKGROUND = ".l-canvas"; //if unknown set empty ""; classname with leading dot
let STYLESHEETHIJACKFORTITLE = '.widgettitle_nuf'; //if unknown set empty ""; classname with leading dot
const DEFAULTTITLEBACKGROUNDCOLOR = '#2c3e50'; //if no hijack class style available use plain color
const DEFAULTBACKGROUNDCOLOR = '#ccc'; //if no hijack class style available use plain color
//const SELECTOR1 = 'td a'; //index/group/readinglist pages , forum
//const SELECTOR2 = '.wpb_wrapper > a, .messageContent a'; //individual serie pages recommendation titles //, .signature a //links in forum signatures
const PREDIFINEDNATIVTITLE = "^Recommended by"; //in case native title is used to display something different
const INDIVIDUALPAGETEST = "novelupdates.com/series/";
const maxWaitingTime = 120;
const IMAGELINKCONTAINERS = '.serieseditimg, .seriesimg'; //instead of single element class name with dot
//const IMAGELINKCONTAINERSnonJquery = 'serieseditimg seriesimg'; //instead of single element class name with dot
const IMAGEBLOCKER = "www.novelupdates.com/img/noimagefound.jpg"; //tested with string.match(). no need for prefixed http https in url. Can even be just the file name
const CONTAINERNUMBER = 0;
const preloadUrlRequests = true;
const preloadImages = false;
const isOnIndex = this.location.href == "https://www.novelupdates.com/" || this.location.href.startsWith("https://www.novelupdates.com/?pg=") == 1
const isOnReadingListIndex = this.location.href.startsWith("https://www.novelupdates.com/user/");
//to know when to switch between popup next to link or next to container of link
//^^^^ frontend settings over this line ^^^^
const version = "1.5.4";
const forceUpdate = false;
const RE = /\s*,\s*/; //Regex for split and remove empty spaces
const defaultHeight = "400"; //in pixel
const IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE);
let showDetails = false;
let ALLSERIENODES;// = document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]');
const offsetToBottomBorderY = 22; //offset to bottom border
const offsetToRightBorderX = 10; //offset to right border
let currentTitelHover, currentCoverData, currentPopupEvent;
let popover, popover2, popoverTitle, popoverContent, popoverCoverImg;
//console.log(this.location)
//console.log(this.location.href)
//console.log("isOnIndex: " + isOnIndex)
//get value from key. Decide if timestamp is older than MAXCACHEAGE than look for new image
function GM_getCachedValue(key) {
const DEBUG = false;
const currentTime = Date.now();
const rawCover = GM_getValue(key, null);
DEBUG && console.group("GM_getCachedValue")
DEBUG && console.log("rawCover: " + rawCover)
let result = null;
if (rawCover === null || rawCover == "null") {
result = null;
}
else {
let coverData;
try { //is json parseable data? if not delete for refreshing
coverData = JSON.parse(rawCover);
DEBUG && console.log("coverData: " + coverData)
DEBUG && console.log(coverData)
if (!(coverData.url && coverData.title && coverData.cachedTime)) //has same variable definitions?
{
GM_deleteValue(key);
result = null;
}
} catch (e) {
GM_deleteValue(key);
result = null;
}
const measuredTimedifference = currentTime - coverData.cachedTime;
if (measuredTimedifference < MAXCACHEAGE) {
result = {
url: coverData.url,
title: coverData.title,
votes: coverData.votes,
status: coverData.status,
genre: coverData.genre,
showTags: coverData.showTags
};
}
else {
{
GM_deleteValue(key);
result = null;
}
}
}
DEBUG && console.groupEnd("GM_getCachedValue")
DEBUG && console.log(result)
return result;
}
//set value and currenttime for key
function GM_setCachedValue(key, coverData) {
const DEBUG = false;
const cD = {
url: coverData.url,
title: coverData.title,
votes: coverData.votes,
status: coverData.status,
genre: coverData.genre,
showTags: coverData.showTags,
cachedTime: Date.now()
};
GM_setValue(key, JSON.stringify(cD));
DEBUG && console.group("GM_setCachedValue")
DEBUG && console.log("save coverdata")
DEBUG && console.log(cD)
DEBUG && console.group("GM_setCachedValue")
}
function inBlocklist(link) {
if (IMAGEBLOCKERARRAY)
if (IMAGEBLOCKERARRAY.length > 0)
for (let i = 0; i < IMAGEBLOCKERARRAY.length; i++)
if (IMAGEBLOCKERARRAY[i] !== "")
if (link.match(IMAGEBLOCKERARRAY[i]))
return true;
return false;
}
//https://medium.com/@alexcambose/js-offsettop-property-is-not-great-and-here-is-why-b79842ef7582
const getOffset = (element, horizontal = false) => {
if (!element) return 0;
return getOffset(element.offsetParent, horizontal) + (horizontal ? element.offsetLeft : element.offsetTop);
}
function getRectOffset(rect) {
return { Rx: rect.left + rect.width, Ry: rect.top }
}
function chooseAndGetRectOffset(nativElement) {
let targetedRect;
if (isOnIndex || isOnReadingListIndex) {
targetedRect = nativElement.parentElement.getBoundingClientRect();
}
else {
targetedRect = nativElement.getBoundingClientRect();
}
return getRectOffset(targetedRect);
}
function getDistanceToBottom(Y, scrollPosY, popoverRect) {
return Y - scrollPosY + popoverRect.height - (window.innerHeight - offsetToBottomBorderY);
}
function getPopupPos(event) {
const DEBUG = false;
const scrollPosY = window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop;
const scrollPosX = window.scrollX || window.scrollLeft || document.getElementsByTagName("html")[0].scrollLeft;
//console.log(event)
const nativElement = event.target;
const parentElement = nativElement.parentElement;
let X, Y;
let distanceToBottom, distanceToRight;
//console.log(element.parents()[0])
DEBUG && console.log(nativElement)
X = scrollPosX;
Y = scrollPosY;
DEBUG && console.group("rects")
DEBUG && console.log(nativElement.getBoundingClientRect())
DEBUG && console.log(parentElement.getBoundingClientRect())
DEBUG && console.groupEnd("rects")
const popoverRect = popover.getBoundingClientRect();
const { Rx, Ry } = chooseAndGetRectOffset(nativElement);
X += Rx;
Y += Ry;
DEBUG && console.log(popoverRect)
DEBUG && console.group("calc vertical offset");
distanceToBottom = getDistanceToBottom(Y, scrollPosY, popoverRect);
//console.log("distanceToBottom: " + distanceToBottom)
if (distanceToBottom > 0) {//bottom offset
Y -= distanceToBottom;
}
//console.log("Y: " + Y + ", scrollPosY: " + scrollPosY);
if (Y < scrollPosY + offsetToBottomBorderY) { //top offset
Y = scrollPosY + offsetToBottomBorderY;
}
DEBUG && console.groupEnd("calc vertical offset");
//console.log(popover.getBoundingClientRect())
DEBUG && console.group("calc horizontal offset");
const maxRightPos = scrollPosX + window.innerWidth;
const popoverRightSide = X + popoverRect.width + offsetToRightBorderX;
distanceToRight = popoverRightSide - maxRightPos;
DEBUG && console.log("X: " + X + ", popoverRightSide: " + popoverRightSide +
", maxRightPos: " + maxRightPos +
", distanceToRight: " + distanceToRight +
", popoverRect.width: " + popoverRect.width + ", scrollPosX: " + scrollPosX);
if (distanceToRight > 0) {
X -= distanceToRight + offsetToRightBorderX;
}
/*
if (X < scrollPosX + offsetToRightBorderX) {
X = scrollPosX + offsetToRightBorderX;
}
*/
DEBUG && console.groupEnd("calc horizontal offset");
return { Px: X, Py: Y }
}
// popupPositioning function
function popupPos(event) {
const DEBUG = false;
DEBUG && console.group("popupPos style:" + style)
//console.log(nativElement.parentElement)
//let computedFontSizeJquery = parseInt(window.getComputedStyle(element.parents()[0]).fontSize);
//const computedFontSize = parseInt(window.getComputedStyle(parentElement).fontSize);
//console.log(computedFontSize);
//Initialising variables (multiple usages)
// console.log(scrollPosX)
//var elementPopup = this[0]; //this = ontop of jquery object
let elementImg = popover.getElementsByTagName("img");
DEBUG && console.log(popover)
DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight)
DEBUG && console.log("popover[0].offsetHeight: " + popover.offsetHeight)
DEBUG && console.log(elementImg)
if (elementImg) {
DEBUG && console.log(elementImg)
}
const { Px, Py } = getPopupPos(event)
popover.style.top = Py + 'px';
popover.style.left = Px + 'px';
const popoverHeightMargin = offsetToBottomBorderY * 2;
const popoverWidthMargin = offsetToRightBorderX * 2;
popover.style.height = "calc(100% - " + popoverHeightMargin + "px)";
popover.style.width = "calc(100% - " + popoverWidthMargin + "px)";
DEBUG && console.log(popover.getBoundingClientRect())
DEBUG && console.log("window.innerHeight: " + window.innerHeight + ", window.innerWidth: " + window.innerWidth +
", maxRightPos: " + maxRightPos + ", popoverHeightMargin: " + popoverHeightMargin)
showPopOver();
DEBUG && console.groupEnd("popupPos")
//console.log("final popup position "+X+' # '+Y);
return this;
};
async function parseSeriePage(elementUrl, title = undefined, event = undefined) {
const DEBUG = false;
DEBUG && console.group("parseSeriePage: " + elementUrl)
let retrievedImgLink;
let PromiseResult = new Promise(async function (resolve, reject) {
DEBUG && console.log("elementUrl: " + elementUrl)
DEBUG && console.log(elementUrl)
const coverData = GM_getCachedValue(elementUrl);
//DEBUG && console.log("elementUrl: " + elementUrl);
//DEBUG && console.log("retrievedImgLink cache value: " + retrievedImgLink)
if (coverData !== null) {//retrievedImgLink !== null || retrievedImgLink!==undefined &&
//currentTitelHover = coverData.title;
DEBUG && console.log(coverData)
retrievedImgLink = coverData.url;
DEBUG && console.log("parseSeriePage has cached retrievedImgLink: " + retrievedImgLink)
return resolve(coverData);
//resolve(retrievedImgLink);
}
else {
// DEBUG && console.log(coverData)
DEBUG && console.log(" - retrievedImgLink cache empty. make ajax request try to save image of page into cache: " + elementUrl);
function onLoad(xhr) {
const domDocument = xhr.response;
//const parser = new DOMParser();
// const domDocument = parser.parseFromString(xhr.responseText, 'text/html');
DEBUG && console.log(domDocument);
try {
DEBUG && console.group("parseSeriePage onLoad: " + title)
if (!domDocument || domDocument === undefined) {
console.log(xhr);
console.log(xhr.response);
console.log(domDocument)
}
const temp = domDocument.querySelectorAll(IMAGELINKCONTAINERS);
const imageLinkByTag = temp[0].getElementsByTagName("img");
const imagelink = imageLinkByTag[CONTAINERNUMBER].getAttribute("src");
const serieTitle = domDocument.querySelector(".seriestitlenu").textContent;
const serieVotes = domDocument.querySelector(".seriesother > .uvotes").textContent;
const serieStatus = domDocument.querySelector("#editstatus").textContent;
const serieGenre = domDocument.querySelector("#seriesgenre").textContent;
const serieShowtags = domDocument.querySelector("#showtags").textContent;
DEBUG && console.log(serieTitle)
DEBUG && console.log(serieVotes)
DEBUG && console.log(serieStatus)
DEBUG && console.log(serieGenre)
DEBUG && console.log(serieShowtags)
DEBUG && console.log('save imageUrl as retrievedImgLink ' + imagelink);
let cData = {
url: imagelink,
title: serieTitle,
votes: serieVotes,
status: serieStatus,
genre: serieGenre,
showTags: serieShowtags
};
retrievedImgLink = imagelink;
//currentTitelHover = serieTitle;
GM_setCachedValue(elementUrl, cData); //cache imageurl link
DEBUG && console.log(elementUrl + " url has been found and is written to temporary cache.\n" + imagelink + ' successfully cached.'); // for testing purposes
DEBUG && console.groupEnd("parseSeriePage onLoad")
return resolve(cData);
//resolve(imagelink);
} catch (error) {
console.log("error: GM_xmlhttpRequest can not get xhr.response")
console.log(error);
// showPopupLoadingSpinner(serieTitle, 1);
return reject(elementUrl);
}
}
function onError() {
const err = new Error('GM_xmlhttpRequest could not load ' + elementUrl + "; url does not exist?");
console.log(err);
return reject(err);
}
GM_xmlhttpRequest({
method: "GET",
responseType: 'document',
url: elementUrl,
onload: onLoad,
onerror: onError,
});
}
});
//DEBUG && console.log(PromiseResult)
if (retrievedImgLink) {
DEBUG && console.log("has retrievedImgLink: " + retrievedImgLink)
}
else {
//DEBUG && console.log("retrievedImgLink still loading ")
if (currentTitelHover == title) {
//console.log(PromiseResult)
//console.log("showPopupLoadingSpinner parseSeriePage: " + title)
if (event)
showPopupLoadingSpinner(title, event);
//console.log("showPopupLoadingSpinner parseSeriePage after showPopupLoadingSpinner function: " + title)
}
}
DEBUG && console.groupEnd("parseSeriePage: " + elementUrl)
await PromiseResult;
//DEBUG && console.log(PromiseResult)
//after GM_xmlhttpRequest PromiseResult
return PromiseResult;
}
function checkDataVersion() {
//Remove possible incompatible old data
const DEBUG = false;
const dataVersion = GM_getValue("version", null)
DEBUG && console.log("dataVersion: " + dataVersion)
if (dataVersion === null || dataVersion != version || forceUpdate) {
const oldValues = GM_listValues();
DEBUG && console.log("oldValues.length: " + oldValues.length)
for (let i = 0; i < oldValues.length; i++) {
GM_deleteValue(oldValues[i]);
//console.log(oldValues[i])
}
DEBUG && console.log(oldValues);
GM_setValue("version", version);
}
}
function preloadCoverData() {
const DEBUG = false;
updateSerieNodes();
DEBUG && console.log("preloadCoverData");
if (preloadUrlRequests) {
const novelLinks = Array.from(
ALLSERIENODES //document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]')
);
DEBUG && console.log(novelLinks);
DEBUG && console.log("parseSeriePage for each url with a link to individual seriepage");
novelLinks.map(function (el) {
//console.log(el)
const elementUrl = el.href;
// console.log(elementUrl)
el.removeEventListener("mouseenter", mouseEnterPopup)
el.removeEventListener("mouseleave", hideOnMouseLeave)
el.addEventListener("mouseenter", mouseEnterPopup)
el.addEventListener("mouseleave", hideOnMouseLeave)
parseSeriePage(elementUrl).then(function (coverData) {
if (preloadImages) {
console.log("preloadCoverData preloadImages: " + preloadImages)
/*
let img = document.createElement("img"); //put img into dom. Let the image preload in background
img.onload = () => {
DEBUG && console.log("onpageload cache init previewImage " + coverData.url);
}
img.src = coverData.url
*/
console.log(coverData)
loadImageFromBrowser(coverData);
}
}, function (Error) {
DEBUG && console.log(Error + ' failed to fetch ' + el);
});
});
}
}
function loadStyleSheets() {
//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 the loading spinner
GM_addStyle(`
@keyframes rotate {
to {transform: rotate(360deg);}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 150;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -35;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -124;
}
}
.popoverContent {
display:flex;
position: relative;
width: 100%;
height: 100%;
border: 1px solid #000;
text-align: center !important;
justify-content: center;
justify-items: center;
background-color:#ccc;
align-items: center;
min-height:0;
min-width:0;
/*max-height:inherit;
max-width:inherit;
height:100%;
width:100%;*/
flex:1;
padding:1px;
}
.spinnerRotation{
animation: rotate 2s linear infinite;
}
.spinner {
/*
z-index: 2;
position: absolute;
top: 0;
left: 0;
margin: 0;*/
width: 100%;
height: 100%;
}
.spinner .path{
stroke: hsl(210, 70%, 75%);
stroke-linecap: round;
animation: dash 1.5s ease-in-out infinite;
}
.blackFont {
color:#000;
}
.whiteFont {
color:#fff
}
.defaultTitleStyle {
color:#fff;
padding:5px 0;
height:auto;
display:inline-block;
width:100%;
max-width:auto;
text-align:center !important;
justify-content: center;
justify-items: center;
border-radius:8px 8px 0 0;
}
.defaultBackgroundStyle {
align-items:center;
pointer-events:none;
width:100%;
height:100%;
max-width:100%;
max-height:100%;
text-align:center !important;
justify-content: center;
justify-items: center;
}
.ImgFitDefault{
object-fit: contain;
width:100%;
height:100%;
}
#popover{
height:100%;
width:100%;
margin:0 0 22px 0;
border: 1px solid #000;
border-radius:10px 10px 5px 5px;
position:absolute;
z-index:10;
box-shadow: 0px 0px 5px #7A7A7A;
display: flex;
flex-direction: column;
text-align: center !important;
justify-content: center;
justify-items: center;
}
.popoverDetail{
flex-direction:unset !important;
}
.popoverTitleDetail{
height:100% !important;
width:auto !important;
max-width:70% !important;
}
.popoverTitle{
height:auto;
display:inline-block;
width:100%;
max-width:auto;
}
.popoverCoverImg{
/*min-height:0;
min-width:0;
max-height:inherit;
max-width:inherit;
height:100%;
width:100%;*/
flex:0;
padding:5px;
}
.smallText{
font-size: 0.8em;
}
.wordBreak {
word-wrap: break-word !important;
word-break: break-word;
}
`);
function styleSheetContainsClass(f) {
var localDomainCheck = '^http://' + document.domain;
var localDomainCheckHttps = '^https://' + document.domain;
// DEBUG && console.log("Domain check with: " + localDomainCheck);
var hasStyle = false;
var stylename = f;
var fullStyleSheets = document.styleSheets;
// DEBUG && console.log("start styleSheetContainsClass " + stylename);
if (fullStyleSheets) {
for (let i = 0; i < fullStyleSheets.length - 1; i++) {
//DEBUG && console.log("loop fullStyleSheets " + stylename);
let styleSheet = fullStyleSheets[i];
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) || styleSheet.href.match(localDomainCheckHttps)) {
if (styleSheet.cssRules) {
//DEBUG && console.log("styleSheet.cssRules.length: " + styleSheet.cssRules.length)
for (let rulePos = 0; rulePos < styleSheet.cssRules.length - 1; rulePos++) {
if (styleSheet.cssRules[rulePos] !== undefined) {
// DEBUG && console.log("styleSheet.cssRules[rulePos] "+ stylename);
if (styleSheet.cssRules[rulePos].selectorText) {
// console.log(styleSheet.cssRules[rulePos].selectorText)
if (styleSheet.cssRules[rulePos].selectorText == stylename) {
// console.log('styleSheet class has been found - class: ' + stylename);
hasStyle = true; //break;
break; //return hasStyle;
}
} //else DEBUG && console.log("undefined styleSheet.cssRules[rulePos] "+rulePos +" - "+ stylename);
}
//else DEBUG && console.log("loop undefined styleSheet.cssRules[rulePos] "+ stylename);
}
} //else DEBUG && console.log("undefined styleSheet.cssRules "+ stylename);
}
// DEBUG && console.log("stylesheet url " + styleSheet.href);
} //else DEBUG && console.log("undefined styleSheet "+ stylename);
if (hasStyle) break;
}
} //else console.log("undefined fullStyleSheets=document.styleSheets "+ stylename);
if (!hasStyle)
console.log("styleSheet class has not been found - style: " + stylename);
return hasStyle;
}
if (STYLESHEETHIJACKFORBACKGROUND !== "")
if (!styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUND))
STYLESHEETHIJACKFORBACKGROUND = "";
else {
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
//works only from firefox 77
//STYLESHEETHIJACKFORBACKGROUND = STYLESHEETHIJACKFORBACKGROUND.replaceAll(".", " ").trim()
STYLESHEETHIJACKFORBACKGROUND = STYLESHEETHIJACKFORBACKGROUND.replace(/\./g, ' ').trim();
}
if (STYLESHEETHIJACKFORTITLE !== "")
if (!styleSheetContainsClass(STYLESHEETHIJACKFORTITLE))
STYLESHEETHIJACKFORTITLE = "";
else {
//STYLESHEETHIJACKFORTITLE = STYLESHEETHIJACKFORTITLE.replaceAll(".", " ").trim()
STYLESHEETHIJACKFORTITLE = STYLESHEETHIJACKFORTITLE.replace(/\./g, ' ').trim();
}
}
function createPopover() {
let bodyElement = document.getElementsByTagName("BODY")[0];
popover = document.createElement("div");
popover.id = "popover";
popoverTitle = document.createElement("header");
popoverContent = document.createElement("content");
// popoverCoverImg = document.createElement("coverImg");
popover.appendChild(popoverTitle);
popover.appendChild(popoverContent);
//popover.appendChild(popoverCoverImg);
popover.className = (STYLESHEETHIJACKFORBACKGROUND + ' defaultBackgroundStyle').trim();
popoverContent.className = "popoverContent blackFont";
popover.style.maxHeight = defaultHeight + "px";
popover.style.maxWidth = defaultHeight + "px";
popover.style.backgroundColor = DEFAULTBACKGROUNDCOLOR;
//console.log(popover)
//console.log(popover.style)
popoverTitle.className = (STYLESHEETHIJACKFORTITLE + ' defaultTitleStyle').trim();
popoverTitle.style.backgroundColor = DEFAULTTITLEBACKGROUNDCOLOR;
//popoverCoverImg.className = "popoverCoverImg";
bodyElement.insertAdjacentElement("beforeend", popover);
}
function stylesheetForTitle() {
if (STYLESHEETHIJACKFORTITLE !== "")
return 'class="' + STYLESHEETHIJACKFORTITLE + ' defaultTitleStyle"';
else
return 'class="defaultTitleStyle" style="background-color:' + DEFAULTTITLEBACKGROUNDCOLOR + '"';
}
function stylesheetForBackground() {
if (STYLESHEETHIJACKFORBACKGROUND !== "")
return 'class="' + STYLESHEETHIJACKFORBACKGROUND + ' defaultBackgroundStyle"';
else
return 'class="defaultBackgroundStyle" style="background-color:' + DEFAULTBACKGROUNDCOLOR + '"';
}
function showPopupLoadingSpinner(title, event, notification = "", coverData = undefined) {
const DEBUG = false;
if (currentTitelHover == title) {
// console.group("showPopupLoadingSpinner")
//popover.empty();
//popover.innerHTML = "";
DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
if (coverData !== undefined) {
//console.log("showPopupLoadingSpinner")
//console.log(coverData)
adjustPopupTitleDetail(coverData, title);
}
else
popoverTitle.textContent = title;
if (notification != "") {
popoverContent.innerHTML = notification;
popoverContent.className = "popoverContent blackFont wordBreak";
}
else {
popoverContent.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
<g transform="translate(25, 25)">
<circle class="" cx="0" cy="0" r="25" fill="black" stroke-width="5" />
<circle class="path" cx="0" cy="0" r="23" fill="none" stroke-width="5">
<animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0" to="360" dur="1.6s" repeatCount="indefinite" />
</circle>
</g>
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" style="fill:#fff;font-size:11px">Loading </text>
</svg>`
//popoverContent.innerHTML = '<div class="forground" style="z-index: 3;">Loading Data</div><svg class="spinner" viewBox="0 0 50 50"><circle class="" cx="25" cy="25" r="22" fill="black" stroke-width="5"></circle><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg>';
popoverContent.className = "popoverContent whiteFont";
}
DEBUG && console.log(popover)
// DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
//console.log(event)
popupPos(event);
// console.groupEnd("showPopupLoadingSpinner")
}
}
/**
* update popupContent and reposition to link
*
* @param {*} title
* @param {*} link
* @param {*} e event
*/
function refreshPopover(coverData, e = undefined) {
//only call when isActivePopup
const DEBUG = false;
DEBUG && console.log("currentTitelHover: " + currentTitelHover)
DEBUG && console.group("refreshPopover");
const link = coverData.url;
const title = coverData.title;
//console.log(coverData)
//console.log(e)
// popoverTitle.textContent = title;
// console.log(link)
if (inBlocklist(link)) {
popoverContent.innerHTML = "Blocked Image<br />No Cover Image<br />Unwanted Image";
} else {
let imgElement = new Image();//document.createElement("img");
imgElement.src = link;
popoverContent.innerHTML = '<img src="' + link + '" class="ImgFitDefault" ></img>';
}
adjustPopupTitleDetail(coverData);
DEBUG && console.groupEnd("refreshPopover");
//if (currentTitelHover == title)
if (e !== undefined)
popupPos(e);
};
const reRating = new RegExp('([0-9\.]+) \/ ([0-9\.]+)');
const reVoteCount = new RegExp('([0-9]+) votes')
function getRatingNumber(ratingString) {
//const ratingString = "Rating(3.3 / 5.0, 1940 votes)"
const matches = ratingString.match(reRating)
const matchesVotes = ratingString.toLowerCase().match(reVoteCount)
//console.log(matches)
//console.log(matches.length)
let hasVotes = true;
// console.log(matchesVotes)
if (matchesVotes && matchesVotes.length > 1) {
//console.log(matchesVotes[1])
if (matchesVotes[1] == 0) {
hasVotes = false;
}
}
let ratingNumber
if (matches && matches.length == 3 && hasVotes) {
//console.log(matches[1])
ratingNumber = matches[1];
}
return ratingNumber;
}
const reChapters = new RegExp('([0-9\.]+)( wn)? chapters');
const reChaptersNumberBehind = new RegExp('chapter ([0-9\.]+)');
function getChapters(statusString) {
let chapterCount;
const matches = statusString.toLowerCase().match(reChapters);
let webnovel = "";
if (matches && matches.length >= 2) {
chapterCount = matches[1];
if (matches[2]) {
webnovel = " WN";
}
}
if (!chapterCount) {
const matchesBehind = statusString.toLowerCase().match(reChaptersNumberBehind);
if (matchesBehind && matchesBehind.length >= 2) {
chapterCount = matchesBehind[1];
}
}
let result;
if (chapterCount) {
result = chapterCount + webnovel + " Chapters"
}
return result;
}
function getCompletedState(statusString) {
let result = false;
if (statusString.toLowerCase().includes("complete")) {//complete | completed
result = true;
}
return result;
}
function geOngoingState(statusString) {
let result = false;
if (statusString.toLowerCase().includes("ongoing")) {
result = true;
}
return result;
}
async function adjustPopupTitleDetail(coverData, title = undefined) {
let titleToShow="";
popoverTitle.textContent = "";
if (coverData && coverData.title)
titleToShow = coverData.title;
else if (title !== undefined) titleToShow = title;
popoverTitle.textContent = titleToShow;
//console.log("adjustPopupTitleDetail - showDetails: " + showDetails)
if (showDetails) {
//console.log("showDetails should be true")
if (coverData.votes && coverData.votes.length > 0) {
popoverTitle.innerHTML += '<hr />Rating: ' + coverData.votes + '<hr />Status: ' + coverData.status + '';
}
if (coverData.genre && coverData.showTags) {
popoverTitle.innerHTML += '<hr />Genre: ' + coverData.genre + "<hr />Tags: " + coverData.showTags;
}
popoverTitle.innerHTML += "<hr /><span>[Press Key 1 to hide details]</span>"
}
else {
//console.log("showDetails should be false")
//if (coverData.votes && coverData.votes.length > 0)
{
let rating = getRatingNumber(coverData.votes);
let chapters = getChapters(coverData.status);
let completed = getCompletedState(coverData.status);
let ongoing = geOngoingState(coverData.status);
if (rating || chapters || completed || ongoing) {
//console.log(rating)
//console.log(chapters)
//console.log(completed)
//console.log(ongoing)
if (rating !== undefined) rating += "★ ";
if (chapters !== undefined) chapters = chapters + " "; else chapters = "";
if (completed) completed = "🗹 "; else completed = ""; //https://www.utf8icons.com/
if (ongoing) ongoing = "✎ "; else ongoing = "";
popoverTitle.innerHTML = titleToShow+
'<span class="smallText" style="white-space: nowrap;"> [' + rating +
chapters +
completed +
ongoing +
']</span>';
}
/*
popoverTitle.innerHTML += '<span class="smallText"> [' + rating + '★] ' + chapters + '</span> ';
if (completed) {
popoverTitle.innerHTML += "🗹 "
}
if (ongoing) {
popoverTitle.innerHTML += "✎ "
}*/
}
popoverTitle.innerHTML += '<br /><span class="smallText">[Press Key 1 to show details]</span>'
}
}
function loadCoverData(coverData, title, e) {
const DEBUG = false;
//GM_getCachedValue
DEBUG && console.group("loadCoverData")
// const coverData = GM_getCachedValue(Href);
DEBUG && console.log(coverData)
const imgUrl = coverData.url;
let serieTitle = title;
if (!title || coverData.title) //pure link without title get title of seriepage
serieTitle = coverData.title;
DEBUG && console.log("imgUrl: " + imgUrl);
DEBUG && console.log("title: " + title + ", serieTitle: " + serieTitle)
if ((currentTitelHover === null || currentTitelHover == "null" || currentTitelHover === undefined) && coverData !== null) {
//console.log(coverData)
currentTitelHover = coverData.title;
}
if (coverData !== undefined && coverData !== null)
currentCoverData = coverData;
if (e)
loadImageFromBrowser(coverData, imgUrl, serieTitle, e, title)
DEBUG && console.groupEnd("loadCoverData")
}
function ajaxLoadImageUrlAndShowPopup(elementUrl, title, e) {
//console.log("mouseenter")
// console.group("ajaxLoadImageUrlAndShowPopup")
return parseSeriePage(elementUrl, title, e)
.then(function (coverData) {
loadCoverData(coverData, title, e)
}, function (Error) {
console.log(Error + ' failed to fetch ' + elementUrl);
});
// console.groupEnd("ajaxLoadImageUrlAndShowPopup")
};
function imageLoaded(coverData, hoveredTitleLink, serieTitle = undefined, e = undefined) {
const DEBUG = false;
const hasMouseEnterEvent = serieTitle && (e !== undefined);
const isActivePopup = ((currentTitelHover !== undefined) && (hoveredTitleLink !== undefined)) && (currentTitelHover == hoveredTitleLink) && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
DEBUG && console.group("loadImageFromBrowser img.onload: " + serieTitle)
DEBUG && console.log("finished loading imgurl: " + coverData.url);
DEBUG && console.log("currentTitelHover: " + currentTitelHover + ", isActivePopup: " + isActivePopup)
DEBUG && console.log("isActivePopup: " + isActivePopup)
if (isActivePopup) {
DEBUG && console.log("refreshPopover")
refreshPopover(coverData, e); //popup only gets refreshed when currentTitelHover == serieTitle
}
DEBUG && console.groupEnd("loadImageFromBrowser img.onload")
}
function imageLoadingError(coverData, error, hoveredTitleLink, serieTitle = undefined, e = undefined) {
console.group("loadImageFromBrowser img.onerror: " + serieTitle)
const hasMouseEnterEvent = serieTitle && (e !== undefined);
const isActivePopup = ((currentTitelHover !== undefined) && (hoveredTitleLink !== undefined)) && (currentTitelHover == hoveredTitleLink) && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
console.log(error);
const errorMessage = "browser blocked/has error loading the file: <br />" + decodeURIComponent(error.target.src);
console.log(errorMessage)
//console.log(window)
console.log(navigator)
//console.log(navigator.userAgent)
const useragentString = navigator.userAgent;
console.log("useragentString: " + useragentString)
const isChrome = useragentString.includes("Chrome")
if (isChrome)
console.log("look in the developer console if 'net::ERR_BLOCKED_BY_CLIENT' is displayed or manually check if the imagelink still exists");
else
console.log("image loading most likely blocked by browser or addon. Check if the imagelink still exists");
if (isActivePopup)
showPopupLoadingSpinner(serieTitle, e, errorMessage, coverData);
console.groupEnd("loadImageFromBrowser img.onerror")
}
function loadImageFromBrowser(coverData, imgUrl, serieTitle = undefined, e = undefined, hoveredTitleLink = undefined) {
const DEBUG = false;
//console.group("loadImageFromBrowser")
let img = document.createElement("img"); //put img into dom. Let the image preload in background
const hasMouseEnterEvent = serieTitle && (e !== undefined);
//console.log(currentCoverData)
//console.log(coverData)
const isActivePopup = ((currentTitelHover !== undefined) && (hoveredTitleLink !== undefined)) && (currentTitelHover == hoveredTitleLink) && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
DEBUG && console.log("loadImageFromBrowser")
DEBUG && console.log(hasMouseEnterEvent)
img.onload = () => { imageLoaded(coverData, hoveredTitleLink, serieTitle, e) };
img.onerror = (error) => { imageLoadingError(coverData, error, hoveredTitleLink, serieTitle, e) }
img.src = imgUrl;
if (img.complete) {
DEBUG && console.log("loadImageFromBrowser preload completed: " + serieTitle)
DEBUG && console.log(img.src)
} else {//if image not available/cached in browser show loading pinner
if (isActivePopup) {
DEBUG && console.log("loadImageFromBrowser image not completely loaded yet. Show loading spinner : " + serieTitle)
showPopupLoadingSpinner(serieTitle, e);
}
}
// console.groupEnd("loadImageFromBrowser")
}
function mouseEnterPopup(e) {
//if (!e.target.matches(concatSelector())) return;
const DEBUG = false;
DEBUG && console.group("mouseEnterPopup")
//let element = undefined;//$(this);
//let nativElement = e.target//this;
//console.log(this)
//console.log(e.target)
let Href = this.href;// element.attr('href');
if (Href.search(INDIVIDUALPAGETEST) != -1) //only trigger for links that point to serie pages
{
//console.log(this)
//console.log(this.text) //shortTitle
//console.log(this.title) //LongTitle
let shortSerieTitle = this.text; //element.text(); //get linkname
//console.log(this)
//console.log(shortSerieTitle)
//move native title to custom data attribute. Suppress nativ title popup
if (!this.getAttribute('datatitle')) {
this.setAttribute('datatitle', this.getAttribute('title'));
this.removeAttribute('title');
}
let serieTitle = this.getAttribute('datatitle');//element.attr('datatitle'); //try to get nativ title if available from datatitle
//console.log(serieTitle)
if (serieTitle === null || serieTitle == "null") //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
currentPopupEvent = e;
//console.log(serieTitle)
//console.log(Href)
//console.log(currentCoverData)
ajaxLoadImageUrlAndShowPopup(Href, currentTitelHover, e);
}
DEBUG && console.groupEnd("mouseEnterPopup")
}
function hidePopOver() {
popover.style.visibility = "hidden";
currentTitelHover = undefined;
currentCoverData = undefined;
}
function showPopOver() {
// popover.style.display = "flex";
popover.style.visibility = "visible";
}
function hideOnMouseLeave() {
//if (!e.target.matches(concatSelector())) return;
//popover.hide();
hidePopOver();
}
function updateSerieNodes() {
if (ALLSERIENODES) {
ALLSERIENODES.forEach(function (selector) {
selector.removeEventListener("mouseleave", hideOnMouseLeave);
selector.removeEventListener("mouseenter", mouseEnterPopup);
})
}
ALLSERIENODES = document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]');
}
function switchDetailsAndUpdatePopup() {
const DEBUG = false;
DEBUG && console.group("switchDetailsAndUpdatePopup")
changeToNewDetailStyle();
//console.log(currentCoverData)
DEBUG && console.log("switchDetails refreshPopup")
DEBUG && console.log(currentCoverData)
if (currentCoverData && currentCoverData !== undefined) {
refreshPopover(currentCoverData, currentPopupEvent); //update on detail change
}
console.groupEnd("switchDetails")
}
function changeToNewDetailStyle(toggleDetails = true) {
if (toggleDetails)
showDetails = !showDetails;
//console.log("switch showDetails to : " + showDetails)
localStorage.setItem("showDetails", showDetails);
if (showDetails) {
popover.classList.add("popoverDetail")
popover.style.maxWidth = defaultHeight * 2 + "px";
popoverTitle.classList.add("popoverTitleDetail")
}
else {
popover.classList.remove("popoverDetail")
popover.style.maxWidth = defaultHeight + "px";
popoverTitle.classList.remove("popoverTitleDetail")
}
}
function reactToKeyPressWhenPopupVisible(event) {
//console.log(event)
//console.log(currentTitelHover)
if (currentTitelHover && currentTitelHover !== undefined) {
if (event.key == "1") {
//switchDetailsAndUpdatePopup();
switchDetailsAndUpdatePopup()
}
}
}
window.addEventListener("blur", hidePopOver);
window.addEventListener("keypress", reactToKeyPressWhenPopupVisible);
window.onunload = function () {
window.removeEventListener("blur", hidePopOver);
window.removeEventListener("keypress", reactToKeyPressWhenPopupVisible)
//possible memoryleaks?
updateSerieNodes();
observer.disconnect();
}
const debouncedpreloadCoverData = debounce(preloadCoverData, 100);
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function (mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
// console.log('A child node has been added or removed.');
//debouncedTest()
debouncedpreloadCoverData();
hidePopOver();
}
else if (mutation.type === 'attributes') {
// console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
function debounce(func, timeout) {
let timer;
return (...args) => {
const next = () => func(...args);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(next, timeout > 0 ? timeout : 300);
};
};
function main() {
checkDataVersion();
loadStyleSheets();
createPopover();
hidePopOver();
showDetails = localStorage.getItem("showDetails") == "true";
//console.log("localStorage state showDetails: " + showDetails)
changeToNewDetailStyle(false);
//console.log("isOnReadingListIndex: " + isOnReadingListIndex)
if (isOnReadingListIndex) {
let targetNode = document.getElementById("profile_content3");
//console.dir(targetNode)
observer.observe(targetNode, config); //observe for update before running debouncedwaitForReadingList();
}
else {
preloadCoverData();
}
}
main();