novelupdates Cover Preview

Previews covers in novelupdates.com when hovering over hyperlinks that lead to novel pages.

当前为 2020-11-20 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// https://greasyfork.org/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.6.1
// @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 PREDIFINEDNATIVTITLE = "Recommended by"; //forum, index
const INDIVIDUALPAGETEST = "https://www.novelupdates.com/series/";


const IMAGELINKCONTAINERS = '.serieseditimg img, .seriesimg img'; //instead of single element class name with dot
const IMAGEBLOCKER = "https://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 seriePageTitle = ".seriestitlenu " //.seriestitlenu
const seriePageVotes = ".seriesother > .uvotes" //.seriesother > .uvotes
const seriePageStatus = "#editstatus" //#editstatus
const seriePageGenre = "#seriesgenre" //#seriesgenre
const seriePageTags = "#showtags" //#showtags
const isOnIndex = this.location.href == "https://www.novelupdates.com/" ||
    this.location.href.startsWith("https://www.novelupdates.com/?pg=") ||
    this.location.href.startsWith("https://www.novelupdates.com/group/")

const preloadUrlRequests = true;
const preloadImages = false;

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.6.1";
const forceUpdate = false;

const maxWaitingTime = 120;
const RE = /\s*,\s*/; //Regex for split and remove empty spaces
const defaultHeight = "400"; //in pixel
const IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE);
const PREDIFINEDNATIVTITLEARRAY = PREDIFINEDNATIVTITLE.split(RE);
const STYLESHEETHIJACKFORBACKGROUNDARRAY = STYLESHEETHIJACKFORBACKGROUND.split(RE);
const STYLESHEETHIJACKFORTITLEARRAY = STYLESHEETHIJACKFORTITLE.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) {
    //console.log(link)
    if (IMAGEBLOCKERARRAY) {
        const hasBlocker = IMAGEBLOCKERARRAY.includes(link);
        if (hasBlocker) return true;
        //console.log(hasBlocker);
    }

    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);
                    DEBUG && console.log(temp)
                    /*
                    const imageLinkByTag = temp.getElementsByTagName("img");
                    console.log(imageLinkByTag)*/
                    let imagelink = temp[CONTAINERNUMBER]
                    if (imagelink !== undefined)
                        imagelink = imagelink.getAttribute("src");
                    let serieTitle = domDocument.querySelector(seriePageTitle);
                    let serieVotes = domDocument.querySelector(seriePageVotes);
                    let serieStatus = domDocument.querySelector(seriePageStatus);
                    let serieGenre = domDocument.querySelector(seriePageGenre);
                    let serieShowtags = domDocument.querySelector(seriePageTags);
                    if (serieTitle && serieTitle !== undefined) serieTitle = serieTitle.textContent;
                    if (serieVotes && serieVotes !== undefined) serieVotes = serieVotes.textContent;
                    if (serieStatus && serieStatus !== undefined) serieStatus = serieStatus.textContent;
                    if (serieGenre && serieGenre !== undefined) serieGenre = serieGenre.textContent;
                    if (serieShowtags && serieShowtags !== undefined) serieShowtags = serieShowtags.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 or script is not compatible")
                    console.log(error);
                    // showPopupLoadingSpinner(serieTitle, 1);
                    DEBUG && console.groupEnd("parseSeriePage onLoad")
                    return reject(elementUrl);
                }

            }

            function onError() {
                const err = new Error('GM_xmlhttpRequest could not load ' + elementUrl + "; script is not compatible or url does not exists.");
                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");
    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)
        if (preloadUrlRequests) {
            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;
                    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 {
                    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) {
        const DEBUG = false
        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);
                                        //DEBUG && console.log(styleSheet.cssRules[rulePos])
                                        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 (STYLESHEETHIJACKFORBACKGROUND !== "") {
        let styleSheetToAdd = "";
        for (let i = 0; i < STYLESHEETHIJACKFORBACKGROUNDARRAY.length; i++) {
            if (styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUNDARRAY[i])) {
                //console.log("+ has found class: " + STYLESHEETHIJACKFORBACKGROUNDARRAY[i])
                styleSheetToAdd += STYLESHEETHIJACKFORBACKGROUNDARRAY[i];
            }
            else {
                console.log("- has not found class: " + STYLESHEETHIJACKFORBACKGROUNDARRAY[i])
            }
        }
        STYLESHEETHIJACKFORBACKGROUND = styleSheetToAdd.replace(/\./g, ' ').trim();
        //console.log("STYLESHEETHIJACKFORBACKGROUND: " + STYLESHEETHIJACKFORBACKGROUND)
    }

    if (STYLESHEETHIJACKFORTITLE !== "") {
        let styleSheetToAdd = "";
        for (let i = 0; i < STYLESHEETHIJACKFORTITLEARRAY.length; i++) {
            if (styleSheetContainsClass(STYLESHEETHIJACKFORTITLEARRAY[i])) {
                //console.log("+ has found class: " + STYLESHEETHIJACKFORTITLEARRAY[i])
                styleSheetToAdd += STYLESHEETHIJACKFORTITLEARRAY[i];
            }
            else {
                console.log("- has not found class: " + STYLESHEETHIJACKFORTITLEARRAY[i])
            }

        }
        STYLESHEETHIJACKFORTITLE = styleSheetToAdd.replace(/\./g, ' ').trim();
        //console.log("STYLESHEETHIJACKFORTITLE: " + STYLESHEETHIJACKFORTITLE)
    }

}

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";
    if (!STYLESHEETHIJACKFORBACKGROUND && DEFAULTBACKGROUNDCOLOR && DEFAULTBACKGROUNDCOLOR != "")
        popover.style.backgroundColor = DEFAULTBACKGROUNDCOLOR;

    popover.style.maxHeight = defaultHeight + "px";
    popover.style.maxWidth = defaultHeight + "px";

    //console.log(popover)
    //console.log(popover.style)
    popoverTitle.className = (STYLESHEETHIJACKFORTITLE + ' defaultTitleStyle').trim();
    if (!STYLESHEETHIJACKFORTITLE && DEFAULTTITLEBACKGROUNDCOLOR && DEFAULTTITLEBACKGROUNDCOLOR != "")
        popoverTitle.style.backgroundColor = DEFAULTTITLEBACKGROUNDCOLOR;


    bodyElement.insertAdjacentElement("beforeend", popover);
}



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 wordBreak";//blackfont

        }
        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)"
    let ratingNumber;
    if (ratingString) {
        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;
            }
        }

        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 result;
    if (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];
            }
        }

        if (chapterCount) {
            result = chapterCount + webnovel + " Chapters"
        }
    }


    return result;
}

