novelupdates Cover Preview

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

当前为 2019-02-24 提交的版本,查看 最新版本

// ==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.3.2
// @description Previews covers in novelupdates.com when hovering over hyperlinks that lead to novel pages.
// @inject-into content
// @grant       GM_xmlhttpRequest
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @run-at   	document-end
// @require     http://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js
// @license     http://creativecommons.org/licenses/by-nc-sa/4.0/
// ==/UserScript==
const MAXCACHEAGE = 24 * 60 * 60 * 1000; // Max Age before Cached data gets overridden with current data. Max Age is 24 hour in milliseconds  //days * h * min  * sec * ms
var STYLESHEETHIJACKFORBACKGROUND = "breadcrumb_nu"; //if unknown set empty ""; classname without leading dot
var STYLESHEETHIJACKFORTITLE = "tbl_sort"; //if unknown set empty ""; classname without leading dot
const DEFAULTTITLEBACKGROUNDCOLOR = '#aac'; //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, .messageContent a'; //index/group/readinglist pages , forum
const SELECTOR2 = '.wpb_wrapper > a'; //individual serie pages recommendation titles
const PREDIFINEDNATIVTITLE = "^Recommended by"; //in case native title is used to display something different
const INDIVIDUALPAGETEST = "novelupdates.com/series/";
const IMAGELINKCONTAINERS = '.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;
//^^^^	frontend settings over this line	^^^^

const DEBUGLOG = false; //de-/activate console.log();
const HASERROR = true;
const RE = /\s*,\s*/; //Regex for split and remove empty spaces
var defaultHeight = "400px";
var IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE);
var onHover = false;
var currentTitelHover;
var windowCached = $(window);
var concatenatedSelectors = $(concatSelector());
var style = 1;
var popover;

//http://stackoverflow.com/questions/1215392/how-to-quickly-and-conveniently-disable-all-console-log-statements-in-my-code
var logger = function () {
    var oldConsoleLog = null;
    var pub = {};

    pub.enableLogger = function enableLogger() {
        if (oldConsoleLog === null)
            return;

        window.console.log = oldConsoleLog;
    };

    pub.disableLogger = function disableLogger() {
        oldConsoleLog = console.log;
        window.console.log = function () { };
    };

    return pub;
} ();

$(document).ready(
    function () {
        if (DEBUGLOG)
            logger.enableLogger();
        else
            logger.disableLogger();
    }
);

//get value from key. Decide if timestamp is older than MAXCACHEAGE than look for new image
function GM_getCachedValue(key) {
    var currentTime = Date.now();
    var rawCover = GM_getValue(key, null);
    if (!rawCover) {
        return null;
    }
    var coverData;
    try { //is json parseable data? if not delete for refreshing
        coverData = JSON.parse(rawCover);
        if (!(coverData.url && coverData.cachedTime)) //has same variable definitions?
        {
            GM_deleteValue(key);
            return null;
        }
    } catch (e) {
        GM_deleteValue(key);
        return null;
    }


    var measuredTimedifference = currentTime - coverData.cachedTime;
    if (measuredTimedifference < MAXCACHEAGE)
        return coverData.url;
    else {
        GM_deleteValue(key);
        return null;
    }
    var imageUrl = coverData.url;
    return imageUrl;
}

//set value and currenttime for key
function GM_setCachedValue(key, value) {
    var coverData = {
        url: value,
        cachedTime: Date.now()
    };
    GM_setValue(key, JSON.stringify(coverData));
}

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;
}

function concatSelector() {
    var result;
    if (SELECTOR1)
        result = SELECTOR1;
    if (SELECTOR2) {
        if (SELECTOR1) //in case selector1 is missing
            result += ', ';
        result += SELECTOR2;
    }

    return result;
}

