novelupdates Cover Preview

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

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

// ==UserScript==
// https://gf.qytechs.cn/scripts/26439-novelupdates-cover-preview/
// @name        novelupdates Cover Preview
// @namespace   somethingthatshouldnotclashwithotherscripts
// @include     https://www.novelupdates.com/*
// @include     http://www.novelupdates.com/*
// @include     https://forum.novelupdates.com/*
// @include     http://forum.novelupdates.com/*
// @version     1.5.4
// @description Previews covers in novelupdates.com when hovering over hyperlinks that lead to novel pages.
// @inject-into content
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @run-at   	document-end
// @license     http://creativecommons.org/licenses/by-nc-sa/4.0/
// ==/UserScript==
const MAXCACHEAGE = 7 * 24 * 60 * 60 * 1000; // Max Age before Cached data gets overridden with current data. Max Age is 3 day in milliseconds  //days * h * min  * sec * ms
let STYLESHEETHIJACKFORBACKGROUND = ".l-canvas"; //if unknown set empty ""; classname with leading dot
let STYLESHEETHIJACKFORTITLE = '.widgettitle_nuf'; //if unknown set empty ""; classname with leading dot
const DEFAULTTITLEBACKGROUNDCOLOR = '#2c3e50'; //if no hijack class style available use plain color
const DEFAULTBACKGROUNDCOLOR = '#ccc'; //if no hijack class style available use plain color

//const SELECTOR1 = 'td a'; //index/group/readinglist pages , forum
//const SELECTOR2 = '.wpb_wrapper > a, .messageContent a'; //individual serie pages recommendation titles //, .signature a //links in forum signatures
const PREDIFINEDNATIVTITLE = "^Recommended by"; //in case native title is used to display something different
const INDIVIDUALPAGETEST = "novelupdates.com/series/";

const maxWaitingTime = 120;
const IMAGELINKCONTAINERS = '.serieseditimg, .seriesimg'; //instead of single element class name with dot
//const IMAGELINKCONTAINERSnonJquery = 'serieseditimg seriesimg'; //instead of single element class name with dot
const IMAGEBLOCKER = "www.novelupdates.com/img/noimagefound.jpg"; //tested with string.match(). no need for prefixed http https in url. Can even be just the file name
const CONTAINERNUMBER = 0;
const preloadUrlRequests = true;
const preloadImages = false;
const isOnIndex = this.location.href == "https://www.novelupdates.com/" || this.location.href.startsWith("https://www.novelupdates.com/?pg=") == 1
const isOnReadingListIndex = this.location.href.startsWith("https://www.novelupdates.com/user/");
//to know when to switch between popup next to link or next to container of link
//^^^^	frontend settings over this line	^^^^
const version = "1.5.4";
const forceUpdate = false;

const RE = /\s*,\s*/; //Regex for split and remove empty spaces
const defaultHeight = "400"; //in pixel
const IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE);
let showDetails = false;
let ALLSERIENODES;// = document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]');
const offsetToBottomBorderY = 22; //offset to bottom border
const offsetToRightBorderX = 10; //offset to right border
let currentTitelHover, currentCoverData, currentPopupEvent;
let popover, popover2, popoverTitle, popoverContent, popoverCoverImg;

//console.log(this.location)
//console.log(this.location.href)

//console.log("isOnIndex: " + isOnIndex)

//get value from key. Decide if timestamp is older than MAXCACHEAGE than look for new image
function GM_getCachedValue(key) {
    const DEBUG = false;
    const currentTime = Date.now();
    const rawCover = GM_getValue(key, null);
    DEBUG && console.group("GM_getCachedValue")
    DEBUG && console.log("rawCover: " + rawCover)
    let result = null;
    if (rawCover === null || rawCover == "null") {
        result = null;
    }
    else {
        let coverData;
        try { //is json parseable data? if not delete for refreshing
            coverData = JSON.parse(rawCover);
            DEBUG && console.log("coverData: " + coverData)
            DEBUG && console.log(coverData)
            if (!(coverData.url && coverData.title && coverData.cachedTime)) //has same variable definitions?
            {
                GM_deleteValue(key);
                result = null;
            }
        } catch (e) {
            GM_deleteValue(key);
            result = null;
        }


        const measuredTimedifference = currentTime - coverData.cachedTime;
        if (measuredTimedifference < MAXCACHEAGE) {
            result = {
                url: coverData.url,
                title: coverData.title,
                votes: coverData.votes,
                status: coverData.status,
                genre: coverData.genre,
                showTags: coverData.showTags
            };
        }
        else {
            {
                GM_deleteValue(key);
                result = null;
            }
        }
    }
    DEBUG && console.groupEnd("GM_getCachedValue")
    DEBUG && console.log(result)

    return result;
}


//set value and currenttime for key
function GM_setCachedValue(key, coverData) {
    const DEBUG = false;
    const cD = {
        url: coverData.url,
        title: coverData.title,
        votes: coverData.votes,
        status: coverData.status,
        genre: coverData.genre,
        showTags: coverData.showTags,
        cachedTime: Date.now()
    };
    GM_setValue(key, JSON.stringify(cD));
    DEBUG && console.group("GM_setCachedValue")
    DEBUG && console.log("save coverdata")
    DEBUG && console.log(cD)
    DEBUG && console.group("GM_setCachedValue")
}

function inBlocklist(link) {
    if (IMAGEBLOCKERARRAY)
        if (IMAGEBLOCKERARRAY.length > 0)
            for (let i = 0; i < IMAGEBLOCKERARRAY.length; i++)
                if (IMAGEBLOCKERARRAY[i] !== "")
                    if (link.match(IMAGEBLOCKERARRAY[i]))
                        return true;
    return false;
}

