novelupdates Cover Preview

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

当前为 2020-12-26 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// https://greasyfork.org/scripts/26439-novelupdates-cover-preview/
// @name        novelupdates Cover Preview
// @namespace   somethingthatshouldnotclashwithotherscripts
// @include     https://www.novelupdates.com/*
// @include     http://www.novelupdates.com/*
// @include     https://forum.novelupdates.com/*
// @include     http://forum.novelupdates.com/*
// @version     2.4.7
// @description Previews covers in novelupdates.com when hovering over hyperlinks that lead to novelupdates seriepages and a few external pages.
// @author      SZ
// @inject-into content
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @run-at   	  document-end
// @license     http://creativecommons.org/licenses/by-nc-sa/4.0/
// ==/UserScript==

//console.log("cover preview start");
"use strict";
//#region frontend settings
const MAXCACHEAGE = 90 * 24 * 60 * 60 * 1000; // Max Age before Cached data of serieinfo gets overridden with current data. Max Age is 90 days in milliseconds  //days * h * min  * sec * ms
const DEFAULTTITLEBACKGROUNDCOLOR = "#2c3e50"; //if no hijack class style available use plain color
const DEFAULTBACKGROUNDCOLOR = "#ccc"; //if no hijack class style available use plain color
let STYLESHEETHIJACKFORBACKGROUND = " .l-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/": {
    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,
  }
}
*/
//test alle 2 sekunden
const defaultRateLimitQueryAfterSeconds = 10; //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
    added custom function getMangaDexIndexData() and additional functions for 
      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
      exception using public mangadex api
      */
  },
  /* http://www.tvmaze.com/api
    added custom function getTVmazeShowData() and additional functions for 
      alternative names http://api.tvmaze.com/shows/(id)/akas
      episode counts http://api.tvmaze.com/shows/(id)/episodes

    */
  "www.tvmaze.com/shows/": {
    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 rateLimitTimeInSeconds/rateLimitCount
    //if incomplete will use defaultRateLimitQueryAfterSeconds
    serieRegex: "([0-9]+)(?!^/)", //(IDNumber)(not followed by a cahracter besides a slash)
    //([0-9]+)[\/]?.*
  },
  //#endregion
  "www.scribblehub.com/series/": {
    serieRegex: "([0-9]+)([/]+.*)*",
    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-]+)([/]+.*)?",
    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/": {
    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
    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/": {
    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]+)-.*",
    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",
  },
  "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 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;
//#endregion
//console.log("after variable settings");
//console.log(this.location)
//console.log(this.location.href)

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

