novelupdates Cover Preview

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

Pada tanggal 10 Desember 2020. Lihat %(latest_version_link).

// ==UserScript==
// https://greasyforks.org/scripts/26439-novelupdates-cover-preview/
// @name        novelupdates Cover Preview
// @namespace   somethingthatshouldnotclashwithotherscripts
// @include     https://www.novelupdates.com/*
// @include     http://www.novelupdates.com/*
// @include     https://forum.novelupdates.com/*
// @include     http://forum.novelupdates.com/*
// @version     1.8.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==

//console.log("cover preview start");
"use strict";
//#region frontend settings
const MAXCACHEAGE = 90 * 24 * 60 * 60 * 1000; // Max Age before Cached data of serieinfo gets overridden with current data. Max Age is 90 days in milliseconds  //days * h * min  * sec * ms
const DEFAULTTITLEBACKGROUNDCOLOR = "#2c3e50"; //if no hijack class style available use plain color
const DEFAULTBACKGROUNDCOLOR = "#ccc"; //if no hijack class style available use plain color
let STYLESHEETHIJACKFORBACKGROUND = ".l-canvas, .breadcrumb"; //if unknown set empty ""; classname with leading dot seperated with comma
let STYLESHEETHIJACKFORTITLE = ".widgettitle_nuf, .navTabs "; //if unknown set empty ""; classname with leading dot seperated with comma

const PREDIFINEDNATIVTITLE = "Recommended by"; //forum, index
const INDIVIDUALPAGETEST = "www.novelupdates.com/series/"; //matched with includes
const IMAGELINKCONTAINERS = ".serieseditimg img, .seriesimg img"; //instead of single element class name with dot seperated with comma
const IMAGEBLOCKER = ""; //"https://www.novelupdates.com/img/noimagefound.jpg"; //tested with includes()
const CONTAINERNUMBER = 0;
const seriePageTitle = ".seriestitlenu";
const seriePageVotes = ".seriesother > .uvotes";
const seriePageStatus = "#editstatus";
const seriePageGenre = "#seriesgenre";
const seriePageTags = "#showtags";
const seriePageDescription = "#editdescription";
const isOnIndex =
  this.location.href == "https://www.novelupdates.com/" ||
  this.location.href.startsWith("https://forum.novelupdates.com/") ||
  this.location.href.startsWith("https://www.novelupdates.com/?pg=") ||
  this.location.href.startsWith("https://www.novelupdates.com/group/"); //popup style next to container instead of next to linkitem
const isOnReadingListIndex = this.location.href.startsWith(
  "https://www.novelupdates.com/user/"
);
const targetContainerIDToObserve = "profile_content3"; //update eventlistener on list change of page isOnReadingListIndex

const preloadUrlRequests = false;
const preloadImages = false;
const eventListenerStyle = 0; //undefined/0 forEach serieLink addeventlistener(mouseenter/mouseleave) / 1 window addeventlistener(mousemove)
//#endregion

//#region backend variables ^^^^	frontend settings over this line	^^^^
const version = "1.8.0";
const forceUpdate = false;
const debugCSSClasses = false; //log if css class is accessible else default include
const lastUpdateCheck = 28 * 24 * 60 * 60 * 1000; //recheck if CSS available

const maxWaitingTime = 120;
const RE = /\s*,\s*/; //Regex for split and remove empty spaces
const REGEX_DOTCOMMA = /[.,]/g;
const reChapters = new RegExp("([0-9.]+)( wn)? chapters");
const reChaptersNumberBehind = new RegExp("chapter ([0-9.]+)");
const reChaptersOnlyNumbers = new RegExp("([0-9.]+)");
const reRating = new RegExp("([0-9.]+) / ([0-9.]+)");
const reVoteCount = new RegExp("([0-9]+) votes");
const offsetToBottomBorderY = 22; //offset to bottom border
const offsetToRightBorderX = 10; //offset to right border
const defaultHeight = "400"; //in pixel
const smallHeight = "250";
const IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE);
const PREDIFINEDNATIVTITLEARRAY = PREDIFINEDNATIVTITLE.split(RE);
const STYLESHEETHIJACKFORBACKGROUNDARRAY = STYLESHEETHIJACKFORBACKGROUND.split(
  RE
);
const STYLESHEETHIJACKFORTITLEARRAY = STYLESHEETHIJACKFORTITLE.split(RE);

let refreshInitValues = false;
let showDetails = false;
let popoverVisible = false; //not all links have a title or text(img link) to set currentTitelHover. Manual state saving needed
let ALLSERIENODES = []; // = document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]');
let currentTitelHover, currentCoverData, currentPopupEvent;
let popover, popoverTitle, popoverContent;
let lastTarget;
let isShowingSpinnerAnimation = false;
let showDescription = false;
let showSmaller = false;
let showHotkeys = false;
let mediumTextStyle = "mediumText";
let smallTextStyle = "smallText";
let pressedKeys = [];
//#endregion
//console.log("after variable settings");
//console.log(this.location)
//console.log(this.location.href)

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

//#region helper functions

// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };

//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,
        description: coverData.description,
      };
    } 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,
    description: coverData.description,
    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 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) {
    const styleSheetsLengthToLoop = fullStyleSheets.length - 1;
    for (let i = 0; i < styleSheetsLengthToLoop; i++) {
      //DEBUG && console.log("loop fullStyleSheets " + stylename);
      let styleSheet = fullStyleSheets[i];
      if (
        styleSheet != null &&
        styleSheet.href !== null &&
        (styleSheet.href.match(localDomainCheck) ||
          styleSheet.href.match(localDomainCheckHttps)) &&
        styleSheet.cssRules //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
      ) {
        //DEBUG && console.log("styleSheet.cssRules.length: " + styleSheet.cssRules.length)
        const ruleLengthToLoop = styleSheet.cssRules.length - 1;
        for (let rulePos = 0; rulePos < ruleLengthToLoop; 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;
}

// Callback function to execute when mutations are observed
/* https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/
    const function can not be overwritten //https://stackoverflow.com/questions/54915917/overwrite-anonymous-const-function-in-javascript
    https://www.digitalocean.com/community/tutorials/understanding-hoisting-in-javascript
    function callback  () is anonymous var function which can be overwritten?
    */
//https://medium.com/@alexcambose/js-offsettop-property-is-not-great-and-here-is-why-b79842ef7582
function getOffset(element, horizontal = false) {
  if (!element) return 0;
  return (
    getOffset(element.offsetParent, horizontal) +
    (horizontal ? element.offsetLeft : element.offsetTop)
  );
}
const debounce = function (func, timeout) {
  let timer;
  return (...args) => {
    const next = () => func(...args);
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(next, timeout > 0 ? timeout : 300);
  };
};

//https://gist.github.com/peduarte/969217eac456538789e8fac8f45143b4#file-index-js
const throttle = function (func, wait = 100) {
  let timer = null;
  return function (...args) {
    if (timer === null) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, wait);
    }
  };
};

const callbackMutationObserver = 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(callbackMutationObserver);
const debouncedpreloadCoverData = debounce(preloadCoverData, 100);
const throttledGetHoveredItem = throttle(getHoveredItem, 50);
//#endregion
function inBlocklist(link) {
  //console.log(link)
  if (IMAGEBLOCKERARRAY) {
    const hasBlocker = IMAGEBLOCKERARRAY.includes(link);
    if (hasBlocker) return true;
    //console.log(hasBlocker);
  }

  return false;
}
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 === undefined ||
    dataVersion != version ||
    forceUpdate
  ) {
    resetDatabase();
  }
}
function resetDatabase() {
  const DEBUG = false;
  const oldValues = GM_listValues();
  DEBUG && console.log("oldValues.length: " + oldValues.length);
  const oldValuesLengthToLoop = oldValues.length;
  for (let i = 0; i < oldValuesLengthToLoop; i++) {
    GM_deleteValue(oldValues[i]);
    //console.log(oldValues[i])
  }
  DEBUG && console.log(oldValues);
  GM_setValue("version", version);
}
function chooseAndGetRectOffset(nativElement) {
  let targetedRect;
  if (isOnIndex || isOnReadingListIndex) {
    targetedRect = nativElement.parentElement.getBoundingClientRect();
  } else {
    targetedRect = nativElement.getBoundingClientRect();
  }
  return getRectOffset(targetedRect);
}

//https://dmitripavlutin.com/differences-between-arrow-and-regular-functions/
const getDistanceToBottom = (Y, scrollPosY, popoverRect) => {
  return (
    Y -
    scrollPosY +
    popoverRect.height -
    (window.innerHeight - offsetToBottomBorderY)
  );
};

function getDistanceToBottom2(Y, scrollPosY, popoverRect) {
  return (
    Y -
    scrollPosY +
    popoverRect.height -
    (window.innerHeight - offsetToBottomBorderY)
  );
}

function getRectOffset(rect) {
  return { Rx: rect.left + rect.width, Ry: rect.top };
}
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);
  if (event && event !== undefined) {
    showPopOver();

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

    // console.log(scrollPosX)
    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.maxHeight =
          "min(400px,calc(100% - " + popoverHeightMargin + "px))";
        popover.style.maxWidth =
          "min(800px,calc(100% - " + popoverWidthMargin + "px))";
          */

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

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

function tryToGetTextContent(element, query, queryName) {
  let result = element;
  if (result && result !== undefined) result = result.textContent;
  else console.log("Wrong querySelector for " + queryName + ". not: " + query);
  return result;
}
async function getCoverDataFromUrl(elementUrl) {
  const DEBUG = false;
  let PromiseResult = new Promise(async function (resolve, reject) {
    DEBUG && console.log("elementUrl: " + elementUrl);

    // 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) {
      //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
      if (xhr.status >= 200 && xhr.status < 399) {
        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: " + currentTitelHover);
          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);
          let serieDescription = domDocument.querySelector(
            seriePageDescription
          );

          serieTitle = tryToGetTextContent(
            serieTitle,
            seriePageTitle,
            "seriePageTitle"
          );
          serieVotes = tryToGetTextContent(
            serieVotes,
            seriePageVotes,
            "seriePageVotes"
          );
          serieStatus = tryToGetTextContent(
            serieStatus,
            seriePageStatus,
            "seriePageStatus"
          );
          serieGenre = tryToGetTextContent(
            serieGenre,
            seriePageGenre,
            "seriePageGenre"
          );
          serieShowtags = tryToGetTextContent(
            serieShowtags,
            seriePageTags,
            "seriePageTags"
          );
          serieDescription = tryToGetTextContent(
            serieDescription,
            seriePageDescription,
            "seriePageDescription"
          );
          DEBUG && console.log(serieTitle);
          DEBUG && console.log(serieVotes);
          DEBUG && console.log(serieStatus);
          DEBUG && console.log(serieGenre);
          DEBUG && console.log(serieShowtags);
          DEBUG && console.log(serieDescription);
          DEBUG &&
            console.log("save imageUrl into coverData.url: " + imagelink);
          let cData = {
            url: imagelink,
            title: serieTitle,
            votes: serieVotes,
            status: serieStatus,
            genre: serieGenre,
            showTags: serieShowtags,
            description: serieDescription,
          };
          //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(xhr);
        }
      }
    }

    function onError(error) {
      console.log(error);
      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,
    });

    return undefined; //reject("status error")
  });
  return PromiseResult;
}
async function parseSeriePage(
  elementUrl,
  forceReload = false,
  hoveredTitle = undefined,
  event = undefined
) {
  const DEBUG = false;
  DEBUG && console.group("parseSeriePage: " + elementUrl);
  const coverData = GM_getCachedValue(elementUrl);

  let retrievedImgLink;
  let PromiseResult;
  if (!forceReload && coverData !== null && coverData.url) {
    retrievedImgLink = coverData.url;
    DEBUG &&
      console.log(
        "parseSeriePage has cached retrievedImgLink: " + retrievedImgLink
      );
    PromiseResult = coverData;
  } else {
    /*
        console.group("parseSeriePage vars before getCoverDataFromUrl")
        console.log("elementUrl: " + elementUrl+", forceReload: " + forceReload+", hoveredTitle: " + hoveredTitle);
        console.log(event);
        console.groupEnd();
        */
    showPopupLoadingSpinner(hoveredTitle, hoveredTitle, event);
    PromiseResult = getCoverDataFromUrl(elementUrl);
  }

  DEBUG && console.groupEnd("parseSeriePage: " + elementUrl);
  PromiseResult = await PromiseResult;
  //console.log(PromiseResult)
  //DEBUG && console.log(PromiseResult)
  //after GM_xmlhttpRequest PromiseResult

  return PromiseResult;
}