//https://medium.com/@alexcambose/js-offsettop-property-is-not-great-and-here-is-why-b79842ef7582
const getOffset = (element, horizontal = false) => {
    if (!element) return 0;
    return getOffset(element.offsetParent, horizontal) + (horizontal ? element.offsetLeft : element.offsetTop);
}

function getRectOffset(rect) {
    return { Rx: rect.left + rect.width, Ry: rect.top }
}
function chooseAndGetRectOffset(nativElement) {
    let targetedRect;
    if (isOnIndex || isOnReadingListIndex) {
        targetedRect = nativElement.parentElement.getBoundingClientRect();
    }
    else {
        targetedRect = nativElement.getBoundingClientRect();
    }
    return getRectOffset(targetedRect);
}
function getDistanceToBottom(Y, scrollPosY, popoverRect) {
    return Y - scrollPosY + popoverRect.height - (window.innerHeight - offsetToBottomBorderY);
}
function getPopupPos(event) {
    const DEBUG = false;

    const scrollPosY = window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop;
    const scrollPosX = window.scrollX || window.scrollLeft || document.getElementsByTagName("html")[0].scrollLeft;
    //console.log(event)
    const nativElement = event.target;
    const parentElement = nativElement.parentElement;

    let X, Y;
    let distanceToBottom, distanceToRight;

    //console.log(element.parents()[0])
    DEBUG && console.log(nativElement)

    X = scrollPosX;
    Y = scrollPosY;

    DEBUG && console.group("rects")
    DEBUG && console.log(nativElement.getBoundingClientRect())
    DEBUG && console.log(parentElement.getBoundingClientRect())
    DEBUG && console.groupEnd("rects")
    const popoverRect = popover.getBoundingClientRect();
    const { Rx, Ry } = chooseAndGetRectOffset(nativElement);
    X += Rx;
    Y += Ry;
    DEBUG && console.log(popoverRect)

    DEBUG && console.group("calc vertical offset");
    distanceToBottom = getDistanceToBottom(Y, scrollPosY, popoverRect);
    //console.log("distanceToBottom: " + distanceToBottom)
    if (distanceToBottom > 0) {//bottom offset
        Y -= distanceToBottom;
    }
    //console.log("Y: " + Y + ", scrollPosY: " + scrollPosY);
    if (Y < scrollPosY + offsetToBottomBorderY) { //top offset
        Y = scrollPosY + offsetToBottomBorderY;
    }
    DEBUG && console.groupEnd("calc vertical offset");
    //console.log(popover.getBoundingClientRect())
    DEBUG && console.group("calc horizontal offset");

    const maxRightPos = scrollPosX + window.innerWidth;
    const popoverRightSide = X + popoverRect.width + offsetToRightBorderX;
    distanceToRight = popoverRightSide - maxRightPos;
    DEBUG && console.log("X: " + X + ", popoverRightSide: " + popoverRightSide +
        ", maxRightPos: " + maxRightPos +
        ", distanceToRight: " + distanceToRight +
        ", popoverRect.width: " + popoverRect.width + ", scrollPosX: " + scrollPosX);
    if (distanceToRight > 0) {
        X -= distanceToRight + offsetToRightBorderX;
    }
    /*
    if (X < scrollPosX + offsetToRightBorderX) {
        X = scrollPosX + offsetToRightBorderX;
    }
    */
    DEBUG && console.groupEnd("calc horizontal offset");
    return { Px: X, Py: Y }
}
// popupPositioning function
function popupPos(event) {
    const DEBUG = false;
    DEBUG && console.group("popupPos style:" + style)

    //console.log(nativElement.parentElement)

    //let computedFontSizeJquery = parseInt(window.getComputedStyle(element.parents()[0]).fontSize);
    //const computedFontSize = parseInt(window.getComputedStyle(parentElement).fontSize);
    //console.log(computedFontSize);

    //Initialising variables (multiple usages)

    // console.log(scrollPosX)
    //var elementPopup = this[0]; //this = ontop of jquery object
    let elementImg = popover.getElementsByTagName("img");

    DEBUG && console.log(popover)
    DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight)
    DEBUG && console.log("popover[0].offsetHeight: " + popover.offsetHeight)
    DEBUG && console.log(elementImg)
    if (elementImg) {
        DEBUG && console.log(elementImg)
    }


    const { Px, Py } = getPopupPos(event)


    popover.style.top = Py + 'px';
    popover.style.left = Px + 'px';

    const popoverHeightMargin = offsetToBottomBorderY * 2;
    const popoverWidthMargin = offsetToRightBorderX * 2;

    popover.style.height = "calc(100% - " + popoverHeightMargin + "px)";
    popover.style.width = "calc(100% - " + popoverWidthMargin + "px)";

    DEBUG && console.log(popover.getBoundingClientRect())
    DEBUG && console.log("window.innerHeight: " + window.innerHeight + ", window.innerWidth: " + window.innerWidth +
        ", maxRightPos: " + maxRightPos + ", popoverHeightMargin: " + popoverHeightMargin)

    showPopOver();
    DEBUG && console.groupEnd("popupPos")
    //console.log("final popup position "+X+' # '+Y);
    return this;
};