//#region helper functions

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

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

    const measuredTimedifference = currentTime - coverData.cachedTime;
    if (measuredTimedifference < MAXCACHEAGE) {
      result = {
        url: coverData.url,
        title: coverData.title,
        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");
  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);
//#endregion
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 showBorderArrowBox(
  event,
  arrowWidth = 20,
  upside = false,
  isExternal = false
) {
  const DEBUG = false;
  /*
  - getPopover Rect
  - generate circleBox
  - generate arrow
  - rotate arrow to target
*/
  const nativElement = event.target;
  let targetElement = nativElement;
  let tableElement = false;
  if (
    nativElement.tagName == "TD" ||
    nativElement.parentElement.tagName == "TD"
  ) {
    tableElement = true;
  }
  const elementLinkHasNoText = targetElement.textContent.length == 0;
  //elementLinkHasNoText
  if (isOnIndex || elementLinkHasNoText || tableElement) {
    targetElement = nativElement.parentElement; //get container element/table cell
  }
  /*
  if(elementLinkHasNoText){
    targetElement = nativElement.parentElement.parentElement;
    console.log(targetElement);
  }
  */

  let targetRect = targetElement.getBoundingClientRect();
  const targetLineRects = targetElement.getClientRects();
  let firstLineRect, lastLineRect;
  DEBUG && console.log(targetRect);
  DEBUG && console.log(targetLineRects);

  if (targetLineRects.length > 0) {
    targetRect = targetLineRects[0];
    firstLineRect = targetLineRects[0];
    lastLineRect = targetLineRects[targetLineRects.length - 1];
  }
  //if (targetLineRects.length > 1) multiLine = true;

  if (lastLineRect != firstLineRect) targetRect = lastLineRect;
  if (!upside) {
    if (firstLineRect) targetRect = firstLineRect;
  }
  let halfWidth = arrowWidth / 2;
  let popoverRect = popover.getBoundingClientRect();
  DEBUG && console.log(popoverRect);
  DEBUG && console.log(targetRect);
  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 arrowContainerStart =
    '<div style="background: lightblue;border-radius: 50%;width: ' +
    "100%" +
    ";height: " +
    "100%" +
    ';">';
  let posY = 0;
  let rotateArrowInDegree = 0;
  let externalStyle = "";
  if (isExternal) externalStyle = "isExternalContentArrow ";
  //|| elementLinkHasNoText
  if (isOnIndex || tableElement) {
    DEBUG && console.log("index/container layout");
    posY = targetRect.top - popoverRect.top;
    if (posY < 0) posY = 0;
    const maxY = popoverRect.height - arrowWidth; //height-arrowHeight-offsetCenter
    if (posY > maxY) posY = maxY;
    arrowContainer.style.left =
      scrollPosX + popoverRect.left - halfWidth + "px";
    arrowContainer.style.top = scrollPosY + popoverRect.top + posY + "px";

    rotateArrowInDegree =
      pointDirection(
        popoverRect.left,
        popoverRect.top + posY + arrowWidth,
        targetRect.right - arrowWidth,
        targetRect.top + arrowWidth
      ) - 45;

    //width:100%;height:100%;
    arrowContainer.innerHTML =
      arrowContainerStart +
      '<div class="' +
      externalStyle +
      'arrowCSS" style="width:' +
      arrowWidth +
      "px;height:" +
      arrowWidth +
      "px;left:" +
      halfWidth +
      "px;top:" +
      halfWidth +
      "px;transform: rotate(" +
      rotateArrowInDegree +
      'deg);"></div></div>';
  } else {
    DEBUG && console.log("under/over link layout");
    //#region horizontal

    //#region targetPosX
    // let targetX = targetRect.right;
    let diffXTarget = targetRect.right - popoverRect.left;

    if (diffXTarget - halfWidth < 0) diffXTarget = halfWidth;
    let targetXSourcePosition = targetRect.right - diffXTarget - halfWidth;
    if (diffXTarget > targetRect.width) targetXSourcePosition = targetRect.left;
    //#endregion targetPosX

    //#region popoverPosX
    let popupSourceX = popoverRect.left - halfWidth;

    if (diffXTarget + halfWidth > 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
    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%;
      arrowContainer.innerHTML =
        arrowContainerStart +
        '<div class="' +
        externalStyle +
        'arrowCSS" style="width:' +
        arrowWidth +
        "px;height:" +
        arrowWidth +
        "px;left:" +
        halfWidth +
        "px;top:" +
        halfWidth +
        "px;transform: rotate(" +
        rotateArrowInDegree +
        'deg);"></div></div>';
    } 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%;
      arrowContainer.innerHTML =
        arrowContainerStart +
        '<div class="' +
        externalStyle +
        'arrowCSS" style="width:' +
        arrowWidth +
        "px;height:" +
        arrowWidth +
        "px;left:" +
        halfWidth +
        "px;top:" +
        halfWidth +
        "px;transform: rotate(" +
        rotateArrowInDegree +
        'deg);"></div></div>';
    }
    //#endregion
  }
  DEBUG && console.log("posY: " + posY);
  // rotateArrowInDegree+=-135; //https://www.w3schools.com/howto/howto_css_arrows.asp
  DEBUG && console.log("rotateArrowInDegree: " + rotateArrowInDegree);

  //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(event)

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

  let tableElement = false;
  if (
    nativElement.tagName == "TD" ||
    nativElement.parentElement.tagName == "TD"
  ) {
    tableElement = true;
  }
  const elementLinkHasNoText = targetElement.textContent.length == 0;
  // || elementLinkHasNoText
  if (isOnIndex || elementLinkHasNoText || tableElement) {
    targetElement = nativElement.parentElement; //get container element/table cell
  }

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

  const popoverRect = popover.getBoundingClientRect();

  DEBUG &&
    console.log(
      "elementLinkHasNoText: " +
        elementLinkHasNoText +
        ", targetElement.textContent: " +
        targetElement.textContent +
        " targetElement.textContent.length: " +
        targetElement.textContent.length
    );
  DEBUG && console.log(nativElement.parentElement.tagName);

  DEBUG && console.log(targetElement);
  DEBUG && console.log(nativElement.parentElement);
  let targetRect = targetElement.getBoundingClientRect();
  const targetLineRects = targetElement.getClientRects();
  let firstLineRect, lastLineRect;

  if (targetLineRects.length > 0) {
    firstLineRect = targetLineRects[0];
    lastLineRect = targetLineRects[targetLineRects.length - 1];
  }
  //if (targetLineRects.length > 1) multiLine = true;

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

  let distanceToTopFromLink = firstLineRect.top - arrowWidth / 2;
  let distanceToBottomFromLink =
    window.innerHeight - targetRect.bottom + arrowWidth / 2; //lastLineRect -> targetRect
  let distanceToRightFromlink = window.innerWidth - targetRect.right;
  let distanceToLeftFromlink = targetRect.left;

  //reset width/height
  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;
  if (!(isOnIndex || tableElement)) {
    /*
    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 bottomHasMoreSpaceThanTop =
      distanceToBottomFromLink > distanceToTopFromLink;
    DEBUG &&
      console.log("bottomHasMoreSpaceThanTop: " + bottomHasMoreSpaceThanTop);
    DEBUG &&
      console.log(
        "distanceToTopFromLink: " +
          distanceToTopFromLink +
          ", distanceToBottomFromLink: " +
          distanceToBottomFromLink
      );
    const bottomSpaceIsBiggerThanTargetHeight =
      distanceToBottomFromLink - offsetToBottomBorderY > targetHeight;
    if (bottomHasMoreSpaceThanTop || bottomSpaceIsBiggerThanTargetHeight) {
      //arrow at bottom
      upside = true;
      DEBUG && console.log("bottom is bigger than top");
      let targetYAdjustment;
      if (bottomSpaceIsBiggerThanTargetHeight) {
        DEBUG && console.log("spacer under link is bigger than target height");

        targetYAdjustment = targetHeight;
        popover.style.height = targetHeight + "px";
      } else {
        DEBUG && console.log("show reduced height");

        targetYAdjustment = distanceToBottomFromLink - offsetToBottomBorderY;

        popover.style.height = targetYAdjustment + "px";
      }
      DEBUG && console.log("targetYAdjustment: " + targetYAdjustment);
      Y = lastLineRect.bottom + arrowWidth / 2;
    } else {
      //arrow at top
      DEBUG && console.log("bottom has less space than top");
      // targetXRect = firstLineRect;
      //Ry = targetRect.top;
      let targetYAdjustment;
      if (distanceToTopFromLink < targetHeight) {
        DEBUG && console.log("targetheight overflows at top side");
        targetYAdjustment = distanceToTopFromLink - offsetToBottomBorderY;
        popover.style.height = targetYAdjustment + "px";
      } else {
        DEBUG && console.log("show heightBetweenLastLineAndBottom");
        targetYAdjustment = targetHeight;
        popover.style.height = targetYAdjustment + "px";
      }
      DEBUG && console.log("targetYAdjustment: " + targetYAdjustment);

      Y = firstLineRect.top - targetYAdjustment - arrowWidth / 2; //offsetToBottomBorderY
      DEBUG &&
        console.log(
          "Y: " +
            Y +
            ", distanceToTopFromLink: " +
            distanceToTopFromLink +
            ", targetHeight: " +
            targetHeight
        );
    }

    Y += scrollPosY;
  } else {
    //auto height push up along table column
    DEBUG &&
      console.log(
        ", distanceToBottomFromLink: " +
          distanceToBottomFromLink +
          ", targetHeight: " +
          targetHeight
      );
    Y = lastLineRect.top;
    if (distanceToBottomFromLink < targetHeight) {
      DEBUG &&
        console.log("space under link smaller targetHeight. move popover up");
      DEBUG && console.log("lastLineRect.top: " + lastLineRect.top + ", Y" + Y);
      const diffY = targetHeight - distanceToBottomFromLink;

      Y -= diffY;
    }
    const posOverTopBorder = Y < offsetToBottomBorderY;
    if (posOverTopBorder) {
      //top offset
      DEBUG && console.log("overflows top border set to offsetToBottomBorderY");
      Y = offsetToBottomBorderY;
    }
    Y += 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");
    X = targetRect.right;
    const rightBiggerThenLeft =
      distanceToLeftFromlink < distanceToRightFromlink;
    if (rightBiggerThenLeft) {
      DEBUG &&
        console.log(
          "right side space bigger than left side. distanceToRightFromlink: " +
            distanceToRightFromlink
        );
      if (distanceToRightFromlink + offsetToRightBorderX * 2 < targetWidth) {
        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");
        popover.style.width = targetWidth;
        // X = lastLineRect.right;
      }
    } else {
      DEBUG && console.log("right side smaller than left side");
    }
  }
  DEBUG &&
    console.log("X: " + X + ", popover.style.width: " + popover.style.width);
  DEBUG && console.groupEnd();
  //#endregion
  DEBUG && console.groupEnd();
  popover.style.top = Y + "px";
  popover.style.left = X + "px";
  showBorderArrowBox(event, arrowWidth, upside, isExternal);
  return { Px: X, Py: Y };
}

// popupPositioning function
function popupPos(event, isExternal = false) {
  const DEBUG = false;
  DEBUG && console.group("popupPos style:" + style);
  if (event && event !== undefined) {
    showPopOver();
    const arrowWidthInPx = 20;

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

    // console.log(scrollPosX)
    let elementImg = popover.getElementsByTagName("img");

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

    const { Px, Py } = getPopupPos(event, arrowWidthInPx, isExternal);

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

    //const popoverHeightMargin = offsetToBottomBorderY * 2;
    //const popoverWidthMargin = offsetToRightBorderX * 2;
    /*
          popover.style.maxHeight =
            "min(400px,calc(100% - " + popoverHeightMargin + "px))";
          popover.style.maxWidth =
            "min(800px,calc(100% - " + popoverWidthMargin + "px))";
            */

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

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

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);
    domain = individualSiteLink.slice(0, individualSiteLink.indexOf("/"));
  }
  return domain;
}

