Pixiv Arts Preview & Followed Atrists Coloring & Extended History

Enlarged preview of arts and manga on mouse hovering. Extended history for non-premium users. Auto-Pagination on Following and Users pages. Click on image preview to open original art in new tab, or MMB-click to open art illustration page, Alt+LMB-click to add art to bookmarks, Ctrl+LMB-click for saving originals of artworks. The names of the authors you are already subscribed to are highlighted with green. Settings can be changed in proper menu.

// ==UserScript==
// @name            Pixiv Arts Preview & Followed Atrists Coloring & Extended History
// @namespace       Pixiv
// @description     Enlarged preview of arts and manga on mouse hovering. Extended history for non-premium users. Auto-Pagination on Following and Users pages. Click on image preview to open original art in new tab, or MMB-click to open art illustration page, Alt+LMB-click to add art to bookmarks, Ctrl+LMB-click for saving originals of artworks. The names of the authors you are already subscribed to are highlighted with green. Settings can be changed in proper menu.
// @author          NightLancerX
// @version         3.89
// @match           https://www.pixiv.net/bookmark_new_illust.php*
// @match           https://www.pixiv.net/discovery*
// @match           https://www.pixiv.net/ranking.php*
// @match           https://www.pixiv.net/*artworks/*
// @match           https://www.pixiv.net/*users/*
// @match           https://www.pixiv.net/history.php*
// @match           https://www.pixiv.net/bookmark_detail.php?illust_id=*
// @match           https://www.pixiv.net/*tags/*
// @match           https://www.pixiv.net/*
// @connect         i.pximg.net
// @connect         i-f.pximg.net
// @connect         i-cf.pximg.net
// @connect         techorus-cdn.com
// @homepageURL     https://github.com/NightLancer/PixivPreview
// @supportURL      https://greasyfork.org/users/167506-nightlancerx
// @license         MIT
// @copyright       NightLancerX
// @grant           GM_xmlhttpRequest
// @grant           GM.xmlHttpRequest
// @grant           GM_setValue
// @grant           GM.setValue
// @grant           GM_getValue
// @grant           GM.getValue
// @require         https://code.jquery.com/jquery-3.3.1.min.js
// @require         https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js
// @compatible      firefox >= 74
// @compatible      chrome >= 80
// @noframes
// ==/UserScript==
//=======================================================================================
(function ()
{
  'use strict';

  if (window.top == window.self && window.jQuery) jQuery(function($) //window.top check may be useless because of @noframes
  {
    console.log('MyPixivJS');

    //---------------------------***CUSTOM PREFERENCES***--------------------------------
    let propList = [
        {paramIndex:0, array:[false,true], name:"PREVIEW_ON_CLICK"},
        {paramIndex:2, array:[0, 100, 200, 300, 500, 1000, 1500], name:"DELAY_BEFORE_PREVIEW"},
        {paramIndex:0, array:["auto", 600, 1200], name:"PREVIEW_SIZE"},
        {paramIndex:1, array:[false,true], name:"ENABLE_AUTO_PAGINATION"},
        {paramIndex:0, array:[false,true], name:"DISABLE_MANGA_PREVIEW_SCROLLING_PROPAGATION"},
        {paramIndex:1, array:[false,true], name:"SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE"},
        {paramIndex:0, array:[false,true], name:"DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING"},
        {paramIndex:0, array:[false,true], name:"HIDE_PEOPLE_WHO_BOOKMARKED_THIS"},
        {paramIndex:0, array:[false,true], name:"KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS"}
    ];
    //---------------------------------DEFAULT VALUES------------------------------------
    // ■ PREVIEW_ON_CLICK =
    // false : showing preview on mouseover (default)
    // true : showing preview after LMB-click
    //
    // ■ DELAY_BEFORE_PREVIEW =
    // 0 : no delay before preview
    // 100 : 0.1 second delay (1000 for 1 second, etc) (default)
    //
    // ■ PREVIEW_SIZE =
    // auto : automatically calculate preview size (1200 or 600) depending of current screen size (default)
    // 600 : up to 600px x 600px
    // 1200 : up to 1200px x 1200px
    //
    // ■ ENABLE_AUTO_PAGINATION =
    // false: disable auto pagination
    // true: enable auto-pagination on Following and Users pages (default)
    //
    // ■ DISABLE_MANGA_PREVIEW_SCROLLLING_PROPAGATION =
    // false : keeping page scrolling after end of manga preview scrolling (default)
    // true : disable page scrolling when viewing manga preview (move mouse out of preview to re-enable scrolling)
    //
    // ■ SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE =
    // true : preview of single image will smoothly fit to vertical screen border after one scroll (default)
    // false : manually scrolling (may need in case of forced 1200px vertical preview with small user screen)
    //
    // ■ DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING =
    // false: standard behavior (default)
    // true : disable page scrolling when viewing single preview (works only if previous setting set to true)
    //
    // ■ HIDE_PEOPLE_WHO_BOOKMARKED_THIS =
    // false: don't change `bookmark_detail.php` page (default)
    // true: hide "People who bookmarked this" section
    //
    // ■ KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS =
    // false: update date every time artwork page opens (default)
    // true: don't renew date and keep first one (NOTE: art will not appear at the top of the history)

    let currentSettings = {};
    //-----------------------------------------------------------------------------------
    let hoverImg = document.createElement('img');
        hoverImg.style = 'display: block;'

    let imgContainer = document.createElement('div');
        imgContainer.id = 'imgPreview';
        imgContainer.style = 'position:absolute; display:block; visibility:visible; z-index:1000; background:#222; padding:5px; margin:-5px;';
        imgContainer.appendChild(hoverImg);

    let mangaContainer = document.createElement('div');
        mangaContainer.id = 'mangaContainer';
        mangaContainer.style = 'display:block; overflow-x:auto; white-space:nowrap; maxWidth:1200px; z-index:1500; background:#111; font-size: 0;';

    let mangaMiddleContainer = document.createElement('div');
        mangaMiddleContainer.style = 'display:block; visibility:inherit; z-index:1250;';
        mangaMiddleContainer.appendChild(mangaContainer);

    let mangaOuterContainer = document.createElement('div');
        mangaOuterContainer.id = 'mangaOuterContainer';
        mangaOuterContainer.style = 'position:absolute; display:block; visibility:hidden; left:0px; right:0px; width:max-content; margin: 0px auto; padding:5px; background:#111; z-index:1000;';
        mangaOuterContainer.appendChild(mangaMiddleContainer);

    let imgsArr = [], //for manga-style image packs...
        followedUsersId = {}, //storing followed users pixiv ID
        BOOKMARK_URL = 'https://www.pixiv.net/ajax/user/XXXXXXXX/following?limit=100&tag=&lang=en',//&offset=0&rest=show'
        USER_ID,
        totalHits = 0,
        lastImgId = -1,
        PREVIEWSIZE,
        siteImgMaxWidth = 184, //2,7,12 [NEW]| quite useless on this pages because of square previews...
        mangaWidth = 1200,
        maxRequestTime = 30000,
        bookmarkContainer,
        pageNumber,
        DELTASCALE = +navigator.userAgent.match(/(?<=Firefox\/)\d+/)?.[0]<83?70:4,       //older than 83.0 FF uses different scrolling scale //[temporary...]
        previewEventType,
        PAGETYPE = checkPageType(),
        followedCheck = {
          id:0,                                                                          //backuping user id in case of cookie errors
          status:0,                                                                      //-1: error, 0:default, 1:in progress, 2:done
          date:0,                                                                        //date of last successful check
          saveState(){
            localStorage.setObj('followedCheck', this);
          },
          loadState(){
            this.id     = localStorage.getObj('followedCheck')?.id     || 0;
            this.status = localStorage.getObj('followedCheck')?.status || 0;
            this.date   = localStorage.getObj('followedCheck')?.date   || 0;
          }
        };

    var timerId, tInt, menuTimer;
    //-----------------------------------------------------------------------------------
    Storage.prototype.setObj = function(key, obj){
      return this.setItem(key, JSON.stringify(obj))
    }
    Storage.prototype.getObj = function(key){
      return JSON.parse(this.getItem(key))
    }
    //-----------------------------------------------------------------------------------
    const GM_setV = (typeof(GM_setValue)==='function')?GM_setValue:GM.setValue;
    const GM_getV = (typeof(GM_getValue)==='function')?GM_getValue:GM.getValue;
    //===================================================================================
    //************************************PageType***************************************
    //===================================================================================
    function checkPageType()
    {
      if (document.URL.match(/https:\/\/www.pixiv.net\/bookmark_new_illust(?:_r18)?.php/))                  return 0; //New illustrations - New +
      if (document.URL.match(/^https:\/\/www.pixiv.net\/discovery(?:\?mode=(safe|r18))?$/))                 return 1; //Discovery page(works) - New +
      if (document.URL.match('https://www.pixiv.net/bookmark_detail.php?'))                                 return 4; //Bookmark information - Old +
      if (document.URL.match('https://www.pixiv.net/ranking.php?'))                                         return 6; //Daily rankings - Old +
      if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?users\/\d+\/bookmarks\/artworks/))        return 7; //Bookmarks page - New +
      if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?users/))                                  return 2; //Artist works page - New +
      if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?tags/))                                   return 8; //Search page - New +
      if (document.URL.match(/https:\/\/www\.pixiv\.net\/(?:en\/)?artworks/))                               return 12; //Illust page - New* +
      if (document.URL.match('https://www.pixiv.net/discovery/users?'))                                     return 13; //Discovery page(users) New +
      if (document.URL.match('https://www.pixiv.net/history.php'))                                          return 14; //History - Old +
      if (document.URL.match(/^https:\/\/www\.pixiv\.net\/(?:en\/)?$/))                                     return 10; //Home page - New +

      return -1;
    }
    console.log('PAGETYPE:',PAGETYPE);
    //-----------------------------------------------------------------------------------
    //Old:                4 6              14
    //New:          0 1 2     7 8 10 12 13
    //==============----------------------------
    //Coloring:     = 1 = 4 6 7 8 10 12 == ~~ //
    //Profile card: 0 1 = 4 6 7 8 10 12 == == //
    //On following: = 1 2 4 6 7 8 ?? 12 13 == //
    //===================================================================================
    function setCurrentSettings(){
      for (let i = 0; i < propList.length; i++){
        currentSettings[propList[i].name] = propList[i].array[propList[i].paramIndex]; //only for options checking, actual settings contains in propList[]
      }
      resetPreviewSize(); //needed because of "auto" feature
      resetPreviewEventType();
    }
    //-----------------------------------------------------------------------------------
    function saveSettings(){
      for (let i = 0; i < propList.length; i++){
        localStorage.setObj(propList[i].name, propList[i].paramIndex);
      }
      console.log("Settings saved");
    }
    //-----------------------------------------------------------------------------------
    function loadSavedSettings(){
      for (let i = 0; i < propList.length; i++){
        propList[i].paramIndex = localStorage.getObj(propList[i].name) ?? propList[i].paramIndex; //load saved setting value, or let default if not found

        if ((propList[i].paramIndex < 0) || (propList[i].paramIndex >= propList[i].array.length)){
          propList[i].paramIndex = 0; // "0" is not default for all settings...
          console.error(`localStorage error! Setting ${propList[i].name} has been reset to default value! [${propList[i].array[propList[i].paramIndex]}]`);
        }
      }
      console.log("Settings loaded");
    }
    //-----------------------------------------------------------------------------------
    loadSavedSettings();
    setCurrentSettings();
    //-----------------------------------------------------------------------------------
    function resetPreviewSize(){PREVIEWSIZE = (currentSettings["PREVIEW_SIZE"] > 0)?currentSettings["PREVIEW_SIZE"]:(window.innerHeight>1200 & document.body.clientWidth>1200)?1200:600}
    function resetPreviewEventType(){previewEventType = (currentSettings["PREVIEW_ON_CLICK"])?'click':'mouseenter'; console.log(previewEventType)}
    //===================================================================================
    //**********************************ColorFollowed************************************
    //===================================================================================
    function makeArgs(baseUrl, total){
      let arr = [];
      for(let i = 1; i < Math.ceil(total / 100); i++){                                   //from 1 - because we already have first 100 users
        arr.push(baseUrl + "&offset=" + i + "00");
      }
      return arr;
    }
    //-----------------------------------------------------------------------------------
    async function getUserId(){
      USER_ID = USER_ID
      || followedCheck && followedCheck?.id
      || document.cookie.match(/user_id=\d+/)?.[0].split("=").pop()
      || Object.keys(localStorage).filter(e => e.match(/viewed_illust_ids_\d+/)).map(a => a.match(/\d+/))[0]
      || (await fetch('https://www.pixiv.net/bookmark.php')).url.match(/\d{3,}/)[0];

      if (!USER_ID) return Promise.reject('FATAL ERROR in obtaining user ID! Please report this on GitHub "Issues"');
    }
    //-----------------------------------------------------------------------------------
    async function checkFollowedArtists()
    {
      followedCheck.loadState();

      if (((Date.now()-23*60*60*1000) > followedCheck.date) || (followedCheck.status < 2) || !localStorage['followedUsersId']){
        console.log('*Followed check started*');

        followedCheck.status = 1;
        followedCheck.saveState();

        await getUserId().catch(e => followedCheckError(e));
        if (USER_ID>0){
          BOOKMARK_URL = BOOKMARK_URL.replace("XXXXXXXX", USER_ID);
        }
        else return -1;

        //make first request separately for obtaining count of followed users, both public/private
        let response0 = [];
        try{
          response0 = await Promise.all([request(BOOKMARK_URL+'&rest=show&offset=0'), request(BOOKMARK_URL+'&rest=hide&offset=0')]);
        }
        catch(error){
          console.error("Error with initial bookmark url!");
          followedCheckError(error);
          return -1;
        }
        for(const i of response0) i.body.users.forEach(user => followedUsersId[user.userId] = 1);

        let args = [];
        let len = response0.map(r => r.body.total);

        args =      makeArgs(BOOKMARK_URL+'&rest=show', len[0]);  //public
        args.concat(makeArgs(BOOKMARK_URL+'&rest=hide', len[1])); //private

        //100 parallel requests in case of 10K users. TODO: find maximum amount and part requests
        let responseArray = [];
        try{
          responseArray = await Promise.all(args.map(e => request(e)));
        }
        catch(error){
          followedCheckError(error);
          return -1;
        }
        for(const r of responseArray) r.body.users.forEach(user => followedUsersId[user.userId] = 1);

        localStorage.setObj('followedUsersId', followedUsersId);
        followedCheck.id = USER_ID;
        followedCheck.status = 2;
        followedCheck.date = Date.now();
        followedCheck.saveState();
        console.log('*Followed check completed*');
        console.log('Obtained', Object.keys(followedUsersId).length, 'followed users');
      }
      else{
        followedUsersId = localStorage.getObj('followedUsersId');
        console.log(`followedCheck is up to date of %c${new Date(followedCheck.date).toLocaleString()}`, 'color:violet;');
      }
    }
    checkFollowedArtists();
    //-----------------------------------------------------------------------------------
    async function request(url, responseType)
    {
      return new Promise(function (resolve, reject){
        let xhr = new XMLHttpRequest();
        xhr.responseType = responseType || 'json';
        xhr.timeout = 10000;
        xhr.open('GET', url, true);
        xhr.onload = function (){
          resolve(this.response);
        };
        xhr.onerror = xhr.ontimeout = function(){
          reject(this);
        };
        xhr.send();
      });
    }
    //-----------------------------------------------------------------------------------
    function followedCheckError(error){
      console.error(error);
      followedCheck.status = -1;
      followedCheck.saveState();
    }
    //-----------------------------------------------------------------------------------
    async function colorFollowed(artsContainers, delay)
    {
      let c = 0, d = 0;
      while (!artsContainers || artsContainers.length === 0) //first call -> daily rankings, illust page
      {
        console.log('waiting for arts...');
        await sleep(delay ?? 2000);

        artsContainers = getAllArtsContainers();
        ++c;
        if (c>5) {console.error('Error while waiting for arts loading! [Timeout 10s]'); return}
      }

      let artsContainersLength = artsContainers.length;

      //wait until last XHR completed if it is not---------------------------------------
      followedCheck.loadState();

      if (followedCheck.status == 1){
        while (followedCheck.status !== 2){
          console.log("waiting for followed users..."); //this could happen in case of huge amount of followed users
          await sleep(2000);
          followedCheck.loadState();

          ++d;
          if (d*2000 > maxRequestTime || followedCheck.status == -1){
            console.error(`ERROR while EXPECTING for subscriptions list! [${d*2000/1000}s]`);
            break;
          }
        }
      }

      //load from localStorage on any errors
      if (followedCheck.status <= 0 || Object.keys(followedUsersId).length == 0){
        console.error(`There was some error during followed users check [Error Code: ${followedCheck.status}]`);
        console.log(`Trying to load cached followedUsersId by date of ${new Date(followedCheck.date).toLocaleString()} ...`);

        followedUsersId = localStorage.getObj('followedUsersId');
        if (followedUsersId && Object.keys(followedUsersId).length > 0){
          console.log("Loaded cached", Object.keys(followedUsersId).length, "followed users");
        }
        else{
          console.error('There is no locally stored followed users entries!');
          return -1;
        }
      }
      //---------------------------------------------------------------------------------
      if (PAGETYPE!==1) console.log('arts loaded:', artsContainersLength, 'Total:', getAllArtsContainers().length);

      let hitContainers = [];
      let currentHits = 0;

      if (PAGETYPE == 12){
        let authorId = +document.querySelector("aside").querySelector("[href*=users]").href.match(/\d+/)[0];
        [].filter.call(artsContainers, container => getAuthorIdFromContainer(container) == authorId) //color current authors arts among suggested
          .forEach(container => container.setAttribute("style", "background-color: deepskyblue; !important"));
      }

      hitContainers = [].filter.call(artsContainers, container => followedUsersId[getAuthorIdFromContainer(container)] == 1);
      hitContainers.forEach(container => container.setAttribute("style", "background-color: green; !important"));

      currentHits = hitContainers.length;
      totalHits += currentHits;

      if (PAGETYPE!==1) console.log('hits: '+currentHits + ' (Total: '+(totalHits)+')'); //containers are constantly being replaced on this page
    }
    //-----------------------------------------------------------------------------------
    function getAllArtsContainers()
    {
      switch (PAGETYPE){
        case 1:
        case 7:
        case 8:
        case 10: return [...document.querySelectorAll('li > div')].filter(e => (e.querySelector('a[href*="/artworks/"]')));

        case 4:
        case 6:  return document.querySelectorAll('.ui-profile-popup');

        case 12: return document.querySelectorAll('.gtm-illust-recommend-title');

        default:  console.error('Unprocessed PAGETYPE in getAllArtsContainers()!');
      }
      return null;
    }
    //-----------------------------------------------------------------------------------
    function getAuthorIdFromContainer(artContainer)
    {
      let authorId = -1;
      //console.log(artContainer);

      if (!artContainer){
        console.error('UNPROCESSED getAuthorIdFromContainer() call!');
      }
      else if (typeof artContainer.hasAttribute !== 'function'){
        console.log(artContainer, 'has been filtered out.');
      }
      else if ([1,7,10,12].includes(PAGETYPE)){
        authorId = searchNearestNode(artContainer,'[href*="/users/"]').getAttribute('href').split('/').pop();
      }
      else if (PAGETYPE===4 || PAGETYPE===6){
        authorId = artContainer.getAttribute('data-user_id') || artContainer.querySelector('.ui-profile-popup').getAttribute('data-user_id');
      }
      else if (PAGETYPE===8){
        let node = searchNearestNode(artContainer,'[href*="/users/"]');
        authorId = (node)? node.getAttribute('href').split('/').pop(): -8;
      }

      return +authorId;
    }
    //-----------------------------------------------------------------------------------
    function sleep(ms)
    {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    //-----------------------------------------------------------------------------------
    function getElementByXpath(path)
    {
      return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }
    //-----------------------------------------------------------------------------------
    function getArtSectionContainers()
    {
      switch(PAGETYPE)
      {
        case 0:
        case 2:
        case 7:  return $('section ul')[0]
        case 1:
        case 4:  return $('.gtm-illust-recommend-zone')[0]
        case 6:  return $('.ranking-items')[0]
        case 8:  return $('body')[0] //$("section>div>ul")[0]
        case 10: return $("div[id='root']>div.charcoal-token>div>div:nth-child(3)")[0]   //fckng sick of this-_-
        case 12: return $('.gtm-illust-recommend-zone ul')[0]

        default: return 0;
      }
    }
    //-----------------------------------------------------------------------------------
    let observer = {
      mutationObserver: null,

      init(func){
        this.mutationObserver = new MutationObserver((mutations)=>{func(mutations)});
      },

      observe(mainDiv, options){
        this.mutationObserver.observe(mainDiv, options);
        console.log('Observer has been set');
      },

      disconnect(){
        this.mutationObserver.disconnect();
      }
    }
    //-----------------------------------------------------------------------------------
    let renewObserver = Object.assign({}, observer); //copy new instance of object
    //-----------------------------------------------------------------------------------
    function observerBody(mutations)
    {
      let arr = [];
      mutations.forEach(function(mutation)
      {
        mutation.addedNodes.forEach(function(node)
        {
          if (PAGETYPE == 10){
            if (node.nodeName == "IMG" && node.matches('img:not([class*=" "])')){    //todo: very unstable condition(>=2 img classes)
              arr.push(searchNearestNode(node, 'a[href*="/artworks/"]'));
            }
            else if (node.nodeName == "DIV"){
              node.querySelectorAll('a[href*="/artworks/"]:only-child').forEach((el)=>arr.push(el));
            }
          }
          else if (PAGETYPE == 12 && (!!node.querySelector('iframe'))){
            node.remove(); //filtering ads
          }
          else if (PAGETYPE == 1 || PAGETYPE == 7){
            node.querySelectorAll('li > div').forEach((el) => arr.push(el));
          }
          else if (PAGETYPE == 8){
            //console.log(node);
            if (node && typeof(node.nodeName)!=='undefined' && node.nodeName==='UL'){
              node.querySelectorAll('li > div').forEach((el) => arr.push(el));
            }
            if (node && typeof(node.nodeName)!=='undefined' && node.nodeName==='LI' && node.querySelector('div [href*="/users/"]')){
              arr.push(node);
            }
          }
          else{
            arr.push(node);
          }

        });
      });

      if (arr.length>0) colorFollowed(arr);
    }
    //-----------------------------------------------------------------------------------
    observer.init(observerBody);
    //-----------------------------------------------------------------------------------
    async function waitForArtSectionContainers()
    {
      let mainDiv = getArtSectionContainers();
      let count = 0;
      while(!mainDiv)
      {
        console.log('Waiting for arts container...');
        await sleep(1000);
        mainDiv = getArtSectionContainers();

        ++count;
        if (count>10) {console.error('Error while waiting for arts containers! [Timeout 10s]'); return -1}
      }
      console.log(mainDiv);

      return mainDiv;
    }
    //-----------------------------------------------------------------------------------
    async function initMutationObject(options)
    {
      observer.observe(await waitForArtSectionContainers(), options);
    }
    //-----------------------------------------------------------------------------------
    function searchNearestNode(el, selector)
    {
      let nearestNode = el.querySelector(selector);
      while ((!nearestNode) && (el != document.body)){
        el = el.parentNode;
        nearestNode = el.querySelector(selector);
      }
      return nearestNode;
    }
    //-----------------------------------------------------------------------------------
    function followage(thisObj, toFollow) //In case of followed check lasting too long, async queue may be a solution
    {
      console.log('toFollow: '+ toFollow);
      let userId = searchNearestNode(thisObj, '[href*="/users/"]').getAttribute('href').split('/').filter(el => el.match(/\d+/))[0];

      if (!(userId>0)) {console.error(`Wrong userId! ${userId}`); return}

      if (localStorage.getObj('followedCheck').status == 2)
      {
        if (Object.keys(followedUsersId).length == 0)
          followedUsersId = localStorage.getObj('followedUsersId');

        if (toFollow){
          followedUsersId[userId] = 1;
          if ([2,12].includes(PAGETYPE)){
            followagePreview();
          }
        }
        else
          delete followedUsersId[userId];

        localStorage.setObj('followedUsersId', followedUsersId);
        console.log('userId ' + userId + [(toFollow)?' added to':' deleted from'] + ' localStorage. Followed: '+ Object.keys(followedUsersId).length);
      }
      else console.info(`${userId} will not be highlighted — too quick subscription before followedCheck is completed. It will be updated in 24 hours, but if you want you can report this on GitHub`);
    }
    //-------------------------------------Followage-------------------------------------
    async function followagePreview()
    {
      let recommendationBlock;
      let c = 0;

      while(!recommendationBlock)
      {
        console.log('Waiting for FollowagePreview');
        await sleep(1000);
        recommendationBlock = getElementByXpath("//div[contains(., 'Recommended users')]");

        ++c;
        if (c>10) {console.error("Error while waiting for recommendationBlock! [Timeout 10s]"); return -1}
      }
      console.log('*FollowagePreview loaded*');

      //let recommendationObserver = Object.assign({}, observer);

      let scrollBackward = recommendationBlock.querySelector('div:nth-child(3) > div:nth-child(2) > button:nth-child(1)');
      let scrollForward  = recommendationBlock.querySelector('div:nth-child(3) > div:nth-child(2) > button:nth-child(2)');

      recommendationBlock.onwheel = function(e){
        e.preventDefault(); //no need
        if (e.deltaY > 0) scrollForward.click()
        else scrollBackward.click();
      };

      $(recommendationBlock).on(previewEventType, 'a:not([href*="/users/"]) img', function(e)
      {
        e.preventDefault();
        //let top = window.innerHeight - PREVIEWSIZE - 5 + window.scrollY + 'px';
        let top = window.scrollY + 5 + 'px';
        checkDelay(setHover, this, top);
      });
    }
    //---------------------------------------History-------------------------------------
    let illust_history = {
      ids: [],
      timestamps: {},

      load(){
        this.ids        = localStorage.getObj('viewed_illust_ids') || GM_getV("viewed_illust_ids") || localStorage.getObj('viewed_illust_ids_' + USER_ID)?.data || [];
        this.timestamps = localStorage.getObj('viewed_illust_timestamps') || GM_getV("viewed_illust_timestamps") || localStorage.getObj('viewed_illust_timestamp_' + USER_ID)?.data || {};
      },

      save(){
        localStorage.setObj('viewed_illust_ids', this.ids);               //viewed_illust_ids
        localStorage.setObj('viewed_illust_timestamps', this.timestamps); //viewed_illust_timestamp
      },

      add_record(illust_id){
        this.load();

        if (this.ids.indexOf(illust_id.toString()) == -1){
          this.ids.push(illust_id.toString());
          this.timestamps[illust_id] = Date.now()/1000;
          console.log(+illust_id, "has been added to history");
        }
        else if (currentSettings["KEEP_OLD_DATE_OF_ALREADY_VIEWED_ARTWORKS"] == false){
          this.timestamps[illust_id] = Date.now()/1000;
          console.log(+illust_id, ": updated view date");
        }
        else console.log(`%c${illust_id}%c already in history (%c${new Date(this.timestamps[illust_id]*1000).toLocaleString()}%c)`, 'color:lime;', 'color:;', 'color:violet;', 'color:;');

        this.save();
      },

      delete_record(illust_id){
        this.load();

        let index = this.ids.indexOf(illust_id.toString());
        if (index > -1){
          this.ids.splice(index, 1);
        }
        delete this.timestamps[illust_id];

        this.save();
        console.log(+illust_id, "has been deleted from history");
      },

      override(){
        this.load();
        let date = Date.now()+365*24*60*60*1000;
        localStorage.setObj('viewed_illust_ids_' + USER_ID, {data:this.ids, expires:date});
        localStorage.setObj('viewed_illust_timestamp_' + USER_ID, {data:this.timestamps, expires:date});
        console.info(`History overridden [%c${this.ids.length}%c records]`, 'color:lime;', 'color:;');

        let count = 0, t = setInterval(()=>{
          document.querySelectorAll('._history-item.trial').forEach(e => {
            e.style.opacity = 1;
            e.classList.remove("trial");
          });
          ++count;
          if (count>10) clearInterval(t);
        }, 1000);
      },

      export(){
        this.load(); //TODO:check history records integrity before export
        GM_setV("viewed_illust_ids", this.ids);
        GM_setV("viewed_illust_timestamps", this.timestamps);
        console.info(`History was exported to script manager storage [%c${this.ids.length}%c records]`, 'color:lime;', 'color:;');
      },

      check_space(){
        let spaceConsumed = +((new Blob([Object.values(localStorage), Object.keys(localStorage),
            localStorage.viewed_illust_ids, localStorage.viewed_illust_timestamps]).size)/(5000*1024)).toFixed(3); //duplicating records not the best solution... but simplest [solve this later if needed]
        if (spaceConsumed > 0.95){
          this.add_record = this.override = ()=>{};
          return Promise.reject(`Too much space consumed [${spaceConsumed*100}%] — history is disabled`); //~100.000 entries
        }
        else console.log('History initialized');
      }
    };
    getUserId().then(() => illust_history.check_space()).catch(e => console.error('History not initialized —', e));
    //===================================================================================
    if      (PAGETYPE===0)                  siteImgMaxWidth = 198;
    else if (PAGETYPE===4)                  siteImgMaxWidth = 150;
    else if (PAGETYPE===6 || PAGETYPE===14) siteImgMaxWidth = 240;
    //-----------------------------------------------------------------------------------
    $(document).ready(function ()
    {
      console.log('$(document).ready');
      mangaWidth = document.body.clientWidth - 60;
      mangaContainer.style.maxWidth = mangaWidth+'px';
      document.body.appendChild(imgContainer);
      document.body.appendChild(mangaOuterContainer);
      //---------------------------------Settings menu-----------------------------------
      let menu = document.createElement("div");
          menu.id = "menu";
          menu.style = `
                        position: absolute;
                        display: block;
                        visibility: hidden;
                        top: 60px;
                        left: 10px;
                        padding: 5px 5px 5px 20px;
                        border: 2px solid deepskyblue;
                        border-radius: 15px;
                        background: white;
                        font-size: 14px;
                        line-height: 17px;
                        color: rgb(0, 0, 0);
                        border-radius: 15px;
                        word-wrap: normal;
          `;

      //filling menu fields with values and property names
      for (let i = 0; i < propList.length; i++){
        menu.innerHTML += `<li style = 'font:inherit;'><button style = 'width: 40px; padding: 0px; margin-right: 5px;'>${propList[i].array[propList[i].paramIndex]}</button>${propList[i].name}</li>`
      }

      document.body.appendChild(menu);
      //---------------------------------------------------------------------------------
      function changeMenuValues(menuDiv){
        let index = Array.prototype.indexOf.call(menuDiv.parentNode.parentNode.children, menuDiv.parentNode);
        propList[index].paramIndex+=1;
        if (propList[index].paramIndex >= propList[index].array.length) propList[index].paramIndex = 0;
        menuDiv.textContent = propList[index].array[propList[index].paramIndex];

        //foolproof protection
        if (propList[0].paramIndex == 1){
          menu.childNodes[1].childNodes[0].disabled = true;
          propList[1].paramIndex = 0;
          menu.childNodes[1].childNodes[0].textContent = "0";
        }
        else{
          menu.childNodes[1].childNodes[0].disabled = false;
        }
      }

      $('#menu').on('click', 'button', function(){
        changeMenuValues(this);
      });
      //---------------------------------------------------------------------------------
      async function initMenu(){
        if ([-1,14].includes(PAGETYPE)) return;

        let buttons, menuButton; //put to global scope if (menuButton) is needed elsewhere

        let count = 0;
        while (!menuButton && count<5){
          if ([0,1,2,7,8,10,12].includes(PAGETYPE))
            buttons = document.querySelectorAll('body > div#root > div.charcoal-token > div > div:nth-child(2) button')
          else
            buttons = document.querySelectorAll('body > div#js-mount-point-header > div:nth-child(1) button');
          menuButton = buttons[buttons.length - 1]; // last is the menu button
          console.log(menuButton);
          await sleep(1000);
          ++count;
        }

        if (menuButton)
          menuButton.addEventListener("click", function(){
            menu.style.visibility = 'visible';
            clearTimeout(menuTimer);
            menuTimer = setTimeout(()=>{menu.style.visibility = 'hidden'}, 60*1000); //closing menu after 60s to prevent "hanging" it in one tab
          });
        else
          console.error("menuButton is undefined!");
      }
      //---------------------------------------------------------------------------------
      $(document).mouseup(function (e){
        if (!($(menu).has(e.target).length) && (menu.style.visibility == 'visible')){
          menu.style.visibility = 'hidden';
          saveSettings();
          if (currentSettings[propList[0].name] !== propList[0].array[propList[0].paramIndex]) setTimeout(()=>{initPreviewListeners(); initProfileCard()}, 0); //reset event listeners only after settings are applied
          setCurrentSettings();
          clearTimeout(menuTimer);
        }
      });
      //---------------------------------------------------------------------------------
      initMenu();
      //-------------------------------Follow onclick------------------------------------
      let toFollow, followSelector;
      //---------------------------------------------------------------------------------
      function initFollowagePreview()
      {
        if ([1,2,7,8,12].includes(PAGETYPE)){
          followSelector = 'button:contains("Follow")';
        }
        else if ([4,6,13].includes(PAGETYPE)){
          followSelector = '.follow-button';
        }
        else return 0;

        $('body').off('mouseup', followSelector); //clearing previous events

        if ([1,2,4,6,7,8,12,13].includes(PAGETYPE))
        {
          $('body').on('mouseup', followSelector, function(){
            toFollow = (this.textContent == 'Follow'); //~mustn't work on non-English locale| todo: add some locale-specific text condition?
            followage(this, toFollow);
          });
        }

        if ([2,7].includes(PAGETYPE)){
          $('body').off('mouseup', '.gtm-profilepage-dotmenu-recommendedusersitem');
          $('body').on('mouseup', '.gtm-profilepage-dotmenu-recommendedusersitem', followagePreview);
        }
      }
      //---------------------------------------------------------------------------------
      initFollowagePreview();
      //====================================PAGINATION===================================
      async function autoPagination(){
        $('section ul').off('click', 'button');
        window.onscroll = null;
        //-------------------------------------------------------------------------------
        if (!currentSettings['ENABLE_AUTO_PAGINATION'] || ![0,2,7].includes(PAGETYPE)) return -1;

        let pageCount = location.href.match(/(?<=[?|&]p=)\d+/)?.[0] || 1;
        let mode = location.href.match(/r18/)?.[0] || "All";
        let maxPageCount = 35; //limit for Following is 35 pages

        let authorId = location.href.match(/(?<=users\/)\d+/)?.[0];
        let artworks = !!location.href.match(/\d+\/artworks/)?.[0];
        let illusts = !!location.href.match(/illustrations/)?.[0];
        let manga = !!location.href.match(/manga/)?.[0];
        let rest = location.href.match(/rest=hide/)?.[0] && "hide" || "show";
        //let tags = location.href.match(/(?<=illustrations\/|manga\/|artworks\/)[^?]+/) || '';
        //-------------------------------------------------------------------------------
        let x_csrf_token; //for bookmarks
        request('/en/', 'document').then(response => x_csrf_token = response.documentElement.innerHTML.match(/(?<=token&quot;:&quot;)[\dA-z]+/));
        //-------------------------------------------------------------------------------
        let artsSection = await waitForArtSectionContainers();
        await sleep(2000);
        let art = $(artsSection.querySelector('a[href*="artworks"]')).parents('li')[0].cloneNode(true);
        let mangaCount = document.createElement('div');
            mangaCount.style = "position: absolute; right: 0px; top: 0px; z-index: 1; display: flex; justify-content: center; align-items: center; flex: 0 0 auto; box-sizing: border-box; height: 20px; min-width: 20px; font-weight: bold; padding: 0px 6px; background: rgba(0, 0, 0, 0.32) none repeat scroll 0% 0%; border-radius: 10px; font-size: 10px; line-height: 10px; color: rgb(255, 255, 255);";
            mangaCount.appendChild(document.createElement('span'));
            mangaCount.querySelector('span').style = "font-size: 10px; line-height: 10px; color: rgb(255, 255, 255); font-family: inherit; font-weight: bold;";
        if (!art.querySelector('span')) art.querySelector('[href]').appendChild(mangaCount);
        art.querySelectorAll('img').forEach(el => el.src='');
        art.classList.add("paginated");
        //-------------------------------------------------------------------------------
        pageNumber = pageNumber ?? mangaCount.cloneNode(true);
        pageNumber.className = "pageNumber";
        pageNumber.style = "position: fixed; right: 5px; bottom: 5px; height: 16px; width: 16px; z-index: 1; display: flex; justify-content: center; align-items: center; flex: 0 0 auto; box-sizing: border-box; font-weight: bold; padding: 0px 6px; background: rgba(0, 0, 0, 0.32) none repeat scroll 0% 0%; border-radius: 16px; font-size: 10px; line-height: 10px; color: rgb(255, 255, 255); opacity: 0%; transition: opacity 1s;";
        document.body.appendChild(pageNumber);
        //-------------------------------------------------------------------------------
        let running = false, urls = [];

        window.onscroll = async function(){
          if ((window.innerHeight*1.8 + window.scrollY) >= document.body.scrollHeight){
            if (running || pageCount>=maxPageCount) return;

            running = true;
            pageCount++;

            let url;
            let tags = location.href.match(/(?<=illustrations\/|manga\/|artworks\/)[^?]+/) || '';

            if (PAGETYPE == 0){
              url = `https:\/\/www\.pixiv\.net\/ajax\/follow_latest\/illust\?p=${pageCount}\&mode=${mode}\&lang=en`;
            }
            if (PAGETYPE == 2){
              if (!urls.length && !tags){
                urls = [];
                await fetch(`https://www.pixiv.net/ajax/user/${authorId}/profile/all?lang=en`).then(r => r.json()).then(response => {
                  let iArr = (illusts || artworks) && Object.keys(response.body.illusts) || [];
                  let mArr = (manga || artworks) && Object.keys(response.body.manga) || [];
                  let arr = iArr.concat(mArr).sort(function(a,b){return a-b}).reverse();
                  for(let i=(pageCount-1)*48; i<arr.length; i+=48){
                    urls.push(`https://www.pixiv.net/ajax/user/${authorId}/profile/illusts?ids[]=`
                      + arr.slice(i, i+48).join('&ids[]=')
                      + "&work_category=illustManga&is_first_page=0&lang=en"
                    );
                  }
                });
                if (!urls.length) return; //maybe check nav element before fetching instead
                maxPageCount = urls.length + 1;
              }

              if (tags){
                let illustManga = artworks && 'illustmanga' || illusts && 'illusts' || manga && 'manga';
                url = `https:\/\/www\.pixiv\.net\/ajax\/user\/${authorId}\/${illustManga}\/tag\?tag=${tags}\&offset=${(pageCount-1)*48}\&limit=48\&lang=en`;
              }
              else{
                url = urls.shift();
              }
            }
            if (PAGETYPE == 7){
              url = `https:\/\/www\.pixiv\.net\/ajax\/user\/${authorId}\/illusts\/bookmarks\?tag=${tags}\&offset=${(pageCount-1)*48}\&limit=48\&rest=${rest}\&lang=en`
            }

            console.log('Loading', pageCount, 'page...');

            fetch(url).then(r => r.json()).then(response => {
              let fragment = new DocumentFragment();
              Array.prototype.forEach.call(response.body?.thumbnails?.illust || Object.values(response.body.works).reverse(), (obj) => {
                let el = art.cloneNode(true);
                if (obj.pageCount > 1) [...(el.querySelectorAll('span'))].pop().textContent = obj.pageCount;
                else $(el.querySelector('span')).parents('a > div')[0].remove();
                //-----------------------------------------------------------------------
                let s = el.querySelector('[href]').href.match('/en/')?.[0] || '/';
                let hrefs = el.querySelectorAll('[href]');

                hrefs[0].setAttribute('data-gtm-value', obj.id);
                hrefs[0].href = s + "artworks/" + obj.id;

                hrefs[1].href = s + "artworks/" + obj.id;
                hrefs[1].textContent = obj.title;

                el.querySelector('img').src = obj.url;

                if (hrefs.length == 4){
                  hrefs[2].setAttribute('data-gtm-value', obj.userId);
                  hrefs[2].href = s + "users/" + obj.userId;

                  hrefs[3].setAttribute('data-gtm-value', obj.userId);
                  hrefs[3].href = s + "users/" + obj.userId;
                  hrefs[3].textContent = obj.userName;

                  el.querySelectorAll('img')[1].src = obj?.profileImageUrl || ''; //for deleted bookmarks
                }

                if (obj.bookmarkData) el.querySelectorAll('path:not(:only-child)').forEach(e => {
                  e.setAttribute("style", "fill: rgb(255, 64, 96); !important")
                });
                //-----------------------------------------------------------------------
                el.style.display = "list-item"; //needed - 'none' otherwise
                fragment.appendChild(el);
              });
              if (PAGETYPE==7 || tags) maxPageCount = Math.ceil(response.body.total/48);

              artsSection.appendChild(fragment);
              running = false;
            });

            pageNumber.querySelector('span').textContent = pageCount;
            pageNumber.style.opacity = "100%";
            setTimeout(()=>pageNumber.style.opacity = "0%", 1500);

            if (pageCount>=maxPageCount){
              console.log('*All pages loaded*');
              [...document.querySelectorAll("nav")].pop().style.opacity = 0.3;
            }
          } //endif
        } //onscroll
        //-------------------------------------------------------------------------------
        $(artsSection).on('click', 'button', function(event){
          event.preventDefault();
          let illust_id = searchNearestNode(this,'[href*="/artworks/"]').href.match(/\d+/)[0];

          fetch('/ajax/illusts/bookmarks/add', {
            method: 'POST',
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json; charset=utf-8',
              'x-csrf-token': x_csrf_token
            },
            body: JSON.stringify({"illust_id":illust_id,"restrict":0,"comment":"","tags":[]})
          })
          .then(() => this.querySelectorAll('path:not(:only-child)').forEach(e => e.setAttribute("style", "fill: rgb(255, 64, 96);")))
          .catch(err => console.log(err));
        });
        //-------------------------------------------------------------------------------
        return 0;
      }
      //=================================================================================
      function initMutationObservers()
      {
        observer.disconnect();
        $('body').off('mouseup', 'a[href*="/discovery"]');
        $('body').off('mouseup', 'a[href*="bookmarks/artworks"]');
        $('body').off('mouseup', 'section>div>a[href*="/artworks"], a[href*="/illustrations"], a[href*="/manga"]');
        //-------------------------------------------------------------------------------
        if (PAGETYPE===0){
          autoPagination().then(v => {
            if (v === 0){
              $('body').on('click', 'a[href*="/bookmark_new_illust"]', function(e){
                e.preventDefault();
                location.href = this.href;
              });
            }
          })
        }
        //-------------------------------------------------------------------------------
        if (PAGETYPE===1){
          colorFollowed();
          initMutationObject({'childList': true, 'subtree': true});

          let timeout_1;
          $('body').on('mouseup', 'a[href*="/discovery"]', function(){
            clearTimeout(timeout_1);
            timeout_1 = setTimeout(() => {
              if (PAGETYPE===1){
                colorFollowed();
                initMutationObject({'childList': true, 'subtree': true});
              }
            }, 2000);
          });
        }
        //---------------------------Bookmark detail page cleaning-----------------------
        if (PAGETYPE===4)
        {
          if (currentSettings["HIDE_PEOPLE_WHO_BOOKMARKED_THIS"])
            $('.bookmark-list-unit')[0].remove();

          initMutationObject({'childList': true});
        }
        //-----------------------------Daily rankings ad cleaning------------------------
        if (PAGETYPE===6)
        {
          colorFollowed();
          $('.ad-printservice').remove();

          initMutationObject({'childList': true});
        }
        //----------------------------------Artwork page---------------------------------
        if (PAGETYPE===12)
        {
          initMutationObject({'childList': true});
          illust_history.add_record(location.href.match(/\d+/)[0]);
        }
        //----------------------------------Search page----------------------------------
        if (PAGETYPE===8)
        {
          initMutationObject({'childList': true, 'subtree': true});
        }
        //----------------------------------Main page------------------------------------
        if (PAGETYPE===10)
        {
          colorFollowed();
          initMutationObject({'childList': true, 'subtree': true});
        }
        //-------------------------------Pixiv User pages--------------------------------
        if (PAGETYPE===2 || PAGETYPE===7)
        {
          let pagination = autoPagination(); //2,7

          $('body').on('mouseup', 'a[href*="bookmarks/artworks"]', function(){
            console.log('PAGETYPE: '+ PAGETYPE+' -> 7');
            PAGETYPE = 7;

            sleep(5000).then(() => {
              initMutationObject({'childList': true});
              autoPagination().then((v)=>{colorFollowed(null, v && 2000)}); //success(0) -> already waited 2+ secs; disabled(-1) -> need to wait
              initProfileCard();
            });
          });

          $('body').on('mouseup', 'section>div>a[href$="/artworks"], a[href$="/illustrations"], a[href$="/manga"]', function(){
            console.log('PAGETYPE: '+ PAGETYPE+' -> 2');
            PAGETYPE = 2;
            sleep(2500).then(autoPagination);
            observer.disconnect();
          });

          if (PAGETYPE===7){
            initMutationObject({'childList': true});
            pagination.then((v)=>{colorFollowed(null, v && 2000)}); //if pagination is enabled we need to wait before it completes*, but no more
            initProfileCard();
          }

          //clearing "cache" of autopaged arts
          $('body').on('mouseup', 'section>div>div>div>a[href*="/illustrations/"], section>div>div>div>a[href*="/artworks/"], section>div>div>div>a[href*="/manga/"]', function(){
            let artsSection = getArtSectionContainers();
            [...artsSection.querySelectorAll('.paginated')].forEach(el => el.remove());
          });
        }
        //------------------------------------History------------------------------------
        if (PAGETYPE===14){
          let trial = document.querySelector('span.trial'); //indicator of non-premium account
          if (trial){
            getUserId().then(() => illust_history.override());
            trial.textContent = "Extended Version";
          }

          //export with Shift+E
          document.onkeyup = function(e){
            if (e.key.toUpperCase() == "E" && e.shiftKey){
              illust_history.export();
            }
          };
        }
        //-------------------------------------------------------------------------------
      }
      //---------------------------------------------------------------------------------
      initMutationObservers();
      //=================================================================================
      //***************************************HOVER*************************************
      //=================================================================================
      //------------------------------------Profile card--------------------------------- //4,6,9 [~0,1,~7,8,10,12]
      function initProfileCard()
      {
        $('body').off('mouseenter click', 'section._profile-popup a[href*="/artworks/"]');
        $('body').off("mouseenter", '.paginated a[href*="/users/"]');
        $('body').off("mouseleave", '.paginated a[href*="/users/"]');
        //-------------------------------------------------------------------------------
        if ([4,6].includes(PAGETYPE)) //rankings
        {
          $('body').on(previewEventType, 'section._profile-popup a[href*="/artworks/"]', function(e)
          {
            console.log('Profile card');
            e.preventDefault();
            checkDelay(setHover, this, getOffsetRect(this).top+200+'px', true);
          });
        }
        //-------------------------------------------------------------------------------
        if ([0,7].includes(PAGETYPE) && currentSettings['ENABLE_AUTO_PAGINATION']) //patch for profile preview with pagination
        {
          //creating profile card(for last 3 arts)
          let profilePopup = document.createElement('section');
              profilePopup.className = '_profile-popup';
              profilePopup.style = `visibility:hidden; position:absolute; height:128px; z-index:10001; padding: 0px;`;
              profilePopup.onmouseleave = function(e){
                profilePopup.style.visibility = "hidden";
                if (e.relatedTarget?.id != 'imgPreview') imgContainer.style.visibility = "hidden";
              }
          let profileImagesDiv = document.createElement('div');
              profileImagesDiv.style = `overflow:hidden; height:128px; border-radius:5px; border: 1px solid #c7d2dc; padding: 0px; background-color: rgb(255,255,255);`;
              profilePopup.appendChild(profileImagesDiv);

          for (let i=0; i<3; i++){
            var a = document.createElement('a');
            a.className = `item_${i}`;
            a.style = `display: inline-block !important; width: 128px; height: 128px;`;
            a.target = "_blank";
            profileImagesDiv.appendChild(a);
          }
          document.body.appendChild(profilePopup);

          let profileCard_timeout, previous_id;
          //handler for showing paginated profile card
          $('body').on("mouseenter", '.paginated a[href*="/users/"]', function(e){
            e.preventDefault();
            let user_id = this.href.match(/\d+/)[0];
            if (user_id == 0) return;
            if (previous_id == user_id){
              profilePopup.style.top = getOffsetRect(this.parentNode).top - 128 + "px";
              profilePopup.style.left = getOffsetRect(this.parentNode).left - 128+24 + "px";
              profilePopup.style.visibility = "visible";
              return;
            }
            clearTimeout(profileCard_timeout); //cancelling previous event
            profilePopup.firstChild.childNodes.forEach(el => el.style.backgroundImage = '');

            profileCard_timeout = setTimeout(fillProfileCard.bind(this, user_id), 500);
          });

          function fillProfileCard(user_id){
            if (!([].indexOf.call(document.querySelectorAll(':hover'), this) > -1)) return; //need to check whether mouse is still over user profile after 500ms

            profilePopup.style.top = getOffsetRect(this.parentNode).top - 128 + "px";
            profilePopup.style.left = getOffsetRect(this.parentNode).left - 128+24 + "px"; //-sq.preview +icon
            profilePopup.style.visibility = "visible";

            fetch(`https://www.pixiv.net/rpc/get_profile.php?user_ids=${user_id}&illust_num=3&novel_num=0`).then(r => r.json()).then(response => {
              response.body[0].illusts.forEach((el,i) => {
                profilePopup.querySelector(`a.item_${i}`).style.backgroundImage = `url(${el.url["128x128"]})`;
                profilePopup.querySelector(`a.item_${i}`).href = `/artworks/${el.illust_id}`;
              })
            });
            previous_id = user_id;
          }
          //actual art preview
          $('body').on(previewEventType, 'section._profile-popup a[href*="/artworks/"]', function(e){
            e.preventDefault();
            checkDelay(setHover, this, getOffsetRect(this).top+128+5+'px', true);
          });

          $('body').on("mouseleave", `.paginated div[aria-haspopup]`, function(e){
            if (!e.relatedTarget?.closest('._profile-popup')) profilePopup.style.visibility = "hidden";
          });
        }
      }

      initProfileCard();
      //=================================================================================
      //*******************************Initialize Preview Listeners**********************
      //=================================================================================
      function initPreviewListeners()
      {
        //clearing-----------------------------------------------------------------------
        $('body').off('click mouseenter', 'a[href*="/artworks/"]');
        $('body').off('click mouseenter', 'a[href*="/artworks/"] img');
        $('body').off('click', '[role="presentation"] img');
        //document.removeEventListener('click'); //not worth bothering
        //-------------------------------------------------------------------------------
        if (previewEventType == 'click'){
          document.addEventListener('click', (e)=>{
            if (e.target.nodeName==="IMG") e.preventDefault();
          }, {capture: true}) //site uses event capturing now which jQuery can't cover
        }
        //-------------------------------------------------------------------------------
        //New illustrations, Discovery[Artworks], Artist pages, Bookmarks, Search, Home page, Artwork page //0,1,2,7,8,10,12
        if (PAGETYPE === 0 || PAGETYPE === 1 || PAGETYPE === 2 || PAGETYPE === 7 || PAGETYPE === 8 || PAGETYPE === 10 || PAGETYPE === 12)
        {
          console.info('new');
          $('body').on(previewEventType, 'a[href*="/artworks/"] img', function(e)
          {
            e.preventDefault();
            //---------------------------filtering preview card--------------------------
            if (getElementByXpath("//a[text()='View Profile']")){
              if (this.parentNode.parentNode.querySelector('span'))
                checkDelay(setMangaHover, this, this.parentNode.parentNode.textContent, getOffsetRect(this).top+112+'px');
              else
                checkDelay(setHover, this, getOffsetRect(this).top+112+5+'px', true);
            }
            //-------------------------filtering recommended users-----------------------
            else if (getElementByXpath("//div[text()='Recommended users']")){
              let top = window.scrollY + 5 + 'px';
              checkDelay(setHover, this, top);
            }
            //--------------------------------Normal case--------------------------------
            else{
              //multiple
              if (this.parentNode.parentNode.querySelector('span'))
                checkDelay(setMangaHover, this, this.parentNode.parentNode.textContent.replace(/R-18(G)?/,""));
              //single
              else checkDelay(setHover, this);
            }
            //---------------------------------------------------------------------------
          });
          //-----------------------------------------------------------------------------
          if (PAGETYPE === 12) $('body').on('click', '[role="presentation"] img', function(event){
            if (event.ctrlKey){
              event.preventDefault();
              event.stopPropagation();
              let isManga = !!document.querySelector('.gtm-manga-viewer-preview-modal-open');
              onClickActions(this, event, isManga);
            }
          });
        }
        //----------------------DAILY RANKINGS & BOOKMARK INFORMATION PAGES-------------- //4,6
        else if (PAGETYPE === 4 || PAGETYPE === 6)
        {
          $('body').on(previewEventType, 'a[href*="/artworks/"]', function(e) //direct div selector works badly with "::before"
          {
            e.preventDefault();
            //single
            if (this.childNodes.length == 1 && this.childNodes[0].nodeName=="DIV"){
              checkDelay(setHover, this.firstChild.firstChild);
            }
            //multiple
            else if (this.children[1] && this.children[1].className == 'page-count'){
              checkDelay(setMangaHover, this.firstChild.firstChild, this.children[1].children[1].textContent);
            }
          });
        }
        //----------------------------------DISCOVERY[USERS]----------------------------- //13
        else if (PAGETYPE === 13)
        {
          $('body').on(previewEventType, 'a[href*="/artworks/"] img', function(e){
            e.preventDefault();
            if      (this.childNodes.length == 0)  checkDelay(setHover, this); //single art
            else if (this.childNodes.length == 1)  checkDelay(setMangaHover, this, this.firstChild.textContent); //manga
          });
        }
        //-------------------------------------History----------------------------------- //14
        else if (PAGETYPE === 14)
        {
          $('body').on(previewEventType, '._history-item', function(e){
            e.preventDefault();
            checkDelay(setHover, this, getOffsetRect(this).top + 'px');
          });
        }
      }
      //---------------------------------------------------------------------------------
      initPreviewListeners();
      //=================================================================================
      if (currentSettings["DELAY_BEFORE_PREVIEW"]>0) $('body').on('mouseleave', 'a[href*="/artworks/"]', function()
      {
        clearTimeout(timerId);
        clearInterval(tInt);
      });
      //---------------------------------Async page change-------------------------------
      function renewAll()
      {
        if (PAGETYPE != checkPageType())
        {
          console.log('PAGETYPE:', PAGETYPE, '->', PAGETYPE = checkPageType());

          clearTimeout(timerId);
          clearInterval(tInt);

          if (PAGETYPE === -1) return;

          initPreviewListeners();
          initMutationObservers();
          initMenu();
          autoPagination();

          initFollowagePreview();
          initProfileCard();
        }
      }
      //---------------------------------------------------------------------------------
      renewObserver.init(renewAll);
      renewObserver.observe($('body')[0], {childList: true, subtree: true});
      //---------------------------------------------------------------------------------
    }); //end of document.ready
    //===================================================================================
    //-----------------------------------------------------------------------------------
    function checkDelay(func, ...args)
    {
      if (currentSettings["DELAY_BEFORE_PREVIEW"]>0){
        clearTimeout(timerId);
        timerId = setTimeout(()=>{
          if ([].indexOf.call(document.querySelectorAll(':hover'), (PAGETYPE!=6)? args[0] : args[0].parentNode.parentNode) > -1) func(...args)
        }, currentSettings["DELAY_BEFORE_PREVIEW"]);
      }
      else func(...args)
    }
    //-----------------------------------------------------------------------------------
    function setHover(thisObj, top, profileCard)
    {
      clearInterval(tInt);
      imgContainer.style.visibility = 'hidden';
      mangaOuterContainer.style.visibility = 'hidden';
      hoverImg.src=''; //just in case

      hoverImg.src = parseImgUrl(thisObj);
      imgContainer.style.top = top || getOffsetRect(thisObj.parentNode.parentNode).top+'px';

      //adjusting preview position considering expected image width
      //---------------------------------------------------------------------------------
      let l = ([4,6,14].includes(PAGETYPE))
          ?getOffsetRect(thisObj.parentNode.parentNode).left //more accurate on discovery users and history
          :getOffsetRect(thisObj).left;
      let dcw = document.body.clientWidth;
      let previewWidth = PREVIEWSIZE;

      if (hoverImg.naturalWidth>0){ //cached (previously viewed)
        adjustSinglePreview(dcw, l, hoverImg.naturalWidth, (PAGETYPE!=6)?thisObj:thisObj.parentNode.parentNode);
        //console.log("cached");
      }
      else{ //on old pages width can be pre-calculated
        if ([4,6,14].includes(PAGETYPE) && !profileCard){
          previewWidth = PREVIEWSIZE*(((PAGETYPE==6 || PAGETYPE==14)?thisObj.clientWidth:thisObj.parentNode.parentNode.clientWidth)/siteImgMaxWidth)+5;
          adjustSinglePreview(dcw, l, previewWidth, (PAGETYPE!=6)?thisObj:thisObj.parentNode.parentNode);
          //console.log("count");
        }
        else{ //if it is obvious that preview will fit on the screen then there is no need in setInterval(trying to use as minimun setInterval`s as possible)
          if (dcw - l - PREVIEWSIZE - 5 > 0){
            imgContainer.style.left = l+'px';
            imgContainer.style.visibility = 'visible';
            //console.log("excessive");
          }
          else{ //when on NEW layout - need to wait until image width is received
            let tLimit = 0;

            tInt = setInterval(function(){
              if (hoverImg.naturalWidth>0){
                clearInterval(tInt);
                adjustSinglePreview(dcw, l, hoverImg.naturalWidth, thisObj); //position mismatching due to old `thisObj` => clearing in hoverImg.mouseleave
              }
              ++tLimit;
              //console.log(tInt, tLimit);

              if (tLimit*40>5000){ //timeout 5s in case of loading errors
                clearInterval(tInt);
                hoverImg.src='';
                console.error('setInterval error');
                return;
              }
            }, 40);
          }
        }
      }
      //---------------------------------------------------------------------------------
      checkBookmark(thisObj, imgContainer);
    }
    //-----------------------------------------------------------------------------------
    function adjustSinglePreview(dcw, l, contentWidth)
    {
      if (l<0) l = 5; //followage preview
      let d = dcw - l - contentWidth - 5; //5 - padding - todo...
      imgContainer.style.left = (d>=0)?l+'px':l+d+'px';
      imgContainer.style.visibility = 'visible';
    }
    //-----------------------------------------------------------------------------------
    function setMangaHover(thisObj, count, top)
    {
      clearInterval(tInt);
      imgContainer.style.visibility = 'hidden'; //just in case

      mangaOuterContainer.style.top = top || getOffsetRect(thisObj.parentNode.parentNode).top+'px';

      checkBookmark(thisObj, mangaOuterContainer);

      imgsArrInit(thisObj, +count);
    }
    //-----------------------------------------------------------------------------------
    function imgsArrInit(thisObj, count)
    {
      let primaryLink = parseImgUrl(thisObj);
      let currentImgId = getImgId(primaryLink);
      //---------------------------------------------------------------------------------
      if (currentImgId != lastImgId)
      {
        for(let j=0; j<imgsArr.length; j++)
        {
          imgsArr[j].src = '';
        }

        lastImgId = currentImgId;

        for(let i=0; i<count; i++)
        {
          if (!(!!imgsArr[i])) //if [i] img element doesn't exist
          {
            imgsArr[i] = document.createElement('img');
            mangaContainer.appendChild(imgsArr[i]);
          }
          imgsArr[i].src = primaryLink.replace('p0','p'+i);
        }
      }
      //---------------------------------------------------------------------------------
      mangaOuterContainer.style.visibility = 'visible';
    }
    //-----------------------------------------------------------------------------------
    function parseImgUrl(thisObj)
    {
      let url = (thisObj.src)? thisObj.src: thisObj.style.backgroundImage.slice(5,-2);
      url = url.replace(/\/...x..[0|8]/, '/'+PREVIEWSIZE+'x'+PREVIEWSIZE).
                replace('_square1200','_master1200').
                replace('_custom1200','_master1200').
                replace('custom-thumb','img-master').
                replace('_80_a2','').
                replace('_70','')
      ;
      return url;
    }
    //-----------------------------------------------------------------------------------
    function checkBookmark(thisContainer, previewContainer)
    {
      if ([0,1,2,7,8,10,12].includes(PAGETYPE))
        bookmarkContainer = searchNearestNode(thisContainer, 'button');
      else if ([4,6].includes(PAGETYPE))
        bookmarkContainer = searchNearestNode(thisContainer, "._one-click-bookmark")
      else return; //no favourite button

      if ($(bookmarkContainer).hasClass("on"))
        $(previewContainer).css("background", "rgb(255, 64, 96)"); //purple
      else
        $(previewContainer).css("background", "rgb(34, 34, 34)"); //grey
    }
    //-----------------------------------------------------------------------------------
    function getImgId(str)
    {
      return str.substring(str.lastIndexOf("/")+1, str.indexOf("_"));
    }
    //-----------------------------------------------------------------------------------
    function getOffsetRect(elem)
    {
      // (1)
      let box = elem.getBoundingClientRect();
      // (2)
      let body = document.body;
      let docElem = document.documentElement;
      // (3)
      let scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
      let scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
      // (4)
      let clientTop = docElem.clientTop || body.clientTop || 0;
      let clientLeft = docElem.clientLeft || body.clientLeft || 0;
      // (5)
      let top  = box.top +  scrollTop - clientTop;
      let left = box.left + scrollLeft - clientLeft;

      return { top: Math.round(top), left: Math.round(left) };
    }
    //===================================================================================
    //**************************************Hide*****************************************
    //===================================================================================
    imgContainer.onmouseleave = function()
    {
      imgContainer.style.visibility = 'hidden';
      hoverImg.src='';
      clearTimeout(timerId);
      clearInterval(tInt);
    };
    //-----------------------------------------------------------------------------------
    mangaOuterContainer.onmouseleave = function()
    {
      mangaOuterContainer.style.visibility = 'hidden';
      clearTimeout(timerId);
    };
    //===================================================================================
    //***********************************Art Clicks**************************************
    //===================================================================================
    //-----------------------------Single arts onclick actions---------------------------
    hoverImg.onmouseup = function (event)
    {
      onClickActions(this, event, false);
    };
    //-----------------------------Manga arts onclick actions----------------------------
    $('body').on('mouseup', 'div#mangaContainer > img', function(event)
    {
      onClickActions(this, event, true);
    });
    //---------------------------------onClickActions------------------------------------
    async function onClickActions(imgContainerObj, event, isManga)
    {
      event.preventDefault();
      let illustId = getImgId(imgContainerObj.src);
      //----------------------------Middle Mouse Button click----------------------------
      if (event.button == 1)
      {
        let illustPageUrl = 'https://www.pixiv.net/artworks/' + illustId;
        window.open(illustPageUrl,'_blank'); //open illust page in new tab(in background — with FF pref "browser.tabs.loadDivertedInBackground" set to "true")
      }
      //----------------------------Left Mouse Button clicks...--------------------------
      else if (event.button == 0)
      {
        //----------------------------Single LMB-click-----------------------------------
        if (event.shiftKey){
          illust_history.delete_record(illustId); //Shift + LMB-click -> delete record from history
          document.querySelector(`[style*="/${illustId}_"]`).style.opacity = ".25";
        }
        else if (!event.altKey) //need to be this way. Don't change.
        {
          let toSave = event.ctrlKey; //Ctrl + LMB-click -> saving image
          let pageNum = 0;

          //Single (general url)
          let ajaxIllustUrl = 'https://www.pixiv.net/ajax/illust/' + illustId;
          //https://www.pixiv.net/rpc/index.php?mode=get_illust_detail_by_ids&illust_ids=

          //Manga
          if (isManga)
          {
            let src = imgContainerObj.src;
            pageNum = src.match(/(?<=\/\d+_p)\d+(?=[_|.])/)[0];
          }

          getOriginalUrl(ajaxIllustUrl, pageNum, toSave);
        }
        //-----------------------------Alt + LMB-click-----------------------------------
        else if (event.altKey){
          $(bookmarkContainer).click();
          if (!isManga) $(imgContainerObj).parent().css("background", "rgb(255, 64, 96)");
          else $(mangaOuterContainer).css("background", "rgb(255, 64, 96)");
        }
        //-------------------------------------------------------------------------------
      }
      //---------------------------------------------------------------------------------
    }
    //---------------------------------getOriginalUrl------------------------------------
    async function getOriginalUrl(illustPageUrl, pageNum, toSave)
    {
      let xhr = new XMLHttpRequest();
      xhr.responseType = 'json';
      xhr.open("GET", illustPageUrl, true);
      xhr.onload = function ()
      {
        let originalArtUrl = this.response.body.urls.original; //this.response.body.url.big;
        if (pageNum>0) originalArtUrl = originalArtUrl.replace('p0','p'+pageNum);

        if (toSave)    saveImgByUrl(originalArtUrl);
        else           window.open(originalArtUrl, '_blank');
      };
      xhr.send();
    }
    //-----------------------------------------------------------------------------------
    async function saveImgByUrl(sourceUrl)
    {
      const filename = sourceUrl.split('/').pop();
      const illustId = filename.split('_')[0];
      const ext = filename.split('.').pop().toLowerCase();
      const GMR = (typeof(GM_xmlhttpRequest)==='function')?GM_xmlhttpRequest:GM.xmlHttpRequest;

      //Thanx to FlandreKawaii(c)
      GMR({
        method: 'GET',
        url: sourceUrl,
        responseType: 'arraybuffer', //TM
        binary: true, //GM
        headers: {
          Referer: `https://www.pixiv.net/en/artworks/${illustId}`,
        },
        onload: function(response)
        {
          if (ext === 'jpg' || ext === 'jpeg')
            saveAs(new File([response.response], filename, { type: 'image/jpeg' }));
          else if (ext === 'png')
            saveAs(new File([response.response], filename, { type: 'image/png' }));
        }
      });
    }
    //===================================================================================
    //**************************************Other****************************************
    //===================================================================================
    mangaContainer.onwheel = function(e)
    {
      if (e.deltaY<0 && (mangaOuterContainer.getBoundingClientRect().top < 0))
      {
        setTimeout(()=>mangaOuterContainer.scrollIntoView({block: "start", behavior: "smooth"}), 0); //aligning to top screen side on scrollUp if needed
      }
      else if (e.deltaY>0 && (mangaOuterContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight))
      {
        setTimeout(()=>mangaOuterContainer.scrollIntoView({block: "end", behavior: "smooth"}), 0); //aligning to bottom screen side on scrollDown if needed
      }

      let scrlLft = mangaContainer.scrollLeft;
      if ((currentSettings["DISABLE_MANGA_PREVIEW_SCROLLING_PROPAGATION"]) || ((scrlLft>0 && e.deltaY<0) || ((scrlLft<(mangaContainer.scrollWidth-mangaContainer.clientWidth)) && e.deltaY>0)))
      {
        e.preventDefault();
        mangaContainer.scrollLeft += e.deltaY*DELTASCALE;
      }
    };
    //-----------------------------------------------------------------------------------
    if (currentSettings["SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE"]) imgContainer.onwheel = function(e)
    {
      if (currentSettings["DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING"]) e.preventDefault();

      if (e.deltaY<0 && (imgContainer.getBoundingClientRect().top < 0))
      {
        imgContainer.scrollIntoView({block: "start", behavior: "smooth"}); //aligning to top screen side on scrollUp if needed
      }
      else if (e.deltaY>0 && (imgContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight))
      {
        imgContainer.scrollIntoView({block: "end", behavior: "smooth"}); //aligning to bottom screen side on scrollDown if needed
      }
    };
    //-----------------------------------------------------------------------------------
    window.onresize = function()
    {
      mangaWidth = document.body.clientWidth - 60;
      mangaContainer.style.maxWidth = mangaWidth+'px';
      resetPreviewSize();
    };
    //-------------------------------fix for Chrome panoraming---------------------------
    if (navigator.userAgent.indexOf("Chrome") != -1){
      hoverImg.onmousedown = function(e){if (e.button == 2) e.preventDefault()};
      $('body').on('mousedown', 'div#mangaContainer > img', (e)=>{if (e.button == 2) e.preventDefault()});
    }
    //===================================================================================
    //***********************************************************************************
    //===================================================================================
  });
}) (); //function