function getCompletedState(statusString) {
    let result = false;
    if (statusString && statusString.toLowerCase().includes("complete")) {//complete | completed
        result = true;
    }
    return result;
}
function geOngoingState(statusString) {
    let result = false;
    if (statusString && 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)
    let completeDetails = titleToShow;
    if (showDetails) {
        //console.log("showDetails should be true")
        if (coverData.votes && coverData.votes.length > 0) {
            completeDetails += '<hr />Rating: ' + coverData.votes;
        }
        if (coverData.status) {
            completeDetails += '<hr />Status: ' + coverData.status;
        }
        if (coverData.genre) {
            completeDetails += '<hr />Genre: ' + coverData.genre;
        }
        if (coverData.showTags) {
            completeDetails += "<hr />Tags: " + coverData.showTags;
        }
        completeDetails += "<hr /><span>[Press Key 1 to hide details]</span>";
    }
    else {
        //console.log("showDetails should be false")

        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 += "★ "; else rating = "";
            if (chapters !== undefined) chapters = chapters + " "; else chapters = "";
            if (completed) completed = "🗹 "; else completed = ""; //https://www.utf8icons.com/
            if (ongoing) ongoing = "✎ "; else ongoing = "";

            completeDetails += '<span class="smallText" style="white-space: nowrap;"> [' +
                rating +
                chapters +
                completed +
                ongoing +
                ']</span>';

        }

        completeDetails += '<br /><span class="smallText">[Press Key 1 to show details]</span>';
        //popoverTitle.innerHTML = completeDetails;
    }
    popoverTitle.innerHTML = completeDetails;
}
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.startsWith(INDIVIDUALPAGETEST)) //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 (PREDIFINEDNATIVTITLEARRAY.includes(serieTitle)) //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 = Array.from(document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]'));

    /*
    console.log(ALLSERIENODES)

    const sliceItemCount = 100;
    if (ALLSERIENODES.length > sliceItemCount) {
        ALLSERIENODES = ALLSERIENODES.slice(0, sliceItemCount);
    }
    console.log(ALLSERIENODES)
    */
}
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();