function getLinkID(link, individualPage) {
  const DEBUG = false;
  let ID;
  if (link && individualPage) {
    //example individualPage = "mangadex.org/title/"
    DEBUG && console.group("getLinkID");
    DEBUG && console.log(link);
    const isApiLink = link.indexOf(individualPage);
    if (isApiLink >= 0) {
      const stringFromID = link.slice(isApiLink + individualPage.length);
      DEBUG && console.log(stringFromID);

      let hasSlashAfterID = stringFromID.indexOf("/");
      if (hasSlashAfterID) ID = stringFromID.slice(0, hasSlashAfterID);
      else ID = stringFromID;
      DEBUG && console.log(ID);
    }

    DEBUG && console.groupEnd();
  }
  return ID;
}
async function getCoverDataFromUrl(
  elementUrl,
  external = false,
  individualPage = undefined
) {
  const DEBUG = false;
  DEBUG &&
    console.log(
      "called getCoverDataFromUrl() with elementUrl: " +
        elementUrl +
        ", external: " +
        external +
        ", individualPage: " +
        individualPage
    );
  let PromiseResult = new Promise(async function (resolve, reject) {
    DEBUG && console.log("elementUrl: " + elementUrl);

    // DEBUG && console.log(coverData)
    DEBUG &&
      console.log(
        " - retrievedImgLink cache empty. make ajax request try to save image of page into cache: " +
          elementUrl
      );

    async function onLoad(xhr) {
      //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
      if (xhr.status >= 200 && xhr.status < 399) {
        const domDocument = xhr.response;
        //const parser = new DOMParser();
        // const domDocument = parser.parseFromString(xhr.responseText, 'text/html');
        DEBUG && console.log(domDocument);
        //try
        {
          DEBUG && console.group("parseSeriePage onLoad: " + currentTitelHover);
          if (!domDocument || domDocument === undefined) {
            console.log(xhr);
            console.log(xhr.response);
            console.log(domDocument);
          }

          let temp;

          /*
                          const imageLinkByTag = temp.getElementsByTagName("img");
                          console.log(imageLinkByTag)*/
          let imagelink;

          let serieTitle;
          let serieAlternativeNames;
          let serieVotes;
          let serieStatus;
          let serieChapters;
          let serieGenre;
          let serieShowtags;
          let serieDescription;
          let serieReadingListIcon, serieReadingListTitle;

          //console.log(externalLinks);

          //exchange with property external
          const isExternal = externalLinkKeys.some((key) => {
            return xhr.finalUrl.includes(key);
          });
          //console.log(xhr);
          let hasExternalTargetPage = externalLinks[individualPage];
          let linkID;
          //console.log(externalLinks[individualPage]);
          let hasApiAccess;
          if (isExternal)
            hasApiAccess = externalLinks[individualPage]["mainAPI"];
          //console.log(hasApiAccess);
          if (hasApiAccess) {
            linkID = getLinkID(xhr.finalUrl, individualPage);
            //console.log(linkID);
          }

          DEBUG &&
            console.log(
              "isExternal: " +
                isExternal +
                ", hasExternalTargetPage: " +
                hasExternalTargetPage +
                " for individualPage: " +
                individualPage
            );
          /*
            const externalLinkKeys = Object.keys(externalLinks);
            console.log(externalLinkKeys);*/
          let targetDomain;
          let containerNumber = 0;
          if (isExternal) {
            if (individualPage != undefined) {
              DEBUG && console.group("external link");
              targetDomain = getTargetDomain(individualPage);
              DEBUG && console.log("targetDomain: " + targetDomain);
              if (hasApiAccess) {
                DEBUG && console.log("individualPage: " + individualPage);
                switch (individualPage) {
                  case "mangadex.org/title/":
                    const mangaDexCoverData = await getMangaDexIndexData(
                      linkID
                    );
                    //console.log(mangaDexCoverData);
                    imagelink = mangaDexCoverData.url;

                    serieTitle = mangaDexCoverData.title;
                    serieAlternativeNames = mangaDexCoverData.alternativeNames;
                    serieVotes = mangaDexCoverData.votes;
                    serieStatus = mangaDexCoverData.status;
                    serieChapters = mangaDexCoverData.chapters;
                    serieGenre = mangaDexCoverData.genre;
                    serieShowtags = mangaDexCoverData.tags;

                    serieDescription = mangaDexCoverData.description;
                    break;
                  case "www.tvmaze.com/shows/":
                    const apiData = await getTVmazeShowData(linkID);
                    imagelink = apiData.url;

                    serieTitle = apiData.title;
                    serieAlternativeNames = apiData.alternativeNames;
                    serieVotes = apiData.votes;
                    //serieStatus = apiData.status; //status property value is overwritten by GM_xmlhttpRequest
                    serieChapters = apiData.chapters;
                    serieGenre = apiData.genre;

                    serieDescription = apiData.description;
                    break;
                }
              } else {
                //console.log(domDocument);
                //external links
                const targetPage = hasExternalTargetPage;

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

                imagelink = temp[containerNumber];

                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("external link");
            }
          } else {
            //internal links
            const hasInternalTargetPage = internalLink[internalLinkKey[0]];
            DEBUG && console.group("internal link");
            targetDomain = getTargetDomain(
              hasInternalTargetPage.INDIVIDUALPAGETEST
            );
            temp = domDocument.querySelectorAll(
              hasInternalTargetPage.IMAGELINKCONTAINERS
            );
            //console.log(IMAGELINKCONTAINERS);
            //console.log(temp);
            DEBUG && console.log(temp);

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

            if (
              useReadingListIconAndTitle &&
              hasInternalTargetPage.serieReadingListIcon
            )
              serieReadingListIcon = domDocument.querySelector(
                hasInternalTargetPage.serieReadingListIcon
              );
            //console.log("hasInternalTargetPage.serieReadingListTitle: " + hasInternalTargetPage.serieReadingListTitle)
            if (
              useReadingListIconAndTitle &&
              hasInternalTargetPage.serieReadingListTitle
            ) {
              serieReadingListTitle = domDocument.querySelector(
                hasInternalTargetPage.serieReadingListTitle
              );
            }
            //console.log("serieReadingListTitle: " + serieReadingListTitle)

            serieTitle = tryToGetTextContent(
              serieTitle,
              hasInternalTargetPage.seriePageTitle,
              "seriePageTitle"
            );
            serieAlternativeNames = tryToGetTextContent(
              serieAlternativeNames,
              hasInternalTargetPage.serieAlternativeNames,
              "serieAlternativeNames"
            );
            serieReadingListTitle = tryToGetTextContent(
              serieReadingListTitle,
              hasInternalTargetPage.serieReadingListTitle,
              "serieReadingListTitle"
            );
            if (serieReadingListTitle == "Add series to...")
              serieReadingListTitle = undefined;
            serieVotes = tryToGetTextContent(
              serieVotes,
              hasInternalTargetPage.seriePageVotes,
              "seriePageVotes"
            );
            serieStatus = tryToGetTextContent(
              serieStatus,
              hasInternalTargetPage.seriePageStatus,
              "seriePageStatus"
            );
            serieChapters = tryToGetTextContent(
              serieChapters,
              hasInternalTargetPage.seriePageChapters,
              "seriePageChapters"
            );
            serieGenre = tryToGetTextContent(
              serieGenre,
              hasInternalTargetPage.seriePageGenre,
              "seriePageGenre"
            );
            serieShowtags = tryToGetTextContent(
              serieShowtags,
              hasInternalTargetPage.seriePageTags,
              "seriePageTags"
            );
            serieDescription = tryToGetTextContent(
              serieDescription,
              hasInternalTargetPage.seriePageDescription,
              "seriePageDescription"
            );
            DEBUG && console.groupEnd("internal link");
          }

          if (imagelink !== undefined) {
            if (!linkID) 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;
          }
          //console.log(serieReadingListIcon)
          if (
            serieReadingListIcon !== undefined &&
            serieReadingListIcon !== null
          ) {
            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;

          DEBUG && console.log(serieTitle);
          DEBUG && console.log(serieVotes);
          DEBUG && console.log(serieStatus);
          DEBUG && console.log(serieChapters);
          DEBUG && console.log(serieGenre);
          DEBUG && console.log(serieShowtags);
          DEBUG && console.log(serieDescription);

          //console.log("save imageUrl into coverData.url: " + imagelink);

          let externalUrl;
          //console.log("targetDomain: " + targetDomain)
          if (targetDomain) externalUrl = targetDomain;
          //console.log("serieAlternativeNames: " + serieAlternativeNames)
          let cData = {
            url: imagelink,
            title: serieTitle,
            alternativeNames: serieAlternativeNames,
            votes: serieVotes,
            status: serieStatus,
            chapters: serieChapters,
            genre: serieGenre,
            showTags: serieShowtags,
            description: serieDescription,
            isExternal: externalUrl,
            readingListIcon: serieReadingListIcon,
            readingListTitle: serieReadingListTitle,
          };
          DEBUG && console.log(cData);
          //currentTitelHover = serieTitle;
          GM_setCachedValue(elementUrl, cData); //cache imageurl link
          
          DEBUG &&  console.log(
              elementUrl +
                " url has been found and is written to temporary cache.\n" +
                imagelink +
                " successfully cached."
            ); // for testing purposes
          DEBUG && console.groupEnd("parseSeriePage onLoad");
          return resolve(cData);
          //resolve(imagelink);
        }
        /* catch (error) {
            console.log(
              "error: GM_xmlhttpRequest can not get xhr.response or script is not compatible"
            );
            console.log(error);
            // showPopupLoadingSpinner(serieTitle, 1);
            DEBUG && console.groupEnd("parseSeriePage onLoad");
            return reject(xhr);
          }*/
      }
    }

    function onError(error) {
      console.log(error);
      const err = new Error(
        "GM_xmlhttpRequest could not load " +
          elementUrl +
          "; script is not compatible or url does not exists."
      );
      console.log(err);
      return reject(err);
    }

    let elementUrlWithoutEndingBackslash = elementUrl; //example where it would not work: "https://wiki.d-addicts.com/Douluo_Continent/"
    if (elementUrl[elementUrl.length - 1] == "/")
      elementUrlWithoutEndingBackslash = elementUrl.slice(
        0,
        elementUrl.length - 1
      );
    GM_xmlhttpRequest({
      method: "GET",
      responseType: "document",
      url: elementUrlWithoutEndingBackslash,
      onload: onLoad,
      onerror: onError,
    });

    return undefined; //reject("status error")
  });
  return await PromiseResult;
}
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
      */
      DEBUG && console.group("link: " + element.href);
      DEBUG && console.log("state before check: " + state);
      if (state === undefined) {
        const coverData = GM_getCachedValue(element.href);
        DEBUG && console.log(coverData);
        if (
          coverData === undefined ||
          coverData === null ||
          coverData === "null"
        ) {
          state = 0; //no coverData wating for interaction
        } else {
          state = 2; //coverData available and loaded
        }
      }
      DEBUG && console.log("state: " + state);
      DEBUG && console.groupEnd();
      element.classList.remove(
        "hasCoverPreviewPopup",
        "loadingUrlPreload",
        "loadingUrl",
        "hasLoadedCoverPreviewPopup"
      );
      switch (state) {
        case 0: //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 1: //currently loading coverData
            if (hasText) element.classList.add("loadingUrl");
          break;
        case 2: //coverData preloaded
          if (hasText) element.classList.add("hasLoadedCoverPreviewPopup");
          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);
    }
  }
}
async function parseSeriePage(
  element,
  forceReload = false,
  hoveredTitle = undefined,
  event = undefined,
  external = false,
  targetPage = undefined
) {
  const DEBUG = false;
  const elementUrl = element.href;
  DEBUG && console.group("parseSeriePage: " + elementUrl);
  const coverData = GM_getCachedValue(elementUrl);
  let retrievedImgLink;
  let PromiseResult;
  if (!forceReload && coverData !== null && coverData.url) {
    retrievedImgLink = coverData.url;
    DEBUG &&
      console.log(
        "parseSeriePage has cached retrievedImgLink: " + retrievedImgLink
      );
    PromiseResult = coverData;
  } else {
    showPopupLoadingSpinner(hoveredTitle, hoveredTitle, event);
    PromiseResult = getCoverDataFromUrl(elementUrl, external, targetPage);
  }

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

  return PromiseResult;
}
function removeEventListenerFromNodes(targetNodeArray, external = false) {
  if (targetNodeArray && targetNodeArray.length > 0) {
    //console.log(targetNodeArray);
    targetNodeArray.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));
}
async function preloadForIndividualPageTest(
  targetNodeArray = [],
  individualLinksToTest,
  external = false,
  forceReload = false
) {
  const DEBUG = false;

  DEBUG && console.log(targetNodeArray);

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

  DEBUG &&
    console.log(
      "before parseSeriePage for each url with a link to individual seriepage"
    );
  //#region addEventlistener
  if (targetNodeArray && targetNodeArray.length > 0) {
    //console.log(targetNodeArray);
    targetNodeArray.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 (targetNodeArray && targetNodeArray.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 (
      targetNodeArray.length > 0 &&
      forceReload &&
      !deactivatePreloadUrlRequestOnUrls.includes(individualLinksToTest)
    ) {
      while (nodeArrayIndex < targetNodeArray.length) {
        const element = targetNodeArray[nodeArrayIndex];
        const elementUrl = element.href;

        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);
        /*
        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, 1, forceReload); //active loading icon
        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 " + el);
          }
        );

        if (hasRateLimitQueryAfterMS) {
          // const coverData = GM_getCachedValue(elementUrl);
          //console.log("preloaded url: " + elementUrl);
          if (
            !(
              coverData !== undefined &&
              coverData !== null &&
              coverData != "null"
            )
          )
            await 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
    );
  }
}
const hasLoadedCoverPreviewPopupBase64 =
  "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGNsYXNzPSJpY29uIGljb24tdGFibGVyIGljb24tdGFibGVyLW1lc3NhZ2UiIHdpZHRoPSI0NCIgaGVpZ2h0PSI0NCIgdmlld0JveD0iMCAwIDI0IDI0IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSJjdXJyZW50Q29sb3IiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPHBhdGggc3Ryb2tlPSJub25lIiBkPSJNMCAwaDI0djI0SDB6IiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0iTTQgMjF2LTEzYTMgMyAwIDAgMSAzIC0zaDEwYTMgMyAwIDAgMSAzIDN2NmEzIDMgMCAwIDEgLTMgM2gtOWwtNCA0IiAvPgogIDxsaW5lIHgxPSI4IiB5MT0iOSIgeDI9IjE2IiB5Mj0iOSIgLz4KICA8bGluZSB4MT0iOCIgeTE9IjEzIiB4Mj0iMTQiIHkyPSIxMyIgLz4KPC9zdmc+";
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::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{

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

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

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

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

  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

  bodyElement.insertAdjacentElement("beforeend", popover);
  /*
    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 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")
      //console.log(coverData)
      adjustPopupTitleDetail(coverData, title);
    } else popoverTitle.textContent = title;

    if (notification != "") {
      isShowingSpinnerAnimation = false;
      popoverContent.innerHTML = notification;
      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 (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 = window.requestAnimationFrame(loop);
      //#endregion
    } else {
      //#region pause animation
      //console.group("loop scrolldata before pause");
      window.cancelAnimationFrame(requestId);
      //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 = 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 = document.getElementById(idToScroll);
  setStartScrollPosition();
  if (autoScrollCoverData) {
    if (coverDataContainer) {
      /*console.log(
          "coverDataContainer.offsetHeight: " +
            coverDataContainer.offsetHeight +
            ", coverDataContainer.scrollHeight: " +
            coverDataContainer.scrollHeight
        );*/
      const hasOverflowValue =
        coverDataContainer.scrollHeight > coverDataContainer.offsetHeight;
      if (hasOverflowValue) {
        if (requestId) window.cancelAnimationFrame(requestId);
        //console.log("currentPercent: " + currentPercent);
        scrollToTarget(coverDataContainer);
      }
    }
  }
}
function resetAutoScroll() {
  //autoScrollCoverData = true;
  direction = 1;

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

  if (coverDataContainer && currentPercent) {
    let scrollOverflow =
      coverDataContainer.scrollHeight - coverDataContainer.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.scrollTop
      );
    coverDataContainer.scrollTop = targetScrollTop;
    DEBUG &&
      console.log(
        "coverDataContainer.scrollTop: " + coverDataContainer.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 == "") {
      popoverContent.innerHTML =
        '<div class="containerPadding">No Cover Image found</div>';
    } else {
      let imgElement = new Image(); //document.createElement("img");
      imgElement.src = link;
      popoverContent.innerHTML =
        '<img src="' + link + '" class="ImgFitDefault ' + '" ></img>';
    }
    adjustPopupTitleDetail(coverData);

    DEBUG && console.groupEnd("refreshPopover");
    DEBUG && console.log(e);
    //if (currentTitelHover == title)
    if (e !== undefined) {
      popupPos(e, 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">' +
      titleToShow +
      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 +
      titleToShow +
      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);
      console.log(Error + " failed to fetch " + elementUrl);
    }
  );
  // console.groupEnd("ajaxLoadImageUrlAndShowPopup")
}

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