async function parseSeriePage(elementUrl, title = undefined, event = undefined) {
    const DEBUG = false;
    DEBUG && console.group("parseSeriePage: " + elementUrl)
    let retrievedImgLink;
    let PromiseResult = new Promise(async function (resolve, reject) {
        DEBUG && console.log("elementUrl: " + elementUrl)
        DEBUG && console.log(elementUrl)
        const coverData = GM_getCachedValue(elementUrl);

        //DEBUG && console.log("elementUrl: " + elementUrl);
        //DEBUG && console.log("retrievedImgLink cache value: " + retrievedImgLink)

        if (coverData !== null) {//retrievedImgLink !== null || retrievedImgLink!==undefined &&
            //currentTitelHover = coverData.title;
            DEBUG && console.log(coverData)
            retrievedImgLink = coverData.url;
            DEBUG && console.log("parseSeriePage has cached retrievedImgLink: " + retrievedImgLink)
            return resolve(coverData);
            //resolve(retrievedImgLink);
        }
        else {
            // DEBUG && console.log(coverData)
            DEBUG && console.log(" - retrievedImgLink cache empty. make ajax request try to save image of page into cache: " + elementUrl);

            function onLoad(xhr) {

                const domDocument = xhr.response;
                //const parser = new DOMParser();
                // const domDocument = parser.parseFromString(xhr.responseText, 'text/html');
                DEBUG && console.log(domDocument);

                try {
                    DEBUG && console.group("parseSeriePage onLoad: " + title)
                    if (!domDocument || domDocument === undefined) {
                        console.log(xhr);
                        console.log(xhr.response);
                        console.log(domDocument)
                    }

                    const temp = domDocument.querySelectorAll(IMAGELINKCONTAINERS);
                    const imageLinkByTag = temp[0].getElementsByTagName("img");
                    const imagelink = imageLinkByTag[CONTAINERNUMBER].getAttribute("src");
                    const serieTitle = domDocument.querySelector(".seriestitlenu").textContent;
                    const serieVotes = domDocument.querySelector(".seriesother > .uvotes").textContent;
                    const serieStatus = domDocument.querySelector("#editstatus").textContent;
                    const serieGenre = domDocument.querySelector("#seriesgenre").textContent;
                    const serieShowtags = domDocument.querySelector("#showtags").textContent;

                    DEBUG && console.log(serieTitle)
                    DEBUG && console.log(serieVotes)
                    DEBUG && console.log(serieStatus)
                    DEBUG && console.log(serieGenre)
                    DEBUG && console.log(serieShowtags)
                    DEBUG && console.log('save imageUrl as retrievedImgLink ' + imagelink);
                    let cData = {
                        url: imagelink,
                        title: serieTitle,
                        votes: serieVotes,
                        status: serieStatus,
                        genre: serieGenre,
                        showTags: serieShowtags
                    };
                    retrievedImgLink = imagelink;
                    //currentTitelHover = serieTitle;
                    GM_setCachedValue(elementUrl, cData); //cache imageurl link
                    DEBUG && console.log(elementUrl + " url has been found and is written to temporary cache.\n" + imagelink + ' successfully cached.'); // for testing purposes
                    DEBUG && console.groupEnd("parseSeriePage onLoad")
                    return resolve(cData);
                    //resolve(imagelink);

                } catch (error) {
                    console.log("error: GM_xmlhttpRequest can not get xhr.response")
                    console.log(error);
                    // showPopupLoadingSpinner(serieTitle, 1);
                    return reject(elementUrl);
                }

            }

            function onError() {
                const err = new Error('GM_xmlhttpRequest could not load ' + elementUrl + "; url does not exist?");
                console.log(err);
                return reject(err);
            }

            GM_xmlhttpRequest({
                method: "GET",
                responseType: 'document',
                url: elementUrl,
                onload: onLoad,
                onerror: onError,
            });
        }
    });
    //DEBUG && console.log(PromiseResult)
    if (retrievedImgLink) {
        DEBUG && console.log("has retrievedImgLink: " + retrievedImgLink)
    }
    else {
        //DEBUG && console.log("retrievedImgLink still loading ")
        if (currentTitelHover == title) {

            //console.log(PromiseResult)
            //console.log("showPopupLoadingSpinner parseSeriePage: " + title)
            if (event)
                showPopupLoadingSpinner(title, event);
            //console.log("showPopupLoadingSpinner parseSeriePage after showPopupLoadingSpinner function: " + title)
        }
    }
    DEBUG && console.groupEnd("parseSeriePage: " + elementUrl)
    await PromiseResult;
    //DEBUG && console.log(PromiseResult)
    //after GM_xmlhttpRequest PromiseResult

    return PromiseResult;
}


function checkDataVersion() {
    //Remove possible incompatible old data
    const DEBUG = false;
    const dataVersion = GM_getValue("version", null)
    DEBUG && console.log("dataVersion: " + dataVersion)

    if (dataVersion === null || dataVersion != version || forceUpdate) {
        const oldValues = GM_listValues();
        DEBUG && console.log("oldValues.length: " + oldValues.length)
        for (let i = 0; i < oldValues.length; i++) {
            GM_deleteValue(oldValues[i]);
            //console.log(oldValues[i])
        }
        DEBUG && console.log(oldValues);
        GM_setValue("version", version);
    }
}