function preloadCoverData() {
  const DEBUG = false;

  updateSerieNodes();
  DEBUG && console.log(ALLSERIENODES);

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

  DEBUG &&
    console.log(
      "before parseSeriePage for each url with a link to individual seriepage"
    );
  if (ALLSERIENODES && ALLSERIENODES.length > 0) {
    ALLSERIENODES.map(function (el) {
      //console.log(el)
      const elementUrl = el.href;
      // console.log(elementUrl)
      if (
        eventListenerStyle === undefined ||
        eventListenerStyle === null ||
        eventListenerStyle == 0
      ) {
        el.removeEventListener("mouseenter", mouseEnterPopup);
        el.removeEventListener("mouseleave", hideOnMouseLeave);
        el.addEventListener("mouseenter", mouseEnterPopup);
        el.addEventListener("mouseleave", hideOnMouseLeave);
      }

      if (preloadUrlRequests) {
        DEBUG && console.log("start parseSeriePage");
        parseSeriePage(elementUrl).then(
          function (coverData) {
            if (coverData !== undefined) {
              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 addStyles() {
  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;
          }
      }
    
      .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 8px;
          min-height:unset;
          height:auto;
          display:inline-block;
          width:100%;
          /*max-width:auto;*/
          text-align:center !important;
          justify-content: center;
          justify-items: center;
          border: 0 !important;
          border-bottom: 1px solid #000 !important;
          border-radius:10px 10px 0 0 !important;                    
          line-height:1.4em;
      }
      .defaultTitleStyleSmall {
        line-height:1.2em;
      }
      .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;
          min-width: 0;
          min-height: 0;
          max-height: 400px;
          max-width: 400px;
          width:100%;
          height:100%;
          padding:2px;
      }
    
      #popover{
        /* min() not compatible with firefox 56
        max-height: min(400px, (100vh - (100vh - 100%) - 44px));
        max-width: min(400px, calc(100vw - (100vw - 100%)));
        */
        max-height: calc(100vh - (100vh - 100%) - 44px);
        max-width: calc(100vw - (100vw - 100%));
        min-height: 0;
        min-width: 0;
        /*height: 400px;*/
        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;
        
        text-align: center !important;
        justify-content: start;
        justify-items: center;
        display: flex;
        flex-shrink: 1;
        flex-direction: column;   
        
        /*
        display: grid;
        grid-template-columns: 1fr;
        grid-template-rows: min-content minmax(20px, 1fr);
        gap: 0px 0px;
        grid-template-areas:
          "."
          ".";*/
      }
      .popoverContent {      
          text-align: center !important;
          justify-content: center;
          justify-items: center;
          align-items: center;
    
          display: flex;
          flex-direction: column;
          min-height: 0;
          min-width: 0;
          padding: 1px !important;
          
          width: 100%;
          height: 100%;
          flex: 1;
          padding:1px !important;
      }
      .popoverDetail{
          flex-direction:unset !important;
          height:400px;
      }
      .containerPadding{
        justify-items:center;
        padding:10px
      }
      .popoverTitleDetail{
          height:100% !important;
          width:auto !important;
          max-width:65% !important;
          border-radius: 10px 0 0 5px !important;
          border:0 !important;
          border-right: 1px solid #000 !important;
      }
      
      .smallText{
          font-size: 0.8em;
      }
      .mediumText{
        font-size: 0.98em;
      }

      .small_smallText{
        display:inline-block; /* line height not working if the element is not a block */
        font-size: 0.82em;
        line-height: 1.4em;
      }
      .small_mediumText{
        display:inline-block;
        font-size: 0.78em;
        line-height: 1.2em;
      }
      .wordBreak {
          word-wrap: break-word !important;
          word-break: break-word;
      } 
      .borderTop {
        width:100%;
        border-top:1px solid#fff;
        margin: 2px 0;
      }
      `);
}
function setStyleClasses() {
  const lastUpdated = GM_getValue("lastUpdated");
  const currentTime = Date.now();
  const timeDifference = currentTime - lastUpdated;
  const cachedBackgroundClasses = GM_getValue("STYLESHEETHIJACKFORBACKGROUND");
  //console.log({lastUpdated,currentTime,timeDifference})
  //console.log("timeDifference: " + timeDifference)
  if (
    lastUpdated === null ||
    lastUpdated === undefined ||
    timeDifference > lastUpdateCheck
  ) {
    GM_setValue("lastUpdated", currentTime);
    refreshInitValues = true;
    // console.log("set lastUpdated to now")
  }
  //console.log(refreshInitValues);

  if (debugCSSClasses) {
    if (
      STYLESHEETHIJACKFORBACKGROUND !== "" &&
      (refreshInitValues ||
        cachedBackgroundClasses === undefined ||
        forceUpdate)
    ) {
      let styleSheetToAddBackground = "";
      for (let i = 0; i < STYLESHEETHIJACKFORBACKGROUNDARRAY.length; i++) {
        if (styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUNDARRAY[i])) {
          console.log(
            "+ has found class: " + STYLESHEETHIJACKFORBACKGROUNDARRAY[i]
          );
          styleSheetToAddBackground += STYLESHEETHIJACKFORBACKGROUNDARRAY[i];
        } else {
          console.log(
            "- has not found class: " + STYLESHEETHIJACKFORBACKGROUNDARRAY[i]
          );
        }
      }
      STYLESHEETHIJACKFORBACKGROUND = styleSheetToAddBackground
        .replace(REGEX_DOTCOMMA, " ")
        .trim();
      GM_setValue(
        "STYLESHEETHIJACKFORBACKGROUND",
        STYLESHEETHIJACKFORBACKGROUND
      );
      //console.log("STYLESHEETHIJACKFORBACKGROUND: " + STYLESHEETHIJACKFORBACKGROUND)
    } else {
      STYLESHEETHIJACKFORBACKGROUND = cachedBackgroundClasses;
      console.log("cachedBackgroundClasses: " + cachedBackgroundClasses);
    }
    const cachedTitleClasses = GM_getValue("STYLESHEETHIJACKFORTITLE");
    if (
      STYLESHEETHIJACKFORTITLE !== "" &&
      (refreshInitValues || cachedTitleClasses === undefined || forceUpdate)
    ) {
      let styleSheetToAddTitle = "";
      for (let i = 0; i < STYLESHEETHIJACKFORTITLEARRAY.length; i++) {
        if (styleSheetContainsClass(STYLESHEETHIJACKFORTITLEARRAY[i])) {
          console.log("+ has found class: " + STYLESHEETHIJACKFORTITLEARRAY[i]);
          styleSheetToAddTitle += STYLESHEETHIJACKFORTITLEARRAY[i];
        } else {
          console.log(
            "- has not found class: " + STYLESHEETHIJACKFORTITLEARRAY[i]
          );
        }
      }
      STYLESHEETHIJACKFORTITLE = styleSheetToAddTitle
        .replace(REGEX_DOTCOMMA, " ")
        .trim();
      GM_setValue("STYLESHEETHIJACKFORTITLE", STYLESHEETHIJACKFORTITLE);
      //console.log("STYLESHEETHIJACKFORTITLE: " + STYLESHEETHIJACKFORTITLE)
    } else {
      STYLESHEETHIJACKFORTITLE = cachedTitleClasses;
      console.log("cachedTitleClasses: " + cachedTitleClasses);
    }
  } else {
    //console.log("not debugging CSS classes")
    STYLESHEETHIJACKFORBACKGROUND = STYLESHEETHIJACKFORBACKGROUND.replace(
      REGEX_DOTCOMMA,
      " "
    ).trim();
    STYLESHEETHIJACKFORTITLE = STYLESHEETHIJACKFORTITLE.replace(
      REGEX_DOTCOMMA,
      " "
    ).trim();
  }
}

function setPopoverHeight() {
  //https://developer.mozilla.org/en-US/docs/Web/CSS/min() not compatible with firefox 56

  let targetHeight = defaultHeight;
  if (showSmaller) targetHeight = smallHeight;

  const minHeightValue =
    "min(" +
    targetHeight +
    "px, (100vh - (100vh - 100%) - " +
    offsetToBottomBorderY * 2 +
    "px))";

  if (CSS.supports("max-Height", minHeightValue)) {
    //console.log("supports min()");
    popover.style.maxHeight = minHeightValue;
  } else {
    console.log("does not support CSS min()");
    popover.style.maxHeight =
      "calc(100vh - (100vh - 100%) - " + offsetToBottomBorderY * 2 + "px))";
    popover.style.height = targetHeight;
  }
}
function setPopoverWidth() {
  let targetHeight = defaultHeight;
  if (showSmaller) targetHeight = smallHeight;

  if (showDetails) {
    popover.classList.add("popoverDetail");
    const minWidthValue =
      "min(" +
      targetHeight * 2 +
      "px, (100vw - (100vw - 100%) - " +
      offsetToRightBorderX * 2 +
      "px))";
    const supportsCSSMin = CSS.supports("max-Width", minWidthValue);
    if (supportsCSSMin) {
      //console.log("supports min()");
      popover.style.maxWidth = minWidthValue;
    } else {
      console.log("does not support CSS min()");
      popover.style.maxWidth =
        "calc(100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))";
      popover.style.height = targetHeight;
      popover.style.width = targetHeight * 2 + "px";
    }

    popoverTitle.classList.add("popoverTitleDetail");
  } else {
    popover.classList.remove("popoverDetail");

    const minWidthValue =
      "min(" +
      targetHeight +
      "px, (100vw - (100vw - 100%) - " +
      offsetToRightBorderX * 2 +
      "px))";
    const supportsCSSMin = CSS.supports("max-Width", minWidthValue);
    if (supportsCSSMin) {
      popover.style.maxWidth = minWidthValue;
    } else {
      popover.style.maxWidth =
        "calc(100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))";
      popover.style.width = targetHeight + "px";
    }

    popoverTitle.classList.remove("popoverTitleDetail");
  }
}
function createPopover() {
  let bodyElement = document.getElementsByTagName("BODY")[0];

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

  popoverTitle = document.createElement("header");
  popoverContent = document.createElement("content");

  popover.appendChild(popoverTitle);
  popover.appendChild(popoverContent);
  popover.className = (
    STYLESHEETHIJACKFORBACKGROUND + " defaultBackgroundStyle"
  ).trim();
  popoverContent.className = "popoverContent blackFont";
  if (
    !STYLESHEETHIJACKFORBACKGROUND &&
    DEFAULTBACKGROUNDCOLOR &&
    DEFAULTBACKGROUNDCOLOR != ""
  )
    popover.style.backgroundColor = DEFAULTBACKGROUNDCOLOR;

  setPopoverHeight();
  setPopoverWidth();
  //console.log(popover)
  //console.log(popover.style)
  popoverTitle.className = (
    STYLESHEETHIJACKFORTITLE + " defaultTitleStyle"
  ).trim();
  if (
    !STYLESHEETHIJACKFORTITLE &&
    DEFAULTTITLEBACKGROUNDCOLOR &&
    DEFAULTTITLEBACKGROUNDCOLOR != ""
  ) {
    popoverTitle.style.backgroundColor = DEFAULTTITLEBACKGROUNDCOLOR;
    popoverTitle.style.color = "#fff";
  }
  popover.addEventListener("mouseleave", hideOnMouseLeave);
  popover.style.left = 0;
  popover.style.top = 0; //avoid invisible popover outside regular site height

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

function showPopupLoadingSpinner(
  hoveredTitleLink,
  title,
  event,
  notification = "",
  coverData = undefined
) {
  const DEBUG = false;

  //console.log(event)
  const isActivePopup =
    currentTitelHover !== undefined &&
    hoveredTitleLink !== undefined &&
    currentTitelHover == hoveredTitleLink;
  /*
        console.group("showPopupLoadingSpinner")
        //"currentCoverData: " +currentCoverData +
        console.log("currentTitelHover: " + currentTitelHover+", hoveredTitleLink: " + hoveredTitleLink+", currentTitelHover == hoveredTitleLink: " + (currentTitelHover == hoveredTitleLink))
        console.log("isActivePopup: " + isActivePopup)
        console.groupEnd();*/
  if (isActivePopup) {
    // 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 != "") {
      isShowingSpinnerAnimation = false;
      popoverContent.innerHTML = notification;
      popoverContent.className = "popoverContent wordBreak"; //blackfont
    } else {
      isShowingSpinnerAnimation = true;
      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")
  }
}

function refreshPopover(coverData, e = undefined) {
  //only call when isActivePopup
  const DEBUG = false;
  if (coverData && coverData !== undefined) {
    isShowingSpinnerAnimation = 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 (link === undefined || inBlocklist(link)) {
      popoverContent.innerHTML =
        '<div class="containerPadding">Blocked Image<br />No Cover Image<br />Unwanted Image</div>';
    } 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");
    DEBUG && console.log(e);
    //if (currentTitelHover == title)
    if (e !== undefined) popupPos(e);
  }
}

//#region get serieDetails
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;
}
function getChapters(statusString) {
  let result;
  if (statusString) {
    let chapterCount;
    let lowerCaseStatusString = statusString.toLowerCase();
    const matches = lowerCaseStatusString.match(reChapters);
    let webnovel = "";
    let hasVolumenInString = false;
    if (matches && matches.length >= 2) {
      chapterCount = matches[1];
      if (matches[2]) {
        webnovel = " WN";
      }
    }
    if (!chapterCount) {
      const matchesBehind = lowerCaseStatusString.match(reChaptersNumberBehind);
      if (matchesBehind && matchesBehind.length >= 2) {
        chapterCount = matchesBehind[1];
      }
    }
    if (!chapterCount) {
      const matchesNumbers = lowerCaseStatusString.match(reChaptersOnlyNumbers); //example string "6892(Ongoing)"
      if (matchesNumbers && matchesNumbers.length >= 2) {
        chapterCount = matchesNumbers[1];
      }
    }
    if (lowerCaseStatusString.includes("vol")) hasVolumenInString = true;

    if (chapterCount) {
      let numberType = " Chapters";
      if (hasVolumenInString) numberType = " Vol";
      result = chapterCount + webnovel + numberType;
    }
  }

  return result;
}
function getCompletedState(statusString) {
  let result = false;
  if (statusString && statusString.toLowerCase().includes("complete")) {
    //complete | completed
    result = true;
  }
  return result;
}
function getOngoingState(statusString) {
  let result = false;
  if (statusString && statusString.toLowerCase().includes("ongoing")) {
    result = true;
  }
  return result;
}
function getDetailsString(coverData) {
  let completeDetails = "";
  if (showDescription) {
    if (coverData.description && coverData.description.length > 0) {
      completeDetails +=
        '<div class="borderTop">Description: ' +
        coverData.description +
        "</div>";
    } else {
      completeDetails +=
        '<div class="borderTop">Description: Description Empty or error in coverData. Please reload seriepage info</div>';
    }
  } else {
    if (coverData.votes) {
      completeDetails +=
        '<div class="borderTop">Rating: ' + coverData.votes + "</div>";
    }
    if (coverData.status) {
      completeDetails +=
        '<div class="borderTop">Status: ' + coverData.status + "</div>";
    }
    if (coverData.genre) {
      completeDetails +=
        '<div class="borderTop">Genre: ' + coverData.genre + "</div>";
    }
    if (coverData.showTags) {
      completeDetails +=
        '<div class="borderTop">Tags: ' + coverData.showTags + "</div>";
    }
  }

  return completeDetails;
}
function getShortendDetailsString(coverData) {
  let completeDetails = "";
  let rating = getRatingNumber(coverData.votes);
  let chapters = getChapters(coverData.status);
  let completed = getCompletedState(coverData.status);
  let ongoing = getOngoingState(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="' +
      smallTextStyle +
      '" style="white-space: nowrap;"> [' +
      rating +
      chapters +
      completed +
      ongoing +
      "]</span>";
  }
  return completeDetails;
}
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 = "";

  if (showDetails) {
    //console.log("showDetails should be true")
    completeDetails +=
      '<span class="' +
      mediumTextStyle +
      '">' +
      titleToShow +
      " " +
      getDetailsString(coverData);
    completeDetails +=
      '<div class="borderTop ' +
      smallTextStyle +
      '">[KeyH show hotkey list]<br />[Key1 Switch detailed and simple popup] [Key2 Switch between description and tags] [Key3 small and big popup style] </div></span>';
  } else {
    completeDetails =
      '<span class="' +
      mediumTextStyle +
      '">' +
      titleToShow +
      " " +
      getShortendDetailsString(coverData);
    completeDetails +=
      ' <span class="' + smallTextStyle + '">[KeyH hotkey list]</span></span>';
  }
  //popoverTitle.innerHTML = completeDetails;

  popoverTitle.innerHTML = completeDetails;
}
//#endregion

function setCurrentCoverDataAndLoadImage(coverData, hoveredTitle, e) {
  const DEBUG = false;
  //GM_getCachedValue
  DEBUG && console.group("setCurrentCoverDataAndLoadImage");
  // const coverData = GM_getCachedValue(Href);

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

  if (coverData !== undefined && coverData !== null)
    currentCoverData = coverData;

  if (e) loadImageFromBrowser(coverData, serieTitle, e, hoveredTitle);

  DEBUG && console.groupEnd("setCurrentCoverDataAndLoadImage");
}

function ajaxLoadImageUrlAndShowPopup(
  forceReload = false,
  elementUrl,
  hoveredTitle,
  e
) {
  const currentEvent = e;
  //console.log(currentEvent)
  //console.log("mouseenter")
  // console.group("ajaxLoadImageUrlAndShowPopup")
  return parseSeriePage(
    elementUrl,
    forceReload,
    hoveredTitle,
    currentEvent
  ).then(
    function (coverData) {
      if (coverData !== undefined) {
        setCurrentCoverDataAndLoadImage(coverData, hoveredTitle, currentEvent);
      }
    },
    function (Error) {
      console.log(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("isActivePopup: " + isActivePopup);*/
  console.log("hoveredTitleLink:" + hoveredTitleLink);
  console.log(error);
  let filename = "";
  console.log("coverData.url: " + coverData.url);

  if (coverData.url != "undefined") {
    filename = decodeURIComponent(coverData.url);
  } else {
    filename = "undefined";
  }
  const errorMessage =
    '<div class="containerPadding">browser blocked/has error loading the file: <br />' +
    filename +
    "</div>";
  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/reload the coverdata"
    );
  else
    console.log(
      "image loading most likely blocked by browser or addon. Check if the imagelink still exists/reload the coverdata"
    );

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

function loadImageFromBrowser(
  coverData,
  serieTitle = undefined,
  e = undefined,
  hoveredTitleLink = undefined
) {
  const DEBUG = false;
  //console.log(e)
  //console.group("loadImageFromBrowser")
  let img = document.createElement("img"); //put img into dom. Let the image preload in background
  const hasMouseEnterEvent = hoveredTitleLink !== undefined && e !== undefined;
  //console.log(currentCoverData)
  //console.log(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 = coverData.url;

  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
    /*
        const isActivePopup =
          currentCoverData !== undefined &&
          currentTitelHover !== undefined &&
          hoveredTitleLink !== undefined &&
          currentTitelHover == hoveredTitleLink &&
          hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
        //console.log(e)
        if (isActivePopup) {      
            */
    DEBUG &&
      console.log(
        "loadImageFromBrowser image not completely loaded yet. Show loading spinner : " +
          serieTitle
      );
    showPopupLoadingSpinner(hoveredTitleLink, serieTitle, e);
    //  }
  }
  // console.groupEnd("loadImageFromBrowser")
}

function hidePopOver() {
  popover.style.visibility = "hidden";
  //popover.style.height = "0";
  //popover.style.width = "0";
  //console.group("hidePopOver")
  //console.log("currentTitelHover: " + currentTitelHover)
  currentTitelHover = undefined;
  currentCoverData = undefined;
  popoverVisible = false;

  if (isShowingSpinnerAnimation) popoverContent.innerHTML = ""; //remove infinite spinner animation when popup not shown
  pressedKeys = []; //window blur release keys
  //console.log("currentTitelHover: " + currentTitelHover)
  //console.groupEnd("hidePopOver")
}
function showPopOver() {
  // popover.style.display = "flex";
  //popover.style.height = "100%";
  // popover.style.width = "100%";
  popover.style.visibility = "visible";
  popoverVisible = true;
}
function hideOnMouseLeave() {
  //if (!e.target.matches(concatSelector())) return;
  //popover.hide();
  hidePopOver();
}

/*
 * get links into ALLSERIENODES and convert this nodearray to array
 *
 */
function updateSerieNodes() {
  if (ALLSERIENODES && ALLSERIENODES.length > 0) {
    ALLSERIENODES.forEach(function (selector) {
      if (
        eventListenerStyle === undefined ||
        eventListenerStyle === null ||
        eventListenerStyle == 0
      ) {
        selector.removeEventListener("mouseleave", hideOnMouseLeave);
        selector.removeEventListener("mouseenter", mouseEnterPopup);
      }
    });
  }
  const serieLinkNodes = document.querySelectorAll(
    'a[href*="' + INDIVIDUALPAGETEST + '"]'
  );
  //console.log(serieLinkNodes)
  ALLSERIENODES = Array.from(serieLinkNodes);
  //console.log(ALLSERIENODES)
  /*
          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");
  if (currentCoverData !== undefined) {
    //has CoverData from seriePage loaded?
    DEBUG && console.log(currentCoverData);
    refreshPopover(currentCoverData, currentPopupEvent); //update on detail change
  }

  console.groupEnd("switchDetails");
}
function switchTagsDescriptionAndUpdatePopup() {
  const DEBUG = false;
  if (showDetails) {
    showDescription = !showDescription;
    //console.log("switch showDetails to : " + showDetails)
    GM_setValue("showDescription", showDescription);

    if (currentCoverData !== undefined) {
      //has CoverData from seriePage loaded?
      DEBUG && console.log(currentCoverData);
      refreshPopover(currentCoverData, currentPopupEvent); //update on detail change
    }
  }
}
function changeToNewDetailStyle(toggleDetails = true) {
  if (toggleDetails) showDetails = !showDetails;
  //console.log("switch showDetails to : " + showDetails)
  GM_setValue("showDetails", showDetails);
  //localStorage.setItem("showDetails", showDetails);
  //https://developer.mozilla.org/en-US/docs/Web/CSS/min() not compatible with firefox 56
  setPopoverWidth();
}

//#region eventListener
function mouseEnterPopup(e, forceReload = false) {
  //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)
  if (e !== undefined) {
    const target = e.target;
    let Href = target.href; // element.attr('href');
    if (Href.includes(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 = target.text; //element.text(); //get linkname
      //console.log(this)
      //console.log(shortSerieTitle)
      const dataTitle = target.getAttribute("datatitle");
      const linkTitle = target.getAttribute("title");
      //console.log("linkTitle: " + linkTitle)
      const hasDataTitle =
        dataTitle === null ||
        dataTitle == "null" ||
        dataTitle === undefined ||
        !dataTitle;
      //move native title to custom data attribute. Suppress nativ title popup
      if (linkTitle !== null && hasDataTitle) {
        target.setAttribute("datatitle", linkTitle);
        target.removeAttribute("title");
      }

      let serieTitle = target.getAttribute("datatitle"); //element.attr('datatitle'); //try to get nativ title if available from datatitle
      //console.log(serieTitle)
      if (
        serieTitle === null || //has no set nativ long title -> use (available shortend) linkname
        serieTitle == "null" ||
        PREDIFINEDNATIVTITLEARRAY.some((nativTitle) =>
          serieTitle.includes(nativTitle)
        )
      )
        //catch on individual serie page nativ title begins with "Recommended by" x people -> use linkname
        serieTitle = shortSerieTitle;
      if (serieTitle === undefined || serieTitle === null || serieTitle == "")
        //image link: example link which content is only the cover image https://www.mangaupdates.com/series.html?letter=A
        serieTitle = Href;
      currentTitelHover = serieTitle; //mark which titel is currently hovered
      currentPopupEvent = e;
      //console.log(serieTitle)
      //console.log(Href)

      //console.log(currentCoverData)
      //console.log("currentTitelHover: " + currentTitelHover)
      ajaxLoadImageUrlAndShowPopup(forceReload, Href, currentTitelHover, e);
    }
  }
  DEBUG && console.groupEnd("mouseEnterPopup");
}
function forceReload(event, forceReload = true) {
  mouseEnterPopup(currentPopupEvent, forceReload);
}

function updatePopoverSize() {
  setPopoverHeight();
  setPopoverWidth();

  if (showSmaller) {
    mediumTextStyle = "small_mediumText";
    smallTextStyle = "small_smallText";
    popoverTitle.classList.add("defaultTitleStyleSmall");
  } else {
    popoverTitle.classList.remove("defaultTitleStyleSmall");
    mediumTextStyle = "mediumText";
    smallTextStyle = "smallText";
  }

  if (currentCoverData !== undefined)
    refreshPopover(currentCoverData, currentPopupEvent);
}

function showHotkeyList() {
  if (!showHotkeys) {
    if (currentCoverData !== undefined)
      refreshPopover(currentCoverData, currentPopupEvent);
  } else {
    popoverContent.innerHTML =
      '<div class="' +
      mediumTextStyle +
      '" style="text-align:start">Key 1: Switch detailed And simple popup<br />' +
      `Key 2: Switch between description and tags<br />
    Key 3: Switch between small and big popup style<br />
    Key 5: Reload coverdata of seriepage<br />
    Key 6: Clear all cover data info<br />
    Key H: Show this hotkey list during holding of key h
    </div>`;
    if (currentCoverData !== undefined) popupPos(currentPopupEvent);
  }
}

function reactToKeyPressWhenPopupVisible(event) {
  //console.log(event);
  //console.log(currentTitelHover)
  const key = event.key;
  if (popoverVisible) {
    if (!pressedKeys.includes(key)) {
      //console.log(event);
      pressedKeys.push(key);
      switch (key) {
        case "1":
          switchDetailsAndUpdatePopup();
          break;
        case "5":
          forceReload(event);
          break;
        case "6":
          resetDatabase();
          preloadCoverData();
          forceReload(event);
          break;
        case "2":
          switchTagsDescriptionAndUpdatePopup();
          break;
        case "3":
          showSmaller = !showSmaller;
          GM_setValue("showSmaller", showSmaller);
          updatePopoverSize();
          break;
        case "h":
          showHotkeys = true;
          showHotkeyList();
          break;
      }
    }
  }
}
function releaseKey(event) {
  const key = event.key;
  //console.log(pressedKeys)
  pressedKeys.splice(pressedKeys.indexOf(key), 1);
  if (event.key == "h") {
    showHotkeys = false;
    showHotkeyList();
  }
  //console.log(pressedKeys)
}
function prepareEventListener() {
  window.addEventListener("blur", hidePopOver);
  window.addEventListener("keypress", reactToKeyPressWhenPopupVisible); //keypress gets repeated during keydown
  window.addEventListener("keyup", releaseKey);
  window.onunload = function () {
    window.removeEventListener("blur", hidePopOver);
    window.removeEventListener("keypress", reactToKeyPressWhenPopupVisible);
    window.addEventListener("keyup", releaseKey);
    popover.removeEventListener("mouseleave", hideOnMouseLeave);
    //possible memoryleaks?
    updateSerieNodes();
    observer.disconnect();
  };
  if (eventListenerStyle == 1)
    window.addEventListener("mousemove", throttledGetHoveredItem);
}

//assumption that a single eventlistener is more performant than dozens of mouseEnter/MouseLeave events
//https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/#web-performance
//https://davidwalsh.name/event-delegate
//https://web.archive.org/web/20170121035049/http://jsperf.com/click-perf
//https://stackoverflow.com/questions/29836326/is-using-a-universal-document-addeventlistenerclick-f-listener-slower-or-wea
/*
      This is the proper pattern for creating event listeners that will work for dynamically-added elements. It's essentially the same approach as used by jQuery's event delegation methods (e.g. .on).
      */

function getHoveredItem(e) {
  if (eventListenerStyle == 1) {
    if (
      e.target &&
      e.target != lastTarget &&
      e.target.nodeName == "A" &&
      e.target.href &&
      e.target.href.includes(INDIVIDUALPAGETEST)
    ) {
      lastTarget = e.target;
      //console.group("target A")
      //console.log(e.target.text)
      //console.log(e)

      mouseEnterPopup(e);
      //console.groupEnd();
    } else {
      if (e.target.nodeName != "A") {
        lastTarget = undefined;
        hideOnMouseLeave();
      }
    }
  }
}

document.addEventListener("DOMContentLoaded", main());
//#endregion

function main() {
  const DEBUG = false;
  DEBUG && console.log("started main function of coverPreview");
  DEBUG && console.log("before starting checkDataVersion");
  checkDataVersion();

  DEBUG && console.log("before starting setStyleClasses");
  addStyles();
  setStyleClasses();
  DEBUG && console.log("before starting createPopover");
  createPopover();
  DEBUG && console.log("before starting hidePopOver");
  hidePopOver();
  showDetails = GM_getValue("showDetails");
  showDescription = GM_getValue("showDescription");
  showSmaller = GM_getValue("showSmaller");
  if (showSmaller) {
    updatePopoverSize();
  }
  //if(showDetails) showDetails = JSON.parse(showDetails);
  //showDetails = localStorage.getItem("showDetails") == "true";
  //console.log("localStorage state showDetails: " + showDetails)
  DEBUG && console.log("before starting changeToNewDetailStyle");
  changeToNewDetailStyle(false);
  //console.log("isOnReadingListIndex: " + isOnReadingListIndex)
  if (isOnReadingListIndex) {
    DEBUG && console.log("before starting observer");
    let targetNode = document.getElementById(targetContainerIDToObserve);
    //console.dir(targetNode)
    observer.observe(targetNode, config); //observe for update before running debouncedwaitForReadingList();
  } else {
    DEBUG && console.log("before starting preloadCoverData");
    preloadCoverData();
  }
  DEBUG && console.log("before starting prepareEventListener");
  prepareEventListener();
  DEBUG && console.log("finished main function of coverPreview");
}
//console.log("cover preview end");
长期地址
遇到问题?请前往 GitHub 提 Issues。