function imageLoadingError(
  coverData,
  error,
  hoveredTitleLink,
  serieTitle = undefined,
  e = undefined
) {
  console.group("loadImageFromBrowser img.onerror: " + serieTitle);
  /*
        const hasMouseEnterEvent = serieTitle && e !== undefined;  
        const isActivePopup =
          currentTitelHover !== undefined &&
          hoveredTitleLink !== undefined &&
          currentTitelHover == hoveredTitleLink &&
          hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData
        console.log("isActivePopup: " + isActivePopup);*/
  console.log("hoveredTitleLink:" + hoveredTitleLink);
  console.log(error);
  let filename = "";
  console.log("coverData.url: " + coverData.url);

  if (coverData.url != "undefined") {
    filename = decodeURIComponent(coverData.url);
  } else {
    filename = "undefined";
  }
  const errorMessage =
    '<div class="containerPadding">browser blocked/has error loading the file: <br />' +
    filename +
    "</div>";
  console.log(errorMessage);
  //console.log(window)
  console.log(navigator);
  //console.log(navigator.userAgent)
  const useragentString = navigator.userAgent;
  console.log("useragentString: " + useragentString);
  const isChrome = useragentString.includes("Chrome");
  if (isChrome)
    console.log(
      "look in the developer console if 'net::ERR_BLOCKED_BY_CLIENT' is displayed or manually check if the imagelink still exists/reload the coverdata"
    );
  else
    console.log(
      "image loading most likely blocked by browser or addon. Check if the imagelink still exists/reload the coverdata"
    );

  // if (isActivePopup)
  showPopupLoadingSpinner(
    hoveredTitleLink,
    serieTitle,
    e,
    errorMessage,
    coverData
  );
  console.groupEnd("loadImageFromBrowser img.onerror");
}
function loadImageFromBrowser({
  coverData,
  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 = (error) => {
    imageLoadingError(coverData, error, hoveredTitleLink, serieTitle, e);
  };
  //console.log(coverData)
  if (coverData !== 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);
      //  }
    }
  }

  // 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
 *
 */
