novelupdates Cover Preview

Previews covers in novelupdates.com when hovering over hyperlinks that lead to novelupdates seriepages and a few external pages.

// ==UserScript==
// https://greasyfork.org/scripts/26439-novelupdates-cover-preview/
// @name        novelupdates Cover Preview
// @namespace   somethingthatshouldnotclashwithotherscripts
// @version     2.6.0
// @description Previews covers in novelupdates.com when hovering over hyperlinks that lead to novelupdates seriepages and a few external pages.
// @author      SZ
// @supportURL  https://greasyfork.org/de/scripts/26439-novelupdates-cover-preview/feedback
// @website     https://forum.novelupdates.com/threads/novel-updates-userscript-to-preview-cover-images-on-greasyfork.117240/
// @include     https://www.novelupdates.com/*
// @include     http://www.novelupdates.com/*
// @include     https://forum.novelupdates.com/*
// @include     http://forum.novelupdates.com/*
// @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==
(function () {
  //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-submain, .breadcrumb"; //if unknown set empty ""; classname with leading dot seperated with comma  //.pageContent
  //index: l-submain;forum: breadcrumb;
  let STYLESHEETHIJACKFORTITLE = ".widgettitle_nuf, .navTabs "; //if unknown set empty ""; classname with leading dot seperated with comma
  //index: widgettitle_nuf; forum:navTabs
  const PREDIFINEDNATIVTITLE = "Recommended by";
  /* forum, index
    display shorttitle or serielink url before coverdata from individualpage is loaded
    can have other starting strings seperated by a comma
    for example mangaupdates is using "Click for series info, Series Info"
    */
  const isOnIndex = false; //now autodetect display style by checking container of link is tablecell "td"
  /*
      this.location.href == "https://www.novelupdates.com/" ||
      this.location.href.startsWith("https://www.novelupdates.com/?pg=");
      this.location.href.startsWith("https://www.novelupdates.com/group/"); //popup style next to container instead of next to linkitem
      */
  const targetContainerIDArrayToObserve = [
    "profile_content3",
    "messageList",
    "myTable",
  ];
  /*
    observer needed for ajax changed page data. (here personal readinglist tab change and in the forum a quickedit of a post)
      update eventlistener on content change if element id found: isOnReadingListIndex [#profile_content3], in forum[#messageList] and on index[myTable] on tablesorting
      Attach container to a MutationObserver function which refreshes the eventlistener on links to seriepages
    */

  const internalLink = {
    "www.novelupdates.com/series/": {
      serieRegex: "([\\w-]+/?).*",
      seriePageTitle: ".seriestitlenu",
      IMAGELINKCONTAINERS: ".serieseditimg img, .seriesimg img", //instead of single element class name with dot seperated with comma
      //CONTAINERNUMBER: 0, //in case that the query for IMAGELINKCONTAINERS has multiple img node results it can be selected by CONTAINERNUMBER or with img:nth-child(index) inside the query
      //the same can be used for external links
      seriePageVotes: ".seriesother > .uvotes",
      seriePageStatus: "#editstatus",
      seriePageGenre: "#seriesgenre",
      seriePageTags: "#showtags", //
      seriePageDescription: "#editdescription",
      serieAlternativeNames: "#editassociated",
      serieReadingListIcon: ".sticon img",
      serieReadingListTitle: ".sttitle > a",
    },
  };
  const internalLinkKey = Object.keys(internalLink);
  const INDIVIDUALPAGETEST = internalLinkKey[0]; //"www.novelupdates.com/series/"; //matched with includes
  /*
    max possible externalLinkObject to insert into externalLinks
    {
      "individualSiteLink":{
        seriePageTitle:undefined,
        IMAGELINKCONTAINERS:undefined,
        CONTAINERNUMBER:0,
        seriePageDescription:undefined,
        seriePageStatus:undefined,
        seriePageChapters:undefined,
        seriePageVotes:undefined,
        seriePageGenre:undefined,
        seriePageTags:undefined,
        serieRegex:undefined,
      }
    }
    */
  const defaultRateLimitQueryAfterSeconds = 0.5;
  const defaultSerieRegex = "([0-9]+)(?!\\w)([/]+.*)?"; //block popup generation for externalLink without appended serie id
  // example: "link/"(id); "link/"(id)/; "link/"(id)/anything
  //not "link/"/; "link/"(id)something; "link/"(id)something/
  const externalLinks = {
    //#region site urls with public api access implemented
    /* https://mangadex.org/thread/351011
          chapter counts https://mangadex.org/api/v2/manga/" + id + "/chapters
          genre/tags id to name: https://mangadex.org/api/v2/tag
        */
    "mangadex.org/title/": {
      serieRegex: "([0-9]+)", //block popup generation for mangadex.org/title/ link without serie id
      mainAPI: "https://mangadex.org/api/v2/manga/",
      /* not possible no unique identification possible (no id or unique class for a container)
          depending on serie div count changing since not all values are always used
          data not at the same position and no unique selector available
          using public mangadex api
          */
    },
    "mangadex.org/manga/": {
      //alternative address same function
      serieRegex: "([0-9]+)",
      mainAPI: "https://mangadex.org/api/v2/manga/",
    },
    /* http://www.tvmaze.com/api
          alternative names http://api.tvmaze.com/shows/(id)/akas
          episode counts http://api.tvmaze.com/shows/(id)/episodes
        */
    "www.tvmaze.com/shows/": {
      serieRegex: "([0-9]+)",
      mainAPI: "http://api.tvmaze.com/shows/",
      //http://www.tvmaze.com/api#rate-limiting
      rateLimitCount: 20,
      rateLimitTimeInSeconds: 10,
      //rateLimitQueryAfterSeconds:"0.5" //if not set, but both other values are available will be calculated into rateLimitQueryAfterSeconds = (rateLimitTimeInSeconds/rateLimitCount)
      //if incomplete will use defaultRateLimitQueryAfterSeconds
    },
    //#endregion
    //https://www.wlnupdates.com/api-docs
    "wlnupdates.com/series-id/": {
      serieRegex: "([0-9]+)",
      mainAPI: "https://www.wlnupdates.com/api",
      /* using public api. no need to update if only page layout changes
        seriePageTitle: "h2",
        IMAGELINKCONTAINERS: ".imgDiv img",
        seriePageDescription: ".description",
        seriePageStatus: ".updating-current", //updating-stalled
        seriePageChapters: ".orig_status",
        seriePageVotes: ".br-current-rating",
        seriePageGenre: "#genre-container > .rowContents",
        seriePageTags: "#tag-container > .rowContents",
        */
      //serieRegex:undefined,
    },
    "www.scribblehub.com/series/": {
      /*
        https://www.scribblehub.com/series/211234/the-impossible-fate-that-leads-to-a-god-of-a-new-world/ 
        exception linkid not forwared to url. linkID needs tobe number+/+stringuntil next slash
        https://www.scribblehub.com/series/211234
        */
      serieRegex: "([0-9]+(?=/)[/]?[\\w-]+[/]?)", //get #ID + / + string
      serieRegex2: "([0-9]+)", //if has not detailed regex get only #ID
      IMAGELINKCONTAINERS: ".fic_image img", //instead of single element class name with dot seperated with comma
      seriePageTitle: ".fic_title",
      seriePageVotes: "#ratefic_user > span",
      seriePageStatus: ".fic_stats > span:nth-child(3)",
      seriePageGenre: ".wi_fic_genre",
      seriePageTags: ".wi_fic_showtags_inner", //
      seriePageDescription: ".wi_fic_desc",
    },
    "www.webnovel.com/book/": {
      serieRegex: "([\\w'-]+[/]?).*", //domain/ + spring-blooms-when-i'm-with-you_12023713305779105
      IMAGELINKCONTAINERS: ".g_thumb > img", //instead of single element class name with dot seperated with comma
      seriePageTitle: ".det-info > div:nth-child(2) > h2",
      seriePageVotes: "._score > strong",
      seriePageStatus: ".det-hd-detail > strong > span",
      seriePageGenre: ".det-hd-detail > a > span",
      seriePageTags: ".m-tags",
      seriePageDescription: ".det-abt > div > p",
    },
    "royalroad.com/fiction/": {
      serieRegex: "([0-9]+)", //"([\\w-]+[/]?).*",
      //alternative serieRegex:"([0-9]+)(?=\/)""  would block detection if string after domain is "21220mother-of-learning"
      IMAGELINKCONTAINERS: ".fic-header > div > img", //instead of single element class name with dot seperated with comma
      seriePageTitle: ".fic-title > h1",
      seriePageVotes: undefined,
      seriePageStatus:
        ".fiction-info > div > div:nth-child(2) > div > span:nth-child(2)",
      seriePageGenre: ".tags",
      seriePageTags: undefined, //
      seriePageDescription: ".description",
    },
    "royalroadl.com/fiction/": {
      //old domain? forwarded to standard domain
      serieRegex: "([0-9]+)",
      IMAGELINKCONTAINERS: ".fic-header > div > img", //instead of single element class name with dot seperated with comma
      seriePageTitle: ".fic-title > h1",
      seriePageVotes: undefined,
      seriePageStatus:
        ".fiction-info > div > div:nth-child(2) > div > span:nth-child(2)",
      seriePageGenre: ".tags",
      seriePageTags: undefined, //
      seriePageDescription: ".description",
    },
    // haven't found a recent used  link no need to activate for incomplete data
    /*
      "wuxiaworld.com/novel/":{
        seriePageTitle: ".novel-body h2",
        IMAGELINKCONTAINERS: ".novel-left img", //instead of single element class name with dot seperated with comma
        seriePageStatus: ".novel-body > div:nth-child(2)",
        seriePageGenre: ".genres",
        seriePageDescription: ".novel-bottom > div:nth-child(4) > div",
      },*/
    "www.mtlnovel.com/": {
      serieRegex: "([\\w-]+[/]?).*",
      seriePageTitle: ".entry-title",
      IMAGELINKCONTAINERS: ".nov-head > amp-img", //instead of single element class name with dot seperated with comma
      seriePageVotes: ".star-avg",
      seriePageStatus: ".info tr:nth-child(3) > td:nth-child(3)",
      seriePageChapters: ".info-wrap div:nth-child(2)",
      seriePageGenre: "#currentgen",
      seriePageTags: ".info tr:nth-child(6)",
      seriePageDescription: ".desc",
      serieAlternativeNames: ".info tr:nth-child(2) > td:nth-child(3)",
    },
    /*
        not possible for the details and haven't seen an api
        example with different div row counts: https://bato.to/series/72644, https://bato.to/series/74357
        data not at the same position and no unique selector available
        */
    "bato.to/series/": {
      serieRegex: "([0-9]+[/]?)",
      seriePageTitle: ".item-title",
      IMAGELINKCONTAINERS: ".attr-cover > img", //instead of single element class name with dot seperated with comma
      seriePageDescription: ".attr-main > pre",
      seriePageChapters: ".episode-list > div.head > h4",
      seriePageGenre: ".attr-main > div:nth-child(3) > span",
      serieAlternativeNames: ".alias-set",
    },
    "mangaupdates.com/series.html?id=": {
      serieRegex: "([0-9]+)",
      seriePageTitle: ".releasestitle",
      serieAlternativeNames:
        "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(11)",
      IMAGELINKCONTAINERS:
        "#main_content > div:nth-child(2) > div > div:nth-child(4) img", //instead of single element class name with dot seperated with comma
      //IMAGEBLOCKER: "images/stat_increase.gif, images/stat_decrease.gif",
      //imageblocker was needed for previous non unique IMAGELINKCONTAINERS selector. ("".sContent img") since a page without cover would query the next available image (in this case rate up/down icon)
      //CONTAINERNUMBER: 0,
      seriePageVotes:
        "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(35)",
      seriePageStatus:
        "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(20)",
      seriePageGenre:
        "#main_content > div:nth-child(2) > div > div:nth-child(4) > div:nth-child(5)",
      seriePageTags:
        "#main_content > div:nth-child(2) > div > div:nth-child(4) > div:nth-child(8) ul", //
      seriePageDescription:
        "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(2)",
    },
    "mydramalist.com/": {
      serieRegex: "([0-9]+-[\\w-]+[/]?).*",
      seriePageTitle: ".film-title",
      serieAlternativeNames: ".mdl-aka-titles",
      IMAGELINKCONTAINERS: ".film-cover img",
      seriePageVotes: "#show-detailsxx .hfs",
      seriePageStatus:
        ".content-side > div:nth-child(2) > div:nth-child(2) li:nth-child(4)",
      seriePageGenre: ".show-genres",
      seriePageTags: ".show-tags", //
      seriePageDescription: ".show-synopsis",
    },
    "www.imdb.com/title/": {
      serieRegex: "(tt[0-9]+[/]?).*",
      seriePageTitle: ".title_wrapper h1",
      IMAGELINKCONTAINERS: ".poster img",
      seriePageVotes: ".imdbRating > .ratingValue",
      seriePageStatus: ".title_wrapper .subtext",
      //seriePageGenre non static position. can be pushed down by Taglines box if available
      seriePageTags: "#titleStoryLine > div:nth-child(6)",
      seriePageDescription: "#titleStoryLine > div > p > span",
    },
    "www.tv.com/shows/": {
      serieRegex: "([\\w-]+[/]?).*",
      seriePageTitle: ".show_head > h1",
      IMAGELINKCONTAINERS: ".image_border img",
      seriePageVotes: ".score",
      seriePageStatus: ".tagline",
      seriePageGenre: ".categories > p",
      seriePageTags: ".themes > p",
      seriePageDescription: ".description > p",
    },
    "wiki.d-addicts.com/": {
      serieRegex: "([\\w-]+)",
      seriePageTitle: "#content .title",
      serieAlternativeNames:
        "#mw-content-text > .mw-parser-output > ul > li:nth-child(1)",
      IMAGELINKCONTAINERS: ".thumbinner img",
      seriePageVotes: ".voteboxrate",
      seriePageStatus:
        "#mw-content-text > .mw-parser-output > ul > li:nth-child(6)",
      seriePageChapters:
        "#mw-content-text > .mw-parser-output > ul > li:nth-child(4)",
      seriePageGenre:
        "#mw-content-text > .mw-parser-output > ul > li:nth-child(3)",
      seriePageDescription: "#mw-content-text p",
    },
    "asianwiki.com/": {
      serieRegex: "([\\w()-]+).*",
      seriePageTitle: ".article > h1",
      serieAlternativeNames: "#mw-content-text > ul > li:nth-child(2)",
      IMAGELINKCONTAINERS: ".thumbimage",
      seriePageVotes: "#w4g_rb_area-1",
      seriePageStatus: "#mw-content-text > ul > li:nth-child(8)",
      seriePageChapters: "#mw-content-text > ul > li:nth-child(7)",
      seriePageDescription: "#mw-content-text > ul ~ p", //next p sibling after ul
    },
    /*
      not possible.
      example https://myanimelist.net/manga/2 and https://myanimelist.net/manga/11 Alternative Titles is changing div child count and no unique selector possible
      no anonymous public api access available
      "https://myanimelist.net/manga/":{
        seriePageTitle: ".h1-title",
        IMAGELINKCONTAINERS: ".borderClass > div > div img",
        seriePageVotes: ".score-label",

        //serieAlternativeNames: ".borderClass > div > div:nth-child(8)",
        //seriePageStatus:".borderClass > div > div:nth-child(16)",
      // seriePageGenre:".borderClass > div > div:nth-child(17)",
        seriePageDescription: 'span[itemprop="description"',
      }
      */
  };
  const externalLinkKeys = Object.keys(externalLinks);

  const defaultshowIconNextToLink = false;
  let preloadUrlRequestsDefault; //if not set will default to true in release
  let deactivatePreloadUrlRequestOnUrls = [
    "wiki.d-addicts.com",
    "https://forum.novelupdates.com/threads/novel-updates-userscript-to-preview-cover-images-on-greasyfork.117240/", //deactivated urlPreload on this forum thread to stop bombardedment of requesting domain access in tampermonkey since all external links are listed in the nu forum post.
  ];
  const preloadImagesDefault = false;
  let useReadingListIconAndTitle = false;
  const eventListenerStyle = 0; //undefined/0 forEach serieLink addeventlistener(mouseenter/mouseleave) / 1 window addeventlistener(mousemove)
  //#endregion end of frontend settings

  /* not available in firefox
    would have liked to automatically set preloadImages to true for wifi and non metered connections with speeds over 2MB/s
    console.log(navigator);
    let connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
    let type = connection.effectiveType;

    function updateConnectionStatus() {
      console.log(connection)
      console.log("Connection type changed from " + type + " to " + connection.effectiveType);
      type = connection.effectiveType;

      if(connection.saveData==false){
        preloadUrlRequests = true;
        if(type=="wifi" || preloadImagesDefault)
          preloadImages = true;
      }
    }
    updateConnectionStatus();
    connection.addEventListener('change', updateConnectionStatus);
    */

  //#region backend variables ^^^^	frontend settings over this line	^^^^
  const version = "2.3.2";
  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 settingsToKeepOnDataReset = [
    "showDescription",
    "showDetails",
    "showSmaller",
    "useReadingListIconAndTitle",
    "showIconNextToLink",
  ];

  //console.log(GM_info);
  //console.log(GM_info.script.name);
  const isReleaseVersion = GM_info.script.name == "novelupdates Cover Preview";
  //console.log("isReleaseVersion: " + isReleaseVersion);
  let preloadUrlRequests =
    isReleaseVersion &&
    (preloadUrlRequestsDefault === undefined || preloadUrlRequestsDefault);
  let preloadImages = false || (preloadImagesDefault && isReleaseVersion);
  //console.log("preloadUrlRequests: " + preloadUrlRequests)
  let showIconNextToLink = defaultshowIconNextToLink;
  //const maxWaitingTime = 120;

  const linkIconEnum = {
    popupPossibleNotLoadedOrMarkedForPreloading: 1,
    popupMarkedForPreloading: 2,
    popupLoading: 3,
    popupHasCoverData: 4,
    error: 5,
  };
  Object.freeze(linkIconEnum);
  const emptyCoverData = {
    url: undefined,
    title: undefined,
    alternativeNames: undefined,
    votes: undefined,
    status: undefined,
    chapters: undefined,
    genre: undefined,
    showTags: undefined,
    description: undefined,
    isExternal: undefined,
    readingListIcon: undefined,
    readingListTitle: undefined,
  };

  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[s]?[ ]*[(]?[ ]*([0-9.]+)");
  const reChaptersOnlyNumbers = new RegExp("([0-9.]+)");
  const reRating = new RegExp("([0-9.]+) / ([0-9.]+)");
  const reRatingSingleNumber = new RegExp("([0-9.]+)");
  const reVoteCount = new RegExp("([0-9.]+)[ ]*(votes|ratings|users)"); //

  //const reStripHTMLLiteral = "(<[\/]?(?!span|b|i|p|br[^\w]+\s*)(([\/]?[^>]+))>)";
  const reWhiteListStripHTML = new RegExp(
    "(<[/]?\\b(?!(b|i|br|p)[^\\w]+\\s*)(([/]?[^>]+))>)",
    "g"
  ); //needed escape character changes remove escaping for / (\/) , specialFunction additional escaping for()
  //const reWhiteListHTML = /(<[\/]?\b(?!(b|u|i|br|p)[^\w]+\s*)(([\/]?[^>]+))>)/g   // /(<[\/]?\b(?!span|b|i|p|br[^\w]+\s*)(([\/]?[^>]+))>)/g;
  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 ALLEXTERNALLINKNODES = [];
  let previousTitelHover,
    currentTitelHover,
    currentCoverData,
    currentPopupEvent;
  let popover, popoverTitle, popoverContent, arrowContainer;
  let lastTarget;
  let isShowingSpinnerAnimation = false;
  let showDescription = false;
  let showSmaller = false;
  let showHotkeys = false;
  let showAlternativeNames = false;
  let autoScrollCoverData = true;
  let coverDataContainer = [];
  let mediumTextStyle = "mediumText";
  let smallTextStyle = "smallText";
  let pressedKeys = [];
  let mangaDexTAGS;
  let currentOpenedUrl;
  let popoverBackgroundColor, popoverForegroundColor;
  let popoverBorderWidth;
  let popoverBorderColor;

  const supportsCSSMin = CSS.supports("max-Height", "min(400px, 100%)");
  const arrowWidthInPx = 40;
  //#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 key request: " + key);
    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.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,
          alternativeNames: coverData.alternativeNames,
          votes: coverData.votes,
          status: coverData.status,
          chapters: coverData.chapters,
          genre: coverData.genre,
          showTags: coverData.showTags,
          description: coverData.description,
          isExternal: coverData.isExternal,
          readingListIcon: coverData.readingListIcon,
          readingListTitle: coverData.readingListTitle,
        };
      } 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,
      alternativeNames: coverData.alternativeNames,
      votes: coverData.votes,
      status: coverData.status,
      chapters: coverData.chapters,
      genre: coverData.genre,
      showTags: coverData.showTags,
      description: coverData.description,
      isExternal: coverData.isExternal,
      readingListIcon: coverData.readingListIcon,
      readingListTitle: coverData.readingListTitle,
      cachedTime: Date.now(),
    };
    GM_setValue(key, JSON.stringify(cD));
    DEBUG && console.group("GM_setCachedValue key: " + key);
    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?
          */

  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()
        hidePopOver();
        debouncedpreloadCoverData();
      } 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);

  //https://math.stackexchange.com/questions/814950/how-can-i-rotate-a-coordinate-around-a-circle
  /**
   *
   *
   * @param {*} cx = pivot
   * @param {*} cy = pivot
   * @param {*} mx = offset
   * @param {*} my = offset
   * @param {*} angle
   * @returns
   */
  function getRotatedPoint(cx, cy, mx, my, angle) {
    let x, y;
    angle = ((angle % 360) / 180) * Math.PI;
    x = ((mx - cx) * Math.cos(angle) - (my - cy) * Math.sin(angle) + cx) | 0;
    y = ((mx - cx) * Math.sin(angle) + (my - cy) * Math.cos(angle) + cy) | 0;
    return { x: x, y: y };
  }

  //https://stackoverflow.com/questions/1887104/how-to-get-the-background-color-of-an-html-element
  const getComputedStyle = function (element, property) {
    return window.getComputedStyle
      ? window.getComputedStyle(element, null).getPropertyValue(property)
      : element.style[
          property.replace(/-([a-z])/g, function (g) {
            return g[1].toUpperCase();
          })
        ];
  };
  //adjusted from https://stackoverflow.com/questions/34980574/how-to-extract-color-values-from-rgb-string-in-javascript/34980657
  function getRGBValues(str) {
    var vals = str.substring(str.indexOf("(") + 1, str.length - 1).split(", ");
    return vals;
  }
  //adjusted from https://gist.github.com/JordanDelcros/518396da1c13f75ee057
  function blendColorsToRGBA(colors = []) {
    var base = [0, 0, 0, 0];
    var mix;
    var added;
    while ((added = colors.shift())) {
      //console.log(added)
      //console.log(typeof added)
      if (typeof added == "string") {
        const arrayColor = getRGBValues(added);
        //console.log(arrayColor)
        added = arrayColor;
      }

      //console.log(added[3])
      //console.log(typeof added[3])
      if (typeof added[3] === "undefined") {
        //if rgb add alpha 1

        added[3] = 1;
      }
      //console.log(added)
      //console.log(base)
      // check if both alpha channels exist.
      if (base[3] && added[3]) {
        mix = [0, 0, 0, 0];
        // alpha
        mix[3] = 1 - (1 - added[3]) * (1 - base[3]);
        //console.log(added)
        //console.log(base)
        // red
        mix[0] = Math.round(
          (added[0] * added[3]) / mix[3] +
            (base[0] * base[3] * (1 - added[3])) / mix[3]
        );
        // green
        mix[1] = Math.round(
          (added[1] * added[3]) / mix[3] +
            (base[1] * base[3] * (1 - added[3])) / mix[3]
        );
        // blue
        mix[2] = Math.round(
          (added[2] * added[3]) / mix[3] +
            (base[2] * base[3] * (1 - added[3])) / mix[3]
        );
        //console.log(mix);
      } else if (added) {
        //console.log(added)
        mix = added;
      } else {
        //console.log(base)
        mix = base;
      }
      //console.log(mix)
      base = mix;
    }
    //console.log(mix)
    return "rgba(" + mix + ")";
  }
  //#endregion helper functions
  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();
      //resetSettings();
    }
  }
  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++) {
      if (!settingsToKeepOnDataReset.includes(oldValues[i])) {
        GM_deleteValue(oldValues[i]);
      } else {
        //console.log("keep setting")
        //console.log(oldValues[i])
      }
      //console.log(oldValues[i])
    }
    GM_setValue("version", version);
    DEBUG && console.log(oldValues);
    !isReleaseVersion && console.log("Finished clearing CoverData");
  }
  function resetSettings() {
    for (let i = 0; i < settingsToKeepOnDataReset.length; i++) {
      GM_deleteValue(settingsToKeepOnDataReset[i]);
    }
    GM_setValue("version", version);
  }
  //https://www.w3resource.com/javascript-exercises/javascript-math-exercise-27.php
  function pointDirection(x1, y1, x2, y2) {
    return (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
  }
  function checkIsTableElement(element) {
    if (
      element.tagName == "TD" ||
      element.parentElement.tagName == "TD" ||
      element.className.includes("col-") || //tailwind column styles  needed for mangaupdates
      element.parentElement.className.includes("col-")
    ) {
      return true;
    }
    return false;
  }

  function showBorderArrowBox(
    event,
    arrowWidth = 20,
    upside = false,
    isExternal = false,
    arrowTableSideRight = true
  ) {
    const DEBUG = false;

    /*
      - getPopover Rect
      - generate circleBox
      - generate arrow
      - rotate arrow to target
    */
    const nativElement = event.target;
    let targetElement = nativElement;
    let isInChildContainer = false;
    let tableElement = checkIsTableElement(nativElement);
    DEBUG && console.group("showBorderArrowBox");
    const elementLinkHasNoText = targetElement.textContent.length == 0;
    //elementLinkHasNoText
    if (isOnIndex || elementLinkHasNoText || tableElement) {
      isInChildContainer = true;
      targetElement = nativElement.parentElement; //get container element/table cell
    }
    /*
      if(elementLinkHasNoText){
        targetElement = nativElement.parentElement.parentElement;
        console.log(targetElement);
      }
      */
    const targetRectBounds = targetElement.getBoundingClientRect();
    let targetRect = targetRectBounds;
    //https://stackoverflow.com/questions/52712760/get-how-many-lines-a-textarea-has
    /* 
    TODO
    novelupdates forum/xenforum script is somehow changing element with class "ugc" which prevents getting the pseudo element a::before from getClientRects()
    */
    DEBUG && console.log(targetElement);
    const targetLineRects = targetElement.getClientRects();

    let firstLineRect, lastLineRect;
    let firstRectIndex;
    DEBUG && console.log(targetRect);
    //console.log(targetLineRects);
    firstRectIndex = 0;
    if (targetLineRects.length > 0) {
      if (showIconNextToLink && targetLineRects.length > 1) {
        firstRectIndex = 1;
      }

      targetRect = targetLineRects[firstRectIndex];
      firstLineRect = targetRect;
      lastLineRect = targetLineRects[targetLineRects.length - 1];
    }

    //if (lastLineRect != firstLineRect)
    if (lastLineRect) targetRect = lastLineRect;
    if (!upside) {
      if (firstLineRect) targetRect = firstLineRect;
    }

    let popoverRect = popover.getBoundingClientRect();
    DEBUG && console.log(popoverRect);
    DEBUG && console.log(targetRect);
    DEBUG && console.log(targetRectBounds);
    const scrollPosY =
      window.scrollY ||
      window.scrollTop ||
      document.getElementsByTagName("html")[0].scrollTop;
    const scrollPosX =
      window.scrollX ||
      window.scrollLeft ||
      document.getElementsByTagName("html")[0].scrollLeft;

    arrowContainer.style.position = "absolute";
    arrowContainer.style.zIndex = "8";

    arrowContainer.style.height = arrowWidth + "px";
    arrowContainer.style.width = arrowWidth + "px";
    let posY = 0;
    let rotateArrowInDegree = 0;
    let targetPosY, sourcePosY;
    let targetXSourcePosition;
    let popupSourceX;
    const targetHalfHeight = targetRect.height / 2;
    const halfArrowWidth = arrowWidth / 2;
    let halfWidth = arrowWidth / 2;
    const quarterArrowWidth = arrowWidth / 4;
    let arrowContainerTopPositionLocal;
    let targetRectTopDistance = targetRect.top - popoverRect.top;
    let targetElementDistanceY =
      targetRectTopDistance - targetRect.height / 4 + 1;
    const minYPosOnPopup = -quarterArrowWidth;
    const maxYPosOnPopup =
      popoverRect.height - (arrowWidth - quarterArrowWidth);
    if (targetElementDistanceY < minYPosOnPopup) {
      targetElementDistanceY = minYPosOnPopup;
    }
    if (targetElementDistanceY > maxYPosOnPopup) {
      targetElementDistanceY = maxYPosOnPopup;
    }

    //if (isExternal) externalStyle = "isExternalContentArrow ";
    //|| elementLinkHasNoText
    if (isOnIndex || tableElement) {
      //#region "index/container layout"
      DEBUG && console.log("index/container layout");
      DEBUG && console.log(targetRect);
      DEBUG && console.log("targetRect.height: " + targetRect.height);

      const upperBorder = popoverRect.top - quarterArrowWidth;
      const lowerBorder =
        popoverRect.bottom - halfArrowWidth - quarterArrowWidth;

      DEBUG &&
        console.log(
          "targetRect.height: " +
            targetRect.height +
            ", targetHalfHeight: " +
            targetHalfHeight
        );
      targetPosY = targetRect.top; //targetRectYMiddle;
      sourcePosY = targetPosY;
      //#region get targetPointY
      DEBUG &&
        console.log(
          "targetPosY: " +
            targetPosY +
            ", upperBorder: " +
            upperBorder +
            ", lowerBorder: " +
            lowerBorder
        );
      if (targetPosY < upperBorder) {
        sourcePosY = upperBorder;
        DEBUG && console.log("set targetPosY = upperBorder");
      }
      if (targetPosY > lowerBorder) {
        sourcePosY = lowerBorder;
        DEBUG && console.log("set  targetPosY = lowerBorder");
      }
      //#endregion
      //#region limit popover sourceY position
      //sourcePosY = targetPosY;
      //#endregion

      DEBUG &&
        console.log(
          "targetPosY: " +
            targetPosY +
            ", sourcePosY: " +
            sourcePosY +
            ", scrollPosY: " +
            scrollPosY +
            ", top: " +
            (scrollPosY + targetPosY)
        );

      DEBUG &&
        console.log(
          "targetElementDistanceY: " +
            targetElementDistanceY +
            ",  popoverRect.top: " +
            popoverRect.top +
            ", targetRect.top: " +
            targetRect.top
        );
      arrowContainerTopPositionLocal = popoverRect.top + targetElementDistanceY;
      arrowContainer.style.top =
        scrollPosY + arrowContainerTopPositionLocal + "px"; //sourcePosY
      //#endregion arrow vertical position

      //#region arrow on leftOrRightSide of popover
      if (arrowTableSideRight) {
        arrowContainer.style.left =
          scrollPosX + popoverRect.left - halfWidth + "px";
      } else {
        arrowContainer.style.left =
          scrollPosX + popoverRect.left + popoverRect.width - halfWidth + "px";
      }
      //#endregion arrow on leftOrRightSide of popover

      DEBUG &&
        console.log(
          "posY: " +
            posY +
            ", popoverRect.top + posY + arrowWidth: " +
            (popoverRect.top + posY + arrowWidth)
        );
      targetXSourcePosition = targetRect.right;
      popupSourceX = popoverRect.left;
      rotateArrowInDegree =
        pointDirection(
          popupSourceX,
          sourcePosY,
          targetXSourcePosition - arrowWidth,
          targetPosY
        ) - 45;

      //width:100%;height:100%;
      //#endregion "index/container layout"
    } else {
      //#region "under/over link layout"
      DEBUG && console.log("under/over link layout");
      //#region horizontal

      //#region targetPosX
      // let targetX = targetRect.right;
      let diffXTarget = targetRect.right - popoverRect.left;
      const diffXTargetWithHalfwidth = diffXTarget + halfWidth;
      if (diffXTarget - halfWidth < 0) diffXTarget = halfWidth;
      let targetXSourcePosition = targetRect.right - diffXTargetWithHalfwidth;
      if (diffXTarget > targetRect.width) {
        targetXSourcePosition = targetRect.left;
      }
      //#endregion targetPosX

      //#region popoverPosX
      popupSourceX = popoverRect.left - halfWidth;
      console.log(
        "diffXTargetWithHalfwidth: " +
          diffXTargetWithHalfwidth +
          ", targetRect.width: " +
          targetRect.width
      );
      if (diffXTargetWithHalfwidth > targetRect.width) {
        popupSourceX = targetXSourcePosition;
      }
      //#endregion popoverPosX
      //#region move arrowHead in targetDirection
      if (diffXTarget < halfWidth) {
        targetXSourcePosition -= halfWidth;
      }
      //#endregion move arrowHead in targetDirection
      if (targetXSourcePosition) {
        DEBUG &&
          console.log(
            "diffXTarget: " +
              diffXTarget +
              ", popupSourceX: " +
              popupSourceX +
              ", targetRect.width: " +
              targetRect.width +
              ", targetXSourcePosition: " +
              targetXSourcePosition
          );
      }
      //#endregion
      //#region vertical position over/under popup
      arrowContainer.style.left = scrollPosX + popupSourceX + "px";
      if (upside) {
        //arrow at topside
        let popoverOutsideTargetHorizontal = 0;

        DEBUG &&
          console.log(
            "popoverOutsideTargetHorizontal: " + popoverOutsideTargetHorizontal
          );
        arrowContainer.style.top =
          scrollPosY + popoverRect.top - arrowWidth / 2 + "px";

        rotateArrowInDegree =
          pointDirection(
            popupSourceX,
            popoverRect.top + posY + arrowWidth,
            targetXSourcePosition, //targetX+popoverOutsideTargetHorizontal
            targetRect.top + arrowWidth
          ) - 45;
        //width:100%;height:100%;
      } else {
        //arrow at bottom
        arrowContainer.style.top =
          scrollPosY + popoverRect.bottom - arrowWidth / 2 + "px";

        rotateArrowInDegree =
          pointDirection(
            popupSourceX,
            popoverRect.bottom + posY + halfWidth,
            targetXSourcePosition,
            targetRect.bottom + halfWidth
          ) - 45;
        //width:100%;height:100%;
      }
      //#endregion vertical position over/under popup
      //#endregion "under/over link layout"
    }
    let targetRotation = rotateArrowInDegree - 45;
    // let targetSourceAbsoluteDiffY = sourcePosY - targetPosY;
    let targetDiffX = popupSourceX - targetXSourcePosition;
    DEBUG && console.log("isInChildContainer:" + isInChildContainer);
    if (isInChildContainer) {
      const nativRect = nativElement.getBoundingClientRect();
      targetDiffX = popupSourceX - nativRect.right - halfArrowWidth;
      DEBUG && console.log(nativRect);
    }

    DEBUG &&
      console.log(
        "popupSourceX: " +
          popupSourceX +
          ", targetXSourcePosition: " +
          targetXSourcePosition +
          ", targetDiffX: " +
          targetDiffX
      );
    let containerOffsetX = 12; //12;

    let offsetPointOnArrow = { x: 20, y: 40 + containerOffsetX };
    let pivotOnArrow = { x: 20, y: 20 }; //svg viewbox height/width = 24
    let newSourcePointXY = getRotatedPoint(
      pivotOnArrow.x,
      pivotOnArrow.y,
      offsetPointOnArrow.x,
      offsetPointOnArrow.y,
      rotateArrowInDegree - 45
    );
    DEBUG && console.log("newSourcePointXY: " + newSourcePointXY);
    DEBUG && console.log(newSourcePointXY);
    let x1, y1, x2, y2;

    let rectDistance = 0;
    // if (targetRect.top < popoverRect.top)
    {
      rectDistance = targetRect.top - popoverRect.top;
    }
    let arrowOffsetY = targetRectTopDistance - targetElementDistanceY;
    DEBUG &&
      console.log(
        "popoverRect.top: " +
          popoverRect.top +
          ", targetRect.top: " +
          targetRect.top +
          ", popoverRect.bottom: " +
          popoverRect.bottom +
          ", targetRect.bottom: " +
          targetRect.bottom +
          ", rectDistance: " +
          rectDistance +
          ", targetDiffX: " +
          targetDiffX +
          ", targetElementDistanceY: " +
          targetElementDistanceY +
          ", popoverRect.height: " +
          popoverRect.height +
          ", arrowContainerTopPositionLocal: " +
          arrowContainerTopPositionLocal +
          ", arrowOffsetY: " +
          arrowOffsetY +
          ", targetRectTopDistance: " +
          targetRectTopDistance
      );

    const circleRadius = 1;
    x1 = -targetDiffX + circleRadius; //-(targetDiffX-newSourcePointXY.x)-newSourcePointXY.x//targetRect.right; //0 - containerOffsetX;
    y1 = arrowOffsetY + targetHalfHeight; //;//rectDistance; // + targetHalfHeight  //targetElementDistanceY  arrowContainerTopPositionLocal-popoverRect.top
    DEBUG &&
      console.log(
        "y1: " +
          y1 +
          ", rectDistance: " +
          rectDistance +
          ", targetHalfHeight: " +
          targetHalfHeight
      );
    //(targetRect.height/2) + targetElementDistanceY;//targetRect.top+targetRect.height/2;//targetSourceAbsoluteDiffY;  //

    x2 = newSourcePointXY.x;
    y2 = newSourcePointXY.y;

    DEBUG &&
      console.log(
        "newSourcePointXY.x: " +
          newSourcePointXY.x +
          ", newSourcePointXY.y: " +
          newSourcePointXY.y +
          ", targetX: " +
          x2 +
          ", targetY: " +
          y2 +
          ", targetElementDistanceY: " +
          targetElementDistanceY
      );
    let point =
      '<circle cx="' +
      newSourcePointXY.x +
      '" cy="' +
      newSourcePointXY.y +
      '" r="' +
      circleRadius +
      '" stroke="red" fill="transparent" stroke-width="1"/>' +
      '<circle cx="' +
      x1 +
      '" cy="' +
      y1 +
      '" r="' +
      circleRadius +
      '" stroke="green" fill="transparent" stroke-width="1"/>';
    let line = //startY
      //'<line x1="'+(x1-targetDiffX+arrowWidth-quarterArrowWidth)+'" y1="'+(y1-(arrowWidth-quarterArrowWidth))+'" x2="'+(x2-targetDiffX)+'" y2="'+(y2)+'" stroke="black" stroke-width="1"/></svg>';
      '<line x1="' +
      x1 +
      '" y1="' +
      y1 +
      '" x2="' +
      x2 +
      '" y2="' +
      y2 +
      '" stroke="black" stroke-width="1"/>';

    let additionalLineSVG = "";
    if (isOnIndex || tableElement) {
      additionalLineSVG =
        '<div style="width:100%;height:100%;position:absolute;top:0;left:0;"><svg xmlns="http://www.w3.org/2000/svg" style="overflow:visible">' +
        line +
        point +
        "</svg></div>";
    }
    let borderColor = "black";
    if (isExternal) borderColor = "red";
    arrowContainer.innerHTML =
      '<div style="width:40px;height:40px;transform: rotate(' +
      targetRotation +
      'deg);padding:2px">' +
      '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-big-down" style="width:100%;height:100%;overflow:visible" viewBox="0 0 24 24" stroke-width="1" vector-effect="non-scaling-stroke" stroke="currentColor" fill="' +
      popoverForegroundColor +
      '" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 ' +
      containerOffsetX +
      ')">' +
      //'<rect width="100%" height="100%" style="fill:rgb(100,155,255);stroke-width:1;stroke:rgb(0,0,0)" />' +
      '<filter id="shadow"><feDropShadow dx="0.5" dy="-0.5" stdDeviation="0.8"/></filter>' +
      '<circle cx="12" cy="12" r="13" stroke="' +
      borderColor +
      '" fill="' +
      popoverBackgroundColor +
      '" stroke-width="' +
      popoverBorderWidth +
      '" style="filter:url(#shadow);"/>' +
      '<path d="M 15 0 v 15 h 3.586 a 1 1 0 0 1 0.707 1.707 l -6.586 6.586 a 1 1 0 0 1 -1.414 0 l -6.586 -6.586 a 1 1 0 0 1 0.707 -1.707 h 3.586 L 9 0 z" transform=""/>' +
      "</svg></div>" +
      additionalLineSVG;
    DEBUG && console.log("scrollPosY: " + scrollPosY);
    arrowContainer.style.pointerEvents = "none";
    DEBUG && console.log("posY: " + posY);
    // rotateArrowInDegree+=-135; //https://www.w3schools.com/howto/howto_css_arrows.asp
    DEBUG && console.log("rotateArrowInDegree: " + rotateArrowInDegree);

    DEBUG && console.groupEnd("showBorderArrowBox");
    //arrowContainer.style.visibility = "visible";
    //arrowContainer.classList.remove("hidePopover");
  }
  function getPopupPos(event, arrowWidth = 0, isExternal = false) {
    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;

    let targetHeight = defaultHeight;
    if (showSmaller) targetHeight = smallHeight;
    let targetWidth = targetHeight;
    if (showDetails) targetWidth = targetHeight * 2;
    //console.log("targetWidth: " + targetWidth)
    //console.log(event)
    popover.style.maxHeight = ""; //reset to max height
    //popover.style.maxWidth = "";  //reset to max height
    const popoverRect = popover.getBoundingClientRect();

    const nativElement = event.target;
    let targetElement = nativElement;

    let tableElement = checkIsTableElement(nativElement);
    //console.log("nativElement.classList")
    //console.log(nativElement.classList)
    //console.log(nativElement.className)
    //console.log('nativElement.classList.contains("externalLink") ' + nativElement.className.split(" ").some(classname => classname.startsWith("externalL")))
    //console.log('nativElement.className.includes("externalL") ' + nativElement.className.includes("externalL"))
    //

    const elementLinkHasNoText = targetElement.textContent.length == 0;
    // || elementLinkHasNoText
    if (isOnIndex || elementLinkHasNoText || tableElement) {
      DEBUG &&
        console.log(
          "(isOnIndex || elementLinkHasNoText || tableElement) -> targetElement = nativElement.parentElement"
        );
      targetElement = nativElement.parentElement; //get container element/table cell
    }

    let X, Y;
    DEBUG && console.group("getPopupPos");

    DEBUG &&
      console.log(
        "elementLinkHasNoText: " +
          elementLinkHasNoText +
          ", targetElement.textContent: " +
          targetElement.textContent +
          " targetElement.textContent.length: " +
          targetElement.textContent.length
      );
    DEBUG && console.log(nativElement.parentElement);
    DEBUG && console.log(nativElement.parentElement.tagName);
    DEBUG && console.log(targetElement);
    let targetRect = targetElement.getBoundingClientRect();
    const targetLineRects = targetElement.getClientRects();
    let firstLineRect, lastLineRect, firstRectIndex;
    DEBUG && console.log("targetLineRects.length: " + targetLineRects.length);
    for (let i = 0; i < targetLineRects.length; i++) {}
    if (targetLineRects.length > 0) {
      firstRectIndex = 0;
      if (showIconNextToLink && targetLineRects.length > 1) {
        firstRectIndex = 1; //TODO to double check if needed sometimes the pseudo element(state icon) seems to be counted in the DOMRectList
        //chrome https://www.novelupdates.com/reading-list/?list=0
        //vs https://forum.novelupdates.com/threads/chapter-numbering-error.119015/ pseudo element not counted?
      }
      //console.log(targetLineRects)
      firstLineRect = targetLineRects[firstRectIndex];
      lastLineRect = targetLineRects[targetLineRects.length - 1];
      //TODO check if line.bottom is visible in container(edge case word wrapped link in container with overflow:hidden) else loop check previous line.
    } else {
      //case empty text link with only an image
      firstLineRect = targetRect;
    }

    if (lastLineRect) targetRect = lastLineRect;

    const Rx = targetRect.right;
    const Ry = targetRect.bottom;

    DEBUG && console.group("debug rects");
    DEBUG && console.log("scrollPosX: " + scrollPosX);
    DEBUG && console.log("scrollPosY: " + scrollPosY);
    DEBUG &&
      console.log(
        "document.body.offsetHeight: " +
          document.body.offsetHeight +
          ", document.body.scrollHeight: " +
          document.body.scrollHeight
      );
    DEBUG && console.log("window.innerHeight: " + window.innerHeight);
    DEBUG && console.log("window.innerWidth: " + window.innerWidth);
    DEBUG && console.log(targetElement);
    DEBUG && console.log(targetLineRects);
    DEBUG && console.log(firstLineRect);
    DEBUG && console.log(lastLineRect);
    DEBUG && console.log("Rx: " + Rx + ", Ry: " + Ry);
    DEBUG && console.groupEnd();
    const nonOverlappingArrowWidth = (arrowWidth * 3) / 4;
    const marginY = offsetToBottomBorderY + nonOverlappingArrowWidth;
    let distanceToTopFromLink = firstLineRect.top - nonOverlappingArrowWidth;
    let distanceToBottomFromLink =
      window.innerHeight -
      targetRect.bottom -
      offsetToBottomBorderY +
      nonOverlappingArrowWidth; //lastLineRect -> targetRect
    let distanceToRightFromlink = window.innerWidth - targetRect.right;
    let distanceToLeftFromlink = targetRect.left;
    let arrowTableSideRight = true;
    //reset width/height
    //popover.style.height="100%";
    if (supportsCSSMin) {
      popover.style.height = "";
      popover.style.width = "";
    } else {
      popover.style.height = targetHeight + "px";
      popover.style.width = targetWidth + "px";
    }

    //#region set to top/bottom position
    DEBUG && console.group("top/bottom placement");
    let upside = false;
    let targetYPosition = 0;
    let maxHeight = targetHeight;
    let maxHeightInsideAvailableSpace;

    //#region  calculate maxHeight between targetHeight and image/popover height
    popover.style.maxHeight = targetHeight + "px";
    //setPopoverHeight(targetHeight,0);
    if (popoverRect.height < targetHeight) {
      maxHeight = popoverRect.height;
    } else maxHeight = targetHeight;
    //#endregion
    maxHeightInsideAvailableSpace = maxHeight;
    if (!(isOnIndex || tableElement)) {
      //#region "not on index/table cell td"
      DEBUG && console.log("not on index/table cell td");
      /*
        bottom bigger than top || bottomSpaceIsBiggerThanTargetHeight
          -> bottom bigger than full height
          -> bottom smaller than full height
        bottom smaller than top
          -> top bigger than full height
          -> top smaller than full height
        */
      const targetHeightMarginAdded = targetHeight + marginY;
      console.log("targetHeightMarginAdded: " + targetHeightMarginAdded);
      const bottomHasMoreSpaceThanTop =
        distanceToBottomFromLink > distanceToTopFromLink;
      const bottomSpaceIsBiggerThanTargetHeight =
        distanceToBottomFromLink > targetHeight + marginY; //maxHeight;
      DEBUG &&
        console.log(
          "bottomHasMoreSpaceThanTop: " +
            bottomHasMoreSpaceThanTop +
            ", distanceToTopFromLink: " +
            distanceToTopFromLink +
            ", distanceToBottomFromLink: " +
            distanceToBottomFromLink
        );
      console.log(
        "bottomSpaceIsBiggerThanTargetHeight: " +
          bottomSpaceIsBiggerThanTargetHeight +
          ", targetHeight: " +
          targetHeight +
          ", maxHeight: " +
          maxHeight +
          ", popoverRect.height: " +
          popoverRect.height +
          ", marginY: " +
          marginY +
          ", distanceToBottomFromLink: " +
          distanceToBottomFromLink +
          ", targetHeight + marginY: " +
          targetHeightMarginAdded
      );
      if (bottomHasMoreSpaceThanTop || bottomSpaceIsBiggerThanTargetHeight) {
        //arrow at top, popup under link
        //#region bottom is bigger than top - show on bottom
        upside = true;
        DEBUG && console.group("bottom is bigger than top");
        console.log(
          "lastLineRect.bottom: " +
            lastLineRect.bottom +
            ", nonOverlappingArrowWidth: " +
            nonOverlappingArrowWidth
        );

        targetYPosition = lastLineRect.bottom + nonOverlappingArrowWidth;
        // let targetYAdjustment;
        if (!bottomSpaceIsBiggerThanTargetHeight) {
          DEBUG &&
            console.log(
              "show reduced height. spacer under link is smaller than target height"
            );
          maxHeight = distanceToBottomFromLink - marginY; //targetHeight;
        }
        DEBUG && console.groupEnd();
        //#endregion bottom is bigger than top - show on bottom
      } else {
        //arrow at bottom
        //#region "bottom has less space than top" -> show on top
        DEBUG && console.group("bottom has less space than top");

        //"show full height BetweenFirstLineAndTop"
        //targetYPosition = firstLineRect.top - maxHeight - halfArrowWidth;
        //maxHeightInsideAvailableSpace = maxHeight;

        console.log(popoverRect);
        console.log(
          "targetHeight: " +
            targetHeight +
            ", popoverRect.height: " +
            popoverRect.height +
            ", maxHeight without offset reduction: " +
            maxHeight +
            ", distanceToTopFromLink: " +
            distanceToTopFromLink +
            ", distanceToTopFromLink < maxHeight: " +
            (distanceToTopFromLink < maxHeight) +
            ", marginY: " +
            marginY
        );

        if (distanceToTopFromLink < maxHeight + marginY) {
          //#region "available maxHeight is smaller than targeted maxHeight -show reduced height BetweenFirstLineAndTop"
          console.log(
            "maxHeight is smaller than distanceToTopFromLink -show reduced height BetweenFirstLineAndTop"
          );
          targetYPosition =
            firstLineRect.top - maxHeight - nonOverlappingArrowWidth;
          maxHeightInsideAvailableSpace = distanceToTopFromLink; //reduced maxHeight
          console.log(
            "maxHeightReducedToAvailableSpace?: " +
              maxHeightInsideAvailableSpace +
              ", targetYPosition: " +
              targetYPosition
          );
          console.log(
            "targetYPosition: " + targetYPosition + ", marginY: " + marginY
          );
          if (targetYPosition < marginY) {
            //if targetYPosition(firstLineRect.top - maxHeight - marginY;) smaller marginY
            console.log(
              "reposition smaller than marginY. Set at offset marginY"
            );

            DEBUG &&
              console.log(
                "targetheight overflows at top side, maxHeight before margin change: " +
                  maxHeight +
                  ", new maxHeightInsideAvailableSpace: " +
                  maxHeightInsideAvailableSpace
              );
            const magicNumber = 3; //weird. container position relative top=-3 why needed?
            targetYPosition =
              offsetToBottomBorderY - magicNumber + nonOverlappingArrowWidth;
            maxHeightInsideAvailableSpace = distanceToTopFromLink - marginY;
          }
          //#endregion "maxHeight is smaller than distanceToTopFromLink -show reduced height BetweenFirstLineAndTop"
        } else {
          //#region "show full height BetweenFirstLineAndTop"
          DEBUG && console.log("show full height BetweenFirstLineAndTop");
          DEBUG &&
            console.log(
              "maxHeight: " +
                maxHeight +
                ", nonOverlappingArrowWidth: " +
                nonOverlappingArrowWidth
            );
          const magicNumber = 3;
          maxHeightInsideAvailableSpace = maxHeight + nonOverlappingArrowWidth;
          targetYPosition =
            firstLineRect.top - maxHeightInsideAvailableSpace - magicNumber;
          //#endregion "show full height BetweenFirstLineAndTop"
        }

        DEBUG &&
          console.log(
            "targetYPosition: " +
              targetYPosition +
              ", maxHeightInsideAvailableSpace: " +
              maxHeightInsideAvailableSpace +
              ", distanceToTopFromLink: " +
              distanceToTopFromLink +
              ", targetHeight: " +
              targetHeight +
              ", firstLineRect.top: " +
              firstLineRect.top +
              ", scrollPosY: " +
              scrollPosY +
              ",  popoverRect.height: " +
              popoverRect.height
          );
        DEBUG && console.groupEnd();
        //#endregion "bottom has less space than top"
      }

      //#endregion "not on index/table cell td"
    } else {
      //#region "on index/table cell td"
      DEBUG && console.log("on index/table cell td");
      //auto height push up along table column
      DEBUG &&
        console.log(
          ", distanceToBottomFromLink: " +
            distanceToBottomFromLink +
            ", targetHeight: " +
            targetHeight
        );
      targetYPosition = lastLineRect.top;
      const overFlowPxY = maxHeight - distanceToBottomFromLink;
      if (overFlowPxY > 0) {
        DEBUG &&
          console.log("space under link smaller targetHeight. move popover up");
        DEBUG &&
          console.log("lastLineRect.top: " + lastLineRect.top + ", Y" + Y);
        targetYPosition -= overFlowPxY;
      }
      const posOverTopBorder = targetYPosition < offsetToBottomBorderY;
      if (posOverTopBorder) {
        //top offset
        DEBUG &&
          console.log("overflows top border set to offsetToBottomBorderY");
        targetYPosition = offsetToBottomBorderY;
      }
      //#endregion "on index/table cell td"
    }
    //Y += scrollPosY;
    DEBUG &&
      console.log(
        "maxHeight: " + maxHeight + ", targetYPosition: " + targetYPosition
      );
    setPopoverHeight(maxHeightInsideAvailableSpace, targetYPosition);
    Y = targetYPosition + scrollPosY;

    DEBUG && console.log("popover.style.height: " + popover.style.height);
    DEBUG && console.groupEnd();
    //#endregion set to top/bottom position

    //#region select from which line to calculate the  right position
    if (!upside) {
      targetRect = firstLineRect;
      distanceToRightFromlink = window.innerWidth - targetRect.right;
    }
    //#endregion
    //#region set to left/right position
    DEBUG && console.group("left/right placement");
    if (!(isOnIndex || tableElement)) {
      DEBUG &&
        console.log(
          "placement on non table cell/container list. non index list"
        );
      X = targetRect.right;
      DEBUG &&
        console.log(
          "full calculated width for non reposition: " +
            (X + distanceToRightFromlink + offsetToRightBorderX)
        );
      DEBUG &&
        console.log(
          "lastLineRect.right: " +
            targetRect.right +
            ", distanceToRightFromlink: " +
            distanceToRightFromlink +
            ", targetWidth: " +
            targetWidth
        );

      if (distanceToRightFromlink - offsetToRightBorderX * 2 > targetWidth) {
        DEBUG &&
          console.log(
            "full popup width with padding without reposition: " + targetWidth
          );
      } else {
        let diffX =
          targetWidth - distanceToRightFromlink + offsetToRightBorderX * 2;
        DEBUG &&
          console.log("touch right side, move left of amount diffX: " + diffX);

        //move left amount diffX
        X -= diffX;
        if (X + targetWidth < targetRect.left) {
          X = targetRect.left - targetWidth;
        }
      }
    } else {
      DEBUG && console.log("on Index/tablecell");
      distanceToRightFromlink = distanceToRightFromlink - arrowWidth;
      X = targetRect.right;
      const rightBiggerThenLeft =
        distanceToLeftFromlink < distanceToRightFromlink;
      const containerWidthWithPadding =
        targetWidth + arrowWidth + offsetToRightBorderX;
      DEBUG &&
        console.log(
          "distanceToRightFromlink: " +
            distanceToRightFromlink +
            ", containerWidthWithPadding: " +
            containerWidthWithPadding
        );
      if (
        rightBiggerThenLeft ||
        distanceToRightFromlink > containerWidthWithPadding
      ) {
        DEBUG &&
          console.log(
            "right side space bigger than left side. distanceToRightFromlink: " +
              distanceToRightFromlink
          );
        const availableSpaceRight =
          distanceToRightFromlink + offsetToRightBorderX * 2;
        DEBUG &&
          console.log(
            "targetWidth: " +
              targetWidth +
              ", containerWidthWithPadding: " +
              containerWidthWithPadding
          );
        if (availableSpaceRight < containerWidthWithPadding) {
          DEBUG &&
            console.log(
              "right space smaller than targetWidth. distanceToRightFromlink: " +
                distanceToRightFromlink
            );
          //X = lastLineRect.right;
          popover.style.width =
            distanceToRightFromlink - offsetToRightBorderX * 2 + "px";
        } else {
          DEBUG &&
            console.log(
              "right space bigger than targetWidth+offset" +
                ", distanceToRightFromlink: " +
                distanceToRightFromlink +
                ", offsetToRightBorderX: " +
                offsetToRightBorderX +
                ", availableSpaceRight : " +
                availableSpaceRight +
                ", containerWidthWithPadding: " +
                containerWidthWithPadding
            );
          popover.style.width = targetWidth + "px";
          // X = lastLineRect.right;
        }
      } else {
        DEBUG && console.log("right side smaller than left side");
        X = targetRect.left - containerWidthWithPadding;
        arrowTableSideRight = false;
        if (distanceToLeftFromlink > containerWidthWithPadding) {
          DEBUG && console.log("move to left side of container");
        } else {
          DEBUG && console.log("shrink popupwidth");
          if (X < offsetToRightBorderX) {
            X = offsetToRightBorderX;
            popover.style.width =
              distanceToLeftFromlink -
              offsetToRightBorderX * 2 -
              arrowWidth +
              "px";
          }
        }
        //X = targetRect.left-;
      }
    }
    DEBUG &&
      console.log("X: " + X + ", popover.style.width: " + popover.style.width);
    DEBUG && console.groupEnd();
    //#endregion
    DEBUG && console.groupEnd();
    //let tableMargin=0;
    //if(isOnIndex || tableElement)
    //tableMargin = arrowWidth;
    //popover.style.top = Y + "px";
    // popover.style.left = X +tableMargin+ "px";
    /*
    showBorderArrowBox(
      event,
      arrowWidth,
      upside,
      isExternal,
      arrowTableSideRight
    );*/
    return {
      Px: X,
      Py: Y,
      upside: upside,
      arrowTableSideRight: arrowTableSideRight,
    };
  }
  function sleep() {
    return new Promise(requestAnimationFrame);
  }
  // popupPositioning function
  async function popupPos(event, isExternal = false) {
    const DEBUG = false;
    DEBUG && console.group("popupPos showSmaller:" + showSmaller);
    if (event && event !== undefined) {
      showPopOver();
      await sleep();
      await sleep();

      DEBUG && console.group("popover rect");
      DEBUG && console.log(popover);
      DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
      DEBUG && console.log("popover[0].offsetHeight: " + popover.offsetHeight);
      DEBUG && console.groupEnd("popover rect");
      const { Px, Py, upside, arrowTableSideRight } = getPopupPos(
        event,
        arrowWidthInPx,
        isExternal
      );

      let tableMargin = 0;
      let tableElement = checkIsTableElement(event.target);
      if (isOnIndex || tableElement) tableMargin = arrowWidthInPx;
      popover.style.left = Px + tableMargin + "px";
      if (!upside) {
        //popover.style.bottom = "unset";
        popover.style.top = Py + "px";
      } else {
        //popover.style.bottom="unset";
        popover.style.top = Py + "px";
      }
      showBorderArrowBox(
        event,
        arrowWidthInPx,
        upside,
        isExternal,
        arrowTableSideRight
      );
      //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
        );

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

  function tryToGetTextContent(element, query, queryName) {
    let result = element;
    if (result && result !== undefined) {
      result = result.innerHTML; //changed from textContent to innerHTML to get html tags(b/i/p/br) and text line breaks
      result = result.replace(reWhiteListStripHTML, ""); //strip all other html tags
    } else if (element !== null && element !== undefined) {
      console.log(
        "Wrong or changed querySelector for " + queryName + ". not: " + query
      );
    }

    return result;
  }

  function getTargetDomain(individualSiteLink) {
    let domain = "";
    if (individualSiteLink) {
      //console.log(individualSiteLink);
      let hasSlashIndex = individualSiteLink.indexOf("/");
      if (hasSlashIndex) hasSlashIndex = hasSlashIndex + 1;
      else hasSlashIndex = individualSiteLink.length - 1;
      domain = individualSiteLink.slice(0, hasSlashIndex);
    }
    return domain;
  }

  function getLinkID(link, individualPage) {
    const DEBUG = false;
    let ID, domainInfrontOfID;
    let stringFromID;
    let boolHasSlashAfterID = false;
    if (link && individualPage) {
      //example individualPage = "mangadex.org/title/"
      DEBUG && console.group("getLinkID");
      DEBUG && console.log(link);
      const isApiLink = link.indexOf(individualPage);
      let linkAfterDomain;
      if (isApiLink >= 0) {
        let indexID = isApiLink + individualPage.length;
        domainInfrontOfID = link.slice(0, indexID);
        linkAfterDomain = link.slice(indexID);
        DEBUG && console.log("linkAfterDomain: " + linkAfterDomain);
        if (individualPage && externalLinks[individualPage]) {
          DEBUG &&
            console.log(
              "externalLink: " +
                individualPage +
                ", match with linkAfterDomain to get full ID: " +
                linkAfterDomain
            );
          //externalLinks
          if (externalLinks[individualPage].serieRegex) {
            stringFromID = linkAfterDomain.match(
              externalLinks[individualPage].serieRegex
            );
          }
          DEBUG && console.log(stringFromID);
          DEBUG && console.log("stringFromID: " + stringFromID);
          if (stringFromID === null) {
            if (externalLinks[individualPage].serieRegex2) {
              DEBUG &&
                console.log("getLinkID serieRegex2 for: " + individualPage);
              stringFromID = linkAfterDomain.match(
                externalLinks[individualPage].serieRegex2
              );
            } else {
              stringFromID = linkAfterDomain.match(defaultSerieRegex);
            }
          }

          //stringFromID = link.slice(indexID);
        } else {
          DEBUG &&
            console.log(
              "internallink, match with linkAfterDomain to get full ID: " +
                linkAfterDomain
            );
          //internalLink
          stringFromID = linkAfterDomain.match(
            internalLink[INDIVIDUALPAGETEST].serieRegex
          );
          //stringFromID = link.slice(indexID);
        }
        DEBUG &&
          console.log(
            "domainInfrontOfID: " +
              domainInfrontOfID +
              ", stringFromID: " +
              stringFromID
          );
        if (stringFromID.length > 0) {
          if (stringFromID[1]) {
            stringFromID = stringFromID[1]; //exact match
          } else {
            stringFromID = stringFromID[0]; //complete matching string after domain
          }
        }
        DEBUG && console.log(stringFromID);
        DEBUG && console.log(stringFromID);
        /*
          let hasSlashAfterID = stringFromID.indexOf("/");
          boolHasSlashAfterID = hasSlashAfterID>=0;
          
          if (boolHasSlashAfterID) ID = stringFromID.slice(0, hasSlashAfterID);
          else */
        //ID = stringFromID;
        //DEBUG && console.log(ID);
      }

      DEBUG && console.groupEnd();
    }
    return { ID: stringFromID, Domain: domainInfrontOfID }; //,hasSlashAfterID:boolHasSlashAfterID
  }
  async function getCoverDataFromUrl(
    elementUrl,
    external = false,
    individualPage = undefined
  ) {
    const DEBUG = false;

    DEBUG && console.group("getCoverDataFromUrl for: " + currentTitelHover);
    const isExternal = external;
    let coverData;
    let linkID;
    let hasApiAccess;

    if (isExternal) hasApiAccess = externalLinks[individualPage].mainAPI;
    else {
      hasApiAccess = internalLink[INDIVIDUALPAGETEST].mainAPI; //todo change with individualPage. Forward individualPage for internal site
    }
    //console.log(hasApiAccess);
    //console.log("xhr.finalUrl: " + xhr.finalUrl +", elementUrl: " + elementUrl)
    DEBUG && console.log("elementUrl: " + elementUrl);
    elementUrl = getLinkToSeriePage(elementUrl, individualPage);
    DEBUG && console.log("getLinkToSeriePage() -> elementUrl: " + elementUrl);
    if (hasApiAccess) {
      const { ID } = getLinkID(elementUrl, individualPage);
      linkID = ID;
      //console.log(linkID);
      DEBUG && console.log("linkID: " + linkID);
    }
    DEBUG && console.log("elementUrl: " + elementUrl);
    let targetDomain;
    let hasExternalTargetPage;
    let targetPage;
    //#region check is external link
    DEBUG &&
      console.log(
        "isExternal: " + isExternal + ", individualPage: " + individualPage
      );
    if (isExternal && individualPage) {
      hasExternalTargetPage = externalLinks[individualPage];
      if (hasExternalTargetPage) {
        targetPage = hasExternalTargetPage;
        targetDomain = getTargetDomain(individualPage);
      }
    } else {
      targetPage = internalLink[internalLinkKey[0]];
      //targetDomain = getTargetDomain(INDIVIDUALPAGETEST); //for now leave empty (since only one internal link possible)
    }
    DEBUG &&
      console.log(
        "hasExternalTargetPage: " +
          hasExternalTargetPage +
          ", targetDomain: " +
          targetDomain
      );
    //#endregion
    let apiData;
    if (hasApiAccess) {
      DEBUG &&
        console.log("hasApiAccess for individualPage: " + individualPage);
      switch (individualPage) {
        //API access
        case "mangadex.org/manga/":
        case "mangadex.org/title/":
          //url: "https://mangadex.org/api/v2/manga/" + id,
          apiData = await getCoverDataFromMangaDex(linkID);
          break;
        case "www.tvmaze.com/shows/":
          apiData = await getCoverDataFromTVmaze(linkID);
          break;
        case "wlnupdates.com/series-id/":
          apiData = await getCoverDataFromWLNupdates(linkID);
          break;
      }
    } else {
      DEBUG &&
        console.log(
          "before getCoverDataFromParsingTargetUrl - elementUrl: " + elementUrl
        );
      //coverData = Object.assign({}, emptyCoverData);
      apiData = await getCoverDataFromParsingTargetUrl(
        elementUrl,
        targetPage,
        isExternal
      );
      DEBUG && console.log(apiData);
    }
    DEBUG && console.log(apiData);
    let imagelink;
    if (apiData !== undefined) {
      coverData = apiData;
      imagelink = coverData.url;
    }
    //console.log(imagelink);
    //console.log("save imageUrl into coverData.url: " + imagelink);

    let externalUrl;
    //console.log("targetDomain: " + targetDomain)
    if (isExternal && targetDomain) externalUrl = targetDomain;

    imagelink = processRelativeImageLink(imagelink, targetDomain, isExternal);
    //console.log("serieAlternativeNames: " + serieAlternativeNames)
    let cData;
    DEBUG && console.log("externalUrl: " + externalUrl);
    DEBUG && console.log(coverData);
    //#region merge final complete coverData
    if (coverData !== undefined) {
      cData = coverData;
      cData.isExternal = externalUrl;
      cData.url = imagelink;
      //console.log(coverData);
      //console.log(cData);
    }
    //#endregion merge final complete coverData
    DEBUG && console.log(cData);
    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 cData;
  }
  function processRelativeImageLink(imagelink, targetDomain, isExternal) {
    const DEBUG = false;
    //#region adjust relative link to absolute domain
    if (imagelink && imagelink !== undefined && imagelink !== null) {
      if (imagelink.tagName && imagelink.tagName == "IMG") {
        imagelink = imagelink.getAttribute("src");
      }
      if (imagelink instanceof HTMLElement) {
        let hasSrc = imagelink.getAttribute("src");
        if (hasSrc) imagelink = imagelink.getAttribute("src");
      }

      DEBUG && console.log(imagelink);

      if (imagelink.startsWith("//")) {
        imagelink = "https://" + imagelink.slice(2);
      }
      if (imagelink.startsWith("/")) {
        DEBUG && console.log(targetDomain);
        DEBUG && console.log(imagelink);
        imagelink = targetDomain + imagelink;
      }

      if (
        isExternal &&
        !(imagelink.startsWith("http://") || imagelink.startsWith("https://"))
      ) {
        //if relativeLink on external site change to absolute link
        imagelink = "https://" + imagelink;
      }
    }
    //#endregion
    return imagelink;
  }
  function rejectErrorStatusMessage(xhr) {
    let rejectNotification = "";
    //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
    switch (true) {
      case xhr.status == 400:
        rejectNotification = "bad request: xhr response == " + xhr.status;
        break;
      case xhr.status == 401:
        rejectNotification = "unauthorized: xhr response == " + xhr.status;
        break;
      case xhr.status == 402:
        rejectNotification = "Payment Required: xhr response == " + xhr.status;
        break;
      case xhr.status == 403:
        rejectNotification = "Forbidden: xhr response == " + xhr.status;
        break;
      case xhr.status == 404:
        rejectNotification =
          xhr.finalUrl + "<br/>page not found: xhr response == " + xhr.status;
        break;
      case xhr.status == 408:
        rejectNotification = "request timeout: xhr response == " + xhr.status;
        break;
      case xhr.status == 410:
        rejectNotification =
          "page gone and not available: xhr response == " + xhr.status;
        break;
      case xhr.status == 425:
        rejectNotification =
          "page request too early: xhr response == " + xhr.status;
        break;
      case xhr.status == 429:
        rejectNotification =
          "rate limit reached. Please notify to increase/adjust rate limit to setting for this domain: xhr response == " +
          xhr.status;
        break;
      case xhr.status == 451:
        rejectNotification =
          "page was removed cause of legal reasons: xhr response == " +
          xhr.status;
        break;
      case xhr.status == 500:
        {
          console.log(xhr.responseHeaders);
          let serverMessage = xhr.responseHeaders
            .toString()
            .match(new RegExp("server:(.*)"));
          if (serverMessage != null)
            rejectNotification =
              "page has an internal server error: xhr response == " +
              xhr.status +
              "<br />" +
              serverMessage;
        }

        break;
      case xhr.status == 503:
        rejectNotification =
          "page Unavailable. Down for maintenance or overloaded: xhr response == " +
          xhr.status;
        break;
      case xhr.status == 511:
        rejectNotification =
          "page Network Authentication Required: xhr response == " + xhr.status;
        break;
      default:
        rejectNotification = xhr;
    }
    return rejectNotification;
  }
  function setLinkState(element, state = undefined, preloadUrlRequest = false) {
    const DEBUG = false;
    if (element) {
      let hasText = element.textContent != "";
      if (showIconNextToLink) {
        /*  0: popup possible/no coverdata preloaded; if preloadUrlRequest true set inactive preloading icon
              1: currently active loading coverData link
              2: coverData preloaded
          */
        const externalTarget = element.getAttribute("coverDataExternalTarget");
        let elementUrl = getLinkToSeriePage(element.href, externalTarget);
        DEBUG && console.group("link: " + elementUrl);
        DEBUG && console.log("state before check: " + state);
        if (state === undefined) {
          const coverData = GM_getCachedValue(elementUrl);
          DEBUG && console.log(coverData);
          if (
            coverData === undefined ||
            coverData === null ||
            coverData === "null" ||
            preloadUrlRequest
          ) {
            state = linkIconEnum.popupPossibleNotLoadedOrMarkedForPreloading; //no coverData wating for interaction or forced reloading/preloading
          } else {
            state = linkIconEnum.popupHasCoverData; //coverData available and loaded
          }
        }
        DEBUG && console.log("state: " + state);
        DEBUG && console.groupEnd();
        element.classList.remove(
          "hasCoverPreviewPopup",
          "loadingUrlPreload",
          "loadingUrl",
          "hasLoadedCoverPreviewPopup"
        );
        switch (state) {
          case linkIconEnum.popupPossibleNotLoadedOrMarkedForPreloading: //popup possible/no coverdata preloaded; if preloadUrlRequest true set inactive preloading icon
            if (hasText) {
              if (preloadUrlRequest) {
                element.classList.add("loadingUrlPreload");
              } else {
                element.classList.add("hasCoverPreviewPopup");
              }
            }
            break;
          case linkIconEnum.popupLoading: //currently loading coverData
            if (hasText) element.classList.add("loadingUrl");
            break;
          case linkIconEnum.popupHasCoverData: //coverData preloaded
            if (hasText) element.classList.add("hasLoadedCoverPreviewPopup");
            break;
          case linkIconEnum.error:
            //don't add any icon
            //console.log("set state to linkError")
            break;
        }
      } else {
        //if not showIconNextToLink -> cleanup icons
        element.classList.remove(
          "hasCoverPreviewPopup",
          "loadingUrlPreload",
          "loadingUrl",
          "hasLoadedCoverPreviewPopup"
        );
      }
    }
  }
  function setLinkStateOfSameLinks(element, state, preloadUrlRequest = false) {
    const DEBUG = false;
    const elementUrl = element.href;
    //console.log("elementUrl: " + elementUrl)
    //console.log(elementUrl)
    const sameLinks = document.querySelectorAll('a[href="' + elementUrl + '"]');
    DEBUG && console.group("setLinkStateOfSameLinks: " + elementUrl);
    DEBUG && console.log(sameLinks);
    DEBUG && console.log("sameLinks.length: " + sameLinks.length);
    DEBUG && console.groupEnd();
    if (sameLinks.length > 0) {
      for (let i = 0; i < sameLinks.length; i++) {
        DEBUG && console.log(sameLinks[i]);
        setLinkState(sameLinks[i], state, preloadUrlRequest);
      }
    }
  }
  function getLinkToSeriePage(elementUrl, individualPage = undefined) {
    const DEBUG = false;
    if (individualPage) {
      let linkID;
      const { ID, Domain } = getLinkID(elementUrl, individualPage);
      linkID = ID;
      //console.log(linkID);

      elementUrl = Domain + linkID; //in case of direct chapter link provided with linkIK
      DEBUG &&
        console.log(
          "new elementUrl: " +
            elementUrl +
            ", individualPage: " +
            individualPage +
            ", linkID: " +
            linkID +
            ", Domain: " +
            Domain
        );
    }
    return elementUrl;
  }
  async function parseSeriePage(
    element,
    forceReload = false,
    hoveredTitle = undefined,
    event = undefined,
    external = false,
    targetPage = undefined
  ) {
    const DEBUG = false;
    DEBUG &&
      console.log("before getLinkToSeriePage - element.href: " + element.href);
    const elementUrl = getLinkToSeriePage(element.href, targetPage);
    DEBUG && console.group("parseSeriePage: " + elementUrl);
    DEBUG && console.log("elementUrl: " + elementUrl);
    let coverData;
    if (!forceReload) coverData = GM_getCachedValue(elementUrl);
    // let retrievedImgLink;
    let PromiseResult;
    DEBUG && console.log(coverData);
    if (
      !forceReload &&
      coverData !== undefined &&
      coverData !== null &&
      coverData.title
    ) {
      //retrievedImgLink = coverData.url;
      DEBUG &&
        console.log(
          "parseSeriePage has cached coverData  for: " +
            coverData.title +
            " at " +
            elementUrl
        );
      PromiseResult = coverData;
    } else {
      //console.log("before showPopupLoadingSpinner")
      showPopupLoadingSpinner(hoveredTitle, hoveredTitle, event);
      //console.log("before getCoverDataFromUrl - elementUrl: " + elementUrl)
      PromiseResult = getCoverDataFromUrl(elementUrl, external, targetPage);
    }

    DEBUG && console.groupEnd("parseSeriePage: " + elementUrl);
    PromiseResult = await PromiseResult;
    setLinkStateOfSameLinks(element, linkIconEnum.popupHasCoverData); //coverData loading finished
    DEBUG && console.log(PromiseResult);
    //DEBUG && console.log(PromiseResult)
    //after GM_xmlhttpRequest PromiseResult

    return PromiseResult;
  }
  //renamed targetNodeArray -> arrayTargetNode to remove compatibility mode in tampermonkey
  function removeEventListenerFromNodes(arrayTargetNode, external = false) {
    if (arrayTargetNode && arrayTargetNode.length > 0) {
      //console.log(targetNodeArray);
      arrayTargetNode.map(function (el) {
        if (
          eventListenerStyle === undefined ||
          eventListenerStyle === null ||
          eventListenerStyle == 0
        ) {
          el.removeEventListener("mouseenter", mouseEnterPopup);
          el.removeEventListener("mouseleave", hideOnMouseLeave);
        }
      });
    }
  }
  function wait(ms) {
    return new Promise((resolve, reject) => setTimeout(resolve, ms));
  }
  //renamed targetNodeArray -> arrayTargetNode to remove compatibility setting in tampermonkey
  async function preloadForIndividualPageTest(
    arrayTargetNode = [],
    individualLinksToTest,
    external = false,
    forceReload = false
  ) {
    const DEBUG = false;

    DEBUG && console.log(arrayTargetNode);

    DEBUG && console.log("preloadCoverData");

    DEBUG &&
      console.log(
        "before parseSeriePage for each url with a link to individual seriepage"
      );
    //#region addEventlistener
    if (arrayTargetNode && arrayTargetNode.length > 0) {
      //console.log(targetNodeArray);
      arrayTargetNode.map(function (el) {
        //console.log(el)
        // const elementUrl = el.href;
        //const externalTarget = el.getAttribute("coverDataExternalTarget");
        // console.log(elementUrl)
        if (
          eventListenerStyle === undefined ||
          eventListenerStyle === null ||
          eventListenerStyle == 0
        ) {
          //console.log(el); //TODO external overwrite/removes previous mouseEnterPopup?

          el.addEventListener("mouseenter", mouseEnterPopup);
          el.addEventListener("mouseleave", hideOnMouseLeave);
          /* if (external)
                el.setAttribute("coverDataExternalTarget", IndividualTargetToTest);*/
        }
      });
    }
    //#endregion addEventlistener
    //#region preload
    if (arrayTargetNode && arrayTargetNode.length > 0) {
      let nodeArrayIndex = 0;
      //rateLimitQueryAfterSeconds:"0.5"
      //if not set, but both other values are available will be calculated into rateLimitTimeInSeconds/rateLimitCount
      let hasRateLimitQueryAfterSeconds,
        hasRateLimitTimeInSeconds,
        hasRateLimitCount,
        hasRateLimitQueryAfterMS;

      if (external) {
        //externalLinks[individualLinksToTest]["lastQuery"] = Date.now();
        hasRateLimitQueryAfterSeconds =
          externalLinks[individualLinksToTest].rateLimitQueryAfterSeconds;
        if (!hasRateLimitQueryAfterSeconds) {
          hasRateLimitTimeInSeconds =
            externalLinks[individualLinksToTest].rateLimitTimeInSeconds;
          hasRateLimitCount =
            externalLinks[individualLinksToTest].rateLimitCount;
          if (hasRateLimitTimeInSeconds && hasRateLimitCount) {
            hasRateLimitQueryAfterSeconds =
              hasRateLimitTimeInSeconds / hasRateLimitCount;
          } else {
            hasRateLimitQueryAfterSeconds = defaultRateLimitQueryAfterSeconds;
          }
        }
      } else {
        hasRateLimitQueryAfterSeconds =
          internalLink[individualLinksToTest].rateLimitQueryAfterSeconds;
        if (!hasRateLimitQueryAfterSeconds) {
          hasRateLimitTimeInSeconds =
            internalLink[individualLinksToTest].rateLimitTimeInSeconds;
          hasRateLimitCount =
            internalLink[individualLinksToTest].rateLimitCount;
          if (hasRateLimitTimeInSeconds && hasRateLimitCount) {
            hasRateLimitQueryAfterSeconds =
              hasRateLimitTimeInSeconds / hasRateLimitCount;
          } else {
            hasRateLimitQueryAfterSeconds = defaultRateLimitQueryAfterSeconds;
          }
        }
      }
      if (hasRateLimitQueryAfterSeconds) {
        hasRateLimitQueryAfterMS = hasRateLimitQueryAfterSeconds * 1000;
      }
      DEBUG &&
        console.log("hasRateLimitQueryAfterMS: " + hasRateLimitQueryAfterMS);
      if (
        arrayTargetNode.length > 0 &&
        (forceReload || preloadUrlRequests) &&
        !deactivatePreloadUrlRequestOnUrls.includes(individualLinksToTest)
      ) {
        while (nodeArrayIndex < arrayTargetNode.length) {
          const element = arrayTargetNode[nodeArrayIndex];
          const elementUrl = getLinkToSeriePage(
            element.href,
            individualLinksToTest
          );
          DEBUG && console.log("elementUrl: " + elementUrl);
          DEBUG &&
            console.log(
              "start parseSeriePage for links of domain: " +
                individualLinksToTest
            );
          let targetPage = undefined;
          if (external) {
            targetPage = individualLinksToTest;

            //console.log("targetPage: " + targetPage);
          }
          // console.log("external: " + external);

          const hoveredTitle = undefined;
          const event = undefined;
          const coverData = GM_getCachedValue(elementUrl);
          DEBUG &&
            console.group(
              "preloadForIndividualPageTest GM_getCachedValue elementUrl: " +
                elementUrl
            );
          DEBUG && console.log("elementUrl: " + elementUrl);
          DEBUG && console.log(coverData);
          DEBUG && console.groupEnd();
          /*
            if (!(coverData!==undefined && coverData !== null && coverData != "null"))
            {//has coverData
              setLinkState(element, 0, forceReload);//set inactive preloadingMarker or preloaded Data
            }else{

              setLinkState(element, 1, forceReload); //active loading icon
            }*/
          setLinkState(element, linkIconEnum.popupLoading, forceReload); //active loading icon
          const promiseParsingPage = parseSeriePage(
            element,
            forceReload,
            hoveredTitle,
            event,
            external,
            targetPage
          ).then(
            function (coverData) {
              if (coverData !== undefined) {
                if (preloadImages) {
                  DEBUG &&
                    console.log(
                      "preloadCoverData preloadImages: " + preloadImages
                    );
                  DEBUG && console.log(coverData);
                  loadImageFromBrowser({ coverData: coverData });
                }
              }
            },
            function (Error) {
              DEBUG && console.log(Error + " failed to fetch " + element);
              GM_deleteValue(element.href);
              setLinkStateOfSameLinks(element, linkIconEnum.error);
            }
          );

          if (hasRateLimitQueryAfterMS) {
            // const coverData = GM_getCachedValue(elementUrl);
            //console.log("preloaded url: " + elementUrl);
            if (
              !(
                coverData !== undefined &&
                coverData !== null &&
                coverData != "null"
              )
            ) {
              await Promise.all([
                promiseParsingPage,
                wait(hasRateLimitQueryAfterMS),
              ]);
            } else {
              setLinkState(element, undefined, forceReload); //coverData already loaded
            }
          }
          nodeArrayIndex++;
        }
      }
    }
  }
  async function preloadCoverData(forceReload = false) {
    const DEBUG = false;
    //#region create complete nodelist
    ALLSERIENODES = [];
    ALLEXTERNALLINKNODES = [];
    let allPreloadingPromises = [];
    //console.log("preloadCoverData forceReload: " + forceReload)
    updateSerieNodes(ALLSERIENODES, INDIVIDUALPAGETEST, forceReload);
    if (externalLinks && externalLinkKeys.length > 0) {
      for (let i = 0; i < externalLinkKeys.length; i++) {
        updateSerieNodes(
          ALLEXTERNALLINKNODES,
          externalLinkKeys[i],
          forceReload,
          true
        );
      }
    }
    //#endregion

    //#region add eventlistener mouseenter/leave to links and preloadCoverData if preloading set to true
    removeEventListenerFromNodes(ALLSERIENODES);
    allPreloadingPromises.push(
      preloadForIndividualPageTest(
        ALLSERIENODES,
        INDIVIDUALPAGETEST,
        false,
        forceReload
      )
    );

    //console.log(externalLinks);
    //console.log(externalLinkKeys[0]);
    //console.log(externalLinkKeys.length);
    removeEventListenerFromNodes(ALLEXTERNALLINKNODES);
    //console.log(ALLEXTERNALLINKNODES)

    if (ALLEXTERNALLINKNODES.length > 0) {
      if (externalLinks && externalLinkKeys.length > 0) {
        for (let i = 0; i < externalLinkKeys.length; i++) {
          allPreloadingPromises.push(
            preloadForIndividualPageTest(
              ALLEXTERNALLINKNODES.filter((link) =>
                link.href.includes(externalLinkKeys[i])
              ),
              externalLinkKeys[i],
              true,
              forceReload
            )
          );
        }
      }
    }
    //#endregion
    if (forceReload) {
      await Promise.all(allPreloadingPromises);
      console.log(
        "has finished preloading all links on this page: " +
          window.location.href
      );
    }
  }

  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;
            }
            #popover_arrow{
              margin: unset;
              padding: unset;
              text-size-adjust: unset;
              line-height: unset;
              font-family: unset;
              font: unset;

              opacity: 1;
              visibility:visible;
              transition: visibility 0.2s, opacity 0.2s linear;
            }
            .arrowCSS{
              box-sizing: border-box;
              border: 1px solid #000;
              box-shadow:1px 1px 0px #7A7A7A;
              border-width: 0 2px 2px 0;
              display: inline-block;
              padding: 0;
              margin: unset;
              text-size-adjust: unset;
              line-height: unset;
              font-family: unset;
              font: unset;
            }


            @keyframes dotsLoading{
              0%{
                opacity: 0;
              }
              50%{
                opacity: 1;
              }
              100%{
                opacity: 0;
              }
            }
            #dotLoading1{
              animation: dotsLoading 1s infinite;
            }

            #dotLoading2{
              animation: dotsLoading 1s infinite;
              animation-delay: 0.2s;
            }

            #dotLoading3{
              animation: dotsLoading 1s infinite;
              animation-delay: 0.4s;
            }
            .loadingUrl, .loadingUrlPreload, .hasCoverPreviewPopup, .hasLoadedCoverPreviewPopup{
             /* position:relative;
              display: inline;*/
              padding:0 !important;
              margin:0 !important;
            }
            .loadingUrl:link, .loadingUrlPreload:link, .hasCoverPreviewPopup:link, .hasLoadedCoverPreviewPopup:link{
               padding:0 !important;
               margin:0 !important;
             }
            .loadingUrl::before{
              content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-dots" width="14" height="14" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none" fill="none"/><path stroke="red" d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" /><line x1="12" y1="11" x2="12" y2="11.01" stroke="red" fill="red" id="dotLoading1" /><line x1="8" y1="11" x2="8" y2="11.01" stroke="red" fill="red" id="dotLoading2" /><line x1="16" y1="11" x2="16" y2="11.01" stroke="red" fill="red" id="dotLoading3" /></svg>');
            }
            .loadingUrlPreload::before{
              content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-dots" width="14" height="14" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4"/><line x1="12" y1="11" x2="12" y2="11.01" stroke="red" fill="red" id="dotLoading1" /><line x1="8" y1="11" x2="8" y2="11.01" stroke="red" fill="red" id="dotLoading2" /><line x1="16" y1="11" x2="16" y2="11.01" stroke="red" fill="red" id="dotLoading3" /></svg>');
            }
            /* https://tablericons.com/ "message" without newlines set width/height to 12px; removed two lines to get empty popup */
            .hasCoverPreviewPopup::before{
              content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="12px" height="12px" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" /></svg>');
            }
            /* https://tablericons.com/ "message" without newlines set width/height to 12px */
            .hasLoadedCoverPreviewPopup::before{
              content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="12px" height="12px" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" /><line x1="8" y1="9" x2="16" y2="9" /><line x1="8" y1="13" x2="14" y2="13" /></svg>');
            }

            .blackFont {
                color:#000;
            }
            .whiteFont {
                color:#fff
            }
            .defaultTitleStyle {
                box-sizing: border-box;
                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;
                height:auto;
                padding:0;
                background-color:#fff;
            }
            .ImgFitDefault{
                object-fit: contain;
                min-width: 0;
                min-height: 0;
                max-height: 400px;
                max-width: 400px;
                width:100%;
                height:100%;
                margin:2px;
                padding:0;
                position:unset;
                border-radius: 10%;
            }

            #coverPreviewAutoScroll#style-4::-webkit-scrollbar-track,#coverPreviewContentAutoScroll::#style-4::-webkit-scrollbar-track
            {
              -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
              background-color: #F5F5F5;
            }

            #coverPreviewAutoScroll::-webkit-scrollbar,#coverPreviewContentAutoScroll::-webkit-scrollbar
            {
              width: 2px;
              background-color: #F5F5F5;
            }

            #coverPreviewAutoScroll::-webkit-scrollbar-thumb, #coverPreviewContentAutoScroll::-webkit-scrollbar-thumb
            {
              background-color: #888;
            }
            #coverPreviewAutoScroll{
              overflow:auto;
              scrollbar-width: thin;
              scrollbar-color: #888 #F5F5F5;
            }
            #coverPreviewContentAutoScroll{
              display:block;
              overflow:auto;
              scrollbar-width: thin;
              scrollbar-color: #888 #F5F5F5;
            }

            #popover{
              box-sizing: border-box;
              overflow: hidden;

              /* 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;
              box-shadow: 0px 0px 5px #7A7A7A;
              position:absolute;
              z-index:10;


              text-align: center !important;
              justify-content: start;
              justify-items: center;
              display: flex;
              flex-shrink: 1;
              flex-direction: column;

              opacity: 1;
              transition: visibility 0.2s, opacity 0.2s linear;
            }
            .hidePopover {
              visibility:hidden !important;
              opacity: 0 !important;
              transition: visibility 0.4s, opacity 0.4s linear !important;
            }
            .isExternalContent{
              border:2px solid red !important;
            }
            .isExternalContentArrow{
              border:2px solid red !important;
              border-width:0 2px 2px 0 !important
            }
            .popoverContent {
              box-sizing: border-box;
              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;
              border-radius:0;
            }
            .popoverDetail{
              flex-direction:unset !important;
              height:400px;
            }
            .coverDataTitle{
              border-bottom:1px solid white;
              padding:2px 0;
            }
            .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;
              word-break: break-word; /* word wrap/break long links/texts */
            }

            .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(
    adjustedTargetHeigt = 400,
    targetTopPxYPosition = 0
  ) {
    //https://developer.mozilla.org/en-US/docs/Web/CSS/min() not compatible with firefox 56

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

    // if (adjustedTargetWidth < targetHeight) targetHeight = adjustedTargetWidth;

    if (showDetails) {
      popover.classList.add("popoverDetail");
      popoverTitle.classList.add("popoverTitleDetail");

      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() for max-Width");
        popover.style.maxWidth =
          "calc(100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))";
        popover.style.width = targetHeight * 2 + "px";
      }
    } else {
      popover.classList.remove("popoverDetail");
      popoverTitle.classList.remove("popoverTitleDetail");

      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.height = targetHeight + "px !important";
            */
        popover.style.width = targetHeight + "px";
      }
    }
  }
  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);
    arrowContainer = document.createElement("div");
    arrowContainer.id = "popover_arrow";
    bodyElement.appendChild(arrowContainer);
    popover.className = (
      "defaultBackgroundStyle " + STYLESHEETHIJACKFORBACKGROUND
    ).trim();
    popoverContent.className =
      "popoverContent blackFont " + STYLESHEETHIJACKFORBACKGROUND;
    if (
      !STYLESHEETHIJACKFORBACKGROUND &&
      DEFAULTBACKGROUNDCOLOR &&
      DEFAULTBACKGROUNDCOLOR != ""
    ) {
      popover.style.backgroundColor = DEFAULTBACKGROUNDCOLOR;
    }
    //setPopoverHeight();
    //setPopoverWidth();
    setTimeout(setPopoverHeight, 500); //hack. why is a wait time needed?
    setTimeout(setPopoverWidth, 500); //hack. Can not apply style.height without a short wait time in older firefox 56
    //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

    hidePopOver();

    bodyElement.insertAdjacentElement("beforeend", popover);
    popoverBorderWidth = 0.6; //getComputedStyle(popoverTitle,"border-bottom-width")
    updateArrowStyle(); //Forum
    updateArrowColor();

    /*
        console.log("popover.style.height: " + popover.style.height);
        popover.style.minHeight="0px";
        popover.style.position="absolute";
        popover.style.height = '333px';
        console.log("popover.style.height: " + popover.style.height);
        setPopoverHeight();*/
  }
  function updateArrowStyle() {
    //console.log("updateArrowStyle");
    popoverForegroundColor = getComputedStyle(popover, "background-color");
    let popoverContentForegroundColor = getComputedStyle(
      popoverContent,
      "background-color"
    );
    const blendedForegroundColor = blendColorsToRGBA([
      popoverForegroundColor,
      popoverContentForegroundColor,
    ]);

    popoverBackgroundColor = getComputedStyle(popoverTitle, "background-color");
    /* console.log(
      "backgroundColor of popoverTitle: " +
        popoverBackgroundColor +
        ", popoverBorderWidth: " +
        popoverBorderWidth +
        ", popoverForegroundColor: " +
        popoverForegroundColor +
        ", popoverContentForegroundColor: " +
        popoverContentForegroundColor +
        ", blendedForegroundColor: " +
        blendedForegroundColor
    );*/
    popoverForegroundColor = blendedForegroundColor;
  }
  function updateArrowColor() {
    let styleSheetIDElement = "mesh-nightmode-css";
    let linkElement = document.getElementById(styleSheetIDElement);

    //console.log(linkElement);
    function loadedCb() {
      //console.log("finished loading stylesheet " + linkElement.href);
      updateArrowStyle();
    }
    function errorCb(error) {
      console.log(error);
      console.log("error loading stylesheet: " + linkElement.href);
    }
    if (linkElement) {
      //console.log(linkElement.href);
      let url = linkElement.href;
      linkElement.href = "";
      linkElement.removeEventListener("loaddata", loadedCb); // or: file.onload = loadedCb;
      linkElement.onload = () => {
        loadedCb();
      };
      linkElement.onerror = (error) => {
        errorCb(error);
      };
      linkElement.href = url;

      if (linkElement.complete) {
        loadedCb();
      }
    }
  }

  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) {
      popover.classList.remove("isExternalContent"); //last link was external. remove isExternal class style
      // console.group("showPopupLoadingSpinner")
      //popover.empty();
      //popover.innerHTML = "";
      DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
      if (coverData !== undefined) {
        //console.log("showPopupLoadingSpinner")
        DEBUG && console.log(coverData);
        adjustPopupTitleDetail(coverData, title);
      } else popoverTitle.textContent = title;

      if (notification != "") {
        isShowingSpinnerAnimation = false;
        popoverContent.innerHTML =
          '<div id="coverPreviewContentAutoScroll" class="popoverContent ">' +
          notification +
          "</div>";
        popoverContent.className =
          "popoverContent wordBreak " + STYLESHEETHIJACKFORBACKGROUND; //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 " + STYLESHEETHIJACKFORBACKGROUND; //whitefont
      }
      DEBUG && console.log(popover);
      //   DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight);
      //console.log(event)
      if (coverData) {
        popupPos(event, coverData.isExternal);
      } else popupPos(event);
      //  console.groupEnd("showPopupLoadingSpinner")
    }
  }
  //#region adapted code from scrollToTarget of https://htmldom.dev/scroll-to-an-element-smoothly/
  let direction = 1;
  let pauseTimeDifference = null;
  let currentPercent = null;
  let percentBeforeStyleChange;
  let hasChangedStyle = false;
  let requestId = [];
  let startTime = null;
  const scrollToTarget = function (idToScroll, node, duration = 7000) {
    const DEBUG = false;
    let scrollOverflow = node.scrollHeight - node.offsetHeight;
    const updateStartValues = function (percent, currentTime) {
      if (percent) {
        DEBUG && console.group("updateStartValues");
        scrollOverflow = node.scrollHeight - node.offsetHeight;
        startTime = currentTime - pauseTimeDifference;
        pauseTimeDifference = null;
        // if (direction == 1) startPos = scrollOverflow * percent;
        // else startPos = scrollOverflow * (1 - percent);

        DEBUG && console.log("percent: " + percent + ", startPos: ");

        let time = currentTime - startTime;
        //console.log("pauseTimeDifference, time: " + time);
        let targetPercent = Math.min(time / duration, 1);
        DEBUG &&
          console.log(
            "percent after pause: " +
              targetPercent +
              ", percent: " +
              percent +
              ", direction: " +
              direction +
              ", scrolltop percent: "
          );
        DEBUG && console.groupEnd("updateStartValues");
      }
    };
    const loop = function (currentTime) {
      if (!startTime) {
        startTime = currentTime;
      }

      //console.log("scrollOverflow: " + scrollOverflow);
      //#region set StartValues
      if (currentPercent != undefined && currentPercent !== null) {
        DEBUG &&
          console.log(
            "currentPercent:" + currentPercent + ", direction: " + direction
          );
        updateStartValues(currentPercent, currentTime);
        currentPercent = null;
      }

      if (hasChangedStyle) {
        DEBUG && console.log("hasChangedStyle");
        updateStartValues(percentBeforeStyleChange, currentTime);
        hasChangedStyle = false;
      }
      //#endregion

      // Elapsed time in miliseconds
      let time = currentTime - startTime;

      const percent = Math.min(time / duration, 1);

      let targetScrollTop, targetScrollTopPercent;
      if (direction == 1) {
        targetScrollTopPercent = easeInOutQuad(percent);
      } else {
        targetScrollTopPercent = 1 - easeInOutQuad(percent);
      }

      targetScrollTop = scrollOverflow * targetScrollTopPercent;
      //console.log(targetScrollTop +", percent: " + percent)
      node.scrollTo(0, targetScrollTop, "auto");
      pauseTimeDifference = currentTime - startTime;
      if (autoScrollCoverData && popoverVisible) {
        //#region loop Animation
        const insideContainerValue =
          targetScrollTop <= scrollOverflow && targetScrollTop >= 0;

        if (time < duration && insideContainerValue) {
          // Continue moving
          //requestId = window.requestAnimationFrame(loop);
          //startPos = 0;
        } else {
          //startPos=0;
          startTime = currentTime;

          direction *= -1;
        }
        percentBeforeStyleChange = percent;
        requestId[idToScroll] = window.requestAnimationFrame(loop);
        //#endregion
      } else {
        //#region pause animation
        //console.group("loop scrolldata before pause");
        window.cancelAnimationFrame(requestId[idToScroll]);
        //pauseTimeDifference = currentTime - startTime;
        currentPercent = percent;
        DEBUG &&
          console.log(
            "scrollPos before pause: " +
              node.scrollTop +
              ", percent: " +
              percent +
              ", direction: " +
              direction +
              ", targetScrollTop: " +
              targetScrollTop
          );

        //console.groupEnd("loop scrolldata before pause");
        //#endregion
      }
    };

    //start animation
    requestId[idToScroll] = window.requestAnimationFrame(loop);
  };
  const easeInOutQuad = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); //https://gist.github.com/gre/1650294
  //#endregion
  function autoScrollData(idToScroll = "coverPreviewAutoScroll") {
    coverDataContainer[idToScroll] = document.getElementById(idToScroll);
    setStartScrollPosition(idToScroll);
    if (autoScrollCoverData) {
      if (coverDataContainer[idToScroll]) {
        /*console.log(
              "coverDataContainer.offsetHeight: " +
                coverDataContainer.offsetHeight +
                ", coverDataContainer.scrollHeight: " +
                coverDataContainer.scrollHeight
            );*/
        const hasOverflowValue =
          coverDataContainer[idToScroll].scrollHeight >
          coverDataContainer[idToScroll].offsetHeight;
        if (hasOverflowValue) {
          if (requestId[idToScroll])
            window.cancelAnimationFrame(requestId[idToScroll]);
          //console.log("currentPercent: " + currentPercent);
          scrollToTarget(idToScroll, coverDataContainer[idToScroll]);
        }
      }
    }
  }
  function resetAutoScroll(idToScroll = "coverPreviewAutoScroll") {
    //autoScrollCoverData = true;
    direction = 1;

    currentPercent = null;
    startTime = null;
    pauseTimeDifference = null;
    hasChangedStyle = false;
    //console.log(requestId);
    if (requestId[idToScroll])
      window.cancelAnimationFrame(requestId[idToScroll]);
  }
  function setStartScrollPosition(idToScroll) {
    const DEBUG = false;

    if (coverDataContainer[idToScroll] && currentPercent) {
      let scrollOverflow =
        coverDataContainer[idToScroll].scrollHeight -
        coverDataContainer[idToScroll].offsetHeight;
      DEBUG &&
        console.log(
          "scrollOverflow: " +
            scrollOverflow +
            ", currentPercent: " +
            currentPercent
        );
      let targetScrollTop, targetScrollTopPercent;
      if (direction == 1) {
        targetScrollTopPercent = easeInOutQuad(currentPercent);
      } else {
        targetScrollTopPercent = 1 - easeInOutQuad(currentPercent);
      }

      targetScrollTop = scrollOverflow * targetScrollTopPercent;
      DEBUG && console.log("targetScrollTop: " + targetScrollTop);
      DEBUG &&
        console.log(
          "coverDataContainer.scrollTop: " +
            coverDataContainer[idToScroll].scrollTop
        );
      coverDataContainer[idToScroll].scrollTop = targetScrollTop;
      DEBUG &&
        console.log(
          "coverDataContainer.scrollTop: " +
            coverDataContainer[idToScroll].scrollTop
        );
    }
  }
  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 ||
        link === null ||
        link == "" ||
        link == "undefined"
      ) {
        popoverContent.innerHTML =
          '<div class="containerPadding">No Cover Image found</div>';
      } else {
        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, coverData.isExternal);
      }
    }
  }

  //#region get serieDetails
  function getRatingNumber(ratingString) {
    //const ratingString = "Rating(3.3 / 5.0, 1940 votes)"
    let ratingNumber;

    if (ratingString) {
      const matchesVotes = ratingString.toLowerCase().match(reVoteCount);
      const matches = ratingString.match(reRating); //"Rating(3.3 / 5.0, 1940 votes)"
      const matchesSingleNumber = ratingString.match(reRatingSingleNumber); //4.5

      //console.log(matches)
      //console.log(matchesSingleNumber)
      //console.log(matches.length)
      let hasVoteCountBigger0 = true;
      let hasVoteString = false;
      // console.log(matchesVotes)
      if (matchesVotes && matchesVotes.length > 1) {
        //console.log(matchesVotes[1])
        hasVoteString = true;
        if (matchesVotes[1] == 0 || matchesVotes[1] == "0") {
          //console.log("no vote count")
          hasVoteCountBigger0 = false;
        }
      }

      if (matches && matches.length == 3 && hasVoteCountBigger0) {
        //display rating when vote count found and more than 0
        //console.log(matches[1])
        ratingNumber = matches[1];
      } else {
        //no votecount found
        //no rating in relation to max rating -> search for single number
        //console.log(matchesSingleNumber[1])
        if (
          hasVoteCountBigger0 &&
          matchesSingleNumber &&
          matchesSingleNumber.length == 2
        ) {
          ratingNumber = matchesSingleNumber[1];
        }
      }
    }

    return ratingNumber;
  }
  function getChapters(statusString) {
    //TODO "Episodes" instead of chapter for tv series
    const DEBUG = false;
    let result;
    if (statusString && statusString.length > 0) {
      DEBUG && console.group("getChapters");
      let chapterCount;
      let lowerCaseStatusString = statusString.toLowerCase();
      DEBUG && console.log("lowerCaseStatusString: " + lowerCaseStatusString);
      const matches = lowerCaseStatusString.match(reChapters);
      let webnovel = "";
      let hasVolumenInString = false;
      let hasChapterInString = false;
      if (matches && matches.length >= 2) {
        hasChapterInString = true;
        chapterCount = matches[1];
        if (matches[2]) {
          webnovel = " WN";
        }
      }
      DEBUG && console.log("chapterCount reChapters: " + chapterCount);
      if (!chapterCount) {
        const matchesBehind = lowerCaseStatusString.match(
          reChaptersNumberBehind
        );
        if (matchesBehind && matchesBehind.length >= 2) {
          hasChapterInString = true;
          chapterCount = matchesBehind[1];
        }
      }
      DEBUG &&
        console.log("chapterCount reChaptersNumberBehind: " + chapterCount);
      if (!chapterCount) {
        const matchesNumbers = lowerCaseStatusString.match(
          reChaptersOnlyNumbers
        ); //example string "6892(Ongoing)"
        if (matchesNumbers && matchesNumbers.length >= 2) {
          chapterCount = matchesNumbers[1];
        }
      }
      DEBUG &&
        console.log("chapterCount reChaptersOnlyNumbers: " + chapterCount);
      if (lowerCaseStatusString.includes("vol")) hasVolumenInString = true;

      if (chapterCount) {
        let numberType = " Chapters";
        if (hasVolumenInString && !hasChapterInString) numberType = " Vol";
        result = chapterCount + webnovel + numberType;
      }
      DEBUG && console.groupEnd();
    }
    DEBUG && console.log("result: " + result);
    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.chapters) {
        completeDetails +=
          '<div class="borderTop">Chapters: ' + coverData.chapters + "</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 serieChapters = getChapters(coverData.chapters);
    let completed = getCompletedState(coverData.status);
    let ongoing = getOngoingState(coverData.status);
    //console.log(rating)
    //console.log(chapters)
    //console.log(serieChapters)
    //console.log(completed)
    //console.log(ongoing)
    if (rating || chapters || serieChapters || completed || ongoing) {
      if (rating !== undefined) rating += "★ ";
      else rating = "";
      //console.log(coverData);

      if (chapters !== undefined) chapters = chapters + " ";
      else chapters = "";

      if (serieChapters !== undefined) serieChapters = serieChapters + " ";
      else serieChapters = "";
      //console.log("chapters: " + chapters);
      //console.log("serieChapters: " + serieChapters);
      if (serieChapters != "") 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 +
        serieChapters +
        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 = "";
    let externalIcon = "";
    let showReadingListIcon = "";

    //console.log(coverData.readingListIcon);
    //console.log(coverData.readingListTitle)
    if (useReadingListIconAndTitle && !coverData.isExternal) {
      //console.log(coverData.readingListIcon)
      // showReadingListIcon="[&nbsp;] ";
      if (coverData.readingListIcon !== undefined) {
        if (showDetails) {
          showReadingListIcon =
            '<img src="' +
            coverData.readingListIcon +
            '" width="16px"; height="16px" /> ';
        } else {
          showReadingListIcon =
            '<img src="' +
            coverData.readingListIcon +
            '" width="12px"; height="12px" /> ';
        }
      }
    }
    //console.log(coverData)
    if (coverData.isExternal) {
      externalIcon = '<span style="background-color:darkred">🔗</span> ';
      popover.classList.add("isExternalContent");
    } else {
      popover.classList.remove("isExternalContent");
    }
    let alternativeNames = "";
    if (showDetails) {
      //console.log("showDetails should be true")

      let showExternalLink = "";
      let showReadingListTitle = "";
      if (useReadingListIconAndTitle && !coverData.isExternal) {
        showReadingListTitle = "";
        if (coverData.readingListTitle) {
          showReadingListTitle =
            "<div>" +
            showReadingListIcon +
            " " +
            coverData.readingListTitle +
            "</div>";
        } else {
          showReadingListTitle =
            "<div>[&nbsp;] not in a reading list or logged in</div>";
        }
      }

      if (coverData.alternativeNames && coverData.alternativeNames != "") {
        alternativeNames = " [Key A]";
      }
      if (coverData.isExternal) {
        showExternalLink =
          ' <div style="background-color:darkred" class="coverDataTitle">🔗[' +
          coverData.isExternal +
          "]</div>";
      }
      completeDetails +=
        '<span class="' +
        mediumTextStyle +
        '" style="height:100%;display:flex;flex-direction:column"><span class="coverDataTitle"><b>' +
        titleToShow +
        "</b>" +
        alternativeNames +
        showReadingListTitle +
        "</span> " +
        showExternalLink +
        '<div id="coverPreviewAutoScroll">' +
        getDetailsString(coverData);
      +"</div>"; //autoscroll

      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 {
      if (coverData.alternativeNames && coverData.alternativeNames != "") {
        alternativeNames = " [A]";
      }
      completeDetails =
        '<span class="' +
        mediumTextStyle +
        '">' +
        externalIcon +
        showReadingListIcon +
        "<b>" +
        titleToShow +
        "</b>" +
        alternativeNames +
        " " +
        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");

    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 &&
      hoveredTitle == currentTitelHover
    ) {
      currentCoverData = coverData;
    }

    if (e) {
      loadImageFromBrowser({
        coverData: currentCoverData,
        e: e,
        serieTitle: serieTitle,
        hoveredTitleLink: hoveredTitle,
      });
    }

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

  function ajaxLoadImageUrlAndShowPopup(
    forceReload = false,
    element,
    hoveredTitle,
    e,
    external = false,
    targetPage = undefined
  ) {
    const currentEvent = e;

    //console.log(currentEvent)
    //console.log("mouseenter")
    // console.group("ajaxLoadImageUrlAndShowPopup")
    return parseSeriePage(
      element,
      forceReload,
      hoveredTitle,
      currentEvent,
      external,
      targetPage
    ).then(
      function (coverData) {
        if (coverData !== undefined) {
          setCurrentCoverDataAndLoadImage(
            coverData,
            hoveredTitle,
            currentEvent
          );
        }
      },
      function (Error) {
        const elementUrl = element.href;
        console.log(Error);
        let errorMessage = '"(' + Error + ')" failed to fetch ' + elementUrl;
        console.log("errorMessage: " + errorMessage);
        setLinkStateOfSameLinks(element, linkIconEnum.error);
        showPopupLoadingSpinner(
          hoveredTitle,
          hoveredTitle,
          currentEvent,
          errorMessage
        );
      }
    );
    // 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,
    errorText = undefined,
    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(errorText);
    let filename = "";
    console.log("coverData.url: " + coverData.url);

    if (coverData.url !== undefined && coverData.url != "undefined") {
      filename = decodeURIComponent(coverData.url);
    } else {
      filename = "";
    }
    let additionalText = "";
    if (errorText) additionalText = errorText;
    let errorMessage =
      '<div class="containerPadding">browser blocked/has error loading the cover: <br />' +
      filename +
      "<br />" +
      additionalText +
      "</div>";
    if (filename == "") {
      errorMessage =
        '<div class="containerPadding">target site has no coverImage<br />[no image tag found]<br /></div>';
    }
    console.log("errorMessage: " + 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");
  }
  async function loadImageFromBrowser({
    coverData,
    e = undefined,
    serieTitle = 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 = async (error) => {
      let imageCanBeLoaded = checkImageServerState(coverData.url);
      console.log("imageCanBeLoaded result before await: ");
      console.log(imageCanBeLoaded);
      imageCanBeLoaded = await imageCanBeLoaded;
      // console.log("imageCanBeLoaded after await: " + imageCanBeLoaded)
      DEBUG &&
        console.log(
          "imageCanBeLoaded: " +
            imageCanBeLoaded +
            ", coverData.url: " +
            coverData.url
        );
      console.log(
        "imageCanBeLoaded: " +
          imageCanBeLoaded +
          ", coverData.url: " +
          coverData.url
      );
      let errorMessage;
      if (imageCanBeLoaded) {
        errorMessage =
          "image fetching is possible, but image is blocked from loading.<br />Check for example ublock/umatrix or similar if domain is allowed to load images or image sizes exceeds allowed media size";
      } else {
        errorMessage =
          "image could not be loaded. Check in console.log what error state checkImageServerState() produces";
      }
      console.group("img node has loading error");
      console.log(error);
      console.log("errorMessage: " + errorMessage);
      console.groupEnd();
      imageLoadingError(
        coverData,
        errorMessage,
        hoveredTitleLink,
        serieTitle,
        e
      );
    };
    //console.log(coverData)
    if (coverData !== undefined) {
      if (coverData.url !== undefined && coverData.url != "undefined") {
        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,
            "",
            coverData
          );
          //  }
        }
      } else {
        imageLoadingError(
          coverData,
          "coverData has no image",
          hoveredTitleLink,
          serieTitle,
          e
        );
      }
    }

    // console.groupEnd("loadImageFromBrowser")
  }

  function hidePopOver() {
    // popover.style.visibility = "hidden";
    //arrowContainer.style.visibility = "hidden";
    arrowContainer.classList.add("hidePopover");
    popover.classList.add("hidePopover");

    //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";
    //popover.style.opacity="1";
    popover.classList.remove("hidePopover");
    arrowContainer.classList.remove("hidePopover");
    popoverVisible = true;
  }
  function hideOnMouseLeave() {
    //if (!e.target.matches(concatSelector())) return;
    //popover.hide();
    hidePopOver();
  }

  /*
   * get links into ALLSERIENODES and convert this nodearray to array
   *
   */
  //renamed targetNodeArray -> arrayTargetNode to remove compatibility setting in tampermonkey () misdetected? (targetNodeArray.foreach, targetNodeArray.map, targetNodeArray.push)
  function updateSerieNodes(
    arrayTargetNode = [],
    individualLinksToTest,
    forceReload = false,
    external = false
  ) {
    const DEBUG = false;
    if (arrayTargetNode && arrayTargetNode.length > 0) {
      arrayTargetNode.forEach(function (selector) {
        if (
          eventListenerStyle === undefined ||
          eventListenerStyle === null ||
          eventListenerStyle == 0
        ) {
          selector.removeEventListener("mouseleave", hideOnMouseLeave);
          //selector.removeEventListener("mouseenter", mouseEnterPopup);
        }
      });
    }
    let serieLinkNodes = document.querySelectorAll(
      ':not(.digg_pagination) > a[href*="' + individualLinksToTest + '"]' //not(.digg_pagination) > fix to block linking from pagination
    );
    //console.log(serieLinkNodes)
    serieLinkNodes = Array.from(serieLinkNodes);
    let prunedSerieLinkNodes = [];
    if (serieLinkNodes && serieLinkNodes.length > 0) {
      //console.log(serieLinkNodes);
      serieLinkNodes.map(function (el) {
        //console.log(el)
        const elementUrl = el.href;

        //#region if has serieRegex check for  externalLink+serieRegex match
        let hasLinkMatch = false;
        if (external) {
          hasLinkMatch = externalLinkKeys.some((key) => {
            let completeKey = key;
            /*
              console.log("key: " + key);
              console.log(externalLinks[key]);
              console.log(
                'externalLinks["serieRegex"]: ' + externalLinks[key]["serieRegex"]
              );*/
            let indexOfDomain = elementUrl.indexOf(key);
            let characterAfterIndividualPage = elementUrl.slice(
              indexOfDomain + key.length
            ); //get characters after targetAdress to parse ID

            if (indexOfDomain >= 0 && characterAfterIndividualPage.length > 0) {
              DEBUG &&
                console.log(
                  "elementUrl: " +
                    elementUrl +
                    ", key: " +
                    key +
                    ", indexOfDomain on key " +
                    indexOfDomain +
                    ",characterAfterIndividualPage: " +
                    characterAfterIndividualPage
                );

              if (externalLinks[key].serieRegex !== undefined) {
                completeKey = externalLinks[key].serieRegex;
                //console.log("serieRegex regex: " + completeKey)
                /*  const hasMatch =  xhr.finalUrl.match(new RegExp(completeKey));
                  return hasMatch; //performance exclusion regex only used for a few links
                  */
              } else {
                completeKey = defaultSerieRegex;
                //console.log("default regex: " + completeKey)
              }

              let hasMatch = characterAfterIndividualPage.match(
                new RegExp(completeKey)
              );
              if (hasMatch === null || hasMatch.length == 0) {
                if (externalLinks[key].serieRegex2 !== undefined) {
                  hasMatch = characterAfterIndividualPage.match(
                    new RegExp(externalLinks[key].serieRegex2)
                  );
                }
              }
              //console.log(hasMatch)
              return hasMatch !== null && hasMatch.length > 1;
              //return elementUrl.includes(key);
            }

            return false; //no further serieid available
          });
        } else {
          let indexOfDomain = elementUrl.indexOf(INDIVIDUALPAGETEST);
          let characterAfterIndividualPage = elementUrl.slice(
            indexOfDomain + INDIVIDUALPAGETEST.length
          ); //get characters after targetAdress to parse ID
          if (characterAfterIndividualPage.length > 0) hasLinkMatch = true; //internal without serieRegex
        }
        //console.log("hasLinkMatch: " + hasLinkMatch);
        //#endregion
        if (hasLinkMatch) {
          prunedSerieLinkNodes.push(el);
          if (external) {
            el.setAttribute("coverDataExternalTarget", individualLinksToTest);
          }

          setLinkState(el, undefined, forceReload || preloadUrlRequests);
        }
        // console.log(elementUrl)
      });
    }
    arrayTargetNode.push(...prunedSerieLinkNodes);

    //console.log(ALLSERIENODES)
    /*
        console.log(ALLSERIENODES)

        const sliceItemCount = 100;
        if (ALLSERIENODES.length > sliceItemCount) {
            ALLSERIENODES = ALLSERIENODES.slice(0, sliceItemCount);
        }
        console.log(ALLSERIENODES)
        */
  }
  function switchShowIconNextToLink() {
    showIconNextToLink = !showIconNextToLink;
    GM_setValue("showIconNextToLink", showIconNextToLink);
    preloadCoverData();
    updateCurrentPopupContent();
  }
  function switchDetailsAndUpdatePopup() {
    const DEBUG = false;

    DEBUG && console.group("switchDetailsAndUpdatePopup");
    changeToNewDetailStyle();
    //console.log(currentCoverData)
    DEBUG && console.log("switchDetails refreshPopup");

    updateCurrentPopupContent();

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

      updateCurrentPopupContent();
    }
  }
  function switchShowReadingListIconAndTitle() {
    useReadingListIconAndTitle = !useReadingListIconAndTitle;
    GM_setValue("useReadingListIconAndTitle", useReadingListIconAndTitle);
    updateCurrentPopupContent();
  }
  function updateCurrentPopupContent() {
    const DEBUG = false;
    if (currentCoverData !== undefined) {
      DEBUG && console.log(currentCoverData);
      loadImageFromBrowser({
        coverData: currentCoverData,
        e: currentPopupEvent,
        serieTitle: currentTitelHover,
        hoveredTitleLink: currentTitelHover,
      });
    } else if (currentTitelHover !== undefined) {
      //currentCoverData not yet set
      showPopupLoadingSpinner(
        currentTitelHover,
        currentTitelHover,
        currentPopupEvent
      );
    }
  }
  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();
    //console.log("changeToNewDetailStyle - setPopoverHeight();");
    //setPopoverHeight();
    updatePopoverSize();
  }

  //#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) {
      e.preventDefault();
      const target = e.target;
      let Href = target.href; // element.attr('href');
      let coverDataExternalTarget = target.getAttribute(
        "coverDataExternalTarget"
      );
      //console.log("coverDataExternalTarget: " + coverDataExternalTarget);
      if (Href && coverDataExternalTarget === undefined) {
        //TODO double check if needed
        //no preloadUrlRequests happend
        /*
          const externalLinkKeys = Object.keys(externalLinks);
          const isExternal = externalLinkKeys.some((key) => Href.includes(key));

          if (isExternal) {
            target.setAttribute("coverDataExternalTarget", key);
            coverDataExternalTarget = key;
          } else target.setAttribute("coverDataExternalTarget", null);
            */
      }
      DEBUG &&
        console.log(
          "Href: " + Href + ", INDIVIDUALPAGETEST: " + INDIVIDUALPAGETEST
        );
      if (
        Href &&
        (Href.includes(INDIVIDUALPAGETEST) ||
          (coverDataExternalTarget !== undefined &&
            coverDataExternalTarget !== null &&
            Href.includes(coverDataExternalTarget)))
      ) {
        //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
        const wasOverDifferentLink = currentTitelHover != previousTitelHover;
        if (wasOverDifferentLink) {
          resetAutoScroll();
          autoScrollCoverData = true;
        }

        if (currentTitelHover != undefined) {
          previousTitelHover = currentTitelHover;
        }
        currentPopupEvent = e;
        //console.log(serieTitle)
        //console.log(Href)

        //console.log(currentCoverData)
        //console.log("currentTitelHover: " + currentTitelHover)
        const external =
          coverDataExternalTarget !== undefined &&
          coverDataExternalTarget !== null;
        let targetPage;
        //console.log("external: " + external);
        //console.log("coverDataExternalTarget: " + coverDataExternalTarget);
        if (external) targetPage = coverDataExternalTarget;
        //console.log("targetPage: " + targetPage);
        let mainSerieHref = getLinkToSeriePage(Href, targetPage);
        let hasCoverData = GM_getCachedValue(mainSerieHref);
        if (!hasCoverData || forceReload) {
          setLinkStateOfSameLinks(
            target,
            linkIconEnum.popupLoading,
            forceReload
          );
        } else {
          if (forceReload) {
            setLinkStateOfSameLinks(target, undefined, forceReload);
          }
        }
        ajaxLoadImageUrlAndShowPopup(
          forceReload,
          target, //Href
          currentTitelHover,
          e,
          external,
          targetPage
        );
      }
    }
    DEBUG && console.groupEnd("mouseEnterPopup");
  }
  function forceReload(forceReload = true) {
    mouseEnterPopup(currentPopupEvent, forceReload);
  }

  function updatePopoverSize() {
    //console.log("updatePopoverSize - setPopoverHeight();");
    //setPopoverHeight(); //happens after updateCurrentPopupContent update
    setPopoverWidth();

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

    updateCurrentPopupContent();
  }

  function showAlternativeNamesList() {
    if (!showAlternativeNames || showAlternativeNames == "") {
      if (currentCoverData !== undefined) updateCurrentPopupContent();
    } else {
      if (
        currentCoverData.alternativeNames &&
        currentCoverData.alternativeNames != ""
      ) {
        let alternativeNames = "";
        alternativeNames = currentCoverData.alternativeNames;

        popoverContent.innerHTML =
          '<div id="coverPreviewContentAutoScroll" class="popoverContent ' +
          STYLESHEETHIJACKFORBACKGROUND +
          " " +
          mediumTextStyle +
          '" style="text-align:start !important; width:100%;"><b>Alternative Titles:</b><br />' +
          alternativeNames +
          "</div>";
        if (currentCoverData !== undefined) popupPos(currentPopupEvent);
        autoScrollData("coverPreviewContentAutoScroll");
      }
    }
  }

  function showHotkeyList() {
    if (!showHotkeys) {
      if (currentCoverData !== undefined) {
        loadImageFromBrowser({
          coverData: currentCoverData,
          e: currentPopupEvent,
          serieTitle: currentTitelHover,
          hoveredTitleLink: currentTitelHover,
        });
      }
    } else {
      popoverContent.innerHTML =
        '<div id="coverPreviewContentAutoScroll" class="popoverContent ' +
        STYLESHEETHIJACKFORBACKGROUND +
        " " +
        mediumTextStyle +
        '" style="text-align:start !important">[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 4]: Pause/unpause autoscrolling coverData<br/>
          [Key 5]: Reload coverdata of hovered link<br />
          [Key 6]: Reload all links of current Page<br/>
          [Key 9]: Clear all cover data info<br />
          [Key A]: If available will show <b>a</b>lternative titles during holding of key A<br />
          [Key I]: Toggle coverPreview pre/loading state <b>i</b>con displaying next to link<br />
          [Key P]: Switch displaying of readinglist icon of <b>p</b>ersonal lists<br />
          [Key H]: Show this <b>h</b>otkey list during holding of key H<br />
          </div>`;

      if (currentCoverData !== undefined) popupPos(currentPopupEvent);
      autoScrollData("coverPreviewContentAutoScroll");
    }
  }

  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();
            break;
          case "6":
            {
              const _forceReload = true;
              preloadCoverData(_forceReload);
            }
            break;
          case "9":
            resetDatabase();
            preloadCoverData();
            forceReload();
            break;
          case "2":
            switchTagsDescriptionAndUpdatePopup();

            resetAutoScroll();
            autoScrollCoverData = true;
            autoScrollData();
            autoScrollData("coverPreviewContentAutoScroll");
            break;
          case "3":
            showSmaller = !showSmaller;
            GM_setValue("showSmaller", showSmaller);
            updatePopoverSize();
            hasChangedStyle = true;
            break;
          case "4":
            autoScrollCoverData = !autoScrollCoverData;
            if (autoScrollCoverData) {
              autoScrollData();
              autoScrollData("coverPreviewContentAutoScroll");
            }
            break;
          case "h":
            showHotkeys = true;
            showHotkeyList();
            break;
          case "a":
            showAlternativeNames = true;
            showAlternativeNamesList();
            break;
          case "p":
            switchShowReadingListIconAndTitle();
            updateCurrentPopupContent();
            //forceReload();
            break;
          case "i":
            switchShowIconNextToLink();
            break;
        }
      }
    }
  }
  function releaseKey(event) {
    const key = event.key;
    //console.log(pressedKeys)
    pressedKeys.splice(pressedKeys.indexOf(key), 1);
    if (event.key == "h") {
      showHotkeys = false;
      showHotkeyList();
    }
    if (event.key == "a") {
      showAlternativeNames = false;
      showAlternativeNamesList();
    }
    //console.log(pressedKeys)
  }

  function prepareEventListener() {
    window.addEventListener("blur", hidePopOver);
    window.addEventListener("keypress", reactToKeyPressWhenPopupVisible); //keypress gets repeated during keydown
    window.addEventListener("keyup", releaseKey);

    if (
      targetContainerIDArrayToObserve &&
      targetContainerIDArrayToObserve.length > 0
    ) {
      for (let i = 0; i < targetContainerIDArrayToObserve.length; i++) {
        let targetNodeList = document.getElementById(
          targetContainerIDArrayToObserve[i]
        ); //forum.novelupdates.com quickedit change. ajax content change
        if (targetNodeList) {
          observer.observe(targetNodeList, config);
        }
      }
    }
    let themeSelector = document.getElementById("wi_themes");
    if (themeSelector)
      themeSelector.addEventListener("change", updateArrowColor);

    window.onunload = function () {
      window.removeEventListener("blur", hidePopOver);
      window.removeEventListener("keypress", reactToKeyPressWhenPopupVisible);
      window.addEventListener("keyup", releaseKey);
      popover.removeEventListener("mouseleave", hideOnMouseLeave);
      if (themeSelector)
        themeSelector.removeEventListener("change", updateArrowColor);
      //possible memoryleaks?
      ALLSERIENODES = [];
      updateSerieNodes(ALLSERIENODES, INDIVIDUALPAGETEST);
      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

  //#region mangaDex api
  function getMangaDexNamedTag(tags, type = "Genre") {
    const DEBUG = false;
    DEBUG && console.group("getMangaDex" + type);
    let namedTags = "";
    DEBUG && console.log(mangaDexTAGS);
    DEBUG && console.log(tags);

    DEBUG && console.log("tags.length: " + tags.length);
    if (tags && tags.length > 0) {
      for (let i = 0; i < tags.length; i++) {
        let searchTag = tags[i];
        //console.log(searchTag)
        const tag = mangaDexTAGS[searchTag];
        if (tag.group == type) {
          //console.log(tag)
          namedTags += " " + tag.name;
        }
      }
    }

    DEBUG && console.groupEnd();
    DEBUG && console.log(namedTags);
    return namedTags;
  }
  async function requestDataFromAPIpostCall(
    apiPoint,
    postData,
    hasDataContainer = undefined
  ) {
    const DEBUG = false;
    let PromiseResult = new Promise(async function (resolve, reject) {
      function onLoad(xhr) {
        DEBUG && console.log(xhr);
        //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
        switch (true) {
          case xhr.status == 304:
            console.log("xhr.status == 304: getting data from browser cache");
          //fall through to status==200 ok
          case xhr.status >= 200 && xhr.status < 399:
            {
              DEBUG &&
                console.group("requestDataFromAPIpostCall onLoad: " + apiPoint);
              DEBUG && console.log(xhr);
              let tempJSON = JSON.parse(xhr.responseText);
              let apiData;
              DEBUG && console.log(tempJSON);
              if ((tempJSON.status = "OK")) {
                if (hasDataContainer) apiData = tempJSON[hasDataContainer];
                else apiData = tempJSON;
              }
              DEBUG && console.log(apiData);
              return resolve(apiData);
            }
            break;
          default:
            /*
              console.group("error in getCoverDataFromUrl");
              console.log(xhr);
              console.groupEnd();*/
            return reject(rejectErrorStatusMessage(xhr));
        }
      }
      function onError(error) {
        console.log(error);
        const err = new Error(
          "GM_xmlhttpRequest could not load " +
            apiPoint +
            "; script is not compatible or url does not exists."
        );
        console.log(err);
        return reject(err);
      }

      GM_xmlhttpRequest({
        method: "POST",
        data: JSON.stringify(postData),
        url: apiPoint,
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json",
        },
        dataType: "json",
        contentType: "application/json",
        overrideMimeType: "application/json",
        onload: onLoad,
        onerror: onError,
      });

      return undefined; //reject("status error")
    });
    PromiseResult = await PromiseResult;

    return PromiseResult;
  }
  async function checkImageServerState(url) {
    const DEBUG = false;
    let PromiseResult = new Promise(async function (resolve, reject) {
      function onLoad(xhr) {
        DEBUG && console.log("checkImageServerState");
        DEBUG && console.log(xhr);
        DEBUG && console.log("xhr.status: " + xhr.status);
        switch (true) {
          case xhr.status == 304:
            console.log(
              "xhr.status == 304: getting data from browser cache for " +
                xhr.finalUrl
            );
          case xhr.status >= 200 && xhr.status < 399: {
            DEBUG && console.log("no error");
            return resolve(true);
          }
          default:
            /*
              console.group("error in getCoverDataFromUrl");
              console.log(xhr);
              console.groupEnd();*/
            console.log(rejectErrorStatusMessage(xhr));
            return reject(false);
        }
      }
      function onError(error) {
        console.log(error);
        const err = new Error(
          "checkImageServerState() could not load " +
            url +
            "; script is not compatible or url does not exists."
        );
        console.log(err);
        return reject(false);
      }
      GM_xmlhttpRequest({
        method: "HEAD",
        url: url,
        onload: onLoad,
        onerror: onError,
        // onreadystatechange,
        // onprogress
      });
      return "error or skipped processing GM_xmlhttpRequest() in checkImageServerState()"; //reject("status error")
    });
    //  if(PromiseResult!=undefined)
    {
      PromiseResult = await PromiseResult;
      return PromiseResult;
    }
    //  return "checkImageServerState function error";
  }
  async function getDataFromAPI(
    apiPoint,
    { hasDataContainer = undefined, responsetype = "json" } = {}
  ) {
    const DEBUG = false;
    DEBUG && console.log("responsetype", responsetype);
    let PromiseResult = new Promise(async function (resolve, reject) {
      function onLoad(xhr) {
        DEBUG && console.log("xhr", xhr);
        //console.log(xhr.responseHeaders)
        DEBUG && console.log("xhr.status: " + xhr.status);
        //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)

        switch (true) {
          case xhr.status == 304:
            DEBUG && console.log(
              "xhr.status == 304: getting data from browser cache for " +
                xhr.finalUrl
            );
          //fall through to status==200 ok
          case xhr.status >= 200 && xhr.status < 399: {
            DEBUG && console.group("getDataFromAPI onLoad: " + apiPoint);
            //DEBUG && console.log(xhr);
            let apiData;
            DEBUG && console.log("responsetype", responsetype);
            switch (responsetype) {
              case "json":
                {
                  let tempJSON = JSON.parse(xhr.responseText);
                  DEBUG && console.log("tempJSON", tempJSON);
                  if ((tempJSON.status = "OK")) {
                    if (hasDataContainer) apiData = tempJSON[hasDataContainer];
                    else apiData = tempJSON;
                  }
                }
                break;
              case "Document":
                {
                  const domDocument = xhr.response;
                  //tampermonkey 4.12.6130 seems to deprecate some old api? document response is now undefined
                  //only responseXML would be usable
                  apiData = domDocument;
                }
                break;
              case "text":
                {
                  const domText = xhr.responseText;
                  let parser = new DOMParser()
                  const domDocument = parser.parseFromString(
                    domText,
                    "text/html"
                  );
                  apiData = domDocument;
                }
                break;
              default:
                const domDocument = xhr.responseText;
                apiData = domDocument;
                break;
            }

            DEBUG && console.log("apiData", apiData);
            if (apiData !== undefined) return resolve(apiData);
            else
              return reject(
                "no apiData error for responsetype: " + responsetype
              );
          }

          default:
            /*
              console.group("error in getCoverDataFromUrl");
              console.log(xhr);
              console.groupEnd();*/
            return reject(rejectErrorStatusMessage(xhr));
        }
      }
      function onError(error) {
        console.log(error);
        const err = new Error(
          "GM_xmlhttpRequest could not load " +
            apiPoint +
            "; script is not compatible or url does not exists."
        );
        console.log(err);
        return reject(err);
      }
      function onreadystatechange(stateChangeResponse) {
        console.log(stateChangeResponse);
      }
      function onprogress(progressResponse) {
        //console.log(progressResponse)
      }

      GM_xmlhttpRequest({
        method: "GET",
        //overrideMimeType:'text/html;',
        responseType: responsetype,
        url: apiPoint,
        onload: onLoad,
        onerror: onError,
        // onreadystatechange,
        // onprogress
      });

      return undefined; //reject("status error")
    });
    PromiseResult = await PromiseResult;

    return PromiseResult;
  }

  async function getCoverDataFromMangaDex(id) {
    const DEBUG = false;
    if (id) {
      if (mangaDexTAGS === undefined || mangaDexTAGS === null) {
        //only needed one time per session
        mangaDexTAGS = getDataFromAPI("https://mangadex.org/api/v2/tag", {
          hasDataContainer: "data",
        }); //getAllMangaDexTags
      }
      DEBUG && console.log(mangaDexTAGS);

      let mangaDexData = getDataFromAPI(
        "https://mangadex.org/api/v2/manga/" + id,
        { hasDataContainer: "data" }
      );
      DEBUG && console.log(mangaDexData);

      let serieChapters = getDataFromAPI(
        "https://mangadex.org/api/v2/manga/" + id + "/chapters",
        { hasDataContainer: "data" }
      ); //await getMangaDexChapterCount(id);

      DEBUG && console.log(serieChapters);
      [mangaDexTAGS, mangaDexData, serieChapters] = await Promise.all([
        mangaDexTAGS,
        mangaDexData,
        serieChapters,
      ]);
      let serieGenre = getMangaDexNamedTag(mangaDexData.tags, "Genre");
      let serieTags = getMangaDexNamedTag(mangaDexData.tags, "Theme");
      DEBUG && console.log(mangaDexTAGS);
      DEBUG && console.log(serieChapters);
      if (serieChapters && serieChapters.chapters) {
        serieChapters = serieChapters.chapters.length;
      }
      DEBUG && console.log("serieChapters: " + serieChapters);
      let status;
      DEBUG && console.log(mangaDexData);
      switch (mangaDexData.publication.status) {
        case 1:
          status = "Ongoing";
          break;
        case 2:
          status = "Completed";
          break;
      }

      let cData = Object.assign({}, emptyCoverData);
      cData.url = mangaDexData.mainCover;
      cData.title = mangaDexData.title;
      cData.alternativeNames = mangaDexData.altTitles.join("<br /> ");
      cData.votes = mangaDexData.rating.bayesian.toString();
      cData.status = status;
      cData.chapters = serieChapters.toString();
      cData.genre = serieGenre;
      cData.showTags = serieTags;
      cData.description = mangaDexData.description;
      return cData;
    }
    return undefined;
  }
  //#endregion mangadex api
  //#region http://api.tvmaze.com/
  async function getCoverDataFromTVmaze(id) {
    const DEBUG = false;
    if (id) {
      let apiData = getDataFromAPI("http://api.tvmaze.com/shows/" + id);
      DEBUG && console.log(apiData);
      let serieAlternativeNames = getDataFromAPI(
        "http://api.tvmaze.com/shows/" + id + "/akas"
      );
      let episodes = getDataFromAPI(
        "http://api.tvmaze.com/shows/" + id + "/episodes"
      );
      [apiData, serieAlternativeNames, episodes] = await Promise.all([
        apiData,
        serieAlternativeNames,
        episodes,
      ]);
      DEBUG && console.log(serieAlternativeNames);
      if (serieAlternativeNames !== undefined) {
        serieAlternativeNames = serieAlternativeNames
          .map((e) => e.name)
          .join("<br /> ");
      }
      ///console.log(serieAlternativeNames);
      let serieEpisodeCount;
      if (episodes && episodes.length) {
        serieEpisodeCount = episodes.length.toString();
      }
      let targetImageUrl;
      if (apiData.image.medium) targetImageUrl = apiData.image.medium;
      else targetImageUrl = apiData.image.original;
      let serieRating;
      if (apiData.rating && apiData.rating.average) {
        serieRating = apiData.rating.average.toString();
      }
      /*
        let cData = {
          url: targetImageUrl,
          title: apiData.name,
          alternativeNames: serieAlternativeNames,
          votes: serieRating,
          //status: apiData.status, //status property value is overwritten by GM_xmlhttpRequest
          chapters: serieEpisodeCount,
          genre: apiData.genres,
          description: apiData.summary,
        };
        */
      let cData = Object.assign({}, emptyCoverData);
      cData.url = targetImageUrl;
      cData.title = apiData.name;
      cData.alternativeNames = serieAlternativeNames;
      cData.votes = serieRating;
      //cData.status = status; //status property value is overwritten by GM_xmlhttpRequest
      cData.chapters = serieEpisodeCount;
      cData.genre = apiData.genres;
      //cData.showTags = serieTags;
      cData.description = apiData.summary;
      DEBUG && console.log(cData);
      return cData;
    }
    return undefined;
  }
  //#endregion http://api.tvmaze.com/
  //#region https://www.wlnupdates.com/api-docs
  async function getCoverDataFromWLNupdates(linkID) {
    const DEBUG = false;
    if (linkID) {
      let apiData = await requestDataFromAPIpostCall(
        "https://www.wlnupdates.com/api",
        { id: linkID, mode: "get-series-id" },
        "data"
      );
      DEBUG && console.log(apiData);
      let imagelink;
      if (apiData.covers.length > 0) imagelink = apiData.covers[0].url;

      let serieTitle = apiData.title;
      let serieAlternativeNames = apiData.alternatenames.join("<br />");
      let serieVotes;
      let serieStatus;
      if (apiData.ratin_count > 0) serieVotes = apiData.rating.avg;
      if (apiData.most_recent) {
        serieStatus = "most_recent: " + apiData.most_recent;
      }
      //console.log(apiData.releases);
      //console.log("apiData.releases: " + apiData.releases.length);

      let serieChapters;
      if (apiData && apiData.releases && apiData.releases.length) {
        serieChapters = apiData.releases.length;
      }
      if (apiData.type) serieChapters += " (" + apiData.type + ")";
      let serieGenre = apiData.genres.map((e) => e.genre).join(", ");
      let serieShowtags = apiData.tags.map((e) => e.tag).join(", ");
      let serieDescription = apiData.description;

      let cData = Object.assign({}, emptyCoverData);
      cData.url = imagelink;
      cData.title = serieTitle;
      cData.alternativeNames = serieAlternativeNames;
      cData.votes = serieVotes;
      cData.status = serieStatus;
      cData.chapters = serieChapters;
      cData.genre = serieGenre;
      cData.showTags = serieShowtags;
      cData.description = serieDescription;
      DEBUG && console.log(cData);
      return cData;
    }

    return undefined;
  }
  //#endregion https://www.wlnupdates.com/api-docs
  //#region getCoverData from parsing document of url
  async function getCoverDataFromParsingTargetUrl(
    elementUrl,
    targetPage,
    isExternal = false
  ) {
    const DEBUG = false;
    if (targetPage) {
      DEBUG &&
        console.log(
          "getCoverDataFromParsingTargetUrl - elementUrl: " + elementUrl
        );
      let domDocument = await getDataFromAPI(elementUrl, {
        responsetype: "text",
      });
      DEBUG && console.log(domDocument);
      //getData from parsed site by query selectors

      let temp;
      let imagelink;
      let containerNumber = 0;
      let serieTitle;
      let serieAlternativeNames;
      let serieVotes;
      let serieStatus;
      let serieChapters;
      let serieGenre;
      let serieShowtags;
      let serieDescription;
      let serieReadingListIcon, serieReadingListTitle;
      //#region general selectors for internal and external sites
      DEBUG && console.group("internal/external link selectors");
      DEBUG && console.log("targetPage: " + targetPage);
      //console.log(domDocument);
      //external links

      //console.log(targetPage);
      temp = domDocument.querySelectorAll(targetPage.IMAGELINKCONTAINERS);
      DEBUG && console.log(temp);
      if (targetPage.CONTAINERNUMBER) {
        containerNumber = targetPage.CONTAINERNUMBER;
      }

      imagelink = temp[containerNumber];
      //console.log(imagelink)
      serieTitle = domDocument.querySelector(targetPage.seriePageTitle);
      serieAlternativeNames = domDocument.querySelector(
        targetPage.serieAlternativeNames
      );
      serieVotes = domDocument.querySelector(targetPage.seriePageVotes);
      serieStatus = domDocument.querySelector(targetPage.seriePageStatus);
      serieChapters = domDocument.querySelector(targetPage.seriePageChapters);
      serieGenre = domDocument.querySelector(targetPage.seriePageGenre);
      serieShowtags = domDocument.querySelector(targetPage.seriePageTags);

      serieDescription = domDocument.querySelector(
        targetPage.seriePageDescription
      );

      serieTitle = tryToGetTextContent(
        serieTitle,
        targetPage.seriePageTitle,
        "seriePageTitle"
      );
      serieAlternativeNames = tryToGetTextContent(
        serieAlternativeNames,
        targetPage.serieAlternativeNames,
        "serieAlternativeNames"
      );
      //console.log(targetPage.seriePageVotes)
      serieVotes = tryToGetTextContent(
        serieVotes,
        targetPage.seriePageVotes,
        "seriePageVotes"
      );
      //console.log(serieVotes)
      serieStatus = tryToGetTextContent(
        serieStatus,
        targetPage.seriePageStatus,
        "seriePageStatus"
      );
      serieChapters = tryToGetTextContent(
        serieChapters,
        targetPage.seriePageChapters,
        "seriePageChapters"
      );
      //console.log(targetPage.seriePageGenre)
      serieGenre = tryToGetTextContent(
        serieGenre,
        targetPage.seriePageGenre,
        "seriePageGenre"
      );
      //console.log(serieGenre)
      serieShowtags = tryToGetTextContent(
        serieShowtags,
        targetPage.seriePageTags,
        "seriePageTags"
      );
      serieDescription = tryToGetTextContent(
        serieDescription,
        targetPage.seriePageDescription,
        "seriePageDescription"
      );
      DEBUG && console.groupEnd("internal/external link selectors");
      //#endregion general selectors for internal and external sites
      //#region internal extra selectors
      DEBUG && console.group("internal extra selectors");
      if (!isExternal) {
        //hasInternalTargetPage- > targetPage

        //console.log(serieReadingListTitle)
        if (useReadingListIconAndTitle && targetPage.serieReadingListIcon) {
          serieReadingListIcon = domDocument.querySelector(
            targetPage.serieReadingListIcon
          );
        }
        //console.log("hasInternalTargetPage.serieReadingListTitle: " + hasInternalTargetPage.serieReadingListTitle)
        if (useReadingListIconAndTitle && targetPage.serieReadingListTitle) {
          serieReadingListTitle = domDocument.querySelector(
            targetPage.serieReadingListTitle
          );
        }
        //console.log(targetPage.serieReadingListTitle)
        //console.log(serieReadingListTitle)
        serieReadingListTitle = tryToGetTextContent(
          serieReadingListTitle,
          targetPage.serieReadingListTitle,
          "serieReadingListTitle"
        );
        if (serieReadingListTitle == "Add series to...") {
          serieReadingListTitle = undefined;
        }
        //console.log(serieReadingListIcon)
        if (
          serieReadingListIcon !== undefined &&
          serieReadingListIcon !== null &&
          serieReadingListIcon.tagName == "IMG"
        ) {
          serieReadingListIcon = serieReadingListIcon.getAttribute("src");
          //console.log("serieReadingListIcon: " + serieReadingListIcon)
        }
        if (
          serieReadingListIcon === null ||
          serieReadingListIcon ==
            "//www.novelupdates.com/wp-content/themes/ndupdates-child/js/selectico/addme.png"
        ) {
          serieReadingListIcon = undefined;
        }
        //console.log("serieReadingListTitle: " + serieReadingListTitle)
      }
      DEBUG && console.groupEnd("internal extra selectors");
      //#endregion internal extra selectors

      let cData = Object.assign({}, emptyCoverData);
      cData.url = imagelink;
      cData.title = serieTitle;
      cData.alternativeNames = serieAlternativeNames;
      cData.description = serieDescription;
      cData.status = serieStatus;
      cData.chapters = serieChapters;
      cData.genre = serieGenre;
      cData.showTags = serieShowtags;
      cData.votes = serieVotes;
      cData.readingListIcon = serieReadingListIcon;
      cData.readingListTitle = serieReadingListTitle;
      DEBUG && console.log(cData);
      return cData;
    }
    return undefined;
  }
  //#endregion getCoverData from parsing document of url

  function main() {
    const DEBUG = false;
    //console.log(window.location)
    currentOpenedUrl = window.location.href;
    //console.log("preloadUrlRequests: " + preloadUrlRequests)
    for (let i = 0; i < deactivatePreloadUrlRequestOnUrls.length; i++) {
      //no need to check on each link hover
      if (deactivatePreloadUrlRequestOnUrls[i] == currentOpenedUrl) {
        preloadUrlRequests = false;
      }
    }
    //console.log("preloadUrlRequests: " + preloadUrlRequests)
    DEBUG && console.log("started main function of coverPreview");
    //#region get greasemonkey settings for popup
    DEBUG && console.log("before starting checkDataVersion");
    checkDataVersion();
    showDetails = GM_getValue("showDetails");
    showDescription = GM_getValue("showDescription");
    showSmaller = GM_getValue("showSmaller");
    useReadingListIconAndTitle = GM_getValue("useReadingListIconAndTitle");
    showIconNextToLink = GM_getValue("showIconNextToLink");
    //deactivatePreloadUrlRequestOnUrls = GM_getValue("deactivatePreloadUrlRequestOnUrls");
    if (showDetails === undefined || showDetails == "undefined") {
      showDetails = false;
    }
    if (showDescription === undefined || showDescription == "undefined") {
      showDescription = false;
    }
    if (showSmaller === undefined || showSmaller == "undefined") {
      showSmaller = false;
    }
    if (
      useReadingListIconAndTitle === undefined ||
      useReadingListIconAndTitle == "undefined"
    ) {
      useReadingListIconAndTitle = false;
    }
    if (showIconNextToLink === undefined || showIconNextToLink == "undefined") {
      showIconNextToLink = defaultshowIconNextToLink;
    }
    //#endregion

    DEBUG && console.log("before starting setStyleClasses");
    addStyles();
    setStyleClasses();
    DEBUG && console.log("before starting createPopover");
    createPopover();

    DEBUG && console.log("before starting hidePopOver");
    //#region preset needed for older browser

    //#endregion
    //hidePopOver();
    /*
      if (showSmaller) {
        //console.log("show smaller style");
        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);

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