// popupPositioning function
jQuery.fn.popupPos = function (event, element, style) {
    var offsetToBottomBorderY = 5; //offset to bottom border
    var offsetToRightBorderX = 5; //offset to right border
    var X, Y;
    var hoveredSelectedPosX, hoveredSelectedPosY;
    var distanceToBottom, distanceToRight;
    let computedFontSize = parseInt(window.getComputedStyle(element.parents()[0]).fontSize);

    //console.log(computedFontSize);

    //Initialising variables (multiple usages)
    var scrollTop = windowCached.scrollTop();
    var scrollLeft = windowCached.scrollLeft();
    var elementPopup = $(this);
    var elementParentOffset = element.parents().offset();
    var elementParentOuterHeight = element.parents().outerHeight();
    var elementParentOuterWidth = element.parents().outerWidth();
    var elementOffset = element.offset();
    var elementOuterHeight = element.outerHeight();
    var elementOuterWidth = element.outerWidth();
    var elementRect = element[0].getBoundingClientRect();
    var elementParentRect = element.parents()[0].getBoundingClientRect();
    if (style == 1) //index: position next to parent table cell (SELECTOR1)
    {
        hoveredSelectedPosX = elementParentOffset.left + elementParentOuterWidth; //link position + tablecell width; + elementOuterWidth;
        hoveredSelectedPosY = elementParentOffset.top + computedFontSize; //link position + tablecell height; + elementOuterHeight;
        //console.log("height " + elementParentOuterHeight  + ' - ' + elementOuterHeight);
    } else if (style == 2) //recommendations: position next to link height and parent (SELECTOR2) width
    {
        hoveredSelectedPosX = elementParentOffset.left + elementParentOuterWidth; //elementOffset.left + elementOuterWidth;
        hoveredSelectedPosY = elementOffset.top + elementOuterHeight;
    } else { //position to mouse hover position
        hoveredSelectedPosX = event.pageX; // + offsetToRightBorderX;
        hoveredSelectedPosY = event.pageY; // + offsetToBottomBorderY;
    }
    X = hoveredSelectedPosX;
    Y = hoveredSelectedPosY;

    // Distance to the right
    distanceToRight = windowCached.width() - (X - scrollLeft);
    // Tooltip too close to the right?
    if (distanceToRight < elementPopup.outerWidth())
        X += distanceToRight - elementPopup.outerWidth();

    // Distance to the bottom
    distanceToBottom = windowCached.height() - (Y - scrollTop);
    // Tooltip too close to the bottom?
    if (distanceToBottom < elementPopup.outerHeight()) //(offsetToBottomBorderY + elementPopup.outerHeight())
        Y += distanceToBottom - elementPopup.outerHeight() + computedFontSize;
    //offsetToRightBorderX
    //offsetToBottomBorderY

    //console.log("Distance to the bottom " + distanceToBottom+" elementPopupHeight " +elementPopup.outerHeight()+ "\nDistance to the right " + distanceToRight+ " elementPopupouterWidth " +elementPopup.outerWidth());
    //Tooltip over top border?
    //if(Y + offsetToBottomBorderY < scrollTop) Y = scrollTop + offsetToBottomBorderY;
    //if(X + offsetToRightBorderX < scrollLeft) X = scrollLeft + offsetToRightBorderX;
    this.css('top', Y + 'px');
    this.css('left', X + 'px');
    popover.show();
    //console.log("final popup position "+X+' # '+Y);
    return this;
};

//.wpb_wrapper a = title links on individual seriepage
$(SELECTOR2).mouseenter(function (e) {
    style = 2;
});
//td a = links in table cells (index and group page)
$(SELECTOR1).mouseenter(function (e) {
    style = 1;
});

function loadImgUrl(elementUrl) {
    // Create new promise with the Promise() constructor;
    // This has as its argument a function
    // with two parameters, resolve and reject
    return new Promise(function (resolve, reject) {
        let retrievedImgLink = GM_getCachedValue(elementUrl);
        if (retrievedImgLink !== null) {
            resolve(retrievedImgLink);
        } else {
            $.ajax({
                url: elementUrl,
                type: "GET",
                dataType: 'text'
            }).done(function (data) {
                let html = data.replace(/src=/g, 'data-src='); //block resources loading when DOM gets opened
                try {
                    var imagelinks = $(html).find(IMAGELINKCONTAINERS).find('img');

                    var imagelink = imagelinks[CONTAINERNUMBER].getAttribute("data-src");
                    console.log('init ' + imagelink);
                    GM_setCachedValue(elementUrl, imagelink); //cache imageurl link
                    resolve(imagelink);
                } catch (error) {
                    showPopupLoadingSpinner(serieTitle, 1);
                    reject(elementUrl);
                }

            })
                .fail(function (xhr) {
                    console.log('error', xhr);
                });
        }
    });
}

main();