function updateSerieNodes(
  targetNodeArray = [],
  individualLinksToTest,
  forceReload = false,
  external = false
) {
  const DEBUG = false;
  if (targetNodeArray && targetNodeArray.length > 0) {
    targetNodeArray.forEach(function (selector) {
      if (
        eventListenerStyle === undefined ||
        eventListenerStyle === null ||
        eventListenerStyle == 0
      ) {
        selector.removeEventListener("mouseleave", hideOnMouseLeave);
        //selector.removeEventListener("mouseenter", mouseEnterPopup);
      }
    });
  }
  let serieLinkNodes = document.querySelectorAll(
    'a[href*="' + individualLinksToTest + '"]'
  );
  //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)
            }

            const hasMatch = characterAfterIndividualPage.match(
              new RegExp(completeKey)
            );
            //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);
      // console.log(elementUrl)
      if (external)
        el.setAttribute("coverDataExternalTarget", individualLinksToTest);

      setLinkState(el, undefined, forceReload);
    });
  }
  targetNodeArray.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();
}
function switchDetailsAndUpdatePopup() {
  const DEBUG = false;

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

  updateCurrentPopup();

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

    updateCurrentPopup();
  }
}
function switchShowReadingListIconAndTitle() {
  useReadingListIconAndTitle = !useReadingListIconAndTitle;
  GM_setValue("useReadingListIconAndTitle", useReadingListIconAndTitle);
  updateCurrentPopup();
}
function updateCurrentPopup() {
  const DEBUG = false;
  if (currentCoverData !== undefined) {
    DEBUG && console.log(currentCoverData);
    //refreshPopover(currentCoverData, currentPopupEvent);
    loadImageFromBrowser({
      coverData: currentCoverData,
      e: currentPopupEvent,
      serieTitle: currentTitelHover,
      hoveredTitleLink: currentTitelHover,
    });
    /*
      ,
        currentTitelHover,
        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();
  setPopoverHeight();
}

//#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) {
      //no preloadUrlRequests happend

      const externalLinkKeys = Object.keys(externalLinks);
      const isExternal = externalLinkKeys.some((key) => Href.includes(key));
      if (isExternal) {
        el.setAttribute("coverDataExternalTarget", IndividualTargetToTest);
        coverDataExternalTarget = IndividualTargetToTest;
      } else el.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 hasCoverData = GM_getCachedValue(Href);
      if (!hasCoverData || forceReload)
        setLinkStateOfSameLinks(target, 1, 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() {
  setPopoverHeight();
  setPopoverWidth();

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

  updateCurrentPopup();
}

function showAlternativeNamesList() {
  if (!showAlternativeNames || showAlternativeNames == "") {
    if (currentCoverData !== undefined)
      // refreshPopover(currentCoverData, currentPopupEvent);
      /*
      loadImageFromBrowser({
        coverData: currentCoverData,
        e: currentPopupEvent,
        serieTitle: currentTitelHover,
        hoveredTitleLink: currentTitelHover,
      });*/
      updateCurrentPopup();
  } 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)
      // refreshPopover(currentCoverData, currentPopupEvent);

      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 alternative titles during holding of key A<br />
      [Key I]: Toggle coverPreview state icon displaying next to link<br />
      [Key P]: Switch displaying of readinglist icon<br />
      [Key H]: Show this hotkey 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();
          break;
        case "3":
          showSmaller = !showSmaller;
          GM_setValue("showSmaller", showSmaller);
          updatePopoverSize();
          hasChangedStyle = true;
          break;
        case "4":
          autoScrollCoverData = !autoScrollCoverData;
          if (autoScrollCoverData) autoScrollData();
          break;
        case "h":
          showHotkeys = true;
          showHotkeyList();
          break;
        case "a":
          showAlternativeNames = true;
          showAlternativeNamesList();
          break;
        case "p":
          switchShowReadingListIconAndTitle();
          forceReload();
        case "i":
          switchShowIconNextToLink();
      }
    }
  }
}
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);
      }
    }
  }

  window.onunload = function () {
    window.removeEventListener("blur", hidePopOver);
    window.removeEventListener("keypress", reactToKeyPressWhenPopupVisible);
    window.addEventListener("keyup", releaseKey);
    popover.removeEventListener("mouseleave", hideOnMouseLeave);
    //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 getDataFromAPI(apiPoint, hasDataContainer = undefined) {
  const DEBUG = false;
  let PromiseResult = new Promise(async function (resolve, reject) {
    function onLoad(xhr) {
      //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
      if (xhr.status >= 200 && xhr.status < 399) {
        DEBUG && console.group("getMangaDexIndexData onLoad");
        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);
      }
    }
    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: "GET",
      responseType: "json",
      url: apiPoint,
      onload: onLoad,
      onerror: onError,
    });

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

  return PromiseResult;
}
async function getAllMangaDexTags() {
  return getDataFromAPI("https://mangadex.org/api/v2/tag", "data");
}

