Previews covers in novelupdates.com when hovering over hyperlinks that lead to novelupdates seriepages and a few external pages.
// ==UserScript== // https://greasyfork.org/scripts/26439-novelupdates-cover-preview/ // @name novelupdates Cover Preview // @namespace somethingthatshouldnot#####withotherscripts // @version 2.6.0 // @description Previews covers in novelupdates.com when hovering over hyperlinks that lead to novelupdates seriepages and a few external pages. // @author SZ // @supportURL https://greasyfork.org/de/scripts/26439-novelupdates-cover-preview/feedback // @website https://forum.novelupdates.com/threads/novel-updates-userscript-to-preview-cover-images-on-greasyfork.117240/ // @include https://www.novelupdates.com/* // @include http://www.novelupdates.com/* // @include https://forum.novelupdates.com/* // @include http://forum.novelupdates.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @run-at document-end // @license http://creativecommons.org/licenses/by-nc-sa/4.0/ // ==/UserScript== (function () { //console.log("cover preview start"); "use strict"; //#region frontend settings const MAXCACHEAGE = 90 * 24 * 60 * 60 * 1000; // Max Age before Cached data of serieinfo gets overridden with current data. Max Age is 90 days in milliseconds //days * h * min * sec * ms const DEFAULTTITLEBACKGROUNDCOLOR = "#2c3e50"; //if no hijack class style available use plain color const DEFAULTBACKGROUNDCOLOR = "#ccc"; //if no hijack class style available use plain color let STYLESHEETHIJACKFORBACKGROUND = " .l-submain, .breadcrumb"; //if unknown set empty ""; classname with leading dot seperated with comma //.pageContent //index: l-submain;forum: breadcrumb; let STYLESHEETHIJACKFORTITLE = ".widgettitle_nuf, .navTabs "; //if unknown set empty ""; classname with leading dot seperated with comma //index: widgettitle_nuf; forum:navTabs const PREDIFINEDNATIVTITLE = "Recommended by"; /* forum, index display shorttitle or serielink url before coverdata from individualpage is loaded can have other starting strings seperated by a comma for example mangaupdates is using "Click for series info, Series Info" */ const isOnIndex = false; //now autodetect display style by checking container of link is tablecell "td" /* this.location.href == "https://www.novelupdates.com/" || this.location.href.startsWith("https://www.novelupdates.com/?pg="); this.location.href.startsWith("https://www.novelupdates.com/group/"); //popup style next to container instead of next to linkitem */ const targetContainerIDArrayToObserve = [ "profile_content3", "messageList", "myTable", ]; /* observer needed for ajax changed page data. (here personal readinglist tab change and in the forum a quickedit of a post) update eventlistener on content change if element id found: isOnReadingListIndex [#profile_content3], in forum[#messageList] and on index[myTable] on tablesorting Attach container to a MutationObserver function which refreshes the eventlistener on links to seriepages */ const internalLink = { "www.novelupdates.com/series/": { serieRegex: "([\\w-]+/?).*", seriePageTitle: ".seriestitlenu", IMAGELINKCONTAINERS: ".serieseditimg img, .seriesimg img", //instead of single element class name with dot seperated with comma //CONTAINERNUMBER: 0, //in case that the query for IMAGELINKCONTAINERS has multiple img node r###lts it can be selected by CONTAINERNUMBER or with img:nth-child(index) inside the query //the same can be used for external links seriePageVotes: ".seriesother > .uvotes", seriePageStatus: "#editstatus", seriePageGenre: "#seriesgenre", seriePageTags: "#showtags", // seriePageDescription: "#editdescription", serieAlternativeNames: "#editassociated", serieReadingListIcon: ".sticon img", serieReadingListTitle: ".sttitle > a", }, }; const internalLinkKey = Object.keys(internalLink); const INDIVIDUALPAGETEST = internalLinkKey[0]; //"www.novelupdates.com/series/"; //matched with includes /* max possible externalLinkObject to insert into externalLinks { "individualSiteLink":{ seriePageTitle:undefined, IMAGELINKCONTAINERS:undefined, CONTAINERNUMBER:0, seriePageDescription:undefined, seriePageStatus:undefined, seriePageChapters:undefined, seriePageVotes:undefined, seriePageGenre:undefined, seriePageTags:undefined, serieRegex:undefined, } } */ const defaultRateLimitQueryAfterSeconds = 0.5; const defaultSerieRegex = "([0-9]+)(?!\\w)([/]+.*)?"; //block popup generation for externalLink without appended serie id // example: "link/"(id); "link/"(id)/; "link/"(id)/anything //not "link/"/; "link/"(id)something; "link/"(id)something/ const externalLinks = { //#region site urls with public api access implemented /* https://mangadex.org/thread/351011 chapter counts https://mangadex.org/api/v2/manga/" + id + "/chapters genre/tags id to name: https://mangadex.org/api/v2/tag */ "mangadex.org/title/": { serieRegex: "([0-9]+)", //block popup generation for mangadex.org/title/ link without serie id mainAPI: "https://mangadex.org/api/v2/manga/", /* not possible no unique identification possible (no id or unique class for a container) depending on serie div count changing since not all values are always used data not at the same position and no unique selector available using public mangadex api */ }, "mangadex.org/manga/": { //alternative address same function serieRegex: "([0-9]+)", mainAPI: "https://mangadex.org/api/v2/manga/", }, /* http://www.tvmaze.com/api alternative names http://api.tvmaze.com/shows/(id)/akas episode counts http://api.tvmaze.com/shows/(id)/episodes */ "www.tvmaze.com/shows/": { serieRegex: "([0-9]+)", mainAPI: "http://api.tvmaze.com/shows/", //http://www.tvmaze.com/api#rate-limiting rateLimitCount: 20, rateLimitTimeInSeconds: 10, //rateLimitQueryAfterSeconds:"0.5" //if not set, but both other values are available will be calculated into rateLimitQueryAfterSeconds = (rateLimitTimeInSeconds/rateLimitCount) //if incomplete will use defaultRateLimitQueryAfterSeconds }, //#endregion //https://www.wlnupdates.com/api-docs "wlnupdates.com/series-id/": { serieRegex: "([0-9]+)", mainAPI: "https://www.wlnupdates.com/api", /* using public api. no need to update if only page layout changes seriePageTitle: "h2", IMAGELINKCONTAINERS: ".imgDiv img", seriePageDescription: ".description", seriePageStatus: ".updating-current", //updating-stalled seriePageChapters: ".orig_status", seriePageVotes: ".br-current-rating", seriePageGenre: "#genre-container > .rowContents", seriePageTags: "#tag-container > .rowContents", */ //serieRegex:undefined, }, "www.scribblehub.com/series/": { /* https://www.scribblehub.com/series/211234/the-impossible-fate-that-leads-to-a-god-of-a-new-world/ exception linkid not forwared to url. linkID needs tobe number+/+stringuntil next slash https://www.scribblehub.com/series/211234 */ serieRegex: "([0-9]+(?=/)[/]?[\\w-]+[/]?)", //get #ID + / + string serieRegex2: "([0-9]+)", //if has not detailed regex get only #ID IMAGELINKCONTAINERS: ".fic_image img", //instead of single element class name with dot seperated with comma seriePageTitle: ".fic_title", seriePageVotes: "#ratefic_user > span", seriePageStatus: ".fic_stats > span:nth-child(3)", seriePageGenre: ".wi_fic_genre", seriePageTags: ".wi_fic_showtags_inner", // seriePageDescription: ".wi_fic_desc", }, "www.webnovel.com/book/": { serieRegex: "([\\w'-]+[/]?).*", //domain/ + spring-blooms-when-i'm-with-you_12023713305779105 IMAGELINKCONTAINERS: ".g_thumb > img", //instead of single element class name with dot seperated with comma seriePageTitle: ".det-info > div:nth-child(2) > h2", seriePageVotes: "._score > strong", seriePageStatus: ".det-hd-detail > strong > span", seriePageGenre: ".det-hd-detail > a > span", seriePageTags: ".m-tags", seriePageDescription: ".det-abt > div > p", }, "royalroad.com/fiction/": { serieRegex: "([0-9]+)", //"([\\w-]+[/]?).*", //alternative serieRegex:"([0-9]+)(?=\/)"" would block detection if string after domain is "21220mother-of-learning" IMAGELINKCONTAINERS: ".fic-header > div > img", //instead of single element class name with dot seperated with comma seriePageTitle: ".fic-title > h1", seriePageVotes: undefined, seriePageStatus: ".fiction-info > div > div:nth-child(2) > div > span:nth-child(2)", seriePageGenre: ".tags", seriePageTags: undefined, // seriePageDescription: ".description", }, "royalroadl.com/fiction/": { //old domain? forwarded to standard domain serieRegex: "([0-9]+)", IMAGELINKCONTAINERS: ".fic-header > div > img", //instead of single element class name with dot seperated with comma seriePageTitle: ".fic-title > h1", seriePageVotes: undefined, seriePageStatus: ".fiction-info > div > div:nth-child(2) > div > span:nth-child(2)", seriePageGenre: ".tags", seriePageTags: undefined, // seriePageDescription: ".description", }, // haven't found a recent used link no need to activate for incomplete data /* "wuxiaworld.com/novel/":{ seriePageTitle: ".novel-body h2", IMAGELINKCONTAINERS: ".novel-left img", //instead of single element class name with dot seperated with comma seriePageStatus: ".novel-body > div:nth-child(2)", seriePageGenre: ".genres", seriePageDescription: ".novel-bottom > div:nth-child(4) > div", },*/ "www.mtlnovel.com/": { serieRegex: "([\\w-]+[/]?).*", seriePageTitle: ".entry-title", IMAGELINKCONTAINERS: ".nov-head > amp-img", //instead of single element class name with dot seperated with comma seriePageVotes: ".star-avg", seriePageStatus: ".info tr:nth-child(3) > td:nth-child(3)", seriePageChapters: ".info-wrap div:nth-child(2)", seriePageGenre: "#currentgen", seriePageTags: ".info tr:nth-child(6)", seriePageDescription: ".desc", serieAlternativeNames: ".info tr:nth-child(2) > td:nth-child(3)", }, /* not possible for the details and haven't seen an api example with different div row counts: https://bato.to/series/72644, https://bato.to/series/74357 data not at the same position and no unique selector available */ "bato.to/series/": { serieRegex: "([0-9]+[/]?)", seriePageTitle: ".item-title", IMAGELINKCONTAINERS: ".attr-cover > img", //instead of single element class name with dot seperated with comma seriePageDescription: ".attr-main > pre", seriePageChapters: ".episode-list > div.head > h4", seriePageGenre: ".attr-main > div:nth-child(3) > span", serieAlternativeNames: ".alias-set", }, "mangaupdates.com/series.html?id=": { serieRegex: "([0-9]+)", seriePageTitle: ".releasestitle", serieAlternativeNames: "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(11)", IMAGELINKCONTAINERS: "#main_content > div:nth-child(2) > div > div:nth-child(4) img", //instead of single element class name with dot seperated with comma //IMAGEBLOCKER: "images/stat_increase.gif, images/stat_decrease.gif", //imageblocker was needed for previous non unique IMAGELINKCONTAINERS selector. ("".sContent img") since a page without cover would query the next available image (in this case rate up/down icon) //CONTAINERNUMBER: 0, seriePageVotes: "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(35)", seriePageStatus: "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(20)", seriePageGenre: "#main_content > div:nth-child(2) > div > div:nth-child(4) > div:nth-child(5)", seriePageTags: "#main_content > div:nth-child(2) > div > div:nth-child(4) > div:nth-child(8) ul", // seriePageDescription: "#main_content > div:nth-child(2) > div > div:nth-child(3) > div:nth-child(2)", }, "mydramalist.com/": { serieRegex: "([0-9]+-[\\w-]+[/]?).*", seriePageTitle: ".film-title", serieAlternativeNames: ".mdl-aka-titles", IMAGELINKCONTAINERS: ".film-cover img", seriePageVotes: "#show-detailsxx .hfs", seriePageStatus: ".content-side > div:nth-child(2) > div:nth-child(2) li:nth-child(4)", seriePageGenre: ".show-genres", seriePageTags: ".show-tags", // seriePageDescription: ".show-synopsis", }, "www.imdb.com/title/": { serieRegex: "(tt[0-9]+[/]?).*", seriePageTitle: ".title_wrapper h1", IMAGELINKCONTAINERS: ".poster img", seriePageVotes: ".imdbRating > .ratingValue", seriePageStatus: ".title_wrapper .subtext", //seriePageGenre non static position. can be pushed down by Taglines box if available seriePageTags: "#titleStoryLine > div:nth-child(6)", seriePageDescription: "#titleStoryLine > div > p > span", }, "www.tv.com/shows/": { serieRegex: "([\\w-]+[/]?).*", seriePageTitle: ".show_head > h1", IMAGELINKCONTAINERS: ".image_border img", seriePageVotes: ".score", seriePageStatus: ".tagline", seriePageGenre: ".categories > p", seriePageTags: ".themes > p", seriePageDescription: ".description > p", }, "wiki.d-addicts.com/": { serieRegex: "([\\w-]+)", seriePageTitle: "#content .title", serieAlternativeNames: "#mw-content-text > .mw-parser-output > ul > li:nth-child(1)", IMAGELINKCONTAINERS: ".thumbinner img", seriePageVotes: ".voteboxrate", seriePageStatus: "#mw-content-text > .mw-parser-output > ul > li:nth-child(6)", seriePageChapters: "#mw-content-text > .mw-parser-output > ul > li:nth-child(4)", seriePageGenre: "#mw-content-text > .mw-parser-output > ul > li:nth-child(3)", seriePageDescription: "#mw-content-text p", }, "asianwiki.com/": { serieRegex: "([\\w()-]+).*", seriePageTitle: ".article > h1", serieAlternativeNames: "#mw-content-text > ul > li:nth-child(2)", IMAGELINKCONTAINERS: ".thumbimage", seriePageVotes: "#w4g_rb_area-1", seriePageStatus: "#mw-content-text > ul > li:nth-child(8)", seriePageChapters: "#mw-content-text > ul > li:nth-child(7)", seriePageDescription: "#mw-content-text > ul ~ p", //next p sibling after ul }, /* not possible. example https://myanimelist.net/manga/2 and https://myanimelist.net/manga/11 Alternative Titles is changing div child count and no unique selector possible no anonymous public api access available "https://myanimelist.net/manga/":{ seriePageTitle: ".h1-title", IMAGELINKCONTAINERS: ".borderClass > div > div img", seriePageVotes: ".score-label", //serieAlternativeNames: ".borderClass > div > div:nth-child(8)", //seriePageStatus:".borderClass > div > div:nth-child(16)", // seriePageGenre:".borderClass > div > div:nth-child(17)", seriePageDescription: 'span[itemprop="description"', } */ }; const externalLinkKeys = Object.keys(externalLinks); const defaultshowIconNextToLink = false; let preloadUrlRequestsDefault; //if not set will default to true in release let deactivatePreloadUrlRequestOnUrls = [ "wiki.d-addicts.com", "https://forum.novelupdates.com/threads/novel-updates-userscript-to-preview-cover-images-on-greasyfork.117240/", //deactivated urlPreload on this forum thread to stop bombardedment of requesting domain access in tampermonkey since all external links are listed in the nu forum post. ]; const preloadImagesDefault = false; let useReadingListIconAndTitle = false; const eventListenerStyle = 0; //undefined/0 forEach serieLink addeventlistener(mouseenter/mouseleave) / 1 window addeventlistener(mousemove) //#endregion end of frontend settings /* not available in firefox would have liked to automatically set preloadImages to true for wifi and non metered connections with speeds over 2MB/s console.log(navigator); let connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; let type = connection.effectiveType; function updateConnectionStatus() { console.log(connection) console.log("Connection type changed from " + type + " to " + connection.effectiveType); type = connection.effectiveType; if(connection.saveData==false){ preloadUrlRequests = true; if(type=="wifi" || preloadImagesDefault) preloadImages = true; } } updateConnectionStatus(); connection.addEventListener('change', updateConnectionStatus); */ //#region backend variables ^^^^ frontend settings over this line ^^^^ const version = "2.3.2"; const forceUpdate = false; const debugCSSClasses = false; //log if css class is accessible else default include const lastUpdateCheck = 28 * 24 * 60 * 60 * 1000; //recheck if CSS available const settingsToKeepOnDataReset = [ "showDescription", "showDetails", "showSmaller", "useReadingListIconAndTitle", "showIconNextToLink", ]; //console.log(GM_info); //console.log(GM_info.script.name); const isReleaseVersion = GM_info.script.name == "novelupdates Cover Preview"; //console.log("isReleaseVersion: " + isReleaseVersion); let preloadUrlRequests = isReleaseVersion && (preloadUrlRequestsDefault === undefined || preloadUrlRequestsDefault); let preloadImages = false || (preloadImagesDefault && isReleaseVersion); //console.log("preloadUrlRequests: " + preloadUrlRequests) let showIconNextToLink = defaultshowIconNextToLink; //const maxWaitingTime = 120; const linkIconEnum = { popupPossibleNotLoadedOrMarkedForPreloading: 1, popupMarkedForPreloading: 2, popupLoading: 3, popupHasCoverData: 4, error: 5, }; Object.freeze(linkIconEnum); const emptyCoverData = { url: undefined, title: undefined, alternativeNames: undefined, votes: undefined, status: undefined, chapters: undefined, genre: undefined, showTags: undefined, description: undefined, isExternal: undefined, readingListIcon: undefined, readingListTitle: undefined, }; const RE = /\s*,\s*/; //Regex for split and remove empty spaces const REGEX_DOTCOMMA = /[.,]/g; const reChapters = new RegExp("([0-9.]+)[ ]*(wn)?[ ]*chapters"); const reChaptersNumberBehind = new RegExp("chapter[s]?[ ]*[(]?[ ]*([0-9.]+)"); const reChaptersOnlyNumbers = new RegExp("([0-9.]+)"); const reRating = new RegExp("([0-9.]+) / ([0-9.]+)"); const reRatingSingleNumber = new RegExp("([0-9.]+)"); const reVoteCount = new RegExp("([0-9.]+)[ ]*(votes|ratings|users)"); // //const reStripHTMLLiteral = "(<[\/]?(?!span|b|i|p|br[^\w]+\s*)(([\/]?[^>]+))>)"; const reWhiteListStripHTML = new RegExp( "(<[/]?\\b(?!(b|i|br|p)[^\\w]+\\s*)(([/]?[^>]+))>)", "g" ); //needed escape character changes remove escaping for / (\/) , specialFunction additional escaping for() //const reWhiteListHTML = /(<[\/]?\b(?!(b|u|i|br|p)[^\w]+\s*)(([\/]?[^>]+))>)/g // /(<[\/]?\b(?!span|b|i|p|br[^\w]+\s*)(([\/]?[^>]+))>)/g; const offsetToBottomBorderY = 22; //offset to bottom border const offsetToRightBorderX = 10; //offset to right border const defaultHeight = 400; //in pixel const smallHeight = 250; //const IMAGEBLOCKERARRAY = IMAGEBLOCKER.split(RE); const PREDIFINEDNATIVTITLEARRAY = PREDIFINEDNATIVTITLE.split(RE); const STYLESHEETHIJACKFORBACKGROUNDARRAY = STYLESHEETHIJACKFORBACKGROUND.split( RE ); const STYLESHEETHIJACKFORTITLEARRAY = STYLESHEETHIJACKFORTITLE.split(RE); let refreshInitValues = false; let showDetails = false; let popoverVisible = false; //not all links have a title or text(img link) to set currentTitelHover. Manual state saving needed let ALLSERIENODES = []; // = document.querySelectorAll('a[href*="' + INDIVIDUALPAGETEST + '"]'); let ALLEXTERNALLINKNODES = []; let previousTitelHover, currentTitelHover, currentCoverData, currentPopupEvent; let popover, popoverTitle, popoverContent, arrowContainer; let lastTarget; let isShowingSpinnerAnimation = false; let showDescription = false; let showSmaller = false; let showHotkeys = false; let showAlternativeNames = false; let autoScrollCoverData = true; let coverDataContainer = []; let mediumTextStyle = "mediumText"; let smallTextStyle = "smallText"; let pressedKeys = []; let mangaDexTAGS; let currentOpenedUrl; let popoverBackgroundColor, popoverForegroundColor; let popoverBorderWidth; let popoverBorderColor; const supportsCSSMin = CSS.supports("max-Height", "min(400px, 100%)"); const arrowWidthInPx = 40; //#endregion //console.log("after variable settings"); //console.log(this.location) //console.log(this.location.href) //console.log("isOnIndex: " + isOnIndex) //#region helper functions // Options for the observer (which mutations to observe) const config = { attributes: true, childList: true, subtree: true }; //get value from key. Decide if timestamp is older than MAXCACHEAGE than look for new image function GM_getCachedValue(key) { const DEBUG = false; const currentTime = Date.now(); const rawCover = GM_getValue(key, null); DEBUG && console.group("GM_getCachedValue key request: " + key); DEBUG && console.log("rawCover: " + rawCover); let r###lt = null; if (rawCover === null || rawCover == "null") { r###lt = null; } else { let coverData; try { //is json parseable data? if not delete for refreshing coverData = JSON.parse(rawCover); DEBUG && console.log("coverData: " + coverData); DEBUG && console.log(coverData); if (!(coverData.title && coverData.cachedTime)) { //has same variable definitions? GM_deleteValue(key); r###lt = null; } } catch (e) { GM_deleteValue(key); r###lt = null; } const measuredTimedifference = currentTime - coverData.cachedTime; if (measuredTimedifference < MAXCACHEAGE) { r###lt = { 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); r###lt = null; } } DEBUG && console.groupEnd("GM_getCachedValue"); DEBUG && console.log(r###lt); return r###lt; } //set value and currenttime for key function GM_setCachedValue(key, coverData) { const DEBUG = false; const cD = { url: coverData.url, title: coverData.title, alternativeNames: coverData.alternativeNames, votes: coverData.votes, status: coverData.status, chapters: coverData.chapters, genre: coverData.genre, showTags: coverData.showTags, description: coverData.description, isExternal: coverData.isExternal, readingListIcon: coverData.readingListIcon, readingListTitle: coverData.readingListTitle, cachedTime: Date.now(), }; GM_setValue(key, JSON.stringify(cD)); DEBUG && console.group("GM_setCachedValue key: " + key); DEBUG && console.log("save coverdata"); DEBUG && console.log(cD); DEBUG && console.group("GM_setCachedValue"); } function styleSheetContainsClass(f) { const DEBUG = false; var localDomainCheck = "^http://" + document.domain; var localDomainCheckHttps = "^https://" + document.domain; // DEBUG && console.log("Domain check with: " + localDomainCheck); var hasStyle = false; var stylename = f; var fullStyleSheets = document.styleSheets; // DEBUG && console.log("start styleSheetContainsClass " + stylename); if (fullStyleSheets) { const styleSheetsLengthToLoop = fullStyleSheets.length - 1; for (let i = 0; i < styleSheetsLengthToLoop; i++) { //DEBUG && console.log("loop fullStyleSheets " + stylename); let styleSheet = fullStyleSheets[i]; if ( styleSheet != null && styleSheet.href !== null && (styleSheet.href.match(localDomainCheck) || styleSheet.href.match(localDomainCheckHttps)) && styleSheet.cssRules //https://gold.xitu.io/entry/586c67c4ac502e12d631836b "However since FF 3.5 (or thereabouts) you don't have access to cssRules collection when the file is hosted on a different domain" -> Access error for Firefox based browser. script error not continuing ) { //DEBUG && console.log("styleSheet.cssRules.length: " + styleSheet.cssRules.length) const ruleLengthToLoop = styleSheet.cssRules.length - 1; for (let rulePos = 0; rulePos < ruleLengthToLoop; rulePos++) { if (styleSheet.cssRules[rulePos] !== undefined) { //DEBUG && console.log("styleSheet.cssRules[rulePos] "+ stylename); //DEBUG && console.log(styleSheet.cssRules[rulePos]) if (styleSheet.cssRules[rulePos].selectorText) { // console.log(styleSheet.cssRules[rulePos].selectorText) if (styleSheet.cssRules[rulePos].selectorText == stylename) { // console.log('styleSheet class has been found - class: ' + stylename); hasStyle = true; //break; break; //return hasStyle; } } //else DEBUG && console.log("undefined styleSheet.cssRules[rulePos] "+rulePos +" - "+ stylename); } //else DEBUG && console.log("loop undefined styleSheet.cssRules[rulePos] "+ stylename); } //else DEBUG && console.log("undefined styleSheet.cssRules "+ stylename); } // DEBUG && console.log("stylesheet url " + styleSheet.href); //else DEBUG && console.log("undefined styleSheet "+ stylename); if (hasStyle) break; } } //else console.log("undefined fullStyleSheets=document.styleSheets "+ stylename); if (!hasStyle) { console.log("styleSheet class has not been found - style: " + stylename); } return hasStyle; } // Callback function to execute when mutations are observed /* https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/ const function can not be overwritten //https://stackoverflow.com/questions/54915917/overwrite-anonymous-const-function-in-javascript https://www.digitalocean.com/community/tutorials/understanding-hoisting-in-javascript function callback () is anonymous var function which can be overwritten? */ const debounce = function (func, timeout) { let timer; return (...args) => { const next = () => func(...args); if (timer) { clearTimeout(timer); } timer = setTimeout(next, timeout > 0 ? timeout : 300); }; }; //https://gist.github.com/peduarte/969217eac456538789e8fac8f45143b4#file-index-js const throttle = function (func, wait = 100) { let timer = null; return function (...args) { if (timer === null) { timer = setTimeout(() => { func.apply(this, args); timer = null; }, wait); } }; }; const callbackMutationObserver = function (mutationsList, observer) { // Use traditional 'for loops' for IE 11 for (const mutation of mutationsList) { if (mutation.type === "childList") { // console.log('A child node has been added or removed.'); //debouncedTest() hidePopOver(); debouncedpreloadCoverData(); } else if (mutation.type === "attributes") { // console.log('The ' + mutation.attributeName + ' attribute was modified.'); } } }; // Create an observer instance linked to the callback function const observer = new MutationObserver(callbackMutationObserver); const debouncedpreloadCoverData = debounce(preloadCoverData, 100); const throttledGetHoveredItem = throttle(getHoveredItem, 50); //https://math.stackexchange.com/questions/814950/how-can-i-rotate-a-coordinate-around-a-circle /** * * * @param {*} cx = pivot * @param {*} cy = pivot * @param {*} mx = offset * @param {*} my = offset * @param {*} angle * @returns */ function getRotatedPoint(cx, cy, mx, my, angle) { let x, y; angle = ((angle % 360) / 180) * Math.PI; x = ((mx - cx) * Math.cos(angle) - (my - cy) * Math.sin(angle) + cx) | 0; y = ((mx - cx) * Math.sin(angle) + (my - cy) * Math.cos(angle) + cy) | 0; return { x: x, y: y }; } //https://stackoverflow.com/questions/1887104/how-to-get-the-background-color-of-an-html-element const getComputedStyle = function (element, property) { return window.getComputedStyle ? window.getComputedStyle(element, null).getPropertyValue(property) : element.style[ property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }) ]; }; //adjusted from https://stackoverflow.com/questions/34980574/how-to-extract-color-values-from-rgb-string-in-javascript/34980657 function getRGBValues(str) { var vals = str.substring(str.indexOf("(") + 1, str.length - 1).split(", "); return vals; } //adjusted from https://gist.github.com/JordanDelcros/518396da1c13f75ee057 function blendColorsToRGBA(colors = []) { var base = [0, 0, 0, 0]; var mix; var added; while ((added = colors.shift())) { //console.log(added) //console.log(typeof added) if (typeof added == "string") { const arrayColor = getRGBValues(added); //console.log(arrayColor) added = arrayColor; } //console.log(added[3]) //console.log(typeof added[3]) if (typeof added[3] === "undefined") { //if rgb add alpha 1 added[3] = 1; } //console.log(added) //console.log(base) // check if both alpha channels exist. if (base[3] && added[3]) { mix = [0, 0, 0, 0]; // alpha mix[3] = 1 - (1 - added[3]) * (1 - base[3]); //console.log(added) //console.log(base) // red mix[0] = Math.round( (added[0] * added[3]) / mix[3] + (base[0] * base[3] * (1 - added[3])) / mix[3] ); // green mix[1] = Math.round( (added[1] * added[3]) / mix[3] + (base[1] * base[3] * (1 - added[3])) / mix[3] ); // blue mix[2] = Math.round( (added[2] * added[3]) / mix[3] + (base[2] * base[3] * (1 - added[3])) / mix[3] ); //console.log(mix); } else if (added) { //console.log(added) mix = added; } else { //console.log(base) mix = base; } //console.log(mix) base = mix; } //console.log(mix) return "rgba(" + mix + ")"; } //#endregion helper functions function checkDataVersion() { //Remove possible incompatible old data const DEBUG = false; const dataVersion = GM_getValue("version", null); DEBUG && console.log("dataVersion: " + dataVersion); if ( dataVersion === null || dataVersion === undefined || dataVersion != version || forceUpdate ) { resetDatabase(); //resetSettings(); } } function resetDatabase() { const DEBUG = false; const oldValues = GM_listValues(); DEBUG && console.log("oldValues.length: " + oldValues.length); const oldValuesLengthToLoop = oldValues.length; for (let i = 0; i < oldValuesLengthToLoop; i++) { if (!settingsToKeepOnDataReset.includes(oldValues[i])) { GM_deleteValue(oldValues[i]); } else { //console.log("keep setting") //console.log(oldValues[i]) } //console.log(oldValues[i]) } GM_setValue("version", version); DEBUG && console.log(oldValues); !isReleaseVersion && console.log("Finished clearing CoverData"); } function resetSettings() { for (let i = 0; i < settingsToKeepOnDataReset.length; i++) { GM_deleteValue(settingsToKeepOnDataReset[i]); } GM_setValue("version", version); } //https://www.w3resource.com/javascript-exercises/javascript-math-exercise-27.php function pointDirection(x1, y1, x2, y2) { return (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI; } function checkIsTableElement(element) { if ( element.tagName == "TD" || element.parentElement.tagName == "TD" || element.className.includes("col-") || //tailwind column styles needed for mangaupdates element.parentElement.className.includes("col-") ) { return true; } return false; } function showBorderArrowBox( event, arrowWidth = 20, upside = false, isExternal = false, arrowTableSideRight = true ) { const DEBUG = false; /* - getPopover Rect - generate circleBox - generate arrow - rotate arrow to target */ const nativElement = event.target; let targetElement = nativElement; let isInChildContainer = false; let tableElement = checkIsTableElement(nativElement); DEBUG && console.group("showBorderArrowBox"); const elementLinkHasNoText = targetElement.textContent.length == 0; //elementLinkHasNoText if (isOnIndex || elementLinkHasNoText || tableElement) { isInChildContainer = true; targetElement = nativElement.parentElement; //get container element/table cell } /* if(elementLinkHasNoText){ targetElement = nativElement.parentElement.parentElement; console.log(targetElement); } */ const targetRectBounds = targetElement.getBoundingClientRect(); let targetRect = targetRectBounds; //https://stackoverflow.com/questions/52712760/get-how-many-lines-a-textarea-has /* TODO novelupdates forum/xenforum script is somehow changing element with class "ugc" which prevents getting the pseudo element a::before from getClientRects() */ DEBUG && console.log(targetElement); const targetLineRects = targetElement.getClientRects(); let firstLineRect, lastLineRect; let firstRectIndex; DEBUG && console.log(targetRect); //console.log(targetLineRects); firstRectIndex = 0; if (targetLineRects.length > 0) { if (showIconNextToLink && targetLineRects.length > 1) { firstRectIndex = 1; } targetRect = targetLineRects[firstRectIndex]; firstLineRect = targetRect; lastLineRect = targetLineRects[targetLineRects.length - 1]; } //if (lastLineRect != firstLineRect) if (lastLineRect) targetRect = lastLineRect; if (!upside) { if (firstLineRect) targetRect = firstLineRect; } let popoverRect = popover.getBoundingClientRect(); DEBUG && console.log(popoverRect); DEBUG && console.log(targetRect); DEBUG && console.log(targetRectBounds); const scrollPosY = window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop; const scrollPosX = window.scrollX || window.scrollLeft || document.getElementsByTagName("html")[0].scrollLeft; arrowContainer.style.position = "absolute"; arrowContainer.style.zIndex = "8"; arrowContainer.style.height = arrowWidth + "px"; arrowContainer.style.width = arrowWidth + "px"; let posY = 0; let rotateArrowInDegree = 0; let targetPosY, sourcePosY; let targetXSourcePosition; let popupSourceX; const targetHalfHeight = targetRect.height / 2; const halfArrowWidth = arrowWidth / 2; let halfWidth = arrowWidth / 2; const quarterArrowWidth = arrowWidth / 4; let arrowContainerTopPositionLocal; let targetRectTopDistance = targetRect.top - popoverRect.top; let targetElementDistanceY = targetRectTopDistance - targetRect.height / 4 + 1; const minYPosOnPopup = -quarterArrowWidth; const maxYPosOnPopup = popoverRect.height - (arrowWidth - quarterArrowWidth); if (targetElementDistanceY < minYPosOnPopup) { targetElementDistanceY = minYPosOnPopup; } if (targetElementDistanceY > maxYPosOnPopup) { targetElementDistanceY = maxYPosOnPopup; } //if (isExternal) externalStyle = "isExternalContentArrow "; //|| elementLinkHasNoText if (isOnIndex || tableElement) { //#region "index/container layout" DEBUG && console.log("index/container layout"); DEBUG && console.log(targetRect); DEBUG && console.log("targetRect.height: " + targetRect.height); const upperBorder = popoverRect.top - quarterArrowWidth; const lowerBorder = popoverRect.bottom - halfArrowWidth - quarterArrowWidth; DEBUG && console.log( "targetRect.height: " + targetRect.height + ", targetHalfHeight: " + targetHalfHeight ); targetPosY = targetRect.top; //targetRectYMiddle; sourcePosY = targetPosY; //#region get targetPointY DEBUG && console.log( "targetPosY: " + targetPosY + ", upperBorder: " + upperBorder + ", lowerBorder: " + lowerBorder ); if (targetPosY < upperBorder) { sourcePosY = upperBorder; DEBUG && console.log("set targetPosY = upperBorder"); } if (targetPosY > lowerBorder) { sourcePosY = lowerBorder; DEBUG && console.log("set targetPosY = lowerBorder"); } //#endregion //#region limit popover sourceY position //sourcePosY = targetPosY; //#endregion DEBUG && console.log( "targetPosY: " + targetPosY + ", sourcePosY: " + sourcePosY + ", scrollPosY: " + scrollPosY + ", top: " + (scrollPosY + targetPosY) ); DEBUG && console.log( "targetElementDistanceY: " + targetElementDistanceY + ", popoverRect.top: " + popoverRect.top + ", targetRect.top: " + targetRect.top ); arrowContainerTopPositionLocal = popoverRect.top + targetElementDistanceY; arrowContainer.style.top = scrollPosY + arrowContainerTopPositionLocal + "px"; //sourcePosY //#endregion arrow vertical position //#region arrow on leftOrRightSide of popover if (arrowTableSideRight) { arrowContainer.style.left = scrollPosX + popoverRect.left - halfWidth + "px"; } else { arrowContainer.style.left = scrollPosX + popoverRect.left + popoverRect.width - halfWidth + "px"; } //#endregion arrow on leftOrRightSide of popover DEBUG && console.log( "posY: " + posY + ", popoverRect.top + posY + arrowWidth: " + (popoverRect.top + posY + arrowWidth) ); targetXSourcePosition = targetRect.right; popupSourceX = popoverRect.left; rotateArrowInDegree = pointDirection( popupSourceX, sourcePosY, targetXSourcePosition - arrowWidth, targetPosY ) - 45; //width:100%;height:100%; //#endregion "index/container layout" } else { //#region "under/over link layout" DEBUG && console.log("under/over link layout"); //#region horizontal //#region targetPosX // let targetX = targetRect.right; let diffXTarget = targetRect.right - popoverRect.left; const diffXTargetWithHalfwidth = diffXTarget + halfWidth; if (diffXTarget - halfWidth < 0) diffXTarget = halfWidth; let targetXSourcePosition = targetRect.right - diffXTargetWithHalfwidth; if (diffXTarget > targetRect.width) { targetXSourcePosition = targetRect.left; } //#endregion targetPosX //#region popoverPosX popupSourceX = popoverRect.left - halfWidth; console.log( "diffXTargetWithHalfwidth: " + diffXTargetWithHalfwidth + ", targetRect.width: " + targetRect.width ); if (diffXTargetWithHalfwidth > targetRect.width) { popupSourceX = targetXSourcePosition; } //#endregion popoverPosX //#region move arrowHead in targetDirection if (diffXTarget < halfWidth) { targetXSourcePosition -= halfWidth; } //#endregion move arrowHead in targetDirection if (targetXSourcePosition) { DEBUG && console.log( "diffXTarget: " + diffXTarget + ", popupSourceX: " + popupSourceX + ", targetRect.width: " + targetRect.width + ", targetXSourcePosition: " + targetXSourcePosition ); } //#endregion //#region vertical position over/under popup arrowContainer.style.left = scrollPosX + popupSourceX + "px"; if (upside) { //arrow at topside let popoverOutsideTargetHorizontal = 0; DEBUG && console.log( "popoverOutsideTargetHorizontal: " + popoverOutsideTargetHorizontal ); arrowContainer.style.top = scrollPosY + popoverRect.top - arrowWidth / 2 + "px"; rotateArrowInDegree = pointDirection( popupSourceX, popoverRect.top + posY + arrowWidth, targetXSourcePosition, //targetX+popoverOutsideTargetHorizontal targetRect.top + arrowWidth ) - 45; //width:100%;height:100%; } else { //arrow at bottom arrowContainer.style.top = scrollPosY + popoverRect.bottom - arrowWidth / 2 + "px"; rotateArrowInDegree = pointDirection( popupSourceX, popoverRect.bottom + posY + halfWidth, targetXSourcePosition, targetRect.bottom + halfWidth ) - 45; //width:100%;height:100%; } //#endregion vertical position over/under popup //#endregion "under/over link layout" } let targetRotation = rotateArrowInDegree - 45; // let targetSourceAbsoluteDiffY = sourcePosY - targetPosY; let targetDiffX = popupSourceX - targetXSourcePosition; DEBUG && console.log("isInChildContainer:" + isInChildContainer); if (isInChildContainer) { const nativRect = nativElement.getBoundingClientRect(); targetDiffX = popupSourceX - nativRect.right - halfArrowWidth; DEBUG && console.log(nativRect); } DEBUG && console.log( "popupSourceX: " + popupSourceX + ", targetXSourcePosition: " + targetXSourcePosition + ", targetDiffX: " + targetDiffX ); let containerOffsetX = 12; //12; let offsetPointOnArrow = { x: 20, y: 40 + containerOffsetX }; let pivotOnArrow = { x: 20, y: 20 }; //svg viewbox height/width = 24 let newSourcePointXY = getRotatedPoint( pivotOnArrow.x, pivotOnArrow.y, offsetPointOnArrow.x, offsetPointOnArrow.y, rotateArrowInDegree - 45 ); DEBUG && console.log("newSourcePointXY: " + newSourcePointXY); DEBUG && console.log(newSourcePointXY); let x1, y1, x2, y2; let rectDistance = 0; // if (targetRect.top < popoverRect.top) { rectDistance = targetRect.top - popoverRect.top; } let arrowOffsetY = targetRectTopDistance - targetElementDistanceY; DEBUG && console.log( "popoverRect.top: " + popoverRect.top + ", targetRect.top: " + targetRect.top + ", popoverRect.bottom: " + popoverRect.bottom + ", targetRect.bottom: " + targetRect.bottom + ", rectDistance: " + rectDistance + ", targetDiffX: " + targetDiffX + ", targetElementDistanceY: " + targetElementDistanceY + ", popoverRect.height: " + popoverRect.height + ", arrowContainerTopPositionLocal: " + arrowContainerTopPositionLocal + ", arrowOffsetY: " + arrowOffsetY + ", targetRectTopDistance: " + targetRectTopDistance ); const circleRadius = 1; x1 = -targetDiffX + circleRadius; //-(targetDiffX-newSourcePointXY.x)-newSourcePointXY.x//targetRect.right; //0 - containerOffsetX; y1 = arrowOffsetY + targetHalfHeight; //;//rectDistance; // + targetHalfHeight //targetElementDistanceY arrowContainerTopPositionLocal-popoverRect.top DEBUG && console.log( "y1: " + y1 + ", rectDistance: " + rectDistance + ", targetHalfHeight: " + targetHalfHeight ); //(targetRect.height/2) + targetElementDistanceY;//targetRect.top+targetRect.height/2;//targetSourceAbsoluteDiffY; // x2 = newSourcePointXY.x; y2 = newSourcePointXY.y; DEBUG && console.log( "newSourcePointXY.x: " + newSourcePointXY.x + ", newSourcePointXY.y: " + newSourcePointXY.y + ", targetX: " + x2 + ", targetY: " + y2 + ", targetElementDistanceY: " + targetElementDistanceY ); let point = '<circle cx="' + newSourcePointXY.x + '" cy="' + newSourcePointXY.y + '" r="' + circleRadius + '" stroke="red" fill="transparent" stroke-width="1"/>' + '<circle cx="' + x1 + '" cy="' + y1 + '" r="' + circleRadius + '" stroke="green" fill="transparent" stroke-width="1"/>'; let line = //startY //'<line x1="'+(x1-targetDiffX+arrowWidth-quarterArrowWidth)+'" y1="'+(y1-(arrowWidth-quarterArrowWidth))+'" x2="'+(x2-targetDiffX)+'" y2="'+(y2)+'" stroke="black" stroke-width="1"/></svg>'; '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" stroke="black" stroke-width="1"/>'; let additionalLineSVG = ""; if (isOnIndex || tableElement) { additionalLineSVG = '<div style="width:100%;height:100%;position:absolute;top:0;left:0;"><svg xmlns="http://www.w3.org/2000/svg" style="overflow:visible">' + line + point + "</svg></div>"; } let borderColor = "black"; if (isExternal) borderColor = "red"; arrowContainer.innerHTML = '<div style="width:40px;height:40px;transform: rotate(' + targetRotation + 'deg);padding:2px">' + '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-big-down" style="width:100%;height:100%;overflow:visible" viewBox="0 0 24 24" stroke-width="1" vector-effect="non-scaling-stroke" stroke="currentColor" fill="' + popoverForegroundColor + '" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 ' + containerOffsetX + ')">' + //'<rect width="100%" height="100%" style="fill:rgb(100,155,255);stroke-width:1;stroke:rgb(0,0,0)" />' + '<filter id="shadow"><feDropShadow dx="0.5" dy="-0.5" stdDeviation="0.8"/></filter>' + '<circle cx="12" cy="12" r="13" stroke="' + borderColor + '" fill="' + popoverBackgroundColor + '" stroke-width="' + popoverBorderWidth + '" style="filter:url(#shadow);"/>' + '<path d="M 15 0 v 15 h 3.586 a 1 1 0 0 1 0.707 1.707 l -6.586 6.586 a 1 1 0 0 1 -1.414 0 l -6.586 -6.586 a 1 1 0 0 1 0.707 -1.707 h 3.586 L 9 0 z" transform=""/>' + "</svg></div>" + additionalLineSVG; DEBUG && console.log("scrollPosY: " + scrollPosY); arrowContainer.style.pointerEvents = "none"; DEBUG && console.log("posY: " + posY); // rotateArrowInDegree+=-135; //https://www.w3schools.com/howto/howto_css_arrows.asp DEBUG && console.log("rotateArrowInDegree: " + rotateArrowInDegree); DEBUG && console.groupEnd("showBorderArrowBox"); //arrowContainer.style.visibility = "visible"; //arrowContainer.classList.remove("hidePopover"); } function getPopupPos(event, arrowWidth = 0, isExternal = false) { const DEBUG = false; const scrollPosY = window.scrollY || window.scrollTop || document.getElementsByTagName("html")[0].scrollTop; const scrollPosX = window.scrollX || window.scrollLeft || document.getElementsByTagName("html")[0].scrollLeft; let targetHeight = defaultHeight; if (showSmaller) targetHeight = smallHeight; let targetWidth = targetHeight; if (showDetails) targetWidth = targetHeight * 2; //console.log("targetWidth: " + targetWidth) //console.log(event) popover.style.maxHeight = ""; //reset to max height //popover.style.maxWidth = ""; //reset to max height const popoverRect = popover.getBoundingClientRect(); const nativElement = event.target; let targetElement = nativElement; let tableElement = checkIsTableElement(nativElement); //console.log("nativElement.classList") //console.log(nativElement.classList) //console.log(nativElement.className) //console.log('nativElement.classList.contains("externalLink") ' + nativElement.className.split(" ").some(classname => classname.startsWith("externalL"))) //console.log('nativElement.className.includes("externalL") ' + nativElement.className.includes("externalL")) // const elementLinkHasNoText = targetElement.textContent.length == 0; // || elementLinkHasNoText if (isOnIndex || elementLinkHasNoText || tableElement) { DEBUG && console.log( "(isOnIndex || elementLinkHasNoText || tableElement) -> targetElement = nativElement.parentElement" ); targetElement = nativElement.parentElement; //get container element/table cell } let X, Y; DEBUG && console.group("getPopupPos"); DEBUG && console.log( "elementLinkHasNoText: " + elementLinkHasNoText + ", targetElement.textContent: " + targetElement.textContent + " targetElement.textContent.length: " + targetElement.textContent.length ); DEBUG && console.log(nativElement.parentElement); DEBUG && console.log(nativElement.parentElement.tagName); DEBUG && console.log(targetElement); let targetRect = targetElement.getBoundingClientRect(); const targetLineRects = targetElement.getClientRects(); let firstLineRect, lastLineRect, firstRectIndex; DEBUG && console.log("targetLineRects.length: " + targetLineRects.length); for (let i = 0; i < targetLineRects.length; i++) {} if (targetLineRects.length > 0) { firstRectIndex = 0; if (showIconNextToLink && targetLineRects.length > 1) { firstRectIndex = 1; //TODO to double check if needed sometimes the pseudo element(state icon) seems to be counted in the DOMRectList //chrome https://www.novelupdates.com/reading-list/?list=0 //vs https://forum.novelupdates.com/threads/chapter-numbering-error.119015/ pseudo element not counted? } //console.log(targetLineRects) firstLineRect = targetLineRects[firstRectIndex]; lastLineRect = targetLineRects[targetLineRects.length - 1]; //TODO check if line.bottom is visible in container(edge case word wrapped link in container with overflow:hidden) else loop check previous line. } else { //case empty text link with only an image firstLineRect = targetRect; } if (lastLineRect) targetRect = lastLineRect; const Rx = targetRect.right; const Ry = targetRect.bottom; DEBUG && console.group("debug rects"); DEBUG && console.log("scrollPosX: " + scrollPosX); DEBUG && console.log("scrollPosY: " + scrollPosY); DEBUG && console.log( "document.body.offsetHeight: " + document.body.offsetHeight + ", document.body.scrollHeight: " + document.body.scrollHeight ); DEBUG && console.log("window.innerHeight: " + window.innerHeight); DEBUG && console.log("window.innerWidth: " + window.innerWidth); DEBUG && console.log(targetElement); DEBUG && console.log(targetLineRects); DEBUG && console.log(firstLineRect); DEBUG && console.log(lastLineRect); DEBUG && console.log("Rx: " + Rx + ", Ry: " + Ry); DEBUG && console.groupEnd(); const nonOverlappingArrowWidth = (arrowWidth * 3) / 4; const marginY = offsetToBottomBorderY + nonOverlappingArrowWidth; let distanceToTopFromLink = firstLineRect.top - nonOverlappingArrowWidth; let distanceToBottomFromLink = window.innerHeight - targetRect.bottom - offsetToBottomBorderY + nonOverlappingArrowWidth; //lastLineRect -> targetRect let distanceToRightFromlink = window.innerWidth - targetRect.right; let distanceToLeftFromlink = targetRect.left; let arrowTableSideRight = true; //reset width/height //popover.style.height="100%"; if (supportsCSSMin) { popover.style.height = ""; popover.style.width = ""; } else { popover.style.height = targetHeight + "px"; popover.style.width = targetWidth + "px"; } //#region set to top/bottom position DEBUG && console.group("top/bottom placement"); let upside = false; let targetYPosition = 0; let maxHeight = targetHeight; let maxHeightInsideAvailableSpace; //#region calculate maxHeight between targetHeight and image/popover height popover.style.maxHeight = targetHeight + "px"; //setPopoverHeight(targetHeight,0); if (popoverRect.height < targetHeight) { maxHeight = popoverRect.height; } else maxHeight = targetHeight; //#endregion maxHeightInsideAvailableSpace = maxHeight; if (!(isOnIndex || tableElement)) { //#region "not on index/table cell td" DEBUG && console.log("not on index/table cell td"); /* bottom bigger than top || bottomSpaceIsBiggerThanTargetHeight -> bottom bigger than full height -> bottom smaller than full height bottom smaller than top -> top bigger than full height -> top smaller than full height */ const targetHeightMarginAdded = targetHeight + marginY; console.log("targetHeightMarginAdded: " + targetHeightMarginAdded); const bottomHasMoreSpaceThanTop = distanceToBottomFromLink > distanceToTopFromLink; const bottomSpaceIsBiggerThanTargetHeight = distanceToBottomFromLink > targetHeight + marginY; //maxHeight; DEBUG && console.log( "bottomHasMoreSpaceThanTop: " + bottomHasMoreSpaceThanTop + ", distanceToTopFromLink: " + distanceToTopFromLink + ", distanceToBottomFromLink: " + distanceToBottomFromLink ); console.log( "bottomSpaceIsBiggerThanTargetHeight: " + bottomSpaceIsBiggerThanTargetHeight + ", targetHeight: " + targetHeight + ", maxHeight: " + maxHeight + ", popoverRect.height: " + popoverRect.height + ", marginY: " + marginY + ", distanceToBottomFromLink: " + distanceToBottomFromLink + ", targetHeight + marginY: " + targetHeightMarginAdded ); if (bottomHasMoreSpaceThanTop || bottomSpaceIsBiggerThanTargetHeight) { //arrow at top, popup under link //#region bottom is bigger than top - show on bottom upside = true; DEBUG && console.group("bottom is bigger than top"); console.log( "lastLineRect.bottom: " + lastLineRect.bottom + ", nonOverlappingArrowWidth: " + nonOverlappingArrowWidth ); targetYPosition = lastLineRect.bottom + nonOverlappingArrowWidth; // let targetYAdjustment; if (!bottomSpaceIsBiggerThanTargetHeight) { DEBUG && console.log( "show reduced height. spacer under link is smaller than target height" ); maxHeight = distanceToBottomFromLink - marginY; //targetHeight; } DEBUG && console.groupEnd(); //#endregion bottom is bigger than top - show on bottom } else { //arrow at bottom //#region "bottom has less space than top" -> show on top DEBUG && console.group("bottom has less space than top"); //"show full height BetweenFirstLineAndTop" //targetYPosition = firstLineRect.top - maxHeight - halfArrowWidth; //maxHeightInsideAvailableSpace = maxHeight; console.log(popoverRect); console.log( "targetHeight: " + targetHeight + ", popoverRect.height: " + popoverRect.height + ", maxHeight without offset reduction: " + maxHeight + ", distanceToTopFromLink: " + distanceToTopFromLink + ", distanceToTopFromLink < maxHeight: " + (distanceToTopFromLink < maxHeight) + ", marginY: " + marginY ); if (distanceToTopFromLink < maxHeight + marginY) { //#region "available maxHeight is smaller than targeted maxHeight -show reduced height BetweenFirstLineAndTop" console.log( "maxHeight is smaller than distanceToTopFromLink -show reduced height BetweenFirstLineAndTop" ); targetYPosition = firstLineRect.top - maxHeight - nonOverlappingArrowWidth; maxHeightInsideAvailableSpace = distanceToTopFromLink; //reduced maxHeight console.log( "maxHeightReducedToAvailableSpace?: " + maxHeightInsideAvailableSpace + ", targetYPosition: " + targetYPosition ); console.log( "targetYPosition: " + targetYPosition + ", marginY: " + marginY ); if (targetYPosition < marginY) { //if targetYPosition(firstLineRect.top - maxHeight - marginY;) smaller marginY console.log( "reposition smaller than marginY. Set at offset marginY" ); DEBUG && console.log( "targetheight overflows at top side, maxHeight before margin change: " + maxHeight + ", new maxHeightInsideAvailableSpace: " + maxHeightInsideAvailableSpace ); const magicNumber = 3; //weird. container position relative top=-3 why needed? targetYPosition = offsetToBottomBorderY - magicNumber + nonOverlappingArrowWidth; maxHeightInsideAvailableSpace = distanceToTopFromLink - marginY; } //#endregion "maxHeight is smaller than distanceToTopFromLink -show reduced height BetweenFirstLineAndTop" } else { //#region "show full height BetweenFirstLineAndTop" DEBUG && console.log("show full height BetweenFirstLineAndTop"); DEBUG && console.log( "maxHeight: " + maxHeight + ", nonOverlappingArrowWidth: " + nonOverlappingArrowWidth ); const magicNumber = 3; maxHeightInsideAvailableSpace = maxHeight + nonOverlappingArrowWidth; targetYPosition = firstLineRect.top - maxHeightInsideAvailableSpace - magicNumber; //#endregion "show full height BetweenFirstLineAndTop" } DEBUG && console.log( "targetYPosition: " + targetYPosition + ", maxHeightInsideAvailableSpace: " + maxHeightInsideAvailableSpace + ", distanceToTopFromLink: " + distanceToTopFromLink + ", targetHeight: " + targetHeight + ", firstLineRect.top: " + firstLineRect.top + ", scrollPosY: " + scrollPosY + ", popoverRect.height: " + popoverRect.height ); DEBUG && console.groupEnd(); //#endregion "bottom has less space than top" } //#endregion "not on index/table cell td" } else { //#region "on index/table cell td" DEBUG && console.log("on index/table cell td"); //auto height push up along table column DEBUG && console.log( ", distanceToBottomFromLink: " + distanceToBottomFromLink + ", targetHeight: " + targetHeight ); targetYPosition = lastLineRect.top; const overFlowPxY = maxHeight - distanceToBottomFromLink; if (overFlowPxY > 0) { DEBUG && console.log("space under link smaller targetHeight. move popover up"); DEBUG && console.log("lastLineRect.top: " + lastLineRect.top + ", Y" + Y); targetYPosition -= overFlowPxY; } const posOverTopBorder = targetYPosition < offsetToBottomBorderY; if (posOverTopBorder) { //top offset DEBUG && console.log("overflows top border set to offsetToBottomBorderY"); targetYPosition = offsetToBottomBorderY; } //#endregion "on index/table cell td" } //Y += scrollPosY; DEBUG && console.log( "maxHeight: " + maxHeight + ", targetYPosition: " + targetYPosition ); setPopoverHeight(maxHeightInsideAvailableSpace, targetYPosition); Y = targetYPosition + scrollPosY; DEBUG && console.log("popover.style.height: " + popover.style.height); DEBUG && console.groupEnd(); //#endregion set to top/bottom position //#region select from which line to calculate the right position if (!upside) { targetRect = firstLineRect; distanceToRightFromlink = window.innerWidth - targetRect.right; } //#endregion //#region set to left/right position DEBUG && console.group("left/right placement"); if (!(isOnIndex || tableElement)) { DEBUG && console.log( "placement on non table cell/container list. non index list" ); X = targetRect.right; DEBUG && console.log( "full calculated width for non reposition: " + (X + distanceToRightFromlink + offsetToRightBorderX) ); DEBUG && console.log( "lastLineRect.right: " + targetRect.right + ", distanceToRightFromlink: " + distanceToRightFromlink + ", targetWidth: " + targetWidth ); if (distanceToRightFromlink - offsetToRightBorderX * 2 > targetWidth) { DEBUG && console.log( "full popup width with padding without reposition: " + targetWidth ); } else { let diffX = targetWidth - distanceToRightFromlink + offsetToRightBorderX * 2; DEBUG && console.log("touch right side, move left of amount diffX: " + diffX); //move left amount diffX X -= diffX; if (X + targetWidth < targetRect.left) { X = targetRect.left - targetWidth; } } } else { DEBUG && console.log("on Index/tablecell"); distanceToRightFromlink = distanceToRightFromlink - arrowWidth; X = targetRect.right; const rightBiggerThenLeft = distanceToLeftFromlink < distanceToRightFromlink; const containerWidthWithPadding = targetWidth + arrowWidth + offsetToRightBorderX; DEBUG && console.log( "distanceToRightFromlink: " + distanceToRightFromlink + ", containerWidthWithPadding: " + containerWidthWithPadding ); if ( rightBiggerThenLeft || distanceToRightFromlink > containerWidthWithPadding ) { DEBUG && console.log( "right side space bigger than left side. distanceToRightFromlink: " + distanceToRightFromlink ); const availableSpaceRight = distanceToRightFromlink + offsetToRightBorderX * 2; DEBUG && console.log( "targetWidth: " + targetWidth + ", containerWidthWithPadding: " + containerWidthWithPadding ); if (availableSpaceRight < containerWidthWithPadding) { DEBUG && console.log( "right space smaller than targetWidth. distanceToRightFromlink: " + distanceToRightFromlink ); //X = lastLineRect.right; popover.style.width = distanceToRightFromlink - offsetToRightBorderX * 2 + "px"; } else { DEBUG && console.log( "right space bigger than targetWidth+offset" + ", distanceToRightFromlink: " + distanceToRightFromlink + ", offsetToRightBorderX: " + offsetToRightBorderX + ", availableSpaceRight : " + availableSpaceRight + ", containerWidthWithPadding: " + containerWidthWithPadding ); popover.style.width = targetWidth + "px"; // X = lastLineRect.right; } } else { DEBUG && console.log("right side smaller than left side"); X = targetRect.left - containerWidthWithPadding; arrowTableSideRight = false; if (distanceToLeftFromlink > containerWidthWithPadding) { DEBUG && console.log("move to left side of container"); } else { DEBUG && console.log("shrink popupwidth"); if (X < offsetToRightBorderX) { X = offsetToRightBorderX; popover.style.width = distanceToLeftFromlink - offsetToRightBorderX * 2 - arrowWidth + "px"; } } //X = targetRect.left-; } } DEBUG && console.log("X: " + X + ", popover.style.width: " + popover.style.width); DEBUG && console.groupEnd(); //#endregion DEBUG && console.groupEnd(); //let tableMargin=0; //if(isOnIndex || tableElement) //tableMargin = arrowWidth; //popover.style.top = Y + "px"; // popover.style.left = X +tableMargin+ "px"; /* showBorderArrowBox( event, arrowWidth, upside, isExternal, arrowTableSideRight );*/ return { Px: X, Py: Y, upside: upside, arrowTableSideRight: arrowTableSideRight, }; } function sleep() { return new Promise(requestAnimationFrame); } // popupPositioning function async function popupPos(event, isExternal = false) { const DEBUG = false; DEBUG && console.group("popupPos showSmaller:" + showSmaller); if (event && event !== undefined) { showPopOver(); await sleep(); await sleep(); DEBUG && console.group("popover rect"); DEBUG && console.log(popover); DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight); DEBUG && console.log("popover[0].offsetHeight: " + popover.offsetHeight); DEBUG && console.groupEnd("popover rect"); const { Px, Py, upside, arrowTableSideRight } = getPopupPos( event, arrowWidthInPx, isExternal ); let tableMargin = 0; let tableElement = checkIsTableElement(event.target); if (isOnIndex || tableElement) tableMargin = arrowWidthInPx; popover.style.left = Px + tableMargin + "px"; if (!upside) { //popover.style.bottom = "unset"; popover.style.top = Py + "px"; } else { //popover.style.bottom="unset"; popover.style.top = Py + "px"; } showBorderArrowBox( event, arrowWidthInPx, upside, isExternal, arrowTableSideRight ); //const popoverHeightMargin = offsetToBottomBorderY * 2; //const popoverWidthMargin = offsetToRightBorderX * 2; /* popover.style.maxHeight = "min(400px,calc(100% - " + popoverHeightMargin + "px))"; popover.style.maxWidth = "min(800px,calc(100% - " + popoverWidthMargin + "px))"; */ DEBUG && console.log(popover.getBoundingClientRect()); DEBUG && console.log( "window.innerHeight: " + window.innerHeight + ", window.innerWidth: " + window.innerWidth ); DEBUG && console.groupEnd("popupPos"); //console.log("final popup position "+X+' # '+Y); // return this; autoScrollData(); autoScrollData("coverPreviewContentAutoScroll"); } } function tryToGetTextContent(element, query, queryName) { let r###lt = element; if (r###lt && r###lt !== undefined) { r###lt = r###lt.innerHTML; //changed from textContent to innerHTML to get html tags(b/i/p/br) and text line breaks r###lt = r###lt.replace(reWhiteListStripHTML, ""); //strip all other html tags } else if (element !== null && element !== undefined) { console.log( "Wrong or changed querySelector for " + queryName + ". not: " + query ); } return r###lt; } function getTargetDomain(individualSiteLink) { let domain = ""; if (individualSiteLink) { //console.log(individualSiteLink); let hasSlashIndex = individualSiteLink.indexOf("/"); if (hasSlashIndex) hasSlashIndex = hasSlashIndex + 1; else hasSlashIndex = individualSiteLink.length - 1; domain = individualSiteLink.slice(0, hasSlashIndex); } return domain; } function getLinkID(link, individualPage) { const DEBUG = false; let ID, domainInfrontOfID; let stringFromID; let boolHasSlashAfterID = false; if (link && individualPage) { //example individualPage = "mangadex.org/title/" DEBUG && console.group("getLinkID"); DEBUG && console.log(link); const isApiLink = link.indexOf(individualPage); let linkAfterDomain; if (isApiLink >= 0) { let indexID = isApiLink + individualPage.length; domainInfrontOfID = link.slice(0, indexID); linkAfterDomain = link.slice(indexID); DEBUG && console.log("linkAfterDomain: " + linkAfterDomain); if (individualPage && externalLinks[individualPage]) { DEBUG && console.log( "externalLink: " + individualPage + ", match with linkAfterDomain to get full ID: " + linkAfterDomain ); //externalLinks if (externalLinks[individualPage].serieRegex) { stringFromID = linkAfterDomain.match( externalLinks[individualPage].serieRegex ); } DEBUG && console.log(stringFromID); DEBUG && console.log("stringFromID: " + stringFromID); if (stringFromID === null) { if (externalLinks[individualPage].serieRegex2) { DEBUG && console.log("getLinkID serieRegex2 for: " + individualPage); stringFromID = linkAfterDomain.match( externalLinks[individualPage].serieRegex2 ); } else { stringFromID = linkAfterDomain.match(defaultSerieRegex); } } //stringFromID = link.slice(indexID); } else { DEBUG && console.log( "internallink, match with linkAfterDomain to get full ID: " + linkAfterDomain ); //internalLink stringFromID = linkAfterDomain.match( internalLink[INDIVIDUALPAGETEST].serieRegex ); //stringFromID = link.slice(indexID); } DEBUG && console.log( "domainInfrontOfID: " + domainInfrontOfID + ", stringFromID: " + stringFromID ); if (stringFromID.length > 0) { if (stringFromID[1]) { stringFromID = stringFromID[1]; //exact match } else { stringFromID = stringFromID[0]; //complete matching string after domain } } DEBUG && console.log(stringFromID); DEBUG && console.log(stringFromID); /* let hasSlashAfterID = stringFromID.indexOf("/"); boolHasSlashAfterID = hasSlashAfterID>=0; if (boolHasSlashAfterID) ID = stringFromID.slice(0, hasSlashAfterID); else */ //ID = stringFromID; //DEBUG && console.log(ID); } DEBUG && console.groupEnd(); } return { ID: stringFromID, Domain: domainInfrontOfID }; //,hasSlashAfterID:boolHasSlashAfterID } async function getCoverDataFromUrl( elementUrl, external = false, individualPage = undefined ) { const DEBUG = false; DEBUG && console.group("getCoverDataFromUrl for: " + currentTitelHover); const isExternal = external; let coverData; let linkID; let hasApiAccess; if (isExternal) hasApiAccess = externalLinks[individualPage].mainAPI; else { hasApiAccess = internalLink[INDIVIDUALPAGETEST].mainAPI; //todo change with individualPage. Forward individualPage for internal site } //console.log(hasApiAccess); //console.log("xhr.finalUrl: " + xhr.finalUrl +", elementUrl: " + elementUrl) DEBUG && console.log("elementUrl: " + elementUrl); elementUrl = getLinkToSeriePage(elementUrl, individualPage); DEBUG && console.log("getLinkToSeriePage() -> elementUrl: " + elementUrl); if (hasApiAccess) { const { ID } = getLinkID(elementUrl, individualPage); linkID = ID; //console.log(linkID); DEBUG && console.log("linkID: " + linkID); } DEBUG && console.log("elementUrl: " + elementUrl); let targetDomain; let hasExternalTargetPage; let targetPage; //#region check is external link DEBUG && console.log( "isExternal: " + isExternal + ", individualPage: " + individualPage ); if (isExternal && individualPage) { hasExternalTargetPage = externalLinks[individualPage]; if (hasExternalTargetPage) { targetPage = hasExternalTargetPage; targetDomain = getTargetDomain(individualPage); } } else { targetPage = internalLink[internalLinkKey[0]]; //targetDomain = getTargetDomain(INDIVIDUALPAGETEST); //for now leave empty (since only one internal link possible) } DEBUG && console.log( "hasExternalTargetPage: " + hasExternalTargetPage + ", targetDomain: " + targetDomain ); //#endregion let apiData; if (hasApiAccess) { DEBUG && console.log("hasApiAccess for individualPage: " + individualPage); switch (individualPage) { //API access case "mangadex.org/manga/": case "mangadex.org/title/": //url: "https://mangadex.org/api/v2/manga/" + id, apiData = await getCoverDataFromMangaDex(linkID); break; case "www.tvmaze.com/shows/": apiData = await getCoverDataFromTVmaze(linkID); break; case "wlnupdates.com/series-id/": apiData = await getCoverDataFromWLNupdates(linkID); break; } } else { DEBUG && console.log( "before getCoverDataFromParsingTargetUrl - elementUrl: " + elementUrl ); //coverData = Object.assign({}, emptyCoverData); apiData = await getCoverDataFromParsingTargetUrl( elementUrl, targetPage, isExternal ); DEBUG && console.log(apiData); } DEBUG && console.log(apiData); let imagelink; if (apiData !== undefined) { coverData = apiData; imagelink = coverData.url; } //console.log(imagelink); //console.log("save imageUrl into coverData.url: " + imagelink); let externalUrl; //console.log("targetDomain: " + targetDomain) if (isExternal && targetDomain) externalUrl = targetDomain; imagelink = processRelativeImageLink(imagelink, targetDomain, isExternal); //console.log("serieAlternativeNames: " + serieAlternativeNames) let cData; DEBUG && console.log("externalUrl: " + externalUrl); DEBUG && console.log(coverData); //#region merge final complete coverData if (coverData !== undefined) { cData = coverData; cData.isExternal = externalUrl; cData.url = imagelink; //console.log(coverData); //console.log(cData); } //#endregion merge final complete coverData DEBUG && console.log(cData); GM_setCachedValue(elementUrl, cData); //cache imageurl link DEBUG && console.log( elementUrl + " url has been found and is written to temporary cache.\n" + imagelink + " successfully cached." ); // for testing purposes DEBUG && console.groupEnd("parseSeriePage onLoad"); return cData; } function processRelativeImageLink(imagelink, targetDomain, isExternal) { const DEBUG = false; //#region adjust relative link to absolute domain if (imagelink && imagelink !== undefined && imagelink !== null) { if (imagelink.tagName && imagelink.tagName == "IMG") { imagelink = imagelink.getAttribute("src"); } if (imagelink instanceof HTMLElement) { let hasSrc = imagelink.getAttribute("src"); if (hasSrc) imagelink = imagelink.getAttribute("src"); } DEBUG && console.log(imagelink); if (imagelink.startsWith("//")) { imagelink = "https://" + imagelink.slice(2); } if (imagelink.startsWith("/")) { DEBUG && console.log(targetDomain); DEBUG && console.log(imagelink); imagelink = targetDomain + imagelink; } if ( isExternal && !(imagelink.startsWith("http://") || imagelink.startsWith("https://")) ) { //if relativeLink on external site change to absolute link imagelink = "https://" + imagelink; } } //#endregion return imagelink; } function rejectErrorStatusMessage(xhr) { let rejectNotification = ""; //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status switch (true) { case xhr.status == 400: rejectNotification = "bad request: xhr response == " + xhr.status; break; case xhr.status == 401: rejectNotification = "unauthorized: xhr response == " + xhr.status; break; case xhr.status == 402: rejectNotification = "Payment Required: xhr response == " + xhr.status; break; case xhr.status == 403: rejectNotification = "Forbidden: xhr response == " + xhr.status; break; case xhr.status == 404: rejectNotification = xhr.finalUrl + "<br/>page not found: xhr response == " + xhr.status; break; case xhr.status == 408: rejectNotification = "request timeout: xhr response == " + xhr.status; break; case xhr.status == 410: rejectNotification = "page gone and not available: xhr response == " + xhr.status; break; case xhr.status == 425: rejectNotification = "page request too early: xhr response == " + xhr.status; break; case xhr.status == 429: rejectNotification = "rate limit reached. Please notify to increase/adjust rate limit to setting for this domain: xhr response == " + xhr.status; break; case xhr.status == 451: rejectNotification = "page was removed cause of legal reasons: xhr response == " + xhr.status; break; case xhr.status == 500: { console.log(xhr.responseHeaders); let serverMessage = xhr.responseHeaders .toString() .match(new RegExp("server:(.*)")); if (serverMessage != null) rejectNotification = "page has an internal server error: xhr response == " + xhr.status + "<br />" + serverMessage; } break; case xhr.status == 503: rejectNotification = "page Unavailable. Down for maintenance or overloaded: xhr response == " + xhr.status; break; case xhr.status == 511: rejectNotification = "page Network Authentication Required: xhr response == " + xhr.status; break; default: rejectNotification = xhr; } return rejectNotification; } function setLinkState(element, state = undefined, preloadUrlRequest = false) { const DEBUG = false; if (element) { let hasText = element.textContent != ""; if (showIconNextToLink) { /* 0: popup possible/no coverdata preloaded; if preloadUrlRequest true set inactive preloading icon 1: currently active loading coverData link 2: coverData preloaded */ const externalTarget = element.getAttribute("coverDataExternalTarget"); let elementUrl = getLinkToSeriePage(element.href, externalTarget); DEBUG && console.group("link: " + elementUrl); DEBUG && console.log("state before check: " + state); if (state === undefined) { const coverData = GM_getCachedValue(elementUrl); DEBUG && console.log(coverData); if ( coverData === undefined || coverData === null || coverData === "null" || preloadUrlRequest ) { state = linkIconEnum.popupPossibleNotLoadedOrMarkedForPreloading; //no coverData wating for interaction or forced reloading/preloading } else { state = linkIconEnum.popupHasCoverData; //coverData available and loaded } } DEBUG && console.log("state: " + state); DEBUG && console.groupEnd(); element.classList.remove( "hasCoverPreviewPopup", "loadingUrlPreload", "loadingUrl", "hasLoadedCoverPreviewPopup" ); switch (state) { case linkIconEnum.popupPossibleNotLoadedOrMarkedForPreloading: //popup possible/no coverdata preloaded; if preloadUrlRequest true set inactive preloading icon if (hasText) { if (preloadUrlRequest) { element.classList.add("loadingUrlPreload"); } else { element.classList.add("hasCoverPreviewPopup"); } } break; case linkIconEnum.popupLoading: //currently loading coverData if (hasText) element.classList.add("loadingUrl"); break; case linkIconEnum.popupHasCoverData: //coverData preloaded if (hasText) element.classList.add("hasLoadedCoverPreviewPopup"); break; case linkIconEnum.error: //don't add any icon //console.log("set state to linkError") break; } } else { //if not showIconNextToLink -> cleanup icons element.classList.remove( "hasCoverPreviewPopup", "loadingUrlPreload", "loadingUrl", "hasLoadedCoverPreviewPopup" ); } } } function setLinkStateOfSameLinks(element, state, preloadUrlRequest = false) { const DEBUG = false; const elementUrl = element.href; //console.log("elementUrl: " + elementUrl) //console.log(elementUrl) const sameLinks = document.querySelectorAll('a[href="' + elementUrl + '"]'); DEBUG && console.group("setLinkStateOfSameLinks: " + elementUrl); DEBUG && console.log(sameLinks); DEBUG && console.log("sameLinks.length: " + sameLinks.length); DEBUG && console.groupEnd(); if (sameLinks.length > 0) { for (let i = 0; i < sameLinks.length; i++) { DEBUG && console.log(sameLinks[i]); setLinkState(sameLinks[i], state, preloadUrlRequest); } } } function getLinkToSeriePage(elementUrl, individualPage = undefined) { const DEBUG = false; if (individualPage) { let linkID; const { ID, Domain } = getLinkID(elementUrl, individualPage); linkID = ID; //console.log(linkID); elementUrl = Domain + linkID; //in case of direct chapter link provided with linkIK DEBUG && console.log( "new elementUrl: " + elementUrl + ", individualPage: " + individualPage + ", linkID: " + linkID + ", Domain: " + Domain ); } return elementUrl; } async function parseSeriePage( element, forceReload = false, hoveredTitle = undefined, event = undefined, external = false, targetPage = undefined ) { const DEBUG = false; DEBUG && console.log("before getLinkToSeriePage - element.href: " + element.href); const elementUrl = getLinkToSeriePage(element.href, targetPage); DEBUG && console.group("parseSeriePage: " + elementUrl); DEBUG && console.log("elementUrl: " + elementUrl); let coverData; if (!forceReload) coverData = GM_getCachedValue(elementUrl); // let retrievedImgLink; let PromiseR###lt; DEBUG && console.log(coverData); if ( !forceReload && coverData !== undefined && coverData !== null && coverData.title ) { //retrievedImgLink = coverData.url; DEBUG && console.log( "parseSeriePage has cached coverData for: " + coverData.title + " at " + elementUrl ); PromiseR###lt = coverData; } else { //console.log("before showPopupLoadingSpinner") showPopupLoadingSpinner(hoveredTitle, hoveredTitle, event); //console.log("before getCoverDataFromUrl - elementUrl: " + elementUrl) PromiseR###lt = getCoverDataFromUrl(elementUrl, external, targetPage); } DEBUG && console.groupEnd("parseSeriePage: " + elementUrl); PromiseR###lt = await PromiseR###lt; setLinkStateOfSameLinks(element, linkIconEnum.popupHasCoverData); //coverData loading finished DEBUG && console.log(PromiseR###lt); //DEBUG && console.log(PromiseR###lt) //after GM_xmlhttpRequest PromiseR###lt return PromiseR###lt; } //renamed targetNodeArray -> arrayTargetNode to remove compatibility mode in tampermonkey function removeEventListenerFromNodes(arrayTargetNode, external = false) { if (arrayTargetNode && arrayTargetNode.length > 0) { //console.log(targetNodeArray); arrayTargetNode.map(function (el) { if ( eventListenerStyle === undefined || eventListenerStyle === null || eventListenerStyle == 0 ) { el.removeEventListener("mouseenter", mouseEnterPopup); el.removeEventListener("mouseleave", hideOnMouseLeave); } }); } } function wait(ms) { return new Promise((resolve, reject) => setTimeout(resolve, ms)); } //renamed targetNodeArray -> arrayTargetNode to remove compatibility setting in tampermonkey async function preloadForIndividualPageTest( arrayTargetNode = [], individualLinksToTest, external = false, forceReload = false ) { const DEBUG = false; DEBUG && console.log(arrayTargetNode); DEBUG && console.log("preloadCoverData"); DEBUG && console.log( "before parseSeriePage for each url with a link to individual seriepage" ); //#region addEventlistener if (arrayTargetNode && arrayTargetNode.length > 0) { //console.log(targetNodeArray); arrayTargetNode.map(function (el) { //console.log(el) // const elementUrl = el.href; //const externalTarget = el.getAttribute("coverDataExternalTarget"); // console.log(elementUrl) if ( eventListenerStyle === undefined || eventListenerStyle === null || eventListenerStyle == 0 ) { //console.log(el); //TODO external overwrite/removes previous mouseEnterPopup? el.addEventListener("mouseenter", mouseEnterPopup); el.addEventListener("mouseleave", hideOnMouseLeave); /* if (external) el.setAttribute("coverDataExternalTarget", IndividualTargetToTest);*/ } }); } //#endregion addEventlistener //#region preload if (arrayTargetNode && arrayTargetNode.length > 0) { let nodeArrayIndex = 0; //rateLimitQueryAfterSeconds:"0.5" //if not set, but both other values are available will be calculated into rateLimitTimeInSeconds/rateLimitCount let hasRateLimitQueryAfterSeconds, hasRateLimitTimeInSeconds, hasRateLimitCount, hasRateLimitQueryAfterMS; if (external) { //externalLinks[individualLinksToTest]["lastQuery"] = Date.now(); hasRateLimitQueryAfterSeconds = externalLinks[individualLinksToTest].rateLimitQueryAfterSeconds; if (!hasRateLimitQueryAfterSeconds) { hasRateLimitTimeInSeconds = externalLinks[individualLinksToTest].rateLimitTimeInSeconds; hasRateLimitCount = externalLinks[individualLinksToTest].rateLimitCount; if (hasRateLimitTimeInSeconds && hasRateLimitCount) { hasRateLimitQueryAfterSeconds = hasRateLimitTimeInSeconds / hasRateLimitCount; } else { hasRateLimitQueryAfterSeconds = defaultRateLimitQueryAfterSeconds; } } } else { hasRateLimitQueryAfterSeconds = internalLink[individualLinksToTest].rateLimitQueryAfterSeconds; if (!hasRateLimitQueryAfterSeconds) { hasRateLimitTimeInSeconds = internalLink[individualLinksToTest].rateLimitTimeInSeconds; hasRateLimitCount = internalLink[individualLinksToTest].rateLimitCount; if (hasRateLimitTimeInSeconds && hasRateLimitCount) { hasRateLimitQueryAfterSeconds = hasRateLimitTimeInSeconds / hasRateLimitCount; } else { hasRateLimitQueryAfterSeconds = defaultRateLimitQueryAfterSeconds; } } } if (hasRateLimitQueryAfterSeconds) { hasRateLimitQueryAfterMS = hasRateLimitQueryAfterSeconds * 1000; } DEBUG && console.log("hasRateLimitQueryAfterMS: " + hasRateLimitQueryAfterMS); if ( arrayTargetNode.length > 0 && (forceReload || preloadUrlRequests) && !deactivatePreloadUrlRequestOnUrls.includes(individualLinksToTest) ) { while (nodeArrayIndex < arrayTargetNode.length) { const element = arrayTargetNode[nodeArrayIndex]; const elementUrl = getLinkToSeriePage( element.href, individualLinksToTest ); DEBUG && console.log("elementUrl: " + elementUrl); DEBUG && console.log( "start parseSeriePage for links of domain: " + individualLinksToTest ); let targetPage = undefined; if (external) { targetPage = individualLinksToTest; //console.log("targetPage: " + targetPage); } // console.log("external: " + external); const hoveredTitle = undefined; const event = undefined; const coverData = GM_getCachedValue(elementUrl); DEBUG && console.group( "preloadForIndividualPageTest GM_getCachedValue elementUrl: " + elementUrl ); DEBUG && console.log("elementUrl: " + elementUrl); DEBUG && console.log(coverData); DEBUG && console.groupEnd(); /* if (!(coverData!==undefined && coverData !== null && coverData != "null")) {//has coverData setLinkState(element, 0, forceReload);//set inactive preloadingMarker or preloaded Data }else{ setLinkState(element, 1, forceReload); //active loading icon }*/ setLinkState(element, linkIconEnum.popupLoading, forceReload); //active loading icon const promiseParsingPage = parseSeriePage( element, forceReload, hoveredTitle, event, external, targetPage ).then( function (coverData) { if (coverData !== undefined) { if (preloadImages) { DEBUG && console.log( "preloadCoverData preloadImages: " + preloadImages ); DEBUG && console.log(coverData); loadImageFromBrowser({ coverData: coverData }); } } }, function (Error) { DEBUG && console.log(Error + " failed to fetch " + element); GM_deleteValue(element.href); setLinkStateOfSameLinks(element, linkIconEnum.error); } ); if (hasRateLimitQueryAfterMS) { // const coverData = GM_getCachedValue(elementUrl); //console.log("preloaded url: " + elementUrl); if ( !( coverData !== undefined && coverData !== null && coverData != "null" ) ) { await Promise.all([ promiseParsingPage, wait(hasRateLimitQueryAfterMS), ]); } else { setLinkState(element, undefined, forceReload); //coverData already loaded } } nodeArrayIndex++; } } } } async function preloadCoverData(forceReload = false) { const DEBUG = false; //#region create complete nodelist ALLSERIENODES = []; ALLEXTERNALLINKNODES = []; let allPreloadingPromises = []; //console.log("preloadCoverData forceReload: " + forceReload) updateSerieNodes(ALLSERIENODES, INDIVIDUALPAGETEST, forceReload); if (externalLinks && externalLinkKeys.length > 0) { for (let i = 0; i < externalLinkKeys.length; i++) { updateSerieNodes( ALLEXTERNALLINKNODES, externalLinkKeys[i], forceReload, true ); } } //#endregion //#region add eventlistener mouseenter/leave to links and preloadCoverData if preloading set to true removeEventListenerFromNodes(ALLSERIENODES); allPreloadingPromises.push( preloadForIndividualPageTest( ALLSERIENODES, INDIVIDUALPAGETEST, false, forceReload ) ); //console.log(externalLinks); //console.log(externalLinkKeys[0]); //console.log(externalLinkKeys.length); removeEventListenerFromNodes(ALLEXTERNALLINKNODES); //console.log(ALLEXTERNALLINKNODES) if (ALLEXTERNALLINKNODES.length > 0) { if (externalLinks && externalLinkKeys.length > 0) { for (let i = 0; i < externalLinkKeys.length; i++) { allPreloadingPromises.push( preloadForIndividualPageTest( ALLEXTERNALLINKNODES.filter((link) => link.href.includes(externalLinkKeys[i]) ), externalLinkKeys[i], true, forceReload ) ); } } } //#endregion if (forceReload) { await Promise.all(allPreloadingPromises); console.log( "has finished preloading all links on this page: " + window.location.href ); } } function addStyles() { GM_addStyle(` @keyframes rotate { to {transform: rotate(360deg);} } @keyframes dash { 0% { stroke-dasharray: 1, 150; stroke-dashoffset: 0; } 50% { stroke-dasharray: 90, 150; stroke-dashoffset: -35; } 100% { stroke-dasharray: 90, 150; stroke-dashoffset: -124; } } .spinner { /* z-index: 2; position: absolute; top: 0; left: 0; margin: 0;*/ width: 100%; height: 100%; } .spinner .path{ stroke: hsl(210, 70%, 75%); stroke-linecap: round; animation: dash 1.5s ease-in-out infinite; } #popover_arrow{ margin: unset; padding: unset; text-size-adjust: unset; line-height: unset; font-family: unset; font: unset; opacity: 1; visibility:visible; transition: visibility 0.2s, opacity 0.2s linear; } .arrowCSS{ box-sizing: border-box; border: 1px solid #000; box-shadow:1px 1px 0px #7A7A7A; border-width: 0 2px 2px 0; display: inline-block; padding: 0; margin: unset; text-size-adjust: unset; line-height: unset; font-family: unset; font: unset; } @keyframes dotsLoading{ 0%{ opacity: 0; } 50%{ opacity: 1; } 100%{ opacity: 0; } } #dotLoading1{ animation: dotsLoading 1s infinite; } #dotLoading2{ animation: dotsLoading 1s infinite; animation-delay: 0.2s; } #dotLoading3{ animation: dotsLoading 1s infinite; animation-delay: 0.4s; } .loadingUrl, .loadingUrlPreload, .hasCoverPreviewPopup, .hasLoadedCoverPreviewPopup{ /* position:relative; display: inline;*/ padding:0 !important; margin:0 !important; } .loadingUrl:link, .loadingUrlPreload:link, .hasCoverPreviewPopup:link, .hasLoadedCoverPreviewPopup:link{ padding:0 !important; margin:0 !important; } .loadingUrl::before{ content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-dots" width="14" height="14" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M0 0h24v24H0z" stroke="none" fill="none"/><path stroke="red" d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" /><line x1="12" y1="11" x2="12" y2="11.01" stroke="red" fill="red" id="dotLoading1" /><line x1="8" y1="11" x2="8" y2="11.01" stroke="red" fill="red" id="dotLoading2" /><line x1="16" y1="11" x2="16" y2="11.01" stroke="red" fill="red" id="dotLoading3" /></svg>'); } .loadingUrlPreload::before{ content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message-dots" width="14" height="14" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4"/><line x1="12" y1="11" x2="12" y2="11.01" stroke="red" fill="red" id="dotLoading1" /><line x1="8" y1="11" x2="8" y2="11.01" stroke="red" fill="red" id="dotLoading2" /><line x1="16" y1="11" x2="16" y2="11.01" stroke="red" fill="red" id="dotLoading3" /></svg>'); } /* https://tablericons.com/ "message" without newlines set width/height to 12px; removed two lines to get empty popup */ .hasCoverPreviewPopup::before{ content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="12px" height="12px" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" /></svg>'); } /* https://tablericons.com/ "message" without newlines set width/height to 12px */ .hasLoadedCoverPreviewPopup::before{ content:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-message" width="12px" height="12px" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 21v-13a3 3 0 0 1 3 -3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-9l-4 4" /><line x1="8" y1="9" x2="16" y2="9" /><line x1="8" y1="13" x2="14" y2="13" /></svg>'); } .blackFont { color:#000; } .whiteFont { color:#fff } .defaultTitleStyle { box-sizing: border-box; padding:5px 8px; min-height:unset; height:auto; display:inline-block; width:100%; /*max-width:auto;*/ text-align:center !important; justify-content: center; justify-items: center; border: 0 !important; border-bottom: 1px solid #000 !important; border-radius:10px 10px 0 0 !important; line-height:1.4em; } .defaultTitleStyleSmall { line-height:1.2em; } .defaultBackgroundStyle { align-items:center; pointer-events:none; /*width:100%; height:100%;*/ max-width:100%; max-height:100%; text-align:center !important; justify-content: center; justify-items: center; height:auto; padding:0; background-color:#fff; } .ImgFitDefault{ object-fit: contain; min-width: 0; min-height: 0; max-height: 400px; max-width: 400px; width:100%; height:100%; margin:2px; padding:0; position:unset; border-radius: 10%; } #coverPreviewAutoScroll#style-4::-webkit-scrollbar-track,#coverPreviewContentAutoScroll::#style-4::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); background-color: #F5F5F5; } #coverPreviewAutoScroll::-webkit-scrollbar,#coverPreviewContentAutoScroll::-webkit-scrollbar { width: 2px; background-color: #F5F5F5; } #coverPreviewAutoScroll::-webkit-scrollbar-thumb, #coverPreviewContentAutoScroll::-webkit-scrollbar-thumb { background-color: #888; } #coverPreviewAutoScroll{ overflow:auto; scrollbar-width: thin; scrollbar-color: #888 #F5F5F5; } #coverPreviewContentAutoScroll{ display:block; overflow:auto; scrollbar-width: thin; scrollbar-color: #888 #F5F5F5; } #popover{ box-sizing: border-box; overflow: hidden; /* min() not compatible with firefox 56 max-height: min(400px, (100vh - (100vh - 100%) - 44px)); max-width: min(400px, calc(100vw - (100vw - 100%))); */ max-height: calc(100vh - (100vh - 100%) - 44px); max-width: calc(100vw - (100vw - 100%)); min-height: 0; min-width: 0; /*height: 400px;*/ width: 100%; margin:0 0 22px 0; border: 1px solid #000; border-radius:10px 10px 5px 5px; box-shadow: 0px 0px 5px #7A7A7A; position:absolute; z-index:10; text-align: center !important; justify-content: start; justify-items: center; display: flex; flex-shrink: 1; flex-direction: column; opacity: 1; transition: visibility 0.2s, opacity 0.2s linear; } .hidePopover { visibility:hidden !important; opacity: 0 !important; transition: visibility 0.4s, opacity 0.4s linear !important; } .isExternalContent{ border:2px solid red !important; } .isExternalContentArrow{ border:2px solid red !important; border-width:0 2px 2px 0 !important } .popoverContent { box-sizing: border-box; text-align: center !important; justify-content: center; justify-items: center; align-items: center; display: flex; flex-direction: column; min-height: 0; min-width: 0; padding: 1px !important; width: 100%; height: 100%; flex: 1; padding:1px !important; border-radius:0; } .popoverDetail{ flex-direction:unset !important; height:400px; } .coverDataTitle{ border-bottom:1px solid white; padding:2px 0; } .containerPadding{ justify-items:center; padding:10px } .popoverTitleDetail{ height:100% !important; width:auto !important; max-width:65% !important; border-radius: 10px 0 0 5px !important; border:0 !important; border-right: 1px solid #000 !important; word-break: break-word; /* word wrap/break long links/texts */ } .smallText{ font-size: 0.8em; } .mediumText{ font-size: 0.98em; } .small_smallText{ /* display:inline-block;*/ /* line height not working if the element is not a block */ font-size: 0.82em; line-height: 1.4em; } .small_mediumText{ /* display:inline-block;*/ font-size: 0.78em; line-height: 1.2em; } .wordBreak { word-wrap: break-word !important; word-break: break-word; } .borderTop { width:100%; border-top:1px solid#fff; margin: 2px 0; } `); } function setStyleClasses() { const lastUpdated = GM_getValue("lastUpdated"); const currentTime = Date.now(); const timeDifference = currentTime - lastUpdated; const cachedBackgroundClasses = GM_getValue( "STYLESHEETHIJACKFORBACKGROUND" ); //console.log({lastUpdated,currentTime,timeDifference}) //console.log("timeDifference: " + timeDifference) if ( lastUpdated === null || lastUpdated === undefined || timeDifference > lastUpdateCheck ) { GM_setValue("lastUpdated", currentTime); refreshInitValues = true; // console.log("set lastUpdated to now") } //console.log(refreshInitValues); if (debugCSSClasses) { if ( STYLESHEETHIJACKFORBACKGROUND !== "" && (refreshInitValues || cachedBackgroundClasses === undefined || forceUpdate) ) { let styleSheetToAddBackground = ""; for (let i = 0; i < STYLESHEETHIJACKFORBACKGROUNDARRAY.length; i++) { if (styleSheetContainsClass(STYLESHEETHIJACKFORBACKGROUNDARRAY[i])) { console.log( "+ has found class: " + STYLESHEETHIJACKFORBACKGROUNDARRAY[i] ); styleSheetToAddBackground += STYLESHEETHIJACKFORBACKGROUNDARRAY[i]; } else { console.log( "- has not found class: " + STYLESHEETHIJACKFORBACKGROUNDARRAY[i] ); } } STYLESHEETHIJACKFORBACKGROUND = styleSheetToAddBackground .replace(REGEX_DOTCOMMA, " ") .trim(); GM_setValue( "STYLESHEETHIJACKFORBACKGROUND", STYLESHEETHIJACKFORBACKGROUND ); //console.log("STYLESHEETHIJACKFORBACKGROUND: " + STYLESHEETHIJACKFORBACKGROUND) } else { STYLESHEETHIJACKFORBACKGROUND = cachedBackgroundClasses; console.log("cachedBackgroundClasses: " + cachedBackgroundClasses); } const cachedTitleClasses = GM_getValue("STYLESHEETHIJACKFORTITLE"); if ( STYLESHEETHIJACKFORTITLE !== "" && (refreshInitValues || cachedTitleClasses === undefined || forceUpdate) ) { let styleSheetToAddTitle = ""; for (let i = 0; i < STYLESHEETHIJACKFORTITLEARRAY.length; i++) { if (styleSheetContainsClass(STYLESHEETHIJACKFORTITLEARRAY[i])) { console.log( "+ has found class: " + STYLESHEETHIJACKFORTITLEARRAY[i] ); styleSheetToAddTitle += STYLESHEETHIJACKFORTITLEARRAY[i]; } else { console.log( "- has not found class: " + STYLESHEETHIJACKFORTITLEARRAY[i] ); } } STYLESHEETHIJACKFORTITLE = styleSheetToAddTitle .replace(REGEX_DOTCOMMA, " ") .trim(); GM_setValue("STYLESHEETHIJACKFORTITLE", STYLESHEETHIJACKFORTITLE); //console.log("STYLESHEETHIJACKFORTITLE: " + STYLESHEETHIJACKFORTITLE) } else { STYLESHEETHIJACKFORTITLE = cachedTitleClasses; console.log("cachedTitleClasses: " + cachedTitleClasses); } } else { //console.log("not debugging CSS classes") STYLESHEETHIJACKFORBACKGROUND = STYLESHEETHIJACKFORBACKGROUND.replace( REGEX_DOTCOMMA, " " ).trim(); STYLESHEETHIJACKFORTITLE = STYLESHEETHIJACKFORTITLE.replace( REGEX_DOTCOMMA, " " ).trim(); } } function setPopoverHeight( adjustedTargetHeigt = 400, targetTopPxYPosition = 0 ) { //https://developer.mozilla.org/en-US/docs/Web/CSS/min() not compatible with firefox 56 let targetHeight = defaultHeight; if (showSmaller) targetHeight = smallHeight; if (adjustedTargetHeigt < targetHeight) targetHeight = adjustedTargetHeigt; const minHeightValue = "min(" + targetHeight + "px, (100vh - (100vh - 100%) - " + offsetToBottomBorderY * 2 + "px - " + targetTopPxYPosition + "px))"; if (supportsCSSMin) { //console.log("supports min()"); popover.style.maxHeight = minHeightValue; popover.style.height = ""; } else { console.log("does not support CSS min() for max-Height"); /* popover.style.maxHeight = "calc(100vh - (100vh - 100%) - " + offsetToBottomBorderY * 2 + "px))";*/ popover.style.height = targetHeight + "px"; } } function setPopoverWidth(adjustedTargetWidth = 400, targetTopPxPostion = 0) { let targetHeight = defaultHeight; if (showSmaller) targetHeight = smallHeight; // if (adjustedTargetWidth < targetHeight) targetHeight = adjustedTargetWidth; if (showDetails) { popover.classList.add("popoverDetail"); popoverTitle.classList.add("popoverTitleDetail"); const minWidthValue = "min(" + targetHeight * 2 + "px, (100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))"; //const supportsCSSMin = CSS.supports("max-Width", minWidthValue); if (supportsCSSMin) { //console.log("supports min()"); popover.style.maxWidth = minWidthValue; } else { console.log("does not support CSS min() for max-Width"); popover.style.maxWidth = "calc(100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))"; popover.style.width = targetHeight * 2 + "px"; } } else { popover.classList.remove("popoverDetail"); popoverTitle.classList.remove("popoverTitleDetail"); const minWidthValue = "min(" + targetHeight + "px, (100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))"; //const supportsCSSMin = CSS.supports("max-Width", minWidthValue); if (supportsCSSMin) { popover.style.maxWidth = minWidthValue; } else { /* popover.style.maxWidth = "calc(100vw - (100vw - 100%) - " + offsetToRightBorderX * 2 + "px))"; //popover.style.height = targetHeight + "px !important"; */ popover.style.width = targetHeight + "px"; } } } function createPopover() { let bodyElement = document.getElementsByTagName("BODY")[0]; popover = document.createElement("div"); popover.id = "popover"; popoverTitle = document.createElement("header"); popoverContent = document.createElement("content"); popover.appendChild(popoverTitle); popover.appendChild(popoverContent); arrowContainer = document.createElement("div"); arrowContainer.id = "popover_arrow"; bodyElement.appendChild(arrowContainer); popover.className = ( "defaultBackgroundStyle " + STYLESHEETHIJACKFORBACKGROUND ).trim(); popoverContent.className = "popoverContent blackFont " + STYLESHEETHIJACKFORBACKGROUND; if ( !STYLESHEETHIJACKFORBACKGROUND && DEFAULTBACKGROUNDCOLOR && DEFAULTBACKGROUNDCOLOR != "" ) { popover.style.backgroundColor = DEFAULTBACKGROUNDCOLOR; } //setPopoverHeight(); //setPopoverWidth(); setTimeout(setPopoverHeight, 500); //hack. why is a wait time needed? setTimeout(setPopoverWidth, 500); //hack. Can not apply style.height without a short wait time in older firefox 56 //console.log(popover) //console.log(popover.style) popoverTitle.className = ( STYLESHEETHIJACKFORTITLE + " defaultTitleStyle" ).trim(); if ( !STYLESHEETHIJACKFORTITLE && DEFAULTTITLEBACKGROUNDCOLOR && DEFAULTTITLEBACKGROUNDCOLOR != "" ) { popoverTitle.style.backgroundColor = DEFAULTTITLEBACKGROUNDCOLOR; popoverTitle.style.color = "#fff"; } popover.addEventListener("mouseleave", hideOnMouseLeave); popover.style.left = 0; popover.style.top = 0; //avoid invisible popover outside regular site height hidePopOver(); bodyElement.insertAdjacentElement("beforeend", popover); popoverBorderWidth = 0.6; //getComputedStyle(popoverTitle,"border-bottom-width") updateArrowStyle(); //Forum updateArrowColor(); /* console.log("popover.style.height: " + popover.style.height); popover.style.minHeight="0px"; popover.style.position="absolute"; popover.style.height = '333px'; console.log("popover.style.height: " + popover.style.height); setPopoverHeight();*/ } function updateArrowStyle() { //console.log("updateArrowStyle"); popoverForegroundColor = getComputedStyle(popover, "background-color"); let popoverContentForegroundColor = getComputedStyle( popoverContent, "background-color" ); const blendedForegroundColor = blendColorsToRGBA([ popoverForegroundColor, popoverContentForegroundColor, ]); popoverBackgroundColor = getComputedStyle(popoverTitle, "background-color"); /* console.log( "backgroundColor of popoverTitle: " + popoverBackgroundColor + ", popoverBorderWidth: " + popoverBorderWidth + ", popoverForegroundColor: " + popoverForegroundColor + ", popoverContentForegroundColor: " + popoverContentForegroundColor + ", blendedForegroundColor: " + blendedForegroundColor );*/ popoverForegroundColor = blendedForegroundColor; } function updateArrowColor() { let styleSheetIDElement = "mesh-nightmode-css"; let linkElement = document.getElementById(styleSheetIDElement); //console.log(linkElement); function loadedCb() { //console.log("finished loading stylesheet " + linkElement.href); updateArrowStyle(); } function errorCb(error) { console.log(error); console.log("error loading stylesheet: " + linkElement.href); } if (linkElement) { //console.log(linkElement.href); let url = linkElement.href; linkElement.href = ""; linkElement.removeEventListener("loaddata", loadedCb); // or: file.onload = loadedCb; linkElement.onload = () => { loadedCb(); }; linkElement.onerror = (error) => { errorCb(error); }; linkElement.href = url; if (linkElement.complete) { loadedCb(); } } } function showPopupLoadingSpinner( hoveredTitleLink, title, event, notification = "", coverData = undefined ) { const DEBUG = false; //console.log(event) const isActivePopup = currentTitelHover !== undefined && hoveredTitleLink !== undefined && currentTitelHover == hoveredTitleLink; /* console.group("showPopupLoadingSpinner") //"currentCoverData: " +currentCoverData + console.log("currentTitelHover: " + currentTitelHover+", hoveredTitleLink: " + hoveredTitleLink+", currentTitelHover == hoveredTitleLink: " + (currentTitelHover == hoveredTitleLink)) console.log("isActivePopup: " + isActivePopup) console.groupEnd();*/ if (isActivePopup) { popover.classList.remove("isExternalContent"); //last link was external. remove isExternal class style // console.group("showPopupLoadingSpinner") //popover.empty(); //popover.innerHTML = ""; DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight); if (coverData !== undefined) { //console.log("showPopupLoadingSpinner") DEBUG && console.log(coverData); adjustPopupTitleDetail(coverData, title); } else popoverTitle.textContent = title; if (notification != "") { isShowingSpinnerAnimation = false; popoverContent.innerHTML = '<div id="coverPreviewContentAutoScroll" class="popoverContent ">' + notification + "</div>"; popoverContent.className = "popoverContent wordBreak " + STYLESHEETHIJACKFORBACKGROUND; //blackfont } else { isShowingSpinnerAnimation = true; popoverContent.innerHTML = `<svg class="spinner" viewBox="0 0 50 50"> <g transform="translate(25, 25)"> <circle class="" cx="0" cy="0" r="25" fill="black" stroke-width="5" /> <circle class="path" cx="0" cy="0" r="23" fill="none" stroke-width="5"> <animateTransform attributeType="xml" attributeName="transform" type="rotate" from="0" to="360" dur="1.6s" repeatCount="indefinite" /> </circle> </g> <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" style="fill:#fff;font-size:11px">Loading </text> </svg>`; //popoverContent.innerHTML = '<div class="forground" style="z-index: 3;">Loading Data</div><svg class="spinner" viewBox="0 0 50 50"><circle class="" cx="25" cy="25" r="22" fill="black" stroke-width="5"></circle><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle></svg>'; popoverContent.className = "popoverContent " + STYLESHEETHIJACKFORBACKGROUND; //whitefont } DEBUG && console.log(popover); // DEBUG && console.log("popover.offsetHeight: " + popover.offsetHeight); //console.log(event) if (coverData) { popupPos(event, coverData.isExternal); } else popupPos(event); // console.groupEnd("showPopupLoadingSpinner") } } //#region adapted code from scrollToTarget of https://htmldom.dev/scroll-to-an-element-smoothly/ let direction = 1; let pauseTimeDifference = null; let currentPercent = null; let percentBeforeStyleChange; let hasChangedStyle = false; let requestId = []; let startTime = null; const scrollToTarget = function (idToScroll, node, duration = 7000) { const DEBUG = false; let scrollOverflow = node.scrollHeight - node.offsetHeight; const updateStartValues = function (percent, currentTime) { if (percent) { DEBUG && console.group("updateStartValues"); scrollOverflow = node.scrollHeight - node.offsetHeight; startTime = currentTime - pauseTimeDifference; pauseTimeDifference = null; // if (direction == 1) startPos = scrollOverflow * percent; // else startPos = scrollOverflow * (1 - percent); DEBUG && console.log("percent: " + percent + ", startPos: "); let time = currentTime - startTime; //console.log("pauseTimeDifference, time: " + time); let targetPercent = Math.min(time / duration, 1); DEBUG && console.log( "percent after pause: " + targetPercent + ", percent: " + percent + ", direction: " + direction + ", scrolltop percent: " ); DEBUG && console.groupEnd("updateStartValues"); } }; const loop = function (currentTime) { if (!startTime) { startTime = currentTime; } //console.log("scrollOverflow: " + scrollOverflow); //#region set StartValues if (currentPercent != undefined && currentPercent !== null) { DEBUG && console.log( "currentPercent:" + currentPercent + ", direction: " + direction ); updateStartValues(currentPercent, currentTime); currentPercent = null; } if (hasChangedStyle) { DEBUG && console.log("hasChangedStyle"); updateStartValues(percentBeforeStyleChange, currentTime); hasChangedStyle = false; } //#endregion // Elapsed time in miliseconds let time = currentTime - startTime; const percent = Math.min(time / duration, 1); let targetScrollTop, targetScrollTopPercent; if (direction == 1) { targetScrollTopPercent = easeInOutQuad(percent); } else { targetScrollTopPercent = 1 - easeInOutQuad(percent); } targetScrollTop = scrollOverflow * targetScrollTopPercent; //console.log(targetScrollTop +", percent: " + percent) node.scrollTo(0, targetScrollTop, "auto"); pauseTimeDifference = currentTime - startTime; if (autoScrollCoverData && popoverVisible) { //#region loop Animation const insideContainerValue = targetScrollTop <= scrollOverflow && targetScrollTop >= 0; if (time < duration && insideContainerValue) { // Continue moving //requestId = window.requestAnimationFrame(loop); //startPos = 0; } else { //startPos=0; startTime = currentTime; direction *= -1; } percentBeforeStyleChange = percent; requestId[idToScroll] = window.requestAnimationFrame(loop); //#endregion } else { //#region pause animation //console.group("loop scrolldata before pause"); window.cancelAnimationFrame(requestId[idToScroll]); //pauseTimeDifference = currentTime - startTime; currentPercent = percent; DEBUG && console.log( "scrollPos before pause: " + node.scrollTop + ", percent: " + percent + ", direction: " + direction + ", targetScrollTop: " + targetScrollTop ); //console.groupEnd("loop scrolldata before pause"); //#endregion } }; //start animation requestId[idToScroll] = window.requestAnimationFrame(loop); }; const easeInOutQuad = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); //https://gist.github.com/gre/1650294 //#endregion function autoScrollData(idToScroll = "coverPreviewAutoScroll") { coverDataContainer[idToScroll] = document.getElementById(idToScroll); setStartScrollPosition(idToScroll); if (autoScrollCoverData) { if (coverDataContainer[idToScroll]) { /*console.log( "coverDataContainer.offsetHeight: " + coverDataContainer.offsetHeight + ", coverDataContainer.scrollHeight: " + coverDataContainer.scrollHeight );*/ const hasOverflowValue = coverDataContainer[idToScroll].scrollHeight > coverDataContainer[idToScroll].offsetHeight; if (hasOverflowValue) { if (requestId[idToScroll]) window.cancelAnimationFrame(requestId[idToScroll]); //console.log("currentPercent: " + currentPercent); scrollToTarget(idToScroll, coverDataContainer[idToScroll]); } } } } function resetAutoScroll(idToScroll = "coverPreviewAutoScroll") { //autoScrollCoverData = true; direction = 1; currentPercent = null; startTime = null; pauseTimeDifference = null; hasChangedStyle = false; //console.log(requestId); if (requestId[idToScroll]) window.cancelAnimationFrame(requestId[idToScroll]); } function setStartScrollPosition(idToScroll) { const DEBUG = false; if (coverDataContainer[idToScroll] && currentPercent) { let scrollOverflow = coverDataContainer[idToScroll].scrollHeight - coverDataContainer[idToScroll].offsetHeight; DEBUG && console.log( "scrollOverflow: " + scrollOverflow + ", currentPercent: " + currentPercent ); let targetScrollTop, targetScrollTopPercent; if (direction == 1) { targetScrollTopPercent = easeInOutQuad(currentPercent); } else { targetScrollTopPercent = 1 - easeInOutQuad(currentPercent); } targetScrollTop = scrollOverflow * targetScrollTopPercent; DEBUG && console.log("targetScrollTop: " + targetScrollTop); DEBUG && console.log( "coverDataContainer.scrollTop: " + coverDataContainer[idToScroll].scrollTop ); coverDataContainer[idToScroll].scrollTop = targetScrollTop; DEBUG && console.log( "coverDataContainer.scrollTop: " + coverDataContainer[idToScroll].scrollTop ); } } function refreshPopover(coverData, e = undefined) { //only call when isActivePopup const DEBUG = false; if (coverData && coverData !== undefined) { isShowingSpinnerAnimation = false; DEBUG && console.log("currentTitelHover: " + currentTitelHover); DEBUG && console.group("refreshPopover"); const link = coverData.url; // const title = coverData.title; //console.log(coverData) //console.log(e) // popoverTitle.textContent = title; // console.log(link) if ( link === undefined || link === null || link == "" || link == "undefined" ) { popoverContent.innerHTML = '<div class="containerPadding">No Cover Image found</div>'; } else { popoverContent.innerHTML = '<img src="' + link + '" class="ImgFitDefault" ></img>'; } adjustPopupTitleDetail(coverData); DEBUG && console.groupEnd("refreshPopover"); DEBUG && console.log(e); //if (currentTitelHover == title) if (e !== undefined) { popupPos(e, coverData.isExternal); } } } //#region get serieDetails function getRatingNumber(ratingString) { //const ratingString = "Rating(3.3 / 5.0, 1940 votes)" let ratingNumber; if (ratingString) { const matchesVotes = ratingString.toLowerCase().match(reVoteCount); const matches = ratingString.match(reRating); //"Rating(3.3 / 5.0, 1940 votes)" const matchesSingleNumber = ratingString.match(reRatingSingleNumber); //4.5 //console.log(matches) //console.log(matchesSingleNumber) //console.log(matches.length) let hasVoteCountBigger0 = true; let hasVoteString = false; // console.log(matchesVotes) if (matchesVotes && matchesVotes.length > 1) { //console.log(matchesVotes[1]) hasVoteString = true; if (matchesVotes[1] == 0 || matchesVotes[1] == "0") { //console.log("no vote count") hasVoteCountBigger0 = false; } } if (matches && matches.length == 3 && hasVoteCountBigger0) { //display rating when vote count found and more than 0 //console.log(matches[1]) ratingNumber = matches[1]; } else { //no votecount found //no rating in relation to max rating -> search for single number //console.log(matchesSingleNumber[1]) if ( hasVoteCountBigger0 && matchesSingleNumber && matchesSingleNumber.length == 2 ) { ratingNumber = matchesSingleNumber[1]; } } } return ratingNumber; } function getChapters(statusString) { //TODO "Episodes" instead of chapter for tv series const DEBUG = false; let r###lt; 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"; r###lt = chapterCount + webnovel + numberType; } DEBUG && console.groupEnd(); } DEBUG && console.log("r###lt: " + r###lt); return r###lt; } function getCompletedState(statusString) { let r###lt = false; if (statusString && statusString.toLowerCase().includes("complete")) { //complete | completed r###lt = true; } return r###lt; } function getOngoingState(statusString) { let r###lt = false; if (statusString && statusString.toLowerCase().includes("ongoing")) { r###lt = true; } return r###lt; } 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="[ ] "; 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>[ ] not in a reading list or logged in</div>"; } } if (coverData.alternativeNames && coverData.alternativeNames != "") { alternativeNames = " [Key A]"; } if (coverData.isExternal) { showExternalLink = ' <div style="background-color:darkred" class="coverDataTitle">🔗[' + coverData.isExternal + "]</div>"; } completeDetails += '<span class="' + mediumTextStyle + '" style="height:100%;display:flex;flex-direction:column"><span class="coverDataTitle"><b>' + titleToShow + "</b>" + alternativeNames + showReadingListTitle + "</span> " + showExternalLink + '<div id="coverPreviewAutoScroll">' + getDetailsString(coverData); +"</div>"; //autoscroll completeDetails += '<div class="borderTop ' + smallTextStyle + '">[KeyH show hotkey list]<br />[Key1 Switch detailed and simple popup] [Key2 Switch between description and tags] [Key3 small and big popup style] </div></span>'; } else { if (coverData.alternativeNames && coverData.alternativeNames != "") { alternativeNames = " [A]"; } completeDetails = '<span class="' + mediumTextStyle + '">' + externalIcon + showReadingListIcon + "<b>" + titleToShow + "</b>" + alternativeNames + " " + getShortendDetailsString(coverData); completeDetails += ' <span class="' + smallTextStyle + '">[KeyH hotkey list]</span></span>'; } //popoverTitle.innerHTML = completeDetails; popoverTitle.innerHTML = completeDetails; } //#endregion function setCurrentCoverDataAndLoadImage(coverData, hoveredTitle, e) { const DEBUG = false; //GM_getCachedValue DEBUG && console.group("setCurrentCoverDataAndLoadImage"); DEBUG && console.log(coverData); let serieTitle = hoveredTitle; if (!hoveredTitle || coverData.title) { //pure link without title get title of seriepage serieTitle = coverData.title; } DEBUG && console.log( "hoveredTitle: " + hoveredTitle + ", serieTitle: " + serieTitle ); if ( coverData !== undefined && coverData !== null && hoveredTitle == currentTitelHover ) { currentCoverData = coverData; } if (e) { loadImageFromBrowser({ coverData: currentCoverData, e: e, serieTitle: serieTitle, hoveredTitleLink: hoveredTitle, }); } DEBUG && console.groupEnd("setCurrentCoverDataAndLoadImage"); } function ajaxLoadImageUrlAndShowPopup( forceReload = false, element, hoveredTitle, e, external = false, targetPage = undefined ) { const currentEvent = e; //console.log(currentEvent) //console.log("mouseenter") // console.group("ajaxLoadImageUrlAndShowPopup") return parseSeriePage( element, forceReload, hoveredTitle, currentEvent, external, targetPage ).then( function (coverData) { if (coverData !== undefined) { setCurrentCoverDataAndLoadImage( coverData, hoveredTitle, currentEvent ); } }, function (Error) { const elementUrl = element.href; console.log(Error); let errorMessage = '"(' + Error + ')" failed to fetch ' + elementUrl; console.log("errorMessage: " + errorMessage); setLinkStateOfSameLinks(element, linkIconEnum.error); showPopupLoadingSpinner( hoveredTitle, hoveredTitle, currentEvent, errorMessage ); } ); // console.groupEnd("ajaxLoadImageUrlAndShowPopup") } function imageLoaded( coverData, hoveredTitleLink, serieTitle = undefined, e = undefined ) { const DEBUG = false; const hasMouseEnterEvent = serieTitle && e !== undefined; const isActivePopup = currentTitelHover !== undefined && hoveredTitleLink !== undefined && currentTitelHover == hoveredTitleLink && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData DEBUG && console.group("loadImageFromBrowser img.onload: " + serieTitle); DEBUG && console.log("finished loading imgurl: " + coverData.url); DEBUG && console.log( "currentTitelHover: " + currentTitelHover + ", isActivePopup: " + isActivePopup ); DEBUG && console.log("isActivePopup: " + isActivePopup); if (isActivePopup) { DEBUG && console.log("refreshPopover"); refreshPopover(coverData, e); //popup only gets refreshed when currentTitelHover == serieTitle } DEBUG && console.groupEnd("loadImageFromBrowser img.onload"); } function imageLoadingError( coverData, errorText = undefined, hoveredTitleLink, serieTitle = undefined, e = undefined ) { console.group("loadImageFromBrowser img.onerror: " + serieTitle); /* const hasMouseEnterEvent = serieTitle && e !== undefined; const isActivePopup = currentTitelHover !== undefined && hoveredTitleLink !== undefined && currentTitelHover == hoveredTitleLink && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData console.log("isActivePopup: " + isActivePopup);*/ console.log("hoveredTitleLink:" + hoveredTitleLink); console.log(errorText); let filename = ""; console.log("coverData.url: " + coverData.url); if (coverData.url !== undefined && coverData.url != "undefined") { filename = decodeURIComponent(coverData.url); } else { filename = ""; } let additionalText = ""; if (errorText) additionalText = errorText; let errorMessage = '<div class="containerPadding">browser blocked/has error loading the cover: <br />' + filename + "<br />" + additionalText + "</div>"; if (filename == "") { errorMessage = '<div class="containerPadding">target site has no coverImage<br />[no image tag found]<br /></div>'; } console.log("errorMessage: " + errorMessage); //console.log(window) console.log(navigator); //console.log(navigator.userAgent) const useragentString = navigator.userAgent; console.log("useragentString: " + useragentString); const isChrome = useragentString.includes("Chrome"); if (isChrome) { console.log( "look in the developer console if 'net::ERR_BLOCKED_BY_CLIENT' is displayed or manually check if the imagelink still exists/reload the coverdata" ); } else { console.log( "image loading most likely blocked by browser or addon. Check if the imagelink still exists/reload the coverdata" ); } // if (isActivePopup) showPopupLoadingSpinner( hoveredTitleLink, serieTitle, e, errorMessage, coverData ); console.groupEnd("loadImageFromBrowser img.onerror"); } async function loadImageFromBrowser({ coverData, e = undefined, serieTitle = undefined, hoveredTitleLink = undefined, }) { const DEBUG = false; //console.log(e) //console.group("loadImageFromBrowser") let img = document.createElement("img"); //put img into dom. Let the image preload in background const hasMouseEnterEvent = hoveredTitleLink !== undefined && e !== undefined; //console.log(currentCoverData) //console.log(coverData) DEBUG && console.log("loadImageFromBrowser"); DEBUG && console.log(hasMouseEnterEvent); img.onload = () => { imageLoaded(coverData, hoveredTitleLink, serieTitle, e); }; img.onerror = async (error) => { let imageCanBeLoaded = checkImageServerState(coverData.url); console.log("imageCanBeLoaded r###lt before await: "); console.log(imageCanBeLoaded); imageCanBeLoaded = await imageCanBeLoaded; // console.log("imageCanBeLoaded after await: " + imageCanBeLoaded) DEBUG && console.log( "imageCanBeLoaded: " + imageCanBeLoaded + ", coverData.url: " + coverData.url ); console.log( "imageCanBeLoaded: " + imageCanBeLoaded + ", coverData.url: " + coverData.url ); let errorMessage; if (imageCanBeLoaded) { errorMessage = "image fetching is possible, but image is blocked from loading.<br />Check for example ublock/umatrix or similar if domain is allowed to load images or image sizes exceeds allowed media size"; } else { errorMessage = "image could not be loaded. Check in console.log what error state checkImageServerState() produces"; } console.group("img node has loading error"); console.log(error); console.log("errorMessage: " + errorMessage); console.groupEnd(); imageLoadingError( coverData, errorMessage, hoveredTitleLink, serieTitle, e ); }; //console.log(coverData) if (coverData !== undefined) { if (coverData.url !== undefined && coverData.url != "undefined") { img.src = coverData.url; if (img.complete) { DEBUG && console.log( "loadImageFromBrowser preload completed: " + serieTitle ); DEBUG && console.log(img.src); } else { //if image not available/cached in browser show loading pinner /* const isActivePopup = currentCoverData !== undefined && currentTitelHover !== undefined && hoveredTitleLink !== undefined && currentTitelHover == hoveredTitleLink && hasMouseEnterEvent; //currentTitelHover == hoveredTitleLink currentCoverData == coverData //console.log(e) if (isActivePopup) { */ DEBUG && console.log( "loadImageFromBrowser image not completely loaded yet. Show loading spinner : " + serieTitle ); showPopupLoadingSpinner( hoveredTitleLink, serieTitle, e, "", coverData ); // } } } else { imageLoadingError( coverData, "coverData has no image", hoveredTitleLink, serieTitle, e ); } } // console.groupEnd("loadImageFromBrowser") } function hidePopOver() { // popover.style.visibility = "hidden"; //arrowContainer.style.visibility = "hidden"; arrowContainer.classList.add("hidePopover"); popover.classList.add("hidePopover"); //popover.style.height = "0"; //popover.style.width = "0"; //console.group("hidePopOver") //console.log("currentTitelHover: " + currentTitelHover) currentTitelHover = undefined; currentCoverData = undefined; popoverVisible = false; if (isShowingSpinnerAnimation) popoverContent.innerHTML = ""; //remove infinite spinner animation when popup not shown pressedKeys = []; //window blur release keys //console.log("currentTitelHover: " + currentTitelHover) //console.groupEnd("hidePopOver") } function showPopOver() { // popover.style.display = "flex"; //popover.style.height = "100%"; // popover.style.width = "100%"; //popover.style.visibility = "visible"; //popover.style.opacity="1"; popover.classList.remove("hidePopover"); arrowContainer.classList.remove("hidePopover"); popoverVisible = true; } function hideOnMouseLeave() { //if (!e.target.matches(concatSelector())) return; //popover.hide(); hidePopOver(); } /* * get links into ALLSERIENODES and convert this nodearray to array * */ //renamed targetNodeArray -> arrayTargetNode to remove compatibility setting in tampermonkey () misdetected? (targetNodeArray.foreach, targetNodeArray.map, targetNodeArray.push) function updateSerieNodes( arrayTargetNode = [], individualLinksToTest, forceReload = false, external = false ) { const DEBUG = false; if (arrayTargetNode && arrayTargetNode.length > 0) { arrayTargetNode.forEach(function (selector) { if ( eventListenerStyle === undefined || eventListenerStyle === null || eventListenerStyle == 0 ) { selector.removeEventListener("mouseleave", hideOnMouseLeave); //selector.removeEventListener("mouseenter", mouseEnterPopup); } }); } let serieLinkNodes = document.querySelectorAll( ':not(.digg_pagination) > a[href*="' + individualLinksToTest + '"]' //not(.digg_pagination) > fix to block linking from pagination ); //console.log(serieLinkNodes) serieLinkNodes = Array.from(serieLinkNodes); let prunedSerieLinkNodes = []; if (serieLinkNodes && serieLinkNodes.length > 0) { //console.log(serieLinkNodes); serieLinkNodes.map(function (el) { //console.log(el) const elementUrl = el.href; //#region if has serieRegex check for externalLink+serieRegex match let hasLinkMatch = false; if (external) { hasLinkMatch = externalLinkKeys.some((key) => { let completeKey = key; /* console.log("key: " + key); console.log(externalLinks[key]); console.log( 'externalLinks["serieRegex"]: ' + externalLinks[key]["serieRegex"] );*/ let indexOfDomain = elementUrl.indexOf(key); let characterAfterIndividualPage = elementUrl.slice( indexOfDomain + key.length ); //get characters after targetAdress to parse ID if (indexOfDomain >= 0 && characterAfterIndividualPage.length > 0) { DEBUG && console.log( "elementUrl: " + elementUrl + ", key: " + key + ", indexOfDomain on key " + indexOfDomain + ",characterAfterIndividualPage: " + characterAfterIndividualPage ); if (externalLinks[key].serieRegex !== undefined) { completeKey = externalLinks[key].serieRegex; //console.log("serieRegex regex: " + completeKey) /* const hasMatch = xhr.finalUrl.match(new RegExp(completeKey)); return hasMatch; //performance exclusion regex only used for a few links */ } else { completeKey = defaultSerieRegex; //console.log("default regex: " + completeKey) } let hasMatch = characterAfterIndividualPage.match( new RegExp(completeKey) ); if (hasMatch === null || hasMatch.length == 0) { if (externalLinks[key].serieRegex2 !== undefined) { hasMatch = characterAfterIndividualPage.match( new RegExp(externalLinks[key].serieRegex2) ); } } //console.log(hasMatch) return hasMatch !== null && hasMatch.length > 1; //return elementUrl.includes(key); } return false; //no further serieid available }); } else { let indexOfDomain = elementUrl.indexOf(INDIVIDUALPAGETEST); let characterAfterIndividualPage = elementUrl.slice( indexOfDomain + INDIVIDUALPAGETEST.length ); //get characters after targetAdress to parse ID if (characterAfterIndividualPage.length > 0) hasLinkMatch = true; //internal without serieRegex } //console.log("hasLinkMatch: " + hasLinkMatch); //#endregion if (hasLinkMatch) { prunedSerieLinkNodes.push(el); if (external) { el.setAttribute("coverDataExternalTarget", individualLinksToTest); } setLinkState(el, undefined, forceReload || preloadUrlRequests); } // console.log(elementUrl) }); } arrayTargetNode.push(...prunedSerieLinkNodes); //console.log(ALLSERIENODES) /* console.log(ALLSERIENODES) const sliceItemCount = 100; if (ALLSERIENODES.length > sliceItemCount) { ALLSERIENODES = ALLSERIENODES.slice(0, sliceItemCount); } console.log(ALLSERIENODES) */ } function switchShowIconNextToLink() { showIconNextToLink = !showIconNextToLink; GM_setValue("showIconNextToLink", showIconNextToLink); preloadCoverData(); updateCurrentPopupContent(); } function switchDetailsAndUpdatePopup() { const DEBUG = false; DEBUG && console.group("switchDetailsAndUpdatePopup"); changeToNewDetailStyle(); //console.log(currentCoverData) DEBUG && console.log("switchDetails refreshPopup"); updateCurrentPopupContent(); console.groupEnd("switchDetails"); } function switchTagsDescriptionAndUpdatePopup() { const DEBUG = false; if (showDetails) { showDescription = !showDescription; //console.log("switch showDetails to : " + showDetails) GM_setValue("showDescription", showDescription); updateCurrentPopupContent(); } } function switchShowReadingListIconAndTitle() { useReadingListIconAndTitle = !useReadingListIconAndTitle; GM_setValue("useReadingListIconAndTitle", useReadingListIconAndTitle); updateCurrentPopupContent(); } function updateCurrentPopupContent() { const DEBUG = false; if (currentCoverData !== undefined) { DEBUG && console.log(currentCoverData); loadImageFromBrowser({ coverData: currentCoverData, e: currentPopupEvent, serieTitle: currentTitelHover, hoveredTitleLink: currentTitelHover, }); } else if (currentTitelHover !== undefined) { //currentCoverData not yet set showPopupLoadingSpinner( currentTitelHover, currentTitelHover, currentPopupEvent ); } } function changeToNewDetailStyle(toggleDetails = true) { if (toggleDetails) showDetails = !showDetails; //console.log("switch showDetails to : " + showDetails) GM_setValue("showDetails", showDetails); //localStorage.setItem("showDetails", showDetails); //https://developer.mozilla.org/en-US/docs/Web/CSS/min() not compatible with firefox 56 //setPopoverWidth(); //console.log("changeToNewDetailStyle - setPopoverHeight();"); //setPopoverHeight(); updatePopoverSize(); } //#region eventListener function mouseEnterPopup(e, forceReload = false) { //if (!e.target.matches(concatSelector())) return; const DEBUG = false; DEBUG && console.group("mouseEnterPopup"); //let element = undefined;//$(this); //let nativElement = e.target//this; //console.log(this) //console.log(e) if (e !== undefined) { e.preventDefault(); const target = e.target; let Href = target.href; // element.attr('href'); let coverDataExternalTarget = target.getAttribute( "coverDataExternalTarget" ); //console.log("coverDataExternalTarget: " + coverDataExternalTarget); if (Href && coverDataExternalTarget === undefined) { //TODO double check if needed //no preloadUrlRequests happend /* const externalLinkKeys = Object.keys(externalLinks); const isExternal = externalLinkKeys.some((key) => Href.includes(key)); if (isExternal) { target.setAttribute("coverDataExternalTarget", key); coverDataExternalTarget = key; } else target.setAttribute("coverDataExternalTarget", null); */ } DEBUG && console.log( "Href: " + Href + ", INDIVIDUALPAGETEST: " + INDIVIDUALPAGETEST ); if ( Href && (Href.includes(INDIVIDUALPAGETEST) || (coverDataExternalTarget !== undefined && coverDataExternalTarget !== null && Href.includes(coverDataExternalTarget))) ) { //only trigger for links that point to serie pages //console.log(this) //console.log(this.text) //shortTitle //console.log(this.title) //LongTitle let shortSerieTitle = target.text; //element.text(); //get linkname //console.log(this) //console.log(shortSerieTitle) const dataTitle = target.getAttribute("datatitle"); const linkTitle = target.getAttribute("title"); //console.log("linkTitle: " + linkTitle) const hasDataTitle = dataTitle === null || dataTitle == "null" || dataTitle === undefined || !dataTitle; //move native title to custom data attribute. Suppress nativ title popup if (linkTitle !== null && hasDataTitle) { target.setAttribute("datatitle", linkTitle); target.removeAttribute("title"); } let serieTitle = target.getAttribute("datatitle"); //element.attr('datatitle'); //try to get nativ title if available from datatitle //console.log(serieTitle) if ( serieTitle === null || //has no set nativ long title -> use (available shortend) linkname serieTitle == "null" || PREDIFINEDNATIVTITLEARRAY.some((nativTitle) => serieTitle.includes(nativTitle) ) ) { //catch on individual serie page nativ title begins with "Recommended by" x people -> use linkname serieTitle = shortSerieTitle; } if ( serieTitle === undefined || serieTitle === null || serieTitle == "" ) { //image link: example link which content is only the cover image https://www.mangaupdates.com/series.html?letter=A serieTitle = Href; } currentTitelHover = serieTitle; //mark which titel is currently hovered const wasOverDifferentLink = currentTitelHover != previousTitelHover; if (wasOverDifferentLink) { resetAutoScroll(); autoScrollCoverData = true; } if (currentTitelHover != undefined) { previousTitelHover = currentTitelHover; } currentPopupEvent = e; //console.log(serieTitle) //console.log(Href) //console.log(currentCoverData) //console.log("currentTitelHover: " + currentTitelHover) const external = coverDataExternalTarget !== undefined && coverDataExternalTarget !== null; let targetPage; //console.log("external: " + external); //console.log("coverDataExternalTarget: " + coverDataExternalTarget); if (external) targetPage = coverDataExternalTarget; //console.log("targetPage: " + targetPage); let mainSerieHref = getLinkToSeriePage(Href, targetPage); let hasCoverData = GM_getCachedValue(mainSerieHref); if (!hasCoverData || forceReload) { setLinkStateOfSameLinks( target, linkIconEnum.popupLoading, forceReload ); } else { if (forceReload) { setLinkStateOfSameLinks(target, undefined, forceReload); } } ajaxLoadImageUrlAndShowPopup( forceReload, target, //Href currentTitelHover, e, external, targetPage ); } } DEBUG && console.groupEnd("mouseEnterPopup"); } function forceReload(forceReload = true) { mouseEnterPopup(currentPopupEvent, forceReload); } function updatePopoverSize() { //console.log("updatePopoverSize - setPopoverHeight();"); //setPopoverHeight(); //happens after updateCurrentPopupContent update setPopoverWidth(); if (showSmaller) { mediumTextStyle = "small_mediumText"; smallTextStyle = "small_smallText"; popoverTitle.classList.add("defaultTitleStyleSmall"); } else { popoverTitle.classList.remove("defaultTitleStyleSmall"); mediumTextStyle = "mediumText"; smallTextStyle = "smallText"; } updateCurrentPopupContent(); } function showAlternativeNamesList() { if (!showAlternativeNames || showAlternativeNames == "") { if (currentCoverData !== undefined) updateCurrentPopupContent(); } else { if ( currentCoverData.alternativeNames && currentCoverData.alternativeNames != "" ) { let alternativeNames = ""; alternativeNames = currentCoverData.alternativeNames; popoverContent.innerHTML = '<div id="coverPreviewContentAutoScroll" class="popoverContent ' + STYLESHEETHIJACKFORBACKGROUND + " " + mediumTextStyle + '" style="text-align:start !important; width:100%;"><b>Alternative Titles:</b><br />' + alternativeNames + "</div>"; if (currentCoverData !== undefined) popupPos(currentPopupEvent); autoScrollData("coverPreviewContentAutoScroll"); } } } function showHotkeyList() { if (!showHotkeys) { if (currentCoverData !== undefined) { loadImageFromBrowser({ coverData: currentCoverData, e: currentPopupEvent, serieTitle: currentTitelHover, hoveredTitleLink: currentTitelHover, }); } } else { popoverContent.innerHTML = '<div id="coverPreviewContentAutoScroll" class="popoverContent ' + STYLESHEETHIJACKFORBACKGROUND + " " + mediumTextStyle + '" style="text-align:start !important">[Key 1]: Switch detailed and simple popup<br />' + `[Key 2]: Switch between description and tags<br /> [Key 3]: Switch between small and big popup style<br /> [Key 4]: Pause/unpause autoscrolling coverData<br/> [Key 5]: Reload coverdata of hovered link<br /> [Key 6]: Reload all links of current Page<br/> [Key 9]: Clear all cover data info<br /> [Key A]: If available will show <b>a</b>lternative titles during holding of key A<br /> [Key I]: Toggle coverPreview pre/loading state <b>i</b>con displaying next to link<br /> [Key P]: Switch displaying of readinglist icon of <b>p</b>ersonal lists<br /> [Key H]: Show this <b>h</b>otkey list during holding of key H<br /> </div>`; if (currentCoverData !== undefined) popupPos(currentPopupEvent); autoScrollData("coverPreviewContentAutoScroll"); } } function reactToKeyPressWhenPopupVisible(event) { //console.log(event); //console.log(currentTitelHover) const key = event.key; if (popoverVisible) { if (!pressedKeys.includes(key)) { //console.log(event); pressedKeys.push(key); switch (key) { case "1": switchDetailsAndUpdatePopup(); break; case "5": forceReload(); break; case "6": { const _forceReload = true; preloadCoverData(_forceReload); } break; case "9": resetDatabase(); preloadCoverData(); forceReload(); break; case "2": switchTagsDescriptionAndUpdatePopup(); resetAutoScroll(); autoScrollCoverData = true; autoScrollData(); autoScrollData("coverPreviewContentAutoScroll"); break; case "3": showSmaller = !showSmaller; GM_setValue("showSmaller", showSmaller); updatePopoverSize(); hasChangedStyle = true; break; case "4": autoScrollCoverData = !autoScrollCoverData; if (autoScrollCoverData) { autoScrollData(); autoScrollData("coverPreviewContentAutoScroll"); } break; case "h": showHotkeys = true; showHotkeyList(); break; case "a": showAlternativeNames = true; showAlternativeNamesList(); break; case "p": switchShowReadingListIconAndTitle(); updateCurrentPopupContent(); //forceReload(); break; case "i": switchShowIconNextToLink(); break; } } } } function releaseKey(event) { const key = event.key; //console.log(pressedKeys) pressedKeys.splice(pressedKeys.indexOf(key), 1); if (event.key == "h") { showHotkeys = false; showHotkeyList(); } if (event.key == "a") { showAlternativeNames = false; showAlternativeNamesList(); } //console.log(pressedKeys) } function prepareEventListener() { window.addEventListener("blur", hidePopOver); window.addEventListener("keypress", reactToKeyPressWhenPopupVisible); //keypress gets repeated during keydown window.addEventListener("keyup", releaseKey); if ( targetContainerIDArrayToObserve && targetContainerIDArrayToObserve.length > 0 ) { for (let i = 0; i < targetContainerIDArrayToObserve.length; i++) { let targetNodeList = document.getElementById( targetContainerIDArrayToObserve[i] ); //forum.novelupdates.com quickedit change. ajax content change if (targetNodeList) { observer.observe(targetNodeList, config); } } } let themeSelector = document.getElementById("wi_themes"); if (themeSelector) themeSelector.addEventListener("change", updateArrowColor); window.onunload = function () { window.removeEventListener("blur", hidePopOver); window.removeEventListener("keypress", reactToKeyPressWhenPopupVisible); window.addEventListener("keyup", releaseKey); popover.removeEventListener("mouseleave", hideOnMouseLeave); if (themeSelector) themeSelector.removeEventListener("change", updateArrowColor); //possible memoryleaks? ALLSERIENODES = []; updateSerieNodes(ALLSERIENODES, INDIVIDUALPAGETEST); observer.disconnect(); }; if (eventListenerStyle == 1) { window.addEventListener("mousemove", throttledGetHoveredItem); } } //assumption that a single eventlistener is more performant than dozens of mouseEnter/MouseLeave events //https://gomakethings.com/why-event-delegation-is-a-better-way-to-listen-for-events-in-vanilla-js/#web-performance //https://davidwalsh.name/event-delegate //https://web.archive.org/web/20170121035049/http://jsperf.com/click-perf //https://stackoverflow.com/questions/29836326/is-using-a-universal-document-addeventlistenerclick-f-listener-slower-or-wea /* This is the proper pattern for creating event listeners that will work for dynamically-added elements. It's essentially the same approach as used by jQuery's event delegation methods (e.g. .on). */ function getHoveredItem(e) { if (eventListenerStyle == 1) { if ( e.target && e.target != lastTarget && e.target.nodeName == "A" && e.target.href && e.target.href.includes(INDIVIDUALPAGETEST) ) { lastTarget = e.target; //console.group("target A") //console.log(e.target.text) //console.log(e) mouseEnterPopup(e); //console.groupEnd(); } else { if (e.target.nodeName != "A") { lastTarget = undefined; hideOnMouseLeave(); } } } } document.addEventListener("DOMContentLoaded", main()); //#endregion //#region mangaDex api function getMangaDexNamedTag(tags, type = "Genre") { const DEBUG = false; DEBUG && console.group("getMangaDex" + type); let namedTags = ""; DEBUG && console.log(mangaDexTAGS); DEBUG && console.log(tags); DEBUG && console.log("tags.length: " + tags.length); if (tags && tags.length > 0) { for (let i = 0; i < tags.length; i++) { let searchTag = tags[i]; //console.log(searchTag) const tag = mangaDexTAGS[searchTag]; if (tag.group == type) { //console.log(tag) namedTags += " " + tag.name; } } } DEBUG && console.groupEnd(); DEBUG && console.log(namedTags); return namedTags; } async function requestDataFromAPIpostCall( apiPoint, postData, hasDataContainer = undefined ) { const DEBUG = false; let PromiseR###lt = new Promise(async function (resolve, reject) { function onLoad(xhr) { DEBUG && console.log(xhr); //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299) switch (true) { case xhr.status == 304: console.log("xhr.status == 304: getting data from browser cache"); //fall through to status==200 ok case xhr.status >= 200 && xhr.status < 399: { DEBUG && console.group("requestDataFromAPIpostCall onLoad: " + apiPoint); DEBUG && console.log(xhr); let tempJSON = JSON.parse(xhr.responseText); let apiData; DEBUG && console.log(tempJSON); if ((tempJSON.status = "OK")) { if (hasDataContainer) apiData = tempJSON[hasDataContainer]; else apiData = tempJSON; } DEBUG && console.log(apiData); return resolve(apiData); } break; default: /* console.group("error in getCoverDataFromUrl"); console.log(xhr); console.groupEnd();*/ return reject(rejectErrorStatusMessage(xhr)); } } function onError(error) { console.log(error); const err = new Error( "GM_xmlhttpRequest could not load " + apiPoint + "; script is not compatible or url does not exists." ); console.log(err); return reject(err); } GM_xmlhttpRequest({ method: "POST", data: JSON.stringify(postData), url: apiPoint, headers: { Accept: "application/json", "Content-Type": "application/json", }, dataType: "json", contentType: "application/json", overrideMimeType: "application/json", onload: onLoad, onerror: onError, }); return undefined; //reject("status error") }); PromiseR###lt = await PromiseR###lt; return PromiseR###lt; } async function checkImageServerState(url) { const DEBUG = false; let PromiseR###lt = new Promise(async function (resolve, reject) { function onLoad(xhr) { DEBUG && console.log("checkImageServerState"); DEBUG && console.log(xhr); DEBUG && console.log("xhr.status: " + xhr.status); switch (true) { case xhr.status == 304: console.log( "xhr.status == 304: getting data from browser cache for " + xhr.finalUrl ); case xhr.status >= 200 && xhr.status < 399: { DEBUG && console.log("no error"); return resolve(true); } default: /* console.group("error in getCoverDataFromUrl"); console.log(xhr); console.groupEnd();*/ console.log(rejectErrorStatusMessage(xhr)); return reject(false); } } function onError(error) { console.log(error); const err = new Error( "checkImageServerState() could not load " + url + "; script is not compatible or url does not exists." ); console.log(err); return reject(false); } GM_xmlhttpRequest({ method: "HEAD", url: url, onload: onLoad, onerror: onError, // onreadystatechange, // onprogress }); return "error or skipped processing GM_xmlhttpRequest() in checkImageServerState()"; //reject("status error") }); // if(PromiseR###lt!=undefined) { PromiseR###lt = await PromiseR###lt; return PromiseR###lt; } // return "checkImageServerState function error"; } async function getDataFromAPI( apiPoint, { hasDataContainer = undefined, responsetype = "json" } = {} ) { const DEBUG = false; DEBUG && console.log("responsetype", responsetype); let PromiseR###lt = new Promise(async function (resolve, reject) { function onLoad(xhr) { DEBUG && console.log("xhr", xhr); //console.log(xhr.responseHeaders) DEBUG && console.log("xhr.status: " + xhr.status); //https://developer.mozilla.org/en-US/docs/Web/HTTP/Status Successful responses (200–299) switch (true) { case xhr.status == 304: DEBUG && console.log( "xhr.status == 304: getting data from browser cache for " + xhr.finalUrl ); //fall through to status==200 ok case xhr.status >= 200 && xhr.status < 399: { DEBUG && console.group("getDataFromAPI onLoad: " + apiPoint); //DEBUG && console.log(xhr); let apiData; DEBUG && console.log("responsetype", responsetype); switch (responsetype) { case "json": { let tempJSON = JSON.parse(xhr.responseText); DEBUG && console.log("tempJSON", tempJSON); if ((tempJSON.status = "OK")) { if (hasDataContainer) apiData = tempJSON[hasDataContainer]; else apiData = tempJSON; } } break; case "Document": { const domDocument = xhr.response; //tampermonkey 4.12.6130 seems to deprecate some old api? document response is now undefined //only responseXML would be usable apiData = domDocument; } break; case "text": { const domText = xhr.responseText; let parser = new DOMParser() const domDocument = parser.parseFromString( domText, "text/html" ); apiData = domDocument; } break; default: const domDocument = xhr.responseText; apiData = domDocument; break; } DEBUG && console.log("apiData", apiData); if (apiData !== undefined) return resolve(apiData); else return reject( "no apiData error for responsetype: " + responsetype ); } default: /* console.group("error in getCoverDataFromUrl"); console.log(xhr); console.groupEnd();*/ return reject(rejectErrorStatusMessage(xhr)); } } function onError(error) { console.log(error); const err = new Error( "GM_xmlhttpRequest could not load " + apiPoint + "; script is not compatible or url does not exists." ); console.log(err); return reject(err); } function onreadystatechange(stateChangeResponse) { console.log(stateChangeResponse); } function onprogress(progressResponse) { //console.log(progressResponse) } GM_xmlhttpRequest({ method: "GET", //overrideMimeType:'text/html;', responseType: responsetype, url: apiPoint, onload: onLoad, onerror: onError, // onreadystatechange, // onprogress }); return undefined; //reject("status error") }); PromiseR###lt = await PromiseR###lt; return PromiseR###lt; } async function getCoverDataFromMangaDex(id) { const DEBUG = false; if (id) { if (mangaDexTAGS === undefined || mangaDexTAGS === null) { //only needed one time per session mangaDexTAGS = getDataFromAPI("https://mangadex.org/api/v2/tag", { hasDataContainer: "data", }); //getAllMangaDexTags } DEBUG && console.log(mangaDexTAGS); let mangaDexData = getDataFromAPI( "https://mangadex.org/api/v2/manga/" + id, { hasDataContainer: "data" } ); DEBUG && console.log(mangaDexData); let serieChapters = getDataFromAPI( "https://mangadex.org/api/v2/manga/" + id + "/chapters", { hasDataContainer: "data" } ); //await getMangaDexChapterCount(id); DEBUG && console.log(serieChapters); [mangaDexTAGS, mangaDexData, serieChapters] = await Promise.all([ mangaDexTAGS, mangaDexData, serieChapters, ]); let serieGenre = getMangaDexNamedTag(mangaDexData.tags, "Genre"); let serieTags = getMangaDexNamedTag(mangaDexData.tags, "Theme"); DEBUG && console.log(mangaDexTAGS); DEBUG && console.log(serieChapters); if (serieChapters && serieChapters.chapters) { serieChapters = serieChapters.chapters.length; } DEBUG && console.log("serieChapters: " + serieChapters); let status; DEBUG && console.log(mangaDexData); switch (mangaDexData.publication.status) { case 1: status = "Ongoing"; break; case 2: status = "Completed"; break; } let cData = Object.assign({}, emptyCoverData); cData.url = mangaDexData.mainCover; cData.title = mangaDexData.title; cData.alternativeNames = mangaDexData.altTitles.join("<br /> "); cData.votes = mangaDexData.rating.bayesian.toString(); cData.status = status; cData.chapters = serieChapters.toString(); cData.genre = serieGenre; cData.showTags = serieTags; cData.description = mangaDexData.description; return cData; } return undefined; } //#endregion mangadex api //#region http://api.tvmaze.com/ async function getCoverDataFromTVmaze(id) { const DEBUG = false; if (id) { let apiData = getDataFromAPI("http://api.tvmaze.com/shows/" + id); DEBUG && console.log(apiData); let serieAlternativeNames = getDataFromAPI( "http://api.tvmaze.com/shows/" + id + "/akas" ); let episodes = getDataFromAPI( "http://api.tvmaze.com/shows/" + id + "/episodes" ); [apiData, serieAlternativeNames, episodes] = await Promise.all([ apiData, serieAlternativeNames, episodes, ]); DEBUG && console.log(serieAlternativeNames); if (serieAlternativeNames !== undefined) { serieAlternativeNames = serieAlternativeNames .map((e) => e.name) .join("<br /> "); } ///console.log(serieAlternativeNames); let serieEpisodeCount; if (episodes && episodes.length) { serieEpisodeCount = episodes.length.toString(); } let targetImageUrl; if (apiData.image.medium) targetImageUrl = apiData.image.medium; else targetImageUrl = apiData.image.original; let serieRating; if (apiData.rating && apiData.rating.average) { serieRating = apiData.rating.average.toString(); } /* let cData = { url: targetImageUrl, title: apiData.name, alternativeNames: serieAlternativeNames, votes: serieRating, //status: apiData.status, //status property value is overwritten by GM_xmlhttpRequest chapters: serieEpisodeCount, genre: apiData.genres, description: apiData.summary, }; */ let cData = Object.assign({}, emptyCoverData); cData.url = targetImageUrl; cData.title = apiData.name; cData.alternativeNames = serieAlternativeNames; cData.votes = serieRating; //cData.status = status; //status property value is overwritten by GM_xmlhttpRequest cData.chapters = serieEpisodeCount; cData.genre = apiData.genres; //cData.showTags = serieTags; cData.description = apiData.summary; DEBUG && console.log(cData); return cData; } return undefined; } //#endregion http://api.tvmaze.com/ //#region https://www.wlnupdates.com/api-docs async function getCoverDataFromWLNupdates(linkID) { const DEBUG = false; if (linkID) { let apiData = await requestDataFromAPIpostCall( "https://www.wlnupdates.com/api", { id: linkID, mode: "get-series-id" }, "data" ); DEBUG && console.log(apiData); let imagelink; if (apiData.covers.length > 0) imagelink = apiData.covers[0].url; let serieTitle = apiData.title; let serieAlternativeNames = apiData.alternatenames.join("<br />"); let serieVotes; let serieStatus; if (apiData.ratin_count > 0) serieVotes = apiData.rating.avg; if (apiData.most_recent) { serieStatus = "most_recent: " + apiData.most_recent; } //console.log(apiData.releases); //console.log("apiData.releases: " + apiData.releases.length); let serieChapters; if (apiData && apiData.releases && apiData.releases.length) { serieChapters = apiData.releases.length; } if (apiData.type) serieChapters += " (" + apiData.type + ")"; let serieGenre = apiData.genres.map((e) => e.genre).join(", "); let serieShowtags = apiData.tags.map((e) => e.tag).join(", "); let serieDescription = apiData.description; let cData = Object.assign({}, emptyCoverData); cData.url = imagelink; cData.title = serieTitle; cData.alternativeNames = serieAlternativeNames; cData.votes = serieVotes; cData.status = serieStatus; cData.chapters = serieChapters; cData.genre = serieGenre; cData.showTags = serieShowtags; cData.description = serieDescription; DEBUG && console.log(cData); return cData; } return undefined; } //#endregion https://www.wlnupdates.com/api-docs //#region getCoverData from parsing document of url async function getCoverDataFromParsingTargetUrl( elementUrl, targetPage, isExternal = false ) { const DEBUG = false; if (targetPage) { DEBUG && console.log( "getCoverDataFromParsingTargetUrl - elementUrl: " + elementUrl ); let domDocument = await getDataFromAPI(elementUrl, { responsetype: "text", }); DEBUG && console.log(domDocument); //getData from parsed site by query selectors let temp; let imagelink; let containerNumber = 0; let serieTitle; let serieAlternativeNames; let serieVotes; let serieStatus; let serieChapters; let serieGenre; let serieShowtags; let serieDescription; let serieReadingListIcon, serieReadingListTitle; //#region general selectors for internal and external sites DEBUG && console.group("internal/external link selectors"); DEBUG && console.log("targetPage: " + targetPage); //console.log(domDocument); //external links //console.log(targetPage); temp = domDocument.querySelectorAll(targetPage.IMAGELINKCONTAINERS); DEBUG && console.log(temp); if (targetPage.CONTAINERNUMBER) { containerNumber = targetPage.CONTAINERNUMBER; } imagelink = temp[containerNumber]; //console.log(imagelink) serieTitle = domDocument.querySelector(targetPage.seriePageTitle); serieAlternativeNames = domDocument.querySelector( targetPage.serieAlternativeNames ); serieVotes = domDocument.querySelector(targetPage.seriePageVotes); serieStatus = domDocument.querySelector(targetPage.seriePageStatus); serieChapters = domDocument.querySelector(targetPage.seriePageChapters); serieGenre = domDocument.querySelector(targetPage.seriePageGenre); serieShowtags = domDocument.querySelector(targetPage.seriePageTags); serieDescription = domDocument.querySelector( targetPage.seriePageDescription ); serieTitle = tryToGetTextContent( serieTitle, targetPage.seriePageTitle, "seriePageTitle" ); serieAlternativeNames = tryToGetTextContent( serieAlternativeNames, targetPage.serieAlternativeNames, "serieAlternativeNames" ); //console.log(targetPage.seriePageVotes) serieVotes = tryToGetTextContent( serieVotes, targetPage.seriePageVotes, "seriePageVotes" ); //console.log(serieVotes) serieStatus = tryToGetTextContent( serieStatus, targetPage.seriePageStatus, "seriePageStatus" ); serieChapters = tryToGetTextContent( serieChapters, targetPage.seriePageChapters, "seriePageChapters" ); //console.log(targetPage.seriePageGenre) serieGenre = tryToGetTextContent( serieGenre, targetPage.seriePageGenre, "seriePageGenre" ); //console.log(serieGenre) serieShowtags = tryToGetTextContent( serieShowtags, targetPage.seriePageTags, "seriePageTags" ); serieDescription = tryToGetTextContent( serieDescription, targetPage.seriePageDescription, "seriePageDescription" ); DEBUG && console.groupEnd("internal/external link selectors"); //#endregion general selectors for internal and external sites //#region internal extra selectors DEBUG && console.group("internal extra selectors"); if (!isExternal) { //hasInternalTargetPage- > targetPage //console.log(serieReadingListTitle) if (useReadingListIconAndTitle && targetPage.serieReadingListIcon) { serieReadingListIcon = domDocument.querySelector( targetPage.serieReadingListIcon ); } //console.log("hasInternalTargetPage.serieReadingListTitle: " + hasInternalTargetPage.serieReadingListTitle) if (useReadingListIconAndTitle && targetPage.serieReadingListTitle) { serieReadingListTitle = domDocument.querySelector( targetPage.serieReadingListTitle ); } //console.log(targetPage.serieReadingListTitle) //console.log(serieReadingListTitle) serieReadingListTitle = tryToGetTextContent( serieReadingListTitle, targetPage.serieReadingListTitle, "serieReadingListTitle" ); if (serieReadingListTitle == "Add series to...") { serieReadingListTitle = undefined; } //console.log(serieReadingListIcon) if ( serieReadingListIcon !== undefined && serieReadingListIcon !== null && serieReadingListIcon.tagName == "IMG" ) { serieReadingListIcon = serieReadingListIcon.getAttribute("src"); //console.log("serieReadingListIcon: " + serieReadingListIcon) } if ( serieReadingListIcon === null || serieReadingListIcon == "//www.novelupdates.com/wp-content/themes/ndupdates-child/js/selectico/addme.png" ) { serieReadingListIcon = undefined; } //console.log("serieReadingListTitle: " + serieReadingListTitle) } DEBUG && console.groupEnd("internal extra selectors"); //#endregion internal extra selectors let cData = Object.assign({}, emptyCoverData); cData.url = imagelink; cData.title = serieTitle; cData.alternativeNames = serieAlternativeNames; cData.description = serieDescription; cData.status = serieStatus; cData.chapters = serieChapters; cData.genre = serieGenre; cData.showTags = serieShowtags; cData.votes = serieVotes; cData.readingListIcon = serieReadingListIcon; cData.readingListTitle = serieReadingListTitle; DEBUG && console.log(cData); return cData; } return undefined; } //#endregion getCoverData from parsing document of url function main() { const DEBUG = false; //console.log(window.location) currentOpenedUrl = window.location.href; //console.log("preloadUrlRequests: " + preloadUrlRequests) for (let i = 0; i < deactivatePreloadUrlRequestOnUrls.length; i++) { //no need to check on each link hover if (deactivatePreloadUrlRequestOnUrls[i] == currentOpenedUrl) { preloadUrlRequests = false; } } //console.log("preloadUrlRequests: " + preloadUrlRequests) DEBUG && console.log("started main function of coverPreview"); //#region get greasemonkey settings for popup DEBUG && console.log("before starting checkDataVersion"); checkDataVersion(); showDetails = GM_getValue("showDetails"); showDescription = GM_getValue("showDescription"); showSmaller = GM_getValue("showSmaller"); useReadingListIconAndTitle = GM_getValue("useReadingListIconAndTitle"); showIconNextToLink = GM_getValue("showIconNextToLink"); //deactivatePreloadUrlRequestOnUrls = GM_getValue("deactivatePreloadUrlRequestOnUrls"); if (showDetails === undefined || showDetails == "undefined") { showDetails = false; } if (showDescription === undefined || showDescription == "undefined") { showDescription = false; } if (showSmaller === undefined || showSmaller == "undefined") { showSmaller = false; } if ( useReadingListIconAndTitle === undefined || useReadingListIconAndTitle == "undefined" ) { useReadingListIconAndTitle = false; } if (showIconNextToLink === undefined || showIconNextToLink == "undefined") { showIconNextToLink = defaultshowIconNextToLink; } //#endregion DEBUG && console.log("before starting setStyleClasses"); addStyles(); setStyleClasses(); DEBUG && console.log("before starting createPopover"); createPopover(); DEBUG && console.log("before starting hidePopOver"); //#region preset needed for older browser //#endregion //hidePopOver(); /* if (showSmaller) { //console.log("show smaller style"); updatePopoverSize(); } */ //if(showDetails) showDetails = JSON.parse(showDetails); //showDetails = localStorage.getItem("showDetails") == "true"; //console.log("localStorage state showDetails: " + showDetails) DEBUG && console.log("before starting changeToNewDetailStyle"); changeToNewDetailStyle(false); DEBUG && console.log("before starting preloadCoverData"); preloadCoverData(); DEBUG && console.log("before starting prepareEventListener"); prepareEventListener(); DEBUG && console.log("finished main function of coverPreview"); } //console.log("cover preview end"); })();