function main() {
    function uniq(a) {
        return Array.from(new Set(a));
    }

    if (preloadUrlRequests) {
        var getNovelLinks = function () {
            const links = Array.from(
                document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]')
            );

            return links;
        };
        const links = getNovelLinks();
        const novelUrlList = links.map(function (el) {
            // console.log('novelUrlList ' + el.href);
            return el.href;
        });

        const uniqueNovelUrlList = uniq(novelUrlList);
        const imageUrlList = uniqueNovelUrlList.map(function (elementUrl) {
            loadImgUrl(elementUrl)
                .then(function (imgUrl) {
                    if (preloadImages) {
                        let img = document.createElement("img"); //put img into dom. Let the image preload in background
                        img.src = imgUrl;
                        console.log("onpageload cache init " + imgUrl);
                    }
                }, function (Error) {
                    console.log(Error + ' failed to fetch ' + elementUrl);
                });
            // console.log("imageUrlList " + elementUrl);
        });
    }
    //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

    var style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = 'body {}';
    document.getElementsByTagName('head')[0].appendChild(style);
    this.stylesheet = document.styleSheets[document.styleSheets.length - 1];

    try {
        this.stylesheet.insertRule(`
@keyframes spin {
100% {
transform: rotate(360deg);
}
}`, this.stylesheet.cssRules.length);
    } catch (e) {
        alert('error');
    }
}
windowCached.on('load', function () {
    function styleSheetContainsClass(f) {
        var localDomainCheck = '^http://' + document.domain;
        //console.log("Domain check with: " + localDomainCheck);
        var hasStyle = false;
        var stylename = '.' + f;
        var fullStyleSheets = document.styleSheets;
        // console.log("start styleSheetContainsClass " + stylename);
        if (fullStyleSheets) {
            for (let i = 0; i < fullStyleSheets.length - 1; i++) {
                //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)) {
                            if (styleSheet.cssRules) {
                                for (let rulePos = 0; rulePos < styleSheet.cssRules.length - 1; rulePos++) {

                                    if (styleSheet.cssRules[rulePos] !== undefined) {
                                        // console.log("styleSheet.cssRules[rulePos] "+ stylename);
                                        if (styleSheet.cssRules[rulePos].selectorText) {
                                            if (styleSheet.cssRules[rulePos].selectorText == stylename) {
                                                console.log('styleSheet class has been found - style: ' + stylename);
                                                hasStyle = true; //break;
                                                return hasStyle;
                                            }
                                        } // else console.log("undefined styleSheet.cssRules[rulePos] "+ stylename);
                                    }
                                    // else console.log("loop undefined styleSheet.cssRules[rulePos] "+ stylename);
                                }
                            } //else console.log("undefined styleSheet.cssRules "+ stylename);
                        }
                    //console.log("stylesheet url " + styleSheet.href);
                } //else console.log("undefined styleSheet "+ stylename);
                if (hasStyle) break;
            }
        } //else console.log("undefined fullStyleSheets "+ stylename);

        console.log("styleSheet class has not been found - style: " + stylename);
        return hasStyle;
    }
    if (STYLESHEETHIJACKFORBACKGROUND !== "")
        if (!styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUND))
            STYLESHEETHIJACKFORBACKGROUND = "";
    if (STYLESHEETHIJACKFORTITLE !== "")
        if (!styleSheetContainsClass(STYLESHEETHIJACKFORTITLE))
            STYLESHEETHIJACKFORTITLE = "";

    $('body').append('<div ID="popover" ' + stylesheetForBackground() + '></div>');
    popover = $('#popover');
    popover.css('position', 'absolute');
    popover.css('z-index', '10');
    popover.css('box-shadow', '0px 0px 5px #7A7A7A');
});


function stylesheetForTitle() {
    if (STYLESHEETHIJACKFORTITLE !== "")
        return 'class="' + STYLESHEETHIJACKFORTITLE + '" style="display:inline-block;width:100%;text-align:center !important"';
    else
        return 'style="background-color:' + DEFAULTTITLEBACKGROUNDCOLOR + ';display:inline-block;width:100%;text-align:center !important"';
}

function stylesheetForBackground() {
    if (STYLESHEETHIJACKFORBACKGROUND !== "")
        return 'class="' + STYLESHEETHIJACKFORBACKGROUND + '" style="display:flex !important;flex-direction: column; align-items:center;pointer-events:none; width:auto; height:auto; max-width:100%; max-height:100%;"';
    else
        return 'style="background-color:' + DEFAULTBACKGROUNDCOLOR + ';display:flex !important;flex-direction: column; align-items:center;pointer-events:none; width:auto; height:auto; max-width:100%; max-height:100%;"';
}