async function getMangaDexChapterCount(id) {
  const DEBUG = false;
  if (id) {
    let PromiseResult = new Promise(async function (resolve, reject) {
      function onLoad(xhr) {
        //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
        let mangaDexData;
        if (xhr.status >= 200 && xhr.status < 399) {
          DEBUG && console.group("getMangaDexChapterCount onLoad");
          DEBUG && console.log(xhr);
          let tempJSON = JSON.parse(xhr.responseText);

          if ((tempJSON.status = "OK"));
          {
            mangaDexData = tempJSON.data;
          }
          DEBUG && console.log(mangaDexData);
          DEBUG && console.log(mangaDexData.chapters.length);
          DEBUG && console.groupEnd("getMangaDexChapterCount onLoad");
        }
        if (mangaDexData) return resolve(mangaDexData.chapters.length);
        else
          return reject(
            "api has not return data value for :" +
              "https://mangadex.org/api/v2/manga/" +
              id +
              "/chapters"
          );
      }
      function onError(error) {
        console.log(error);
        const err = new Error(
          "GM_xmlhttpRequest could not load " +
            "https://mangadex.org/api/v2/tag" +
            "; script is not compatible or url does not exists."
        );
        console.log(err);
        return reject(err);
      }

      GM_xmlhttpRequest({
        method: "GET",
        responseType: "json",
        url: "https://mangadex.org/api/v2/manga/" + id + "/chapters",
        onload: onLoad,
        onerror: onError,
      });

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

async function getMangaDexIndexData(id) {
  const DEBUG = false;
  if (id) {
    let PromiseResult = new Promise(async function (resolve, reject) {
      async function onLoad(xhr) {
        //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
        if (xhr.status >= 200 && xhr.status < 399) {
          DEBUG && console.group("getMangaDexIndexData onLoad");
          DEBUG && console.log(xhr);
          let tempJSON = JSON.parse(xhr.responseText);
          let mangaDexData;
          if ((tempJSON.status = "OK"));
          {
            mangaDexData = tempJSON.data;
          }
          DEBUG && console.log(mangaDexData);
          if (mangaDexTAGS === undefined || mangaDexTAGS === null) {
            mangaDexTAGS = await getAllMangaDexTags(
              "https://mangadex.org/api/v2/tag",
              "data"
            ); //getAllMangaDexTags();
            DEBUG && console.log(mangaDexTAGS);
          }

          DEBUG && console.log(mangaDexTAGS);
          let serieGenre = getMangaDexNamedTag(mangaDexData.tags, "Genre");
          let serieTags = getMangaDexNamedTag(mangaDexData.tags, "Theme");
          let serieChapters = await getMangaDexChapterCount(id);
          DEBUG && console.log(serieGenre);
          DEBUG && console.log(serieTags);
          let status;
          switch (mangaDexData.publication.status) {
            case 1:
              status = "Ongoing";
              break;
            case 2:
              status = "Completed";
              break;
          }
          //console.log(mangaDexData.altTitles.join(", "))
          let cData = {
            url: mangaDexData.mainCover,
            title: mangaDexData.title,
            alternativeNames: mangaDexData.altTitles.join("<br /> "),
            votes: mangaDexData.rating.bayesian.toString(),
            status: status,
            chapters: serieChapters.toString(),
            genre: serieGenre,
            tags: serieTags,
            description: mangaDexData.description,
            isExternal: true,
          };
          DEBUG && console.log(cData);
          DEBUG && console.groupEnd("getMangaDexIndexData onLoad");
          return resolve(cData);
        }
      }
      function onError(error) {
        console.log(error);
        const err = new Error(
          "GM_xmlhttpRequest could not load " +
            "https://mangadex.org/api/v2/tag" +
            "; script is not compatible or url does not exists."
        );
        console.log(err);
        return reject(err);
      }

      GM_xmlhttpRequest({
        method: "GET",
        responseType: "json",
        url: "https://mangadex.org/api/v2/manga/" + id,
        onload: onLoad,
        onerror: onError,
      });
      return undefined; //reject("status error")
    });
    return PromiseResult;
  }
  return undefined;
}
//#endregion mangadex api
//#region http://api.tvmaze.com/
async function getTVmazeShowData(id) {
  const DEBUG = false;
  if (id) {
    let PromiseResult = new Promise(async function (resolve, reject) {
      async function onLoad(xhr) {
        //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299)
        if (xhr.status >= 200 && xhr.status < 399) {
          DEBUG && console.group("getTVmazeShowData onLoad");
          DEBUG && console.log(xhr);
          let tempJSON = JSON.parse(xhr.responseText);
          let apiData;
          //console.log(tempJSON);
          if ((tempJSON.status = "OK"));
          {
            apiData = tempJSON;
          }
          DEBUG && console.log(apiData);
          let serieAlternativeNames = await getDataFromAPI(
            "http://api.tvmaze.com/shows/" + id + "/akas"
          );
          DEBUG && console.log(serieAlternativeNames);
          if (serieAlternativeNames !== undefined)
            serieAlternativeNames = serieAlternativeNames
              .map((e) => e.name)
              .join("<br /> ");
          ///console.log(serieAlternativeNames);
          let episodes = await getDataFromAPI(
            "http://api.tvmaze.com/shows/" + id + "/episodes"
          );
          let serieEpisodeCount = episodes.length;
          let targetImageUrl;
          if (apiData.image.medium) targetImageUrl = apiData.image.medium;
          else targetImageUrl = apiData.image.original;
          let cData = {
            url: targetImageUrl,
            title: apiData.name,
            alternativeNames: serieAlternativeNames,
            votes: apiData.rating.average.toString(),
            //status: apiData.status, //status property value is overwritten by GM_xmlhttpRequest
            chapters: serieEpisodeCount.toString(),
            genre: apiData.genres,
            description: apiData.summary,
            isExternal: true,
          };
          DEBUG && console.log(cData);
          DEBUG && console.groupEnd("getMangaDexIndexData onLoad");
          return resolve(cData);
        }
      }
      function onError(error) {
        console.log(error);
        const err = new Error(
          "GM_xmlhttpRequest could not load " +
            "http://api.tvmaze.com/shows/" +
            id +
            "; script is not compatible or url does not exists."
        );
        console.log(err);
        return reject(err);
      }

      GM_xmlhttpRequest({
        method: "GET",
        responseType: "json",
        url: "http://api.tvmaze.com/shows/" + id,
        onload: onLoad,
        onerror: onError,
      });
      return undefined; //reject("status error")
    });
    return PromiseResult;
  }
  return undefined;
}
//#endregion http://api.tvmaze.com/

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 = false;
  if (showDescription === undefined) showDescription = false;
  if (showSmaller === undefined) showSmaller = false;
  if (useReadingListIconAndTitle === undefined)
    useReadingListIconAndTitle = false;
  if (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");