function preloadCoverData() {
    const DEBUG = false;

    updateSerieNodes();


    DEBUG && console.log("preloadCoverData");
    if (preloadUrlRequests) {
        const novelLinks = Array.from(
            ALLSERIENODES //document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]')
        );
        DEBUG && console.log(novelLinks);

        DEBUG && console.log("parseSeriePage for each url with a link to individual seriepage");
        novelLinks.map(function (el) {
            //console.log(el)
            const elementUrl = el.href;
            // console.log(elementUrl)
            el.removeEventListener("mouseenter", mouseEnterPopup)
            el.removeEventListener("mouseleave", hideOnMouseLeave)
            el.addEventListener("mouseenter", mouseEnterPopup)
            el.addEventListener("mouseleave", hideOnMouseLeave)

            parseSeriePage(elementUrl).then(function (coverData) {
                if (preloadImages) {
                    console.log("preloadCoverData preloadImages: " + preloadImages)
                    /*
                    let img = document.createElement("img"); //put img into dom. Let the image preload in background
                    img.onload = () => {
                        DEBUG && console.log("onpageload cache init previewImage " + coverData.url);
                    }
                    img.src = coverData.url
                    */
                    console.log(coverData)
                    loadImageFromBrowser(coverData);
                }

            }, function (Error) {
                DEBUG && console.log(Error + ' failed to fetch ' + el);
            });
        });

    }
}

function loadStyleSheets() {
    //circle spinner from http://codepen.io/Beaugust/pen/DByiE
    //add additional stylesheet for "@keyframe spin" into head after document finishes loading
    //@keyframes spin is used for the loading spinner
    GM_addStyle(`
                @keyframes rotate {
                        to {transform: rotate(360deg);}
                    }

                @keyframes dash {
                    0% {
                    stroke-dasharray: 1, 150;
                    stroke-dashoffset: 0;
                    }
                    50% {
                    stroke-dasharray: 90, 150;
                    stroke-dashoffset: -35;
                    }
                    100% {
                    stroke-dasharray: 90, 150;
                    stroke-dashoffset: -124;
                    }
                }

                .popoverContent {
                    display:flex;
                    position: relative;
                    width: 100%;
                    height: 100%;
                    border: 1px solid #000;
                    text-align: center !important;
                    justify-content: center;
                    justify-items: center;
                    background-color:#ccc;
                    align-items: center;


                    min-height:0;
                    min-width:0;
                    /*max-height:inherit;
                    max-width:inherit;
                    height:100%;
                    width:100%;*/
                    flex:1;
                    padding:1px;
                }
                .spinnerRotation{
                    animation: rotate 2s linear infinite;
                }
                .spinner {
                    /*
                    z-index: 2;
                    position: absolute;
                    top: 0;
                    left: 0;
                    margin: 0;*/
                    width: 100%;
                    height: 100%;
                }

                .spinner .path{
                    stroke: hsl(210, 70%, 75%);
                    stroke-linecap: round;
                    animation: dash 1.5s ease-in-out infinite;
                }

                .blackFont {
                    color:#000;
                }
                .whiteFont {
                    color:#fff
                }
                .defaultTitleStyle {
                    color:#fff;
                    padding:5px 0;
                    height:auto;
                    display:inline-block;
                    width:100%;
                    max-width:auto;
                    text-align:center !important;
                    justify-content: center;
                    justify-items: center;
                    border-radius:8px 8px 0 0;
                }
                .defaultBackgroundStyle {
                    align-items:center;
                    pointer-events:none;
                    width:100%;
                    height:100%;
                    max-width:100%;
                    max-height:100%;
                    text-align:center !important;
                    justify-content: center;
                    justify-items: center;
                }
                .ImgFitDefault{
                    object-fit: contain;
                    width:100%;
                    height:100%;
                }

                #popover{
                    height:100%;
                    width:100%;
                    margin:0 0 22px 0;
                    border: 1px solid #000;
                    border-radius:10px 10px 5px 5px;
                    position:absolute;
                    z-index:10;
                    box-shadow: 0px 0px 5px #7A7A7A;

                    display: flex;
                    flex-direction: column;
                    text-align: center !important;
                    justify-content: center;
                    justify-items: center;
                }
                .popoverDetail{
                    flex-direction:unset !important;
                }
                .popoverTitleDetail{
                    height:100% !important;
                    width:auto !important;
                    max-width:70% !important;
                }
                
                .popoverTitle{
                    height:auto;
                    display:inline-block;
                    width:100%;
                    max-width:auto;

                }
                .popoverCoverImg{
                    /*min-height:0;
                    min-width:0;
                    max-height:inherit;
                    max-width:inherit;
                    height:100%;
                    width:100%;*/
                    flex:0;
                    padding:5px;
                }
                .smallText{
                    font-size: 0.8em;
                }
                .wordBreak {
                    word-wrap: break-word !important;
                    word-break: break-word;
                }

                `);
    function styleSheetContainsClass(f) {
        var localDomainCheck = '^http://' + document.domain;
        var localDomainCheckHttps = '^https://' + document.domain;
        // DEBUG && console.log("Domain check with: " + localDomainCheck);
        var hasStyle = false;
        var stylename = f;
        var fullStyleSheets = document.styleSheets;
        // DEBUG && console.log("start styleSheetContainsClass " + stylename);
        if (fullStyleSheets) {
            for (let i = 0; i < fullStyleSheets.length - 1; i++) {
                //DEBUG && console.log("loop fullStyleSheets " + stylename);
                let styleSheet = fullStyleSheets[i];
                if (styleSheet != null) {
                    if (styleSheet.href !== null) //https://gold.xitu.io/entry/586c67c4ac502e12d631836b "However since FF 3.5 (or thereabouts) you don't have access to cssRules collection when the file is hosted on a different domain" -> Access error for Firefox based browser. script error not continuing
                        if (styleSheet.href.match(localDomainCheck) || styleSheet.href.match(localDomainCheckHttps)) {
                            if (styleSheet.cssRules) {
                                //DEBUG && console.log("styleSheet.cssRules.length: " + styleSheet.cssRules.length)
                                for (let rulePos = 0; rulePos < styleSheet.cssRules.length - 1; rulePos++) {
                                    if (styleSheet.cssRules[rulePos] !== undefined) {
                                        // DEBUG && console.log("styleSheet.cssRules[rulePos] "+ stylename);
                                        if (styleSheet.cssRules[rulePos].selectorText) {
                                            //  console.log(styleSheet.cssRules[rulePos].selectorText)
                                            if (styleSheet.cssRules[rulePos].selectorText == stylename) {
                                                // console.log('styleSheet class has been found - class: ' + stylename);
                                                hasStyle = true; //break;
                                                break; //return hasStyle;
                                            }
                                        }  //else DEBUG && console.log("undefined styleSheet.cssRules[rulePos] "+rulePos +" - "+ stylename);
                                    }
                                    //else DEBUG && console.log("loop undefined styleSheet.cssRules[rulePos] "+ stylename);
                                }
                            } //else DEBUG && console.log("undefined styleSheet.cssRules "+ stylename);
                        }
                    //   DEBUG && console.log("stylesheet url " + styleSheet.href);
                } //else DEBUG && console.log("undefined styleSheet "+ stylename);
                if (hasStyle) break;
            }
        } //else console.log("undefined fullStyleSheets=document.styleSheets "+ stylename);
        if (!hasStyle)
            console.log("styleSheet class has not been found - style: " + stylename);
        return hasStyle;
    }

    if (STYLESHEETHIJACKFORBACKGROUND !== "")
        if (!styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUND))
            STYLESHEETHIJACKFORBACKGROUND = "";
        else {
            //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll
            //works only from firefox 77
            //STYLESHEETHIJACKFORBACKGROUND = STYLESHEETHIJACKFORBACKGROUND.replaceAll(".", " ").trim()
            STYLESHEETHIJACKFORBACKGROUND = STYLESHEETHIJACKFORBACKGROUND.replace(/\./g, ' ').trim();
        }
    if (STYLESHEETHIJACKFORTITLE !== "")
        if (!styleSheetContainsClass(STYLESHEETHIJACKFORTITLE))
            STYLESHEETHIJACKFORTITLE = "";
        else {
            //STYLESHEETHIJACKFORTITLE = STYLESHEETHIJACKFORTITLE.replaceAll(".", " ").trim()
            STYLESHEETHIJACKFORTITLE = STYLESHEETHIJACKFORTITLE.replace(/\./g, ' ').trim();
        }
}

