novelupdates Cover Preview

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

As of 2020-12-26. See the latest version.

// ==UserScript==
// https://greasyforks.org/scripts/26439-novelupdates-cover-preview/
// @name        novelupdates Cover Preview
// @namespace   somethingthatshouldnotclashwithotherscripts
// @include     https://www.novelupdates.com/*
// @include     http://www.novelupdates.com/*
// @include     https://forum.novelupdates.com/*
// @include     http://forum.novelupdates.com/*
// @version     2.4.9
// @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,
  }
}
*/
const defaultRateLimitQueryAfterSeconds = 0.5;
const defaultSerieRegex = "([0-9]+)(?!\\w)([/]+.*)?"; //block popup generation for externalLink without appended serie id
// example: "link/"(id); "link/"(id)/; "link/"(id)/anything
//not "link/"/; "link/"(id)something; "link/"(id)something/
const externalLinks = {
  //#region site urls with public api access implemented
  /* https://mangadex.org/thread/351011
    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" ) || preloadUrlRequest
        ) {
          state = 0; //no coverData wating for interaction or forced reloading/preloading
        } 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||preloadUrlRequests) &&
      !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||preloadUrlRequests);
    });
  }
  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");
长期地址
遇到问题?请前往 GitHub 提 Issues。