Pixiv Arts Preview & Followed Atrists Coloring

Enlarged preview of arts and manga on mouse hovering on most pages. Click on image preview to open original art in new tab, or MMB-click to open art illustration page, Alt+LMB-click to 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.

Per 28-11-2019. Zie de nieuwste versie.

// ==UserScript==
// @name            Pixiv Arts Preview & Followed Atrists Coloring
// @name:ru         Pixiv Arts Preview & Followed Atrists Coloring
// @namespace       Pixiv
// @description     Enlarged preview of arts and manga on mouse hovering on most pages. Click on image preview to open original art in new tab, or MMB-click to open art illustration page, Alt+LMB-click to 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.
// @description:ru  Увеличённый предпросмотр артов и манги по наведению мышки на большинстве страниц. Клик ЛКМ по превью арта для открытия исходника в новой вкладке, СКМ для открытия страницы с артом, Alt + клик ЛКМ для добавления в закладки, Ctrl + клик ЛКМ для сохранения оригиналов артов. Имена авторов, на которых вы уже подписаны, подсвечиваются зелёным цветом.
// @author          NightLancerX
// @version         1.43.1
// @match           https://www.pixiv.net/bookmark_new_illust.php*
// @match           https://www.pixiv.net/discovery*
// @match           https://www.pixiv.net/bookmark_detail.php?illust_id=*
// @match           https://www.pixiv.net/member_illust.php*
// @match           https://www.pixiv.net/ranking.php?mode=*
// @match           https://www.pixiv.net/member.php?id=*
// @match           https://www.pixiv.net/bookmark.php*
// @match           https://www.pixiv.net/search.php*
// @match           https://www.pixiv.net/
// @match           https://www.pixiv.net/stacc*
// @match           https://www.pixiv.net/*artworks/*
// @match           https://www.pixiv.net/*tags/*
// @connect         i.pximg.net
// @homepageURL     https://github.com/NightLancer/PixivPreview
// @license         MIT License
// @grant           GM_xmlhttpRequest
// @grant           GM.xmlHttpRequest
// @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
// ==/UserScript==
//=======================================================================================
(function ()
{
  'use strict';

  if (window.top == window.self && window.jQuery) jQuery(function($)
  {
    console.log('MyPixivJS');

    //---------------------------***CUSTOM PREFERENCES***--------------------------------
    const PREVIEW_ON_CLICK = false; //if "true" — showing arts preview after LMB-click on art instead of hovering over it
    const DELAY_BEFORE_PREVIEW = 0; //if you need delay before showing art preview, set it here (1000 = 1 second)
    const previewSize = 0; //you can manually set size of preview to 1200 or 600 (pixels); Default value (0) means it will be calculated automatically with "live" dependence of current screen size
    const ACCURATE_MANGA_PREVIEW = false; //if `true` - increases time before manga preview appearing(to 1sec) but shows it at more accurate position considering width(for case of few arts)
    const DISABLE_MANGA_PREVIEW_SCROLLLING_PROPAGATION = false; //defines whether to keep on scrolling propagation when reaching end of manga preview container
    const SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE = true; //apply scrollIntoView for single preview
    const DISABLE_SINGLE_PREVIEW_BACKGROUND_SCROLLING = false; //defines background scrolling for single preview when `SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE` set to `true`
    //-----------------------------------------------------------------------------------
    //const EXPERIMENTAL_WORKSPAGE_RESTYLE = false; //currently raw style adjustment on illust page; may broke at any time, so turn it on your risk (yet) --> currently broken
    //-----------------------------------------------------------------------------------

    let hoverImg = document.createElement('img');

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

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

    let mangaOuterContainer = document.createElement('div');
        mangaOuterContainer.style = 'position:absolute; display:block; visibility:hidden; z-index:1000; padding:5px; background:#111; maxWidth:1200px; marginY:-5px; marginX: auto; left: 30px;';
        mangaOuterContainer.appendChild(mangaContainer);

    let imgsArr = [], //for manga-style image packs...
        followedUsersId = {}, //storing followed users pixiv ID
        BOOKMARK_URL = 'https://www.pixiv.net/bookmark.php',
        CheckedPublic = false,
        CheckedPrivate = false,
        artsLoaded = 0,
        lastHits = 0,
        lastImgId = -1,
        PREVIEW_SIZE,
        siteImgMaxWidth = 184, //2,3,7,12 [NEW]| quite useless on this pages because of square previews...
        mangaWidth = 1200,
        maxRequestTime = 30000,
        bookmarkObj,
        isBookmarked = false, //rework or delete. Arts can be bookmarked on art page.
        DELTASCALE = ('mozInnerScreenX' in window)?70:4,
        previewEventType = (PREVIEW_ON_CLICK)?'click':'mouseenter',
        PAGETYPE = checkPageType();

    var timerId, tInt;
    //-----------------------------------------------------------------------------------
    Storage.prototype.setObj = function(key, obj){
      return this.setItem(key, JSON.stringify(obj))
    }
    Storage.prototype.getObj = function(key){
      return JSON.parse(this.getItem(key))
    }
    //===================================================================================
    //************************************PageType***************************************
    //===================================================================================
    function checkPageType()
    {
      if (document.URL.match('https://www.pixiv.net/bookmark_new_illust.php?'))                             return 0; //New illustrations - Old +
      if (document.URL==='https://www.pixiv.net/discovery')                                                 return 1; //Discovery page(works) - Old +
      if (document.URL.match('https://www.pixiv.net/member_illust.php?'))                                   return 2; //Artist works page - New + | Todo: merge 2 and 3 pages?...
      if (document.URL.match('https://www.pixiv.net/member.php?'))                                          return 3; //Artist "Home" page - 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\/bookmark\.php\?id/))                               return 7; //Someone's bookmarks 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/bookmark.php?'))                                        return 9; //Your bookmarks page - Old +
      //if (document.URL==='https://www.pixiv.net/')                                                          return 10; //Home page - //todo: settings menu?
      if (document.URL.match('https://www.pixiv.net/stacc?'))                                               return 11; //Feed ('stacc') Old +
      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 +

      return -1;
    }
    console.log('PAGETYPE: '+ PAGETYPE);
    //Old: 0,1,4,6,9,11
    //New: 2,3,7,12,13,8
    //-----------------------------------------------------------------------------------
    function resetPreviewSize(){PREVIEW_SIZE = (previewSize)?previewSize:(window.innerHeight>1200 & document.body.clientWidth>1200)?1200:600}
    //===================================================================================
    //**********************************ColorFollowed************************************
    //===================================================================================
    if ([1,4,6,7].includes(PAGETYPE)) //+12 in initMutationParentObject -> user may not scroll to bottom, so it is better to stay in mutaionObserver
    {
      checkFollowedArtistsInit();
    }

    function checkFollowedArtistsInit()
    {
      if ((Date.now()-23*60*60*1000)>localStorage.getItem('followedCheckDate') && !localStorage.getObj('followedCheckStarted')) //forcing update followed list(in case of errors) at least every 23 hours
      {
        console.log('followedCheckStarted');
        localStorage.setObj('followedCheckCompleted', false);
        localStorage.setObj('followedCheckStarted', true);
        checkFollowedArtists(BOOKMARK_URL+'?type=user');           //public
        checkFollowedArtists(BOOKMARK_URL+'?type=user&rest=hide'); //private
      }
      else if ([6].includes(PAGETYPE)) colorFollowed();
    }
    //-----------------------------------------------------------------------------------
    async function checkFollowedArtists(url)
    {
      if (url === undefined || url.length === 0) return; //just in case

      let xhr = new XMLHttpRequest();
      xhr.open('GET', url, true);
      xhr.timeout = 15000;
      xhr.onreadystatechange = function ()
      {
        if (xhr.readyState == 4 && xhr.status == 200)
        {
          console.log("XHR done");

          let doc = document.implementation.createHTMLDocument("Followed");
          doc.documentElement.innerHTML = xhr.responseText;

          let followedProfiles = doc.querySelectorAll('div>a.ui-profile-popup');
          for(let i = 0; i < followedProfiles.length; i++)
          {
            //followedUsersId.push(followedProfiles[i].getAttribute("data-user_id"));
            followedUsersId[followedProfiles[i].getAttribute("data-user_id")] = true;
          }
          console.log(Object.keys(followedUsersId).length);

          let urlTail = $(doc).find('a[rel="next"]').attr('href');
          if (urlTail !== undefined && urlTail.length)
          {
            console.log(urlTail);
            checkFollowedArtists(BOOKMARK_URL+urlTail);
          }
          else
          {
            if      (doc.querySelectorAll('li.current')[0].textContent==='Public')  CheckedPublic  = true;
            else if (doc.querySelectorAll('li.current')[0].textContent==='Private') CheckedPrivate = true;

            if (CheckedPublic && CheckedPrivate)
            {
              localStorage.setObj('followedCheckCompleted', true);
              localStorage.setObj('followedCheckStarted', false);
              localStorage.setObj('followedUsersId', followedUsersId);
              localStorage.setObj('followedCheckDate', Date.now());
              localStorage.setObj('followedCheckError', false);
              console.log('Followed check completed');

              if (PAGETYPE===6) colorFollowed(); //only for daily rankings? | for loading on same page case
            }
          }
          doc = followedProfiles = null;
        }
      };
      xhr.onerror = function()
      {
        console.error('ERROR while GETTING subscriptions list!');
        localStorage.setObj('followedCheckError', true);
        localStorage.setObj('followedCheckCompleted', false);
        localStorage.setObj('followedCheckStarted', false);
      };
      xhr.send();
    }
    //-----------------------------------------------------------------------------------
    async function colorFollowed(artsContainers)
    {
      let c = 0, d = 0;
      while (!artsContainers || artsContainers.length === 0) //first call -> daily rankings, illust page
      {
        console.log('waiting for arts...');
        await sleep(2000);

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

      let artsContainersLength = artsContainers.length;
      //console.log(artsContainersLength);

      //wait until last XHR completed if it is not---------------------------------------
      if (localStorage.getObj('followedCheckCompleted') === null || localStorage.getObj('followedCheckCompleted') === false)
      {
        if (localStorage.getObj('followedCheckStarted'))
        {
          while (!localStorage.getObj('followedCheckCompleted'))
          {
            console.log("waiting for followed users..."); //this could happen in case of huge followed users amount
            await sleep(2000);

            if (localStorage.getObj('followedCheckError'))
            {
              console.error('ERROR while RECEIVING subscriptions list!');
              break;
            }

            ++d;
            if (d*2000>maxRequestTime)
            {
              console.error('ERROR while EXPECTING for subscriptions list!');
              localStorage.setObj('followedCheckError', true);
              localStorage.setObj('followedCheckCompleted', false);
              localStorage.setObj('followedCheckStarted', false);
              break;
            }
          }
          followedUsersId = localStorage.getObj('followedUsersId');
          console.log('Succesfully received followedUsersId: '+ Object.keys(followedUsersId).length);
        }
        else console.error('Subscriptions check was not STARTED for some reason!');
      }
      else
      {
        followedUsersId = localStorage.getObj('followedUsersId');
        console.log('Succesfully loaded cached followedUsersId: '+ Object.keys(followedUsersId).length);
      }
      //---------------------------------------------------------------------------------
      artsLoaded = (PAGETYPE===12)?$('.gtm-illust-recommend-user-name').length:$('.ui-profile-popup').length; //if it brokes - get info from getArtsContainers()
      console.log('arts loaded: '+artsContainersLength + ' (Total: '+(artsLoaded)+')');

      let currentHits = 0;
      let userId = 0;
      //console.dir(artsContainers);
      for(let i = 0; i < artsContainersLength; i++)
      {
        userId = getUserId(artsContainers[i]);
        //console.log(userId);
        //if (followedUsersId.indexOf(userId)>=0)
        if (followedUsersId[userId]==true)
        {
          ++currentHits;
          artsContainers[i].setAttribute("style", "background-color: green; !important");
        }
      }
      lastHits += currentHits;
      console.log('hits: '+currentHits + ' (Total: '+(lastHits)+')');
    }
    //-----------------------------------------------------------------------------------
    function getArtsContainers()
    {
      switch (PAGETYPE){
        case 1,4: return document.querySelectorAll('.gtm-illust-recommend-user-name');
        case 12:  return document.querySelectorAll('.gtm-illust-recommend-title');
        case 6:   return document.querySelectorAll('.ui-profile-popup');
        case 7:   return document.querySelectorAll('a[href*="/member.php?id="]');
        default:  console.error('Unprocessed PAGETYPE in getArtsContainers()!');
      }
      return null;
    }
    //-----------------------------------------------------------------------------------
    let getUserId = (artContainer) =>
    {
      let userId = -1;

      if (typeof artContainer.hasAttribute !== 'function'){
        console.log(artContainer,'has been filtered out.');
      }
      else if (PAGETYPE===1 || PAGETYPE===6){
        userId = (artContainer.hasAttribute('data-user_id'))
          ?artContainer.getAttribute('data-user_id')
          :artContainer.querySelectorAll('.ui-profile-popup')[0].getAttribute('data-user_id');
      }
      else if (PAGETYPE===4 || PAGETYPE===12){
        artContainer = artContainer.querySelectorAll('.gtm-illust-recommend-title')[0] || artContainer; // -_-'
        userId = artContainer.parentNode.querySelectorAll('[href*="/member.php?id="]')[0].getAttribute('href').split('=').pop();
      }
      else if (PAGETYPE===7){
        userId = artContainer.getAttribute('href').split('=').pop();
      }
      else{
        console.error('UNPROCESSED getUserId() call!');
      }

      return userId;
    }
    //-----------------------------------------------------------------------------------
    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;
    }
    //-----------------------------------------------------------------------------------
    let getArtSectionContainers =
    ([1,4,12].includes(PAGETYPE))? () => $('.gtm-illust-recommend-zone')[0]
    :                              () => $('.ranking-items')[0]; //6
    //-----------------------------------------------------------------------------------
    function createObserver(mainDiv)
    {
      let observer = new MutationObserver(function(mutations)
      {
        let arr = [];
        mutations.forEach(function(mutation)
        {
          mutation.addedNodes.forEach(function(node)
          {
            //console.log(mutation);
            arr.push(node);
          });
        });
        colorFollowed(arr);
      });

      let options = {
        'childList': true
      };

      observer.observe(mainDiv, options);
      console.log('Observer has been set');
    }
    //-----------------------------------------------------------------------------------
    async function initMutationObject()
    {
      let mainDiv = getArtSectionContainers();
      while(!mainDiv)
      {
        console.log('Waiting for arts container...');
        await sleep(1000);
        mainDiv = getArtSectionContainers();
      }
      console.log(mainDiv);
      createObserver(mainDiv);
    }
    //-----------------------------------------------------------------------------------
    async function initMutationParentObject(parentSelector, nodesSelector)
    {
      let observerParent = new MutationObserver(function(mutations)
      {
        let mainDiv;
        mutations.forEach(function(mutation)
        {
          mainDiv = mutation.addedNodes[0].getElementsByClassName(nodesSelector);
          console.log(mainDiv);
          if (mainDiv.length>0)
          {
            checkFollowedArtistsInit();

            createObserver(mainDiv[0]);
            observerParent.disconnect();
            observerParent = null;
          }
        });
      });
      let options2 = {
        'childList': true
      };

      let parentDiv = getElementByXpath(parentSelector);
      while(!parentDiv)
      {
        console.log('Waiting for getElementByXpath');
        await sleep(1000);
        parentDiv = getElementByXpath(parentSelector);
      }
      console.log(parentDiv);

      observerParent.observe(parentDiv, options2);
      console.log('observerParent set');
    }
    //-----------------------------------------------------------------------------------
    /*
    function initGallery() {
      let _ttt = $('figure > div[role="presentation"]');
      let _sss = _ttt[0].textContent.split("⧸").pop(); //NOT slash! charCodeAt(0) == 10744
      console.log(_ttt);
      console.log(_sss);
    }
    */
    //-----------------------------------------------------------------------------------
    function followage(thisObj, toFollow, isNew) //In case of followed check lasting too long, async queue may be a solution
    {
      console.log('toFollow: '+ toFollow);
      let userId;
      switch (isNew){
        case 0: userId = thisObj.parentNode.parentNode.querySelectorAll('a.user-name')[0].getAttribute('href').split('&')[0].split('=')[1]; break; //OLD
        case 1: userId = document.querySelectorAll('[href*="/member.php?id="]')[0].getAttribute('href').split('=').pop(); break; //NEW
        case 2: userId = thisObj.getAttribute('data-user-id'); break; //recommended users
      }
      //console.log(userId);

      if (localStorage.getObj('followedCheckCompleted')) //at least basic check until queue is developed
      {
        let followedUsersId = localStorage.getObj('followedUsersId'); //local
        if (toFollow){
          followedUsersId[userId] = true;
          if ([2,3,7,12].includes(PAGETYPE)) initFollowagePreview();
        }
        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.error('Slow down! You have subscribed too many to handle this by now! Wait for the next updates');
    }
    //===================================================================================
    if      (PAGETYPE===0 || PAGETYPE===1 || PAGETYPE===8)  siteImgMaxWidth = 198;
    else if (PAGETYPE===4 || PAGETYPE===9 || PAGETYPE===10) siteImgMaxWidth = 150;
    else if (PAGETYPE===6 || PAGETYPE===11)                 siteImgMaxWidth = 240;
    //-----------------------------------------------------------------------------------
    $(document).ready(function ()
    {
      console.log('$(document).ready');
      mangaWidth = document.body.clientWidth - 80;
      mangaContainer.style.maxWidth = mangaOuterContainer.style.maxWidth = mangaWidth+'px';
      document.body.appendChild(imgContainer);
      document.body.appendChild(mangaOuterContainer);
      resetPreviewSize();
      //-------------------------------Follow onclick------------------------------------
      let toFollow, isNew, followSelector;
      if ([2,3,7,12].includes(PAGETYPE)){
        followSelector = '[data-click-label*="follow"]';
        isNew = 1;
      }
      else if([13].includes(PAGETYPE)){
        followSelector = '.follow-button';
        isNew = 2;
      }
      else {
        followSelector = '.follow-button';
        isNew = 0;
      }

      $('body').on('mouseup', followSelector, function(){
        toFollow = (this.textContent == 'Follow');
        followage(this, toFollow, isNew);
      });
      //----------------------------Bookmark detail page cleaning------------------------
      if (PAGETYPE===4)
      {
        let _bkmrklst = $('.bookmark-list-unit')[0];
        _bkmrklst.parentNode.removeChild(_bkmrklst);
        _bkmrklst = null;
      }
      //------------------------------Dayli rankings ad cleaning-------------------------
      if (PAGETYPE===6)
      {
        $('.ad-printservice').remove();
      }
      //-------------------------------Illust page extra check---------------------------
      if (PAGETYPE===12)
      {
        let parentSelector = '/html/body/div[1]/div[1]/div/aside[2]/div',  //replace with selector if possible
            zoneSelector = 'gtm-illust-recommend-zone',
            zone = document.getElementsByClassName(zoneSelector)[0];

        if (zone === undefined) initMutationParentObject(parentSelector, zoneSelector);
        else
        {
          console.log(zone);
          checkFollowedArtistsInit();
          colorFollowed(); //process missed arts containers
          createObserver(zone);
        }
      }
      //-----------------------------Init illust fetch listener--------------------------
      if (PAGETYPE===1 || PAGETYPE===4 || PAGETYPE===6)
      {
        initMutationObject();
      }
      //---------------------------------Pixiv Member pages------------------------------
      if (PAGETYPE===2|| PAGETYPE===3 || PAGETYPE===7)
      {
        $('body').on('mouseup', 'a[href*="&rest=show"]', function(){
          console.log('PAGETYPE: '+ PAGETYPE+' -> 7');
          PAGETYPE = 7;
          bookmarksInit();
        });
        // $('body').on('mouseup', 'a[href*="/member.php?id="]', function(){
        //   console.log('PAGETYPE: '+ PAGETYPE+' -> 3');
        //   PAGETYPE = 3;
        // });
        //
        // $('body').on('mouseup', 'a[href*="&type=illust"]', function(){
        //   console.log('PAGETYPE: '+ PAGETYPE+' -> 2');
        //   PAGETYPE = 2;
        // });
      }
      //-------------------------------
      async function bookmarksInit()
      {
        checkFollowedArtistsInit();
        let mainSelector = '/html/body/div[1]/div[1]/div/div[2]/div[1]/section/ul';
        let mainDiv = getElementByXpath(mainSelector);
        while(!mainDiv)
        {
          console.log('Waiting for getElementByXpath');
          await sleep(1000);
          mainDiv = getElementByXpath(mainSelector);
        }
        colorFollowed();
      }
      //------------------------------------Bookmarks------------------------------------
      if (PAGETYPE===7)
      {
        bookmarksInit();
      }
      //=================================================================================
      //***************************************HOVER*************************************
      //=================================================================================
      //------------------------------------Profile card--------------------------------- //0,1,4,6,8,9,11
      if ([0,1,4,6,9,11].includes(PAGETYPE))
      {
        $('body').on(previewEventType, 'a[href*="/artworks/"]', function(e)
        {
          e.preventDefault();
          if (this.childNodes.length === 0) //for preventing issues with 4 and 6 pages
          {
            setHover(this);
            imgContainer.style.top = getOffsetRect(this).top+200+'px';
          }
        });
      }
      //-------------------------------------Followage----------------------------------- //2,3,7,12
      function initFollowagePreview()
      {
        $('body').on(previewEventType, '.gtm-recommend-user-thumbnail', function(e)
        {
          e.preventDefault();

          let top = window.innerHeight - PREVIEW_SIZE - 5 + window.scrollY + 'px';
          setHover(this.firstChild.firstChild, top);

          let scroll = getElementByXpath('/html/body/div[6]/div/div/div/div/ul');
          scroll.onwheel = function(ev)
          {
            ev.preventDefault();
            scroll.scrollLeft += ev.deltaY*DELTASCALE;
          };
        });
      }
      //--------------------NEW ILLUSTRATIONS, DISCOVERY[ARTWORKS] AND SEARCH------------ //0,1,8
      function setPreviewEventListeners()
      {
        //single art hover---------------------------------------------------------------
        $('body').on(previewEventType, 'a[href*="/artworks/"] > div:only-child', function(e)
        {
          e.preventDefault();
          bookmarkObj = $(this).parent().parent().children(".thumbnail-menu").children("._one-click-bookmark");
          checkBookmark(this);
          setHover(this);
        });

        //manga-style arts hover---------------------------------------------------------
        $('body').on(previewEventType, 'a[href*="/artworks/"] > div:nth-child(2) ', function(e)
        {
          e.preventDefault();
          if (this.parentNode.firstChild.childNodes.length)
          {
            bookmarkObj = $(this).parent().parent().children(".thumbnail-menu").children("._one-click-bookmark");
            checkBookmark(this);
            setMangaHover(this, this.parentNode.firstChild.firstChild.textContent);
          }
        });
      }
      //-----------------------------------DISCOVERY[USERS]------------------------------ //13
      function setDiscoveryUsersPreviewEventListeners()
      {
        $('body').on(previewEventType, 'a[href*="/artworks/"]', function(e)
        {
          e.preventDefault();
          if      (this.childNodes.length == 0)  setHover(this); //single art
          else if (this.childNodes.length == 1)  setMangaHover(this, this.firstChild.textContent); //manga
        });
      }
      //--------------------------------------------------------------------------------- //1->13
      function setTabSwitchingListenerW_U()
      {
        $('body').on('mouseup','a[href="/discovery/users"]', function()
        {
          console.log('Works -> Users');
          PAGETYPE = 13;
          artsLoaded = lastHits = 0; //clearing loaded arts count when switching on tabs
          $('body').off(); //may cause some removal of timeout events?
          setDiscoveryUsersPreviewEventListeners();
          setTabSwitchingListenerU_W();
        });
      }
      //--------------------------------------------------------------------------------- //13->1
      function setTabSwitchingListenerU_W()
      {
        $('body').on('mouseup','a[href="/discovery"]', function()
        {
          console.log('Users -> Works');
          PAGETYPE = 1;
          artsLoaded = lastHits = 0; //not necessary
          checkFollowedArtistsInit();
          initMutationObject();

          $('body').off();//(previewEventType, 'a[href*="member_illust.php?mode=medium&illust_id="]', discoveryUsersPreview);
          setPreviewEventListeners();
          setTabSwitchingListenerW_U();
        });
      }
      //--------------------------------DISCOVERY[Artworks]------------------------------ //1
      if (PAGETYPE === 1) //Works
      {
        setTabSwitchingListenerW_U();
        setPreviewEventListeners();
      }
      //----------------------------------DISCOVERY[Users]------------------------------- //13
      else if (PAGETYPE === 13) //Users
      {
        setTabSwitchingListenerU_W();
        setDiscoveryUsersPreviewEventListeners();
      }
      //----------------------------NEW ILLUSTRATIONS AND SEARCH------------------------- //0,8
      else if (PAGETYPE === 0)
      {
        setPreviewEventListeners();
      }
      //--------------------ARTIST WORKS, "TOP" PAGES, Someone's Bookmarks--------------- //2,3,7,12
      else if (PAGETYPE === 2 || PAGETYPE === 3 || PAGETYPE === 7 || PAGETYPE === 12 || PAGETYPE === 8)     //TODO!!! do smthng with that amorphous pixiv styleshit!
      {
        /*
        $('body').on(previewEventType, 'a[href*="member_illust.php?mode=medium&illust_id="] > div:only-child', function()
        {
          //single art hover-------------------------------------------------------------
          if (this.firstChild.childNodes.length===1) //single
          {
            //console.log('single');
            bookmarkObj = this.parentNode.parentNode.childNodes[1].childNodes[0].childNodes[0];
            //checkBookmark_NewLayout(this);
            setHover(this.childNodes[1]);
          }
          //manga-style arts hover-------------------------------------------------------
          else
          {
            //console.log('manga');
            bookmarkObj = this.parentNode.parentNode.childNodes[1].childNodes[0].childNodes[0];
            //checkBookmark_NewLayout(this);
            setMangaHover(this.childNodes[1], this.firstChild.childNodes[1].textContent);
          }
        });
        */
        $('body').on(previewEventType, 'a[href*="/artworks/"] > div:nth-child(2) ', function(e)
        {
          e.preventDefault();
          //single art hover-------------------------------------------------------------
          if (this.parentNode.firstChild.childNodes.length===1) //single
          {
            bookmarkObj = this.parentNode.parentNode.childNodes[1].childNodes[0].childNodes[0];
            setHover(this.childNodes[0]); //todo? - zero child-node trying?
          }
          //manga-style arts hover-------------------------------------------------------
          else
          {
            bookmarkObj = this.parentNode.parentNode.childNodes[1].childNodes[0].childNodes[0];
            setMangaHover(this.childNodes[0], this.parentNode.firstChild.childNodes[1].textContent);
          }
        });
      }
      //----------------------DAILY RANKINGS & BOOKMARK INFORMATION PAGES---------------- //4,6 [10]
      else if (PAGETYPE === 4 || PAGETYPE === 6)
      {
        $('body').on(previewEventType, 'a[href*="/artworks/"]', function(e) //direct div selector works badly with "::before"
        {
          e.preventDefault();

          if (this.childNodes.length == 1 && this.childNodes[0].nodeName=="DIV") //single art
          {
            bookmarkObj = $(this.firstChild.firstChild).parent().children("._one-click-bookmark");
            checkBookmark(this);
            setHover(this.firstChild.firstChild);
          }
          else if (this.children[1] && this.children[1].className == 'page-count') //manga
          {
            bookmarkObj = $(this.firstChild.firstChild).parent().children("._one-click-bookmark");
            checkBookmark(this);
            setMangaHover(this.firstChild.firstChild, this.children[1].children[1].textContent);
          }
        });
      }
      //--------------------------------------BOOKMARKS---------------------------------- //9
      else if (PAGETYPE === 9)
      {
        $('body').on(previewEventType, 'a[href*="member_illust.php?mode=medium&illust_id="]', function(e) //direct div selector works badly with "::before"
        {
          e.preventDefault();

          if (this.childNodes.length == 1 && this.childNodes[0].nodeName=="DIV") //single art
          {
            bookmarkObj = $(this.firstChild.firstChild).parent().children("._one-click-bookmark");
            checkBookmark(this);
            setHover(this.firstChild.firstChild);
          }
          else if (this.children[1] && this.children[1].className == 'page-count') //manga
          {
            bookmarkObj = $(this.firstChild.firstChild).parent().children("._one-click-bookmark");
            checkBookmark(this);
            setMangaHover(this.firstChild.firstChild, this.children[1].children[1].textContent);
          }
        });
      }
      //------------------------------------Feed('stacc')-------------------------------- //11
      else if  (PAGETYPE === 11)
      {
        $('body').on(previewEventType, 'a[href*="member_illust.php?mode=medium&illust_id="]', function(e)
        {
          e.preventDefault();

          if (this.childNodes.length == 1 && this.childNodes[0].nodeName=="DIV")
          {
            if ($(this).hasClass('multiple')) //manga
            {
              let link = 'https://www.pixiv.net/ajax/illust/' + this.href.split('&')[1].split('=')[1];
              let that = this;

              let xhr = new XMLHttpRequest();
              xhr.responseType = 'json';
              xhr.open("GET", link, true);
              xhr.onreadystatechange = function ()
              {
                if (xhr.readyState == 4 && xhr.status == 200)
                {
                  let count = this.response.body.pageCount;
                  setMangaHover(that.firstChild.firstChild, count);

                  if (!(that.parentNode.parentNode.parentNode.parentNode.getElementsByClassName('imageCount').length>0)) //todo?..
                  {
                    let s = document.createElement('span');
                    s.className = 'imageCount';
                    s.style = 'position:relative; display: inline-block; float: right; top:-240px;'
                    s.textContent = count;
                    that.parentNode.parentNode.parentNode.parentNode.appendChild(s);
                  }
                }
              };
              xhr.send();
            }
            else setHover(this.firstChild.firstChild); //single art
          }
        });
      }
      //---------------------------------------------------------------------------------
      if (DELAY_BEFORE_PREVIEW>0) $('body').on('mouseleave', 'a[href*="member_illust.php?mode=medium&illust_id="]', function()
      {
        clearTimeout(timerId);
      });
    });
    //===================================================================================
    //-----------------------------------------------------------------------------------
    function checkDelay(callback)
    {
      if (DELAY_BEFORE_PREVIEW>0){
        clearTimeout(timerId);
        timerId = setTimeout(callback, DELAY_BEFORE_PREVIEW);
      }
      else callback();
    }
    //-----------------------------------------------------------------------------------
    function setHover(thisObj, top)
    {
      clearTimeout(timerId);
      clearTimeout(tInt);
      mangaOuterContainer.style.visibility = 'hidden';

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

      //adjusting preview position considering expected image width
      //---------------------------------------------------------------------------------
      let l = (![2,3,7,12,13].includes(PAGETYPE)) //more accurate on discovery users
          ?getOffsetRect(thisObj.parentNode.parentNode).left
          :getOffsetRect(thisObj).left;
      let dcw = document.body.clientWidth;
      let previewWidth = 0;

      if (hoverImg.naturalWidth>0){ //cached (previously viewed)
        adjustSinglePreview(dcw, l, hoverImg.naturalWidth);
      }
      else{
        if (![2,3,7,12,13].includes(PAGETYPE)){
          let previewWidth = PREVIEW_SIZE*(((PAGETYPE==6)?thisObj.clientWidth:thisObj.parentNode.parentNode.clientWidth)/siteImgMaxWidth)+5; //if not on NEW layout - width is predictable
          adjustSinglePreview(dcw, l, previewWidth);
        }
        else{
          if (dcw - l - PREVIEW_SIZE - 5 > 0){ //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)
            imgContainer.style.left = l+'px';
            checkDelay(function(){imgContainer.style.display='block';});
          }else{ //when on NEW layout - need to wait until image width is received
            let tLimit;
            tInt = setInterval(function(){
              if (hoverImg.naturalWidth>0){
                clearTimeout(tInt);
                //elementMouseIsOver = document.elementFromPoint(x, y); if (thisObj != elementMouseIsOver) break;
                adjustSinglePreview(dcw, l, hoverImg.naturalWidth); //position mismatching due to old `thisObj` => clearing in hoverImg.mouseleave
                ++tLimit;
              }
              if (tLimit*20>3000){ //in case of loading errors
                clearTimeout(tInt);
                hoverImg.src='';
                console.error('setInterval error');
              }
            }, 20);
          }
        }
      }
      //---------------------------------------------------------------------------------
      if (isBookmarked) $(imgContainer).css("background", "rgb(255, 64, 96)");
      else $(imgContainer).css("background", "rgb(34, 34, 34)");
    }
    //-----------------------------------------------------------------------------------
    function adjustSinglePreview(dcw, l, contentWidth)
    {
      let d = dcw - l - contentWidth - 5; //5 - padding - todo...
      imgContainer.style.left = (d>=0)?l+'px':l+d+'px';
      checkDelay(function(){imgContainer.style.display='block';});
    }
    //-----------------------------------------------------------------------------------
    function setMangaHover(thisObj, count)
    {
      clearTimeout(timerId);
      clearTimeout(tInt);
      imgContainer.style.display='none'; //just in case

      mangaOuterContainer.style.top = getOffsetRect(thisObj.parentNode.parentNode).top+'px';
      if (!ACCURATE_MANGA_PREVIEW) mangaOuterContainer.style.left = '30px';

      if (isBookmarked) $(mangaOuterContainer).css("background", "rgb(255, 64, 96)");
      else $(mangaOuterContainer).css("background", "rgb(34, 34, 34)");

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

        lastImgId = currentImgId;

        for(let i=0; i<l; 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);
        }

        if (ACCURATE_MANGA_PREVIEW == true || DELAY_BEFORE_PREVIEW > 1000) //more accurate frame adjusting
        {
          setTimeout(function()
          {
            adjustMargins(mangaOuterContainer.scrollWidth);
            checkDelay(function(){mangaOuterContainer.style.visibility='visible';});
          }, 1000);
        }
        else //some blind frame adjusting
        {
          adjustMargins(l*PREVIEW_SIZE);
          checkDelay(function(){mangaOuterContainer.style.visibility='visible';});
        }
      }
      //---------------------------------------------------------------------------------
      else
      {
        adjustMargins(mangaOuterContainer.scrollWidth);
        checkDelay(function(){mangaOuterContainer.style.visibility='visible';});
      }
    }
    //-----------------------------------------------------------------------------------
    function parseImgUrl(thisObj, PREVIEW_SIZE)
    {
      let url = (thisObj.src)? thisObj.src: thisObj.style.backgroundImage.slice(5,-2);
      url = url.replace(/\/...x..[0|8]/, '/'+PREVIEW_SIZE+'x'+PREVIEW_SIZE).replace('_80_a2','').replace('_square1200','_master1200').replace('_70','');
      return url;
    }
    //-----------------------------------------------------------------------------------
    function adjustMargins(width)
    {
      let margins = document.body.clientWidth - width;
      if (margins > 0) mangaOuterContainer.style.left = margins/2-10+'px';
      else mangaOuterContainer.style.left = '30px';
    }
    //-----------------------------------------------------------------------------------
    function checkBookmark(thisObj) //let this be until it works
    {
      isBookmarked = ($(bookmarkObj).hasClass("on"));
    }
    //-----------------------------------------------------------------------------------
    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.display='none';
      hoverImg.src='';
      clearTimeout(timerId);
      clearTimeout(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.altKey)
        {
          let toSave = event.ctrlKey;// Ctrl + LMB-click -> saving image
          let pageNum = 0;

          //Single (general url)
          let ajaxIllustUrl = 'https://www.pixiv.net/ajax/illust/' + illustId;

          //Manga
          if (isManga)
          {
            let src = imgContainerObj.src;
            pageNum = src.substring(src.indexOf("_")+2, src.lastIndexOf("_"));
          }

          getOriginalUrl(ajaxIllustUrl, event, pageNum, toSave);
        }
        //-----------------------------Alt + LMB-click-----------------------------------
        else if (event.altKey) {
          $(bookmarkObj).click();
          if (!isManga) $(imgContainerObj).parent().css("background", "rgb(255, 64, 96)");
          else $(mangaOuterContainer).css("background", "rgb(255, 64, 96)");
        }
        //-------------------------------------------------------------------------------
      }
      //---------------------------------------------------------------------------------
    }
    //---------------------------------getOriginalUrl------------------------------------
    async function getOriginalUrl(illustPageUrl, event, pageNum, toSave)
    {
      let xhr = new XMLHttpRequest();
      xhr.responseType = 'json';
      xhr.open("GET", illustPageUrl, true);
      xhr.onreadystatechange = function ()
      {
        if (xhr.readyState == 4 && xhr.status == 200)
        {
          let originalArtUrl = this.response.body.urls.original;
          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))
      {
        mangaOuterContainer.scrollIntoView({block: "start", behavior: "smooth"}); //aligning to top screen side on scrollUp if needed
      }
      else if (e.deltaY>0 && (mangaOuterContainer.getBoundingClientRect().bottom > document.documentElement.clientHeight))
      {
        mangaOuterContainer.scrollIntoView({block: "end", behavior: "smooth"}); //aligning to bottom screen side on scrollDown if needed
      }

      let scrlLft = mangaContainer.scrollLeft;
      if ((DISABLE_MANGA_PREVIEW_SCROLLLING_PROPAGATION) || ((scrlLft>0 && e.deltaY<0) || ((scrlLft<(mangaContainer.scrollWidth-mangaContainer.clientWidth)) && e.deltaY>0)))
      {
        e.preventDefault();
        mangaContainer.scrollLeft += e.deltaY*DELTASCALE;
      }
    };
    //-----------------------------------------------------------------------------------
    if (SCROLL_INTO_VIEW_FOR_SINGLE_IMAGE) imgContainer.onwheel = function(e)
    {
      if (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 - 80;
      mangaContainer.style.maxWidth = mangaOuterContainer.style.maxWidth = mangaWidth+'px';
      resetPreviewSize();
    };
    //-----------------------------------------------------------------------------------
    document.onkeyup = function(e) //Enlarge with shift
    {
      //console.log(e.keyCode);
      if (e.keyCode == 16 && hoverImg.src && PREVIEW_SIZE<1200)
      {
        let l = getOffsetRect(imgContainer).left;
        let w = hoverImg.naturalWidth*2+5;
        hoverImg.src = hoverImg.src.replace(/\/...x..[0|8]/, '/1200x1200');
        imgContainer.style.left = (document.body.clientWidth-l < w)? document.body.clientWidth-w +'px': l +'px';
      }
    }
    //===================================================================================
    //***********************************************************************************
    //===================================================================================
  });
}) (); //function