function createPopover() {
    let bodyElement = document.getElementsByTagName("BODY")[0];

    popover = document.createElement("div");
    popover.id = "popover";

    popoverTitle = document.createElement("header");
    popoverContent = document.createElement("content");
    // popoverCoverImg = document.createElement("coverImg");
    popover.appendChild(popoverTitle);
    popover.appendChild(popoverContent);
    //popover.appendChild(popoverCoverImg);


    popover.className = (STYLESHEETHIJACKFORBACKGROUND + ' defaultBackgroundStyle').trim();
    popoverContent.className = "popoverContent blackFont";


    popover.style.maxHeight = defaultHeight + "px";
    popover.style.maxWidth = defaultHeight + "px";
    popover.style.backgroundColor = DEFAULTBACKGROUNDCOLOR;
    //console.log(popover)
    //console.log(popover.style)
    popoverTitle.className = (STYLESHEETHIJACKFORTITLE + ' defaultTitleStyle').trim();
    popoverTitle.style.backgroundColor = DEFAULTTITLEBACKGROUNDCOLOR;

    //popoverCoverImg.className = "popoverCoverImg";


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


function stylesheetForTitle() {
    if (STYLESHEETHIJACKFORTITLE !== "")
        return 'class="' + STYLESHEETHIJACKFORTITLE + ' defaultTitleStyle"';
    else
        return 'class="defaultTitleStyle" style="background-color:' + DEFAULTTITLEBACKGROUNDCOLOR + '"';
}

function stylesheetForBackground() {
    if (STYLESHEETHIJACKFORBACKGROUND !== "")
        return 'class="' + STYLESHEETHIJACKFORBACKGROUND + ' defaultBackgroundStyle"';
    else
        return 'class="defaultBackgroundStyle" style="background-color:' + DEFAULTBACKGROUNDCOLOR + '"';
}

function showPopupLoadingSpinner(title, event, notification = "", coverData = undefined) {
    const DEBUG = false;
    if (currentTitelHover == title) {
        // console.group("showPopupLoadingSpinner")
        //popover.empty();
        //popover.innerHTML = "";
        DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
        if (coverData !== undefined) {
            //console.log("showPopupLoadingSpinner")
            //console.log(coverData)
            adjustPopupTitleDetail(coverData, title);
        }
        else
            popoverTitle.textContent = title;

        if (notification != "") {
            popoverContent.innerHTML = notification;
            popoverContent.className = "popoverContent blackFont wordBreak";

        }
        else {
            popoverContent.innerHTML = `<svg class="spinner" viewBox="0 0 50 50">
                <g transform="translate(25, 25)">
                <circle class="" cx="0" cy="0" r="25" fill="black" stroke-width="5" />
                <circle class="path" cx="0" cy="0" r="23" fill="none" stroke-width="5">
                    <animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0" to="360"  dur="1.6s" repeatCount="indefinite" />
                </circle>
                </g>
                <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" style="fill:#fff;font-size:11px">Loading </text>
            </svg>`

            //popoverContent.innerHTML = '<div class="forground" style="z-index: 3;">Loading Data</div><svg class="spinner" viewBox="0 0 50 50"><circle class="" cx="25" cy="25" r="22" fill="black" stroke-width="5"></circle><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg>';
            popoverContent.className = "popoverContent whiteFont";
        }
        DEBUG && console.log(popover)
        //   DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
        //console.log(event)
        popupPos(event);
        //  console.groupEnd("showPopupLoadingSpinner")
    }

}
/**
 * update popupContent and reposition to link
 *
 * @param {*} title
 * @param {*} link
 * @param {*} e event
 */
function refreshPopover(coverData, e = undefined) {
    //only call when isActivePopup
    const DEBUG = false;
    DEBUG && console.log("currentTitelHover: " + currentTitelHover)

    DEBUG && console.group("refreshPopover");
    const link = coverData.url;
    const title = coverData.title;
    //console.log(coverData)
    //console.log(e)
    // popoverTitle.textContent = title;
    // console.log(link)
    if (inBlocklist(link)) {

        popoverContent.innerHTML = "Blocked Image<br />No Cover Image<br />Unwanted Image";
    } else {
        let imgElement = new Image();//document.createElement("img");
        imgElement.src = link;
        popoverContent.innerHTML = '<img src="' + link + '" class="ImgFitDefault" ></img>';
    }
    adjustPopupTitleDetail(coverData);

    DEBUG && console.groupEnd("refreshPopover");
    //if (currentTitelHover == title)
    if (e !== undefined)
        popupPos(e);
};

const reRating = new RegExp('([0-9\.]+) \/ ([0-9\.]+)');
const reVoteCount = new RegExp('([0-9]+) votes')
function getRatingNumber(ratingString) {
    //const ratingString = "Rating(3.3 / 5.0, 1940 votes)"

    const matches = ratingString.match(reRating)
    const matchesVotes = ratingString.toLowerCase().match(reVoteCount)
    //console.log(matches)
    //console.log(matches.length)
    let hasVotes = true;
    // console.log(matchesVotes)
    if (matchesVotes && matchesVotes.length > 1) {
        //console.log(matchesVotes[1])
        if (matchesVotes[1] == 0) {
            hasVotes = false;
        }
    }
    let ratingNumber
    if (matches && matches.length == 3 && hasVotes) {
        //console.log(matches[1])
        ratingNumber = matches[1];
    }

    return ratingNumber;
}
const reChapters = new RegExp('([0-9\.]+)( wn)? chapters');
const reChaptersNumberBehind = new RegExp('chapter ([0-9\.]+)');
function getChapters(statusString) {

    let chapterCount;
    const matches = statusString.toLowerCase().match(reChapters);
    let webnovel = "";
    if (matches && matches.length >= 2) {
        chapterCount = matches[1];
        if (matches[2]) {
            webnovel = " WN";
        }
    }
    if (!chapterCount) {
        const matchesBehind = statusString.toLowerCase().match(reChaptersNumberBehind);
        if (matchesBehind && matchesBehind.length >= 2) {
            chapterCount = matchesBehind[1];
        }
    }
    let result;
    if (chapterCount) {
        result = chapterCount + webnovel + " Chapters"
    }

    return result;
}

function getCompletedState(statusString) {
    let result = false;
    if (statusString.toLowerCase().includes("complete")) {//complete | completed
        result = true;
    }
    return result;
}
function geOngoingState(statusString) {
    let result = false;
    if (statusString.toLowerCase().includes("ongoing")) {
        result = true;
    }
    return result;
}

async function adjustPopupTitleDetail(coverData, title = undefined) {

    let titleToShow="";
    popoverTitle.textContent = "";
    if (coverData && coverData.title)
    titleToShow = coverData.title;
    else if (title !== undefined) titleToShow = title;
    popoverTitle.textContent = titleToShow;
    //console.log("adjustPopupTitleDetail - showDetails: " + showDetails)
    if (showDetails) {
        //console.log("showDetails should be true")
        if (coverData.votes && coverData.votes.length > 0) {
            popoverTitle.innerHTML += '<hr />Rating: ' + coverData.votes + '<hr />Status: ' + coverData.status + '';
        }
        if (coverData.genre && coverData.showTags) {
            popoverTitle.innerHTML += '<hr />Genre: ' + coverData.genre + "<hr />Tags: " + coverData.showTags;
        }
        popoverTitle.innerHTML += "<hr /><span>[Press Key 1 to hide details]</span>"
    }
    else {
        //console.log("showDetails should be false")

        //if (coverData.votes && coverData.votes.length > 0) 
        {
            let rating = getRatingNumber(coverData.votes);
            let chapters = getChapters(coverData.status);
            let completed = getCompletedState(coverData.status);
            let ongoing = geOngoingState(coverData.status);
            if (rating || chapters || completed || ongoing) {

                //console.log(rating)
                //console.log(chapters)
                //console.log(completed)
                //console.log(ongoing)

                if (rating !== undefined) rating += "★ ";
                if (chapters !== undefined) chapters = chapters + " "; else chapters = "";
                if (completed) completed = "🗹 "; else completed = ""; //https://www.utf8icons.com/
                if (ongoing) ongoing = "✎ "; else ongoing = "";

                
                popoverTitle.innerHTML = titleToShow+
                '<span class="smallText" style="white-space: nowrap;"> [' + rating +
                    chapters +
                    completed +
                    ongoing +
                    ']</span>';
            }

            /*
            popoverTitle.innerHTML += '<span class="smallText"> [' + rating + '★] ' + chapters + '</span> ';
            if (completed) {
                popoverTitle.innerHTML += "🗹 "
            }
            if (ongoing) {
                popoverTitle.innerHTML += "✎ "
            }*/
        }
        popoverTitle.innerHTML += '<br /><span class="smallText">[Press Key 1 to show details]</span>'
    }
}
function loadCoverData(coverData, title, e) {
    const DEBUG = false;
    //GM_getCachedValue
    DEBUG && console.group("loadCoverData")
    // const coverData = GM_getCachedValue(Href);



    DEBUG && console.log(coverData)
    const imgUrl = coverData.url;
    let serieTitle = title;
    if (!title || coverData.title)  //pure link without title get title of seriepage
        serieTitle = coverData.title;
    DEBUG && console.log("imgUrl: " + imgUrl);
    DEBUG && console.log("title: " + title + ", serieTitle: " + serieTitle)

    if ((currentTitelHover === null || currentTitelHover == "null" || currentTitelHover === undefined) && coverData !== null) {
        //console.log(coverData)
        currentTitelHover = coverData.title;
    }
    if (coverData !== undefined && coverData !== null)
        currentCoverData = coverData;

    if (e)
        loadImageFromBrowser(coverData, imgUrl, serieTitle, e, title)

    DEBUG && console.groupEnd("loadCoverData")
}

function ajaxLoadImageUrlAndShowPopup(elementUrl, title, e) {
    //console.log("mouseenter")
    // console.group("ajaxLoadImageUrlAndShowPopup")
    return parseSeriePage(elementUrl, title, e)
        .then(function (coverData) {
            loadCoverData(coverData, title, e)
        }, function (Error) {
            console.log(Error + ' failed to fetch ' + elementUrl);
        });
    // console.groupEnd("ajaxLoadImageUrlAndShowPopup")
};

function imageLoaded(coverData, hoveredTitleLink, serieTitle = undefined, e = undefined) {
    const DEBUG = false;
    const hasMouseEnterEvent = serieTitle && (e !== undefined);
    const isActivePopup = ((currentTitelHover !== undefined) && (hoveredTitleLink !== undefined)) && (currentTitelHover == hoveredTitleLink) && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
    DEBUG && console.group("loadImageFromBrowser img.onload: " + serieTitle)
    DEBUG && console.log("finished loading imgurl: " + coverData.url);
    DEBUG && console.log("currentTitelHover: " + currentTitelHover + ", isActivePopup: " + isActivePopup)
    DEBUG && console.log("isActivePopup: " + isActivePopup)
    if (isActivePopup) {
        DEBUG && console.log("refreshPopover")
        refreshPopover(coverData, e); //popup only gets refreshed when currentTitelHover == serieTitle
    }
    DEBUG && console.groupEnd("loadImageFromBrowser img.onload")
}

function imageLoadingError(coverData, error, hoveredTitleLink, serieTitle = undefined, e = undefined) {
    console.group("loadImageFromBrowser img.onerror: " + serieTitle)
    const hasMouseEnterEvent = serieTitle && (e !== undefined);
    const isActivePopup = ((currentTitelHover !== undefined) && (hoveredTitleLink !== undefined)) && (currentTitelHover == hoveredTitleLink) && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
    console.log(error);
    const errorMessage = "browser blocked/has error loading the file: <br />" + decodeURIComponent(error.target.src);
    console.log(errorMessage)
    //console.log(window)
    console.log(navigator)
    //console.log(navigator.userAgent)
    const useragentString = navigator.userAgent;
    console.log("useragentString: " + useragentString)
    const isChrome = useragentString.includes("Chrome")
    if (isChrome)
        console.log("look in the developer console if 'net::ERR_BLOCKED_BY_CLIENT' is displayed or manually check if the imagelink still exists");
    else
        console.log("image loading most likely blocked by browser or addon. Check if the imagelink still exists");

    if (isActivePopup)
        showPopupLoadingSpinner(serieTitle, e, errorMessage, coverData);
    console.groupEnd("loadImageFromBrowser img.onerror")
}

function loadImageFromBrowser(coverData, imgUrl, serieTitle = undefined, e = undefined, hoveredTitleLink = undefined) {
    const DEBUG = false;
    //console.group("loadImageFromBrowser")
    let img = document.createElement("img"); //put img into dom. Let the image preload in background
    const hasMouseEnterEvent = serieTitle && (e !== undefined);
    //console.log(currentCoverData)
    //console.log(coverData)
    const isActivePopup = ((currentTitelHover !== undefined) && (hoveredTitleLink !== undefined)) && (currentTitelHover == hoveredTitleLink) && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData

    DEBUG && console.log("loadImageFromBrowser")
    DEBUG && console.log(hasMouseEnterEvent)
    img.onload = () => { imageLoaded(coverData, hoveredTitleLink, serieTitle, e) };

    img.onerror = (error) => { imageLoadingError(coverData, error, hoveredTitleLink, serieTitle, e) }

    img.src = imgUrl;

    if (img.complete) {
        DEBUG && console.log("loadImageFromBrowser preload completed: " + serieTitle)
        DEBUG && console.log(img.src)
    } else {//if image not available/cached in browser show loading pinner
        if (isActivePopup) {
            DEBUG && console.log("loadImageFromBrowser image not completely loaded yet. Show loading spinner : " + serieTitle)
            showPopupLoadingSpinner(serieTitle, e);
        }
    }
    // console.groupEnd("loadImageFromBrowser")
}

function mouseEnterPopup(e) {

    //if (!e.target.matches(concatSelector())) return;
    const DEBUG = false;
    DEBUG && console.group("mouseEnterPopup")
    //let element = undefined;//$(this);
    //let nativElement = e.target//this;
    //console.log(this)
    //console.log(e.target)
    let Href = this.href;// element.attr('href');
    if (Href.search(INDIVIDUALPAGETEST) != -1) //only trigger for links that point to serie pages
    {
        //console.log(this)
        //console.log(this.text) //shortTitle
        //console.log(this.title) //LongTitle
        let shortSerieTitle = this.text; //element.text(); //get linkname
        //console.log(this)
        //console.log(shortSerieTitle)

        //move native title to custom data attribute. Suppress nativ title popup
        if (!this.getAttribute('datatitle')) {
            this.setAttribute('datatitle', this.getAttribute('title'));
            this.removeAttribute('title');
        }

        let serieTitle = this.getAttribute('datatitle');//element.attr('datatitle'); //try to get nativ title if available from datatitle
        //console.log(serieTitle)
        if (serieTitle === null || serieTitle == "null") //has no set nativ long title -> use (available shortend) linkname
            serieTitle = shortSerieTitle;
        else //no need to run check if it is already shortSerieTitle
            if (serieTitle.match(PREDIFINEDNATIVTITLE)) //catch on individual serie page nativ title begins with "Recommended by" x people -> use linkname
                serieTitle = shortSerieTitle;
        currentTitelHover = serieTitle; //mark which titel is currently hovered
        currentPopupEvent = e;
        //console.log(serieTitle)
        //console.log(Href)


        //console.log(currentCoverData)
        ajaxLoadImageUrlAndShowPopup(Href, currentTitelHover, e);

    }
    DEBUG && console.groupEnd("mouseEnterPopup")
}

function hidePopOver() {
    popover.style.visibility = "hidden";
    currentTitelHover = undefined;
    currentCoverData = undefined;
}
function showPopOver() {
    // popover.style.display = "flex";
    popover.style.visibility = "visible";
}
function hideOnMouseLeave() {
    //if (!e.target.matches(concatSelector())) return;
    //popover.hide();
    hidePopOver();
}

function updateSerieNodes() {
    if (ALLSERIENODES) {
        ALLSERIENODES.forEach(function (selector) {
            selector.removeEventListener("mouseleave", hideOnMouseLeave);
            selector.removeEventListener("mouseenter", mouseEnterPopup);
        })
    }
    ALLSERIENODES = document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]');
}
function switchDetailsAndUpdatePopup() {
    const DEBUG = false;
    DEBUG && console.group("switchDetailsAndUpdatePopup")
    changeToNewDetailStyle();
    //console.log(currentCoverData)
    DEBUG && console.log("switchDetails refreshPopup")
    DEBUG && console.log(currentCoverData)
    if (currentCoverData && currentCoverData !== undefined) {
        refreshPopover(currentCoverData, currentPopupEvent); //update on detail change
    }

    console.groupEnd("switchDetails")


}
function changeToNewDetailStyle(toggleDetails = true) {
    if (toggleDetails)
        showDetails = !showDetails;
    //console.log("switch showDetails to : " + showDetails)
    localStorage.setItem("showDetails", showDetails);
    if (showDetails) {
        popover.classList.add("popoverDetail")
        popover.style.maxWidth = defaultHeight * 2 + "px";
        popoverTitle.classList.add("popoverTitleDetail")
    }
    else {
        popover.classList.remove("popoverDetail")
        popover.style.maxWidth = defaultHeight + "px";
        popoverTitle.classList.remove("popoverTitleDetail")
    }

}
function reactToKeyPressWhenPopupVisible(event) {
    //console.log(event)
    //console.log(currentTitelHover)
    if (currentTitelHover && currentTitelHover !== undefined) {
        if (event.key == "1") {
            //switchDetailsAndUpdatePopup();
            switchDetailsAndUpdatePopup()
        }

    }
}
window.addEventListener("blur", hidePopOver);
window.addEventListener("keypress", reactToKeyPressWhenPopupVisible);
window.onunload = function () {
    window.removeEventListener("blur", hidePopOver);
    window.removeEventListener("keypress", reactToKeyPressWhenPopupVisible)
    //possible memoryleaks?
    updateSerieNodes();
    observer.disconnect();
}
const debouncedpreloadCoverData = debounce(preloadCoverData, 100);
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };

// Callback function to execute when mutations are observed
const callback = function (mutationsList, observer) {
    // Use traditional 'for loops' for IE 11
    for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
            // console.log('A child node has been added or removed.');
            //debouncedTest()
            debouncedpreloadCoverData();
            hidePopOver();
        }
        else if (mutation.type === 'attributes') {
            //   console.log('The ' + mutation.attributeName + ' attribute was modified.');
        }
    }
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);

function debounce(func, timeout) {
    let timer;
    return (...args) => {
        const next = () => func(...args);
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(next, timeout > 0 ? timeout : 300);
    };
};


function main() {
    checkDataVersion();


    loadStyleSheets();
    createPopover();
    hidePopOver();
    showDetails = localStorage.getItem("showDetails") == "true";
    //console.log("localStorage state showDetails: " + showDetails)
    changeToNewDetailStyle(false);
    //console.log("isOnReadingListIndex: " + isOnReadingListIndex)
    if (isOnReadingListIndex) {
        let targetNode = document.getElementById("profile_content3");
        //console.dir(targetNode)
        observer.observe(targetNode, config); //observe for update before running debouncedwaitForReadingList();
    }
    else {
        preloadCoverData();
    }
}
main();

QingJ © 2025

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