//when selected link is entered load imageurl and write popover content
concatenatedSelectors.mouseenter(function (e) {
    var element = $(this);
    var Href = element.attr('href');

    if (Href.search(INDIVIDUALPAGETEST) != -1) //only trigger for links that point to serie pages
    {
        let refreshPopover = function (title, link) {
			/*	clear popup
			 *	append title and image into popup
			 *	when img loading is finished reposition (popupPos) to element/border
			 */
            if (currentTitelHover == title) //popup only gets refreshed when currentTitelHover == title
            {
                popover.empty();
                if (inBlocklist(link)) {
                    popover.append('<div ' + stylesheetForTitle() + '>' + title + '</div>Blocked Image<br />No Cover Image<br />Unwanted Image');
                    popover.popupPos(e, element, style);
                } else {
                    popover.append('<div ' + stylesheetForTitle() + '>' + title + '</div><img src="' + link + '" style="margin:5px; width:auto; height:' + defaultHeight + ' !important; max-width:100%;min-height:0; max-height:100% !important; align-items: stretch;align-self: stretch;object-fit: contain;"></img>');
                    $('#popover img').on('load', function () {
                        //console.log(Href + "onload is executed"); // for testing purposes
                        if (onHover) //is mouse still hovering over same title after loading finishes?
                            popover.popupPos(e, element, style);
                    });
                }
            }
        };

        //popup loading spinner
        var showPopupLoadingSpinner = function (title, error = false) {
            popover.empty();
            if (error)
                popover.append('<div ' + stylesheetForTitle() + '>' + title + '</div><div style="position: relative;width:150px; height:150px;color:#000;display: flex; justify-content: center; flex-direction: column; text-align: center;">imagecontainer setting is invalid</div>');
            else
                popover.append('<div ' + stylesheetForTitle() + '>' + title + '</div><div style="position: relative;width:150px; height:150px;color:#fff;display: flex; justify-content: center; flex-direction: column; text-align: center;">Loading image<div style="z-index: -100;position:absolute;top:0;left:0;background-color:#000; box-sizing: border-box; width: 150px; height: 150px; border-radius: 100%; border: 10px solid rgba(255, 255, 255, 0.2); border-top-color: #FFF; animation: spin 1s infinite linear;"></div></div>');
            popover.popupPos(e, element, style);
        };


        //async wait until image is loaded before refreshing popup
        var ajaxLoadImageUrlAndShowPopup = function (elementUrl, title) {
            loadImgUrl(elementUrl)
                .then(function (imgUrl) {
                    let img = document.createElement("img"); //put img into dom. Let the image preload in background
                    img.src = imgUrl;
                    img.onload = () => {
                        console.log("imgurl " + imgUrl);
                        refreshPopover(title, imgUrl); //popup only gets refreshed when currentTitelHover == serieTitle      
                        // GM_setCachedValue(elementUrl, imgUrl); //cache imageurl link
                        console.log(elementUrl + " url has been found and is written to temporary cache.\n" + imgUrl + ' successfully cached.'); // for testing purposes                      
                    };
                }, function (Error) {
                    console.log(Error + ' failed to fetch ' + elementUrl);
                });

        };


        onHover = true;

        var shortSerieTitle = element.text(); //get linkname

        //move native title to custom data attribute. Suppress nativ title
        if (!element.attr('datatitle')) {
            element.attr('datatitle', element.attr('title'));
            element.removeAttr('title');
        }

        var serieTitle = element.attr('datatitle'); //try to get nativ title if available from datatitle
        if (!serieTitle) //has no set nativ long title -> use (available shortend) linkname
            serieTitle = shortSerieTitle;
        else //no need to run check if it is already shortSerieTitle
            if (serieTitle.match(PREDIFINEDNATIVTITLE)) //catch on individual serie page nativ title begins with "Recommended by" x people -> use linkname
                serieTitle = shortSerieTitle;

        currentTitelHover = serieTitle; //mark which titel is currently hovered

		/* var retrievedImgLink = GM_getCachedValue(Href); //was imageurl cached?
		 if (retrievedImgLink) {
		     refreshPopover(serieTitle, retrievedImgLink); //popup only gets refreshed when currentTitelHover == serieTitle
		     console.log(retrievedImgLink + ' on the page ' + Href + " has been found and retrieved from the cache."); // for testing purposes
		 }
		 else*/
        {
            showPopupLoadingSpinner(serieTitle);
            ajaxLoadImageUrlAndShowPopup(Href, serieTitle);
        }
    }
});

//hide and empty popup when mouse is not over title
concatenatedSelectors.mouseleave(function () { //close popup when mouse leaves titlelink
    //popover.empty();
    popover.hide();
    onHover = false;
});

$(document).mouseleave(function () { //force close when mouse is outside window and previous mouseleave does not get called
    popover.hide();
    onHover = false;
});

windowCached.blur(function () { //chrome fix -> force close when mouse is outside window alt + tab
    popover.hide();
    onHover = false;
});

QingJ © 2025

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