novelupdates Cover Preview

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

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

// ==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.6.0
// @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"; //forum, index
const INDIVIDUALPAGETEST = "https://www.novelupdates.com/series/";

const maxWaitingTime = 120;
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=") == 1

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

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();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址