Greasy Fork is available in English.

Enhanced viewer for acomics.ru

Preload, navigation and other enhancements for acomics.ru comics viewer

// ==UserScript==
// @name            Enhanced viewer for acomics.ru
// @name:ru         Улучшенный просмотрщик для acomics.ru
// @description:ru  Предзагрузка, удобная навигация и прочие улучшения для сайта acomics.ru
// @description     Preload, navigation and other enhancements for acomics.ru comics viewer
// @author          Sanya_Zol (https://github.com/SanyaZol)
// @license         Unlicense
// @icon            data:image/gif;base64,R0lGODlhIAAgAMQAAP////22bNMQNf/MXP63WOprSP/DWPvQsP/gZ/7Hl/CFTOjazv7hzvzAgP/VXdgkONK9udeylN3Nwujj4OdWRPzx6uDDp/Hr5/Px8Pv18vv5+f/uedDBwea6cvehWOA8PSH5BAAAAAAALAAAAAAgACAAAAX/ICCOopQwWaWR7LgkSdMErdgQQXJg9XhVt8GAQKhNhAZCo9IDXGYDxMYRrVkGG4QhwFjVIA6EdINFtCYEw4ZsOHhbDcTQ4wkEPC2IUhHIJt4kVw4KAoWFHy0dDgEPBVkBFwCAaAQOH4aFeCRHCIQCA4snTC5CHpgCHwMsFgYDlwIKDklcIxM3BhSnsSQLW6aFDwMGw24irAa/hh/DIxkdabmGHlMODSsaDcLRhrEWPkkEpxRUAw0ivUOnD2kSIxLDBacCHsMBBwARYvGYCuUjGAGSPJBXoFW5C4oMDDS0zkCEERyGeVJXacCJbPsMFUBAIIEIDBFaLZTXbwuDCYwwyzXs+DHgRHnLWh3IcGCkgJUNMmhIkOQVQ0z0yh1okLHQxiEnhSWTV2jcIhkpGRqQFSACjm2oCmAVUKkjtq0bqSKzqaCBAZ8CClBpY2HpTQJy0hR9MKzBy3VaAlQ4sDXWWrIOOgAkq4UAAwZug4W5W8mChg5FPzwKMMFCXwSWMI2zgEFD21NxtvBwm+rlPAcLRFyIgFWtAW8SAqCdh/ZDBwupP0Y4Rc+bBpCnPnAYPlxCbhbDIXCAYDwSROUQIkBoQr269evYs2vfXj0EADs=
// @homepageURL     https://greasyfork.org/ru/scripts/10521
// @supportURL      https://greasyfork.org/ru/scripts/10521
// @namespace       Sanya_Zol
// @version         0.3.0
// @match           https://acomics.ru/*
// @run-at          document-start
// @grant           none
// @require         https://code.jquery.com/jquery-3.7.1.min.js
// ==/UserScript==

// @ts-check
// Greasy Fork: https://greasyfork.org/ru/scripts/10521
// Github: https://github.com/SanyaZol/acomics-enhanced-viewer-userscript

/** @typedef {{Page:number,LastPage:number,Image:string|null,ContentSerialNomargin:string,ContentMargin:string,Title:string}} parsedPage */

(function () {

  class AcomicsViewer {
    settings = {
      // Блокировать случайное закрытие страницы
      blockUnload: false,
      // таймаут запроса в милисекундах (секундах*1000)
      AjaxRequestTimeoutMs: 10000,
      // чтобы деактивировать или снова активировать быстрый скролл, нажми Shift+[˄] или Shift+[˅]
      scrollFasterDefault: true,
      // сейчас при скролле страницы стрелками по вертикали [˄] и [˅] будет быстрый скролл на 300 пикселей
      scrollFasterPx: 300,
      // а при зажатом Ctrl - на 100
      scrollFasterCtrlPx: 100,
      // время в милисекундах, за которое скроллить при использовании быстрого скролла
      scrollFasterSpeed: 75,
      // время в милисекундах, за которое скроллить наверх при смене страниц
      scrollSpeed: 75,
      // выпилить комментарии - они будут вылазить при нажатии [˄]+[˃]
      // (стрелки вправо при зажатой стрелке вверх) и выезжать при нажатии [˂]+[˃]
      contentMarginMover: false,
      // выводить отладочную информацию в левом верхнем углу страницы
      visualLog: false,
    };
    constants = {
      eventNamespace: 'acomics-enhanced-viewer-userscript',
      // префикс localStorage
      localStorageKeyPrefix: '**zol**page**',
      logPrefix: '[acViewer] ',
    };
    stage = 'noInit';
    /** @type {JQuery<HTMLElement> | null} */
    logOverlay = null;
    log(text) {
      let s = `[${this.stage}] ${text}`;
      if (this.settings.visualLog) {
        if (!this.logOverlay) {
          this.logOverlay = $('<div/>');
          this.logOverlay.css({
            position: 'fixed', // absolute
            zIndex: 1e6,
            left: '1px',
            top: '1px',
            padding: '1px',
            fontFamily: 'Verdana',
            fontSize: '7px',
            color: '#fff',
            textShadow: '#000 0px 0px 1px',//, #000 2px 2px 2px
            background: 'rgba(255,255,255,0.3)',
            borderRadius: '2px',
            border: '1px rgba(255,255,255,0.5) solid',
            overflowY: 'hidden',
            maxHeight: '60px'
          }).appendTo('body');
        }
        $('<div/>').text(s).hide().prependTo(this.logOverlay).slideDown(200).delay(5000).slideUp(200, function () { $(this).remove(); });
        this.logOverlay.fadeIn().finish().delay(5000).slideUp(200);
      }
      console.log(`${this.constants.logPrefix}${s}`);
    }
    preInitFailed = false;
    preInitGuard = false;
    preInitSucceeded = false;
    /** @type {string|null} */
    comicsName = null;
    /** @type {number|null} */
    comicsPage = null;
    /**
     * @param {number} id
     * @returns {string}
     */
    makeUrl(id) {
      if (!this.comicsName) throw new Error("makeUrl() requires this.comicsName to be set");
      if (id === null) throw new Error("makeUrl() requires id to be set");
      return '/~' + this.comicsName + '/' + id;
    }
    /** @type {string|null} */
    localStorageKey = null;
    /** @type {string|null} */
    localStorageKeyPage = null;
    /** @returns {void} */
    store() {
      if (!this.localStorageKey || this.comicsPage === null) throw new Error("store() requires this.localStorageKey to be set");
      if (this.comicsPage !== this.wantPage) throw new Error("store() requires this.comicsPage === this.wantPage");
      window.localStorage.setItem(this.localStorageKey, `${this.comicsPage}`);
    }
    /** @type {number|null} */
    wantPage = null;
    static _rx1 = /(?:(?:Мой )?(?:патреон|яндекс|yandex|paypal|wm(?:z|r|e|u)|кошелек|счет)[\s.-]*(?:кошелек|money|деньги)?:?|(?:https?:\/\/)?(?:www\.)?patreon.com\/[^\s]*|41\d{13}\b|(?:wm)?(?:z|r|e|u)\d{12}\b)/gi;
    static _rx2 = /(?:(?:\s*(?:<p>\s*<\/p>|<br ?\/? ?>)\s*)+|^(?:\s*<br ?\/? ?>)+|(?:<br ?\/? ?>\s*)+$)/gi;
    static _dangerous_tags = (['script', 'style', 'link', 'embed', 'object'].join(', '));
    static removeInlineHandlers(element) {
      // http://stackoverflow.com/a/3593250 http://stackoverflow.com/q/3593242 http://stackoverflow.com/questions/3593242/how-to-remove-all-attributes-from-body-with-js-or-jquery
      for (var i = element.attributes.length; i-- > 0;) {
        var attr = element.attributes[i];
        if (attr.nodeName.toLowerCase().indexOf('on') == 0) {
          element.removeAttributeNode(attr);
        }
      }
    }
    static removeScripts(el) {
      var f;
      while (f = el.querySelector(AcomicsViewer._dangerous_tags)) { // we querying selector every time because tags can contain each other
        f.parentNode.removeChild(f);
      }
      var f = el.querySelectorAll('*');
      for (var i = f.length; i-- > 0;) {
        AcomicsViewer.removeInlineHandlers(f[i]);
      }
    }
    /**
     *
     * @param {string} s
     * @returns {parsedPage|null}
     */
    parsePageString(s) {
      // TODO: validate comics page is really from the comics that is in URL
      var o = {};
      var parser = new DOMParser();
      var d = parser.parseFromString(s, "text/html");
      {
        let x1 = d.querySelector('.common-content .button-goto span');
        if (!x1) { return null; }
        let x = $.trim(x1.innerHTML).match(/^(\d+)\/(\d+)$/);
        if (!x) { return null; }
        o.Page = parseInt(x[1]);
        o.LastPage = parseInt(x[2]);
      }
      {
        let x = d.querySelector('.common-content .issue-description-buttons');
        if (x) { x.parentNode?.removeChild(x); };
      }
      {
        let x = d.querySelector('.common-content .reader-comment-form');
        if (x) { x.parentNode?.removeChild(x); };
      }
      {
        let x = d.querySelector('.common-content .reader-issue img.issue')?.getAttribute('src');
        o.Image = x || null;
      }
      {
        let x = d.querySelector('.common-content .reader-issue');
        if (!x) return null;
        AcomicsViewer.removeScripts(x);
        o.ContentSerialNomargin = x.innerHTML;
      }
      {
        let x = d.querySelector('.common-content .view-container .view-article');
        if (!x) return null;
        AcomicsViewer.removeScripts(x);
        let y = x.querySelector('.issue-description-text');
        if (y) y.innerHTML = y.innerHTML.replace(AcomicsViewer._rx1, '').replace(AcomicsViewer._rx2, '');
        o.ContentMargin = x.innerHTML;
      }
      {
        let x = d.querySelector('title');
        o.Title = x ? ($.trim(x.innerText)) : '?';
      }
      return o;
    }
    /** @type {number|null} */
    comicsLastPage = null;
    /** @type {number|null} */
    init_comicsPage = null;
    /** @type {number|null} */
    init_wantPage = null;
    /** @type {parsedPage|null} */
    tmp_curr = null;
    /** @returns {boolean} */
    preInit() {
      if (this.preInitSucceeded) return true;
      if (this.preInitFailed || this.preInitGuard) { return false; }
      this.preInitFailed = true;
      this.preInitGuard = true;
      this.stage = 'preInit.1';

      {
        const m = /^\/~([^\/]+)\/(\d*)(?:\D|$)/.exec(location.pathname.toString());
        if (!m) { return false; }

        if (!window.$) {
          this.log('no window.$');
          return false;
        }

        this.comicsName = m[1];
        const page = parseInt(m[2], 10);
        if (m[2] == '' || isNaN(page) || page < 1) {
          return false;
        }
        this.comicsPage = page;
      }

      this.localStorageKey = this.constants.localStorageKeyPrefix + this.comicsName;
      this.localStorageKeyPage = this.localStorageKey + '*';

      {
        /** @var {number|null} */
        let stored = null;
        const storedString = window.localStorage.getItem(this.localStorageKey);
        if (storedString) {
          stored = parseInt(storedString, 10) || null;
        }
        if (stored !== null && stored != this.comicsPage) {
          this.wantPage = stored;
        } else {
          this.wantPage = this.comicsPage;
        }
      }

      this.stage = 'preInit.2';

      let curr = this.parsePageString($('html')[0].outerHTML);
      if (!curr) { return false; }

      this.comicsLastPage = curr.LastPage;
      this.comicsPageTitle = curr.Title;

      this.init_comicsPage = this.comicsPage;
      this.init_wantPage = this.wantPage;

      this.stage = 'preInit.3';

      if (this.comicsPage != curr.Page) {
        if (
          confirm(
            'Что-то пошло не так.'
            + '\n'
            + '\n[1] Сохраненная страница:\t' + this.comicsPage
            + '\n[2] Страница комикса (html):\t' + curr.Page
            + '\n[3] Максимальная страница (html):\t' + this.comicsLastPage
            + '\n'
            + '\nЗаменить сохраненную страницу ' + this.comicsPage + ' на ' + curr.Page + '?'
          )
        ) {
          this.wantPage = this.comicsPage = curr.Page;
          this.store();
          location.href = this.makeUrl(this.comicsPage);
        }
        return false;
      }

      this.tmp_curr = curr;
      // this.putCached( this.comicsPage, curr );
      // this.putCached( this.tmp_curr.Page, this.tmp_curr ); delete this.tmp_curr;
      this.stage = 'preInit.4';
      this.preInitSucceeded = true;
      return true;
    }
    initSucceeded = false;
    initFailed = false;
    initGuard = false;
    noPushState = false;
    firstPushState = false;
    changeHandlers() {
      this.store();
      this.ShouldIDoSomething();
      if (this.noPushState) {
        this.noPushState = false;
        return;
      }
      let stateObj = { page: this.comicsPage };
      let title = `#${this.comicsPage} | ${this.comicsPageTitle}`;
      let url = this.makeUrl(this.comicsPage);
      if (this.firstPushState) {
        this.firstPushState = false;
        history.replaceState(stateObj, title, url);
      } else {
        history.pushState(stateObj, title, url);
      }
    }
    $htmlbody = null;
    doScroll(relative, scrollAmount) {
      this.$htmlbody || (this.$htmlbody = $('html,body'));
      if (relative) {
        this.$htmlbody.finish().animate({
          scrollTop: Math.max(0, $(window).scrollTop() + scrollAmount)
        }, this.settings.scrollFasterSpeed);
      } else {
        this.$htmlbody.finish().animate({
          scrollTop: scrollAmount
        }, this.settings.scrollSpeed);
      }
    }
    SessCacheQuery(page) {
      if (!this.localStorageKeyPage) throw new Error("No localStorageKeyPage");
      var r = window.sessionStorage.getItem(this.localStorageKeyPage + page);
      if (!r) return false;
      try {
        r = JSON.parse(r);
      } catch (e) {
        return false;
      }
      return r;
    };
    SessCachePut(page, data) {
      if (!this.localStorageKeyPage) throw new Error("No localStorageKeyPage");
      var d = JSON.stringify(data);
      window.sessionStorage.setItem(this.localStorageKeyPage + page, d);
    };
    /** @type {number|null} */
    handleSwapTimeout = null;
    handleSwap(page, o) {
      this.log('page.swap: swapping page to #' + page + (
        o.Preloaded ? '' : ' which doesn\'t have preloaded image'
      ));
      this.$contentMargin.html(o.ContentMargin);
      this.$contentSer.find('img.issue').attr('src', o.Image);
      if (this.handleSwapTimeout !== null) clearTimeout(this.handleSwapTimeout);
      this.handleSwapTimeout = setTimeout(() => {
        this.$contentSer.html(o.ContentSerialNomargin);
        this.handleSwapTimeout = null;
      }, 50);
      this.$contentCounter.html(`${o.Page}/${o.LastPage}`);
      if (o.Page < 2) this.$contentBtnPrev.addClass('button-inactive'); else this.$contentBtnPrev.removeClass('button-inactive');
      if (o.Page >= o.LastPage) this.$contentBtnNext.addClass('button-inactive'); else this.$contentBtnNext.removeClass('button-inactive');

      // title
      this.comicsPageTitle = o.Title;
      this.comicsPage = page;

      document.title = '#' + this.comicsPage + ' | ' + this.comicsPageTitle;

      // $(window).scrollTop(0);
      this.doScroll(false, 0);
      this.changeHandlers();
    };
    /** @type {Object} */
    plData = Object.create(null);
    putCached(page, data) {
      this.plData[page] = data;
      this.SessCachePut(page, data);
      this.putCachedPreloadImage(page);
    };
    putCachedPreloadImage(page) {
      this.plData[page].Preloaded = false;
      this.plData[page].DomImage = new Image();
      this.plData[page].DomImage.onerror = (err) => {
        console.error('Cannot load image #' + page + ' (window._err_img): ' + this.plData[page].Image);
        console.error({ "this.plData[page].Image": this.plData[page].Image, "err (event)": err });
        // TODO: debugger;
        window._err_img = this.plData[page].Image;
        if (typeof (this.plData[page]) != 'undefined') {
          delete this.plData[page];
        }
      };
      var log = false;
      this.plData[page].DomImage.onload = () => {
        delete this.plData[page].DomImage.onload;

        if (typeof (this.plData[page]) == 'object') {
          if (log) {
            this.log('page.cache: IMG preloaded #' + page);
          }
          this.plData[page].Preloaded = true;
        } else {
          this.log('page.cache: IMG preloaded #' + page + ' but OBJ DOES NOT EXISTS!!');
        }
      };
      this.plData[page].DomImage.src = this.plData[page].Image;
      setTimeout(() => { log = true; }, 30);
    };
    /** @type {JQuery<HTMLElement>|null} */
    $contentMargin = null;
    /** @type {JQuery<HTMLElement>|null} */
    $content = null;
    /** @type {JQuery<HTMLElement>|null} */
    $contentSer = null;
    /** @type {JQuery<HTMLImageElement>|null} */
    $contentImage = null;
    /** @type {JQuery<HTMLElement>|null} */
    $contentCounter = null;
    /** @type {JQuery<HTMLElement>|null} */
    $contentBtnPrev = $('.common-content .button-previous');
    /** @type {JQuery<HTMLElement>|null} */
    $contentBtnNext = $('.common-content .button-next');

    gcAllowed = false;
    gcAllowedSet() { this.gcAllowed = true; }
    gc() {
      if (!this.gcAllowed) { return; }

      var r = this.populateList(5);
      r.push(this.wantPage);
      for (var page_str in this.plData) {
        page = parseInt(page_str, 10);
        if ($.inArray(page, r) == -1) {
          this.log('this.gc: removing old page #' + page);
          delete this.plData[page];
          window.sessionStorage.removeItem(this.localStorageKeyPage + page);
        }
      }

      var prefixl = this.localStorageKeyPage.length;
      for (var i = window.sessionStorage.length; i-- > 0;) {
        var k = window.sessionStorage.key(i);
        if (typeof (k) == 'string' && k.indexOf(this.localStorageKeyPage) == 0) {
          var page = parseInt(k.substr(prefixl), 10);
          if ($.inArray(page, r) == -1) {
            this.log('this.gc: removing sessionStorage page #' + page);
            window.sessionStorage.removeItem(this.localStorageKeyPage + page);
          }
        }
      }
      // key()
      this.gcAllowed = false;
      setTimeout(() => this.gcAllowedSet(), 10000);
    };
    swapPage() {
      var page = this.wantPage;
      if (typeof (this.plData[page]) == 'undefined') {
        this.log('page.swap: NO CACHE FOR #' + page);
        return false;
      }
      if (typeof (this.plData[page]) == 'boolean') {
        this.log('page.swap: CACHE STILL WAITING #' + page);
        return true;
      }
      if (typeof (this.plData[page]) == 'object') {
        var o = this.plData[page];
        this.handleSwap(page, o);
      }
      return true;
    };
    ShouldIDoSomething() {
      if (this.wantPage < 1) {
        this.log('page.swap: wrong wantPage = ' + this.wantPage);
        this.wantPage = 1;
      } else if (this.wantPage > this.comicsLastPage) {
        this.log('page.swap: wrong wantPage = ' + this.wantPage);
        this.wantPage = this.comicsLastPage;
      }
      if (this.wantPage != this.comicsPage) {
        if (!this.swapPage()) {
          this.log('page.cache: requesting non-preloaded page #' + this.wantPage);
          this.ajaxPreload(this.wantPage);
          return true;
        }
      }

      var r = this.populateList(0);
      var rl = r.length;
      for (var i = 0; i < rl; i++) {
        var page = r[i];
        if (typeof (this.plData[page]) == 'undefined') {
          var cached = this.ajaxPreload(page);
          if (!cached) {
            this.log('page.cache: preloading page #' + page);
            return true;
          }
        }
      }
      this.gc();
      return false;
    };
    ticker() {
      var to = this.ShouldIDoSomething() ? 300 : 1000;
      this.tickerTO = setTimeout(() => this.ticker(), to);
    };
    populateList(Add) {
      var a = [];
      for (var i = 1; i <= (4 + Add); i++) {
        var np = this.wantPage + i;
        if (np <= this.comicsLastPage) {
          a.push(np);
        }
      }
      for (var i = 1; i <= (2 + Add); i++) {
        var np = this.wantPage - i;
        if (np >= 1) {
          a.push(np);
        }
      }
      return a;
    };
    ajaxPreload(page) { // return cached
      var sc = this.SessCacheQuery(page);
      if (sc) {
        this.plData[page] = sc;
        // this.SessCachePut( page, data );
        this.putCachedPreloadImage(page);
        return true;
      }

      this.plData[page] = true;
      $.ajax({
        type: 'GET',
        dataType: 'text',
        cache: true,
        url: this.makeUrl(page),
        timeout: this.settings.AjaxRequestTimeoutMs,
        error: (...args) => {
          console.error({ _: `ajaxPreload(${page}) failed!`, args });
          if (typeof (this.plData[page]) != 'undefined') {
            delete this.plData[page];
          }
        },
        success: (d) => {
          var parsed = this.parsePageString(d);
          if (!parsed) {
            console.error('Cannot parse data (see window._err_data)');
            window._err_data = d;
            if (typeof (this.plData[page]) != 'undefined') {
              delete this.plData[page];
            }
          } else {
            this.log('page.ajax: preloaded #' + page);
            this.putCached(page, parsed);
            this.ShouldIDoSomething();
          }
        }
      });

      return false;
    };
    handlePrev() {
      // this.wantPage = this.comicsPage;
      this.wantPage--;
      this.ShouldIDoSomething();
    };
    handleNext() {
      this.wantPage++;
      this.ShouldIDoSomething();
    };
    uparrow_pressed = false;
    last_scroll = 0;
    documentKeydown(e) {
      var prevent = true;
      // if( e.which==13 && e.shiftKey ) { // Shift+Enter
      // } else
      if ((e.which == 38 || e.which == 40) && e.shiftKey) { // ^/v
        this.scroll_faster = !this.scroll_faster;
      }
      if (e.which == 37) { // <
        if (this.settings.contentMarginMover) { this.$contentMargin.hide(); }
        if (this.uparrow_pressed) {
          if (this.settings.contentMarginMover) { $(window).scrollTop(this.last_scroll); }
          this.$content.addClass('notitle');
        } else {
          this.handlePrev();
        }
      } else if (e.which == 39) { // >
        if (this.uparrow_pressed) {
          if (this.settings.contentMarginMover) { this.$contentMargin.show(); }
          this.$content.removeClass('notitle');
          // $(window).scrollTop(0);
          this.doScroll(false, 0);
        } else {
          if (this.settings.contentMarginMover) { this.$contentMargin.hide(); }
          this.handleNext();
        }
      } else if (e.which == 38) { // ^
        this.uparrow_pressed = true;
        this.last_scroll = $(window).scrollTop();
        if (this.settings.scrollFasterPx && this.scroll_faster) {
          // $(window).scrollTop( Math.max( 0, $(window).scrollTop()-this.settings.scrollFasterPx ) );
          this.doScroll(true, -(e.ctrlKey ? this.settings.scrollFasterCtrlPx : this.settings.scrollFasterPx));
        } else {
          prevent = false;
        }
      } else if (e.which == 40) { // v
        if (this.settings.scrollFasterPx && this.scroll_faster) {
          // $(window).scrollTop( $(window).scrollTop()+this.settings.scrollFasterPx );
          this.doScroll(true, (e.ctrlKey ? this.settings.scrollFasterCtrlPx : this.settings.scrollFasterPx));
        } else {
          prevent = false;
        }
      } else {
        prevent = false;
      }
      if (prevent) { e.preventDefault(); return false; }
    }
    documentKeyup(e) {
      if (e.which == 38) { // ^
        this.uparrow_pressed = false;
      }
    }
    scroll_faster = false;
    /** @returns {boolean} */
    init() {
      if (!this.preInit()) { return false; }
      if (this.initSucceeded) return true;
      if (this.initFailed || this.initGuard) { return false; }

      this.initFailed = true;
      this.initGuard = true;
      this.stage = 'init.1';

      this.noPushState = false;
      // this.firstPushState = true;

      $(window)
        .off(`popstate.${this.constants.eventNamespace}`)
        .on(`popstate.${this.constants.eventNamespace}`, (e) => {
          var d = e?.originalEvent?.state || false;
          if (d && d.page) {
            this.wantPage = d.page;
            this.noPushState = true;
            this.ShouldIDoSomething();
          }
        });

      // TODO: add button and click event for this thing and script settings
      window.eval('window.z_SetPage=function(){ $(window).trigger(\'' +
        this.constants.localStorageKeyPrefix + ':setpage'
        + '\') };');
      this.storePage = () => { // z_SetPage
        var p = parseInt(prompt('Set page to:', this.comicsPage), 10);
        if (isNaN(p) || p < 1 || p > this.comicsLastPage) {
          alert('Wrong page!\nMust be: 1 < page < ' + this.comicsLastPage);
          return;
        }
        if (!confirm('Warning! Really update page from ' + this.comicsPage + ' to ' + p + ' ?')) {
          return;
        }
        this.wantPage = p;
        this.ShouldIDoSomething();
      };
      $(window).on(this.constants.localStorageKeyPrefix + ':setpage', this.storePage);

      // this.wantPage = this.comicsPage;
      setTimeout(() => {
        if (this.wantPage == this.comicsPage) {
          this.swapPage();
        } else {
          this.ShouldIDoSomething();
        }
      }, 1);

      this.plData = Object.create(null);

      // this.wantPage = this.comicsPage;

      this.$content = $('.common-content');
      this.$contentMargin = $('.common-content .view-container .view-article');
      this.$contentSer = $('.common-content .reader-issue');
      // this.$contentImage = $('.common-content .reader-issue img.issue');
      this.$contentCounter = $('.common-content .button-goto span, .common-content .reader-issue-title > .number-without-name');
      this.$contentBtnPrev = $('.common-content .button-previous');
      this.$contentBtnNext = $('.common-content .button-next');

      // custom stylesheets
      $('<style/>').attr('type', 'text/css').html(''
        + ' .common-header-background, .common-header {display:none !important;}'
        + ' .common-content > .serial-header, .common-content > .serial-reader-menu {display:none;}'
        + ' .common-content .reader-issue-title > a, .common-content #title > a {display:none;}'
        + ' .common-content .reader-issue-title, .common-content #title {padding:0 !important;min-height:0 !important;}'
        + ' .common-content.notitle .reader-issue-title, .common-content.notitle #title {display:none;}'
        // + ' .common-content img.issue {max-width: none !important; width: auto; }'
        + ' .page-margins {max-width: none !important;}'
        + ' .page-margins {background-color: transparent !important;}'
        + ' div.page-background {padding-top: 0 !important;}'
        + ' .common-footer {display:none;}'
        + ' body {grid-template-rows: 1fr !important;}'

        + ' .view-container {max-width:var(--max-width);margin-right:auto;margin-left:auto;background-color:#fff;box-shadow:0 3px 6px 1px rgba(0,0,0,0.05);}'

        + ' .common-content .reader-navigator .button-content>* {display:none;}'
        + ' .common-content .reader-navigator .button-first>* {display:none;}'
        + ' .common-content .reader-navigator .button-last>* {display:none;}'
        + ' .common-content .reader-navigator .button-random>* {display:none;}'
      ).appendTo('body');

      this.$content.addClass('notitle');
      if (this.settings.contentMarginMover) {
        this.$contentMargin
          .detach()
          .css({
            position: 'absolute',
            left: '5px',
            top: '64px',
            border: '1px #000 solid',
            background: '#fff'
          })
          .appendTo('body')
          .hide();
      }
      this.gcAllowedSet();

      this.tickerTO = setTimeout(() => this.ticker(), 10);

      this.uparrow_pressed = false;
      this.last_scroll = 0;

      this.scroll_faster = this.settings.scrollFasterDefault;
      $(document)
        .off(`keydown.${this.constants.eventNamespace}`)
        .on(`keydown.${this.constants.eventNamespace}`, (e) => this.documentKeydown(e))
        .off(`keyup.${this.constants.eventNamespace}`)
        .on(`keyup.${this.constants.eventNamespace}`, (e) => this.documentKeyup(e))
        ;

      this.$contentSer
        .off(`click.${this.constants.eventNamespace}`, 'a.reader-issue-next')
        .on(`click.${this.constants.eventNamespace}`, 'a.reader-issue-next', (e) => { e.preventDefault(); this.handleNext(); });
      this.$contentSer
        .off(`click.${this.constants.eventNamespace}`, 'a.reader-issue-previous')
        .on(`click.${this.constants.eventNamespace}`, 'a.reader-issue-previous', (e) => { e.preventDefault(); this.handlePrev(); });
      this.$contentBtnPrev.find('a')
        .attr('href', '#js-prev')
        .off(`click.${this.constants.eventNamespace}`)
        .on(`click.${this.constants.eventNamespace}`, (e) => {
          e.preventDefault();
          this.handlePrev();
          return false;
        });
      this.$contentBtnNext.find('a')
        .attr('href', '#js-next')
        .off(`click.${this.constants.eventNamespace}`)
        .on(`click.${this.constants.eventNamespace}`, (e) => {
          e.preventDefault();
          this.handleNext();
          return false;
        });
      $(window)
        .off(`beforeunload.${this.constants.eventNamespace}`)
        .on(`beforeunload.${this.constants.eventNamespace}`, (event) => {
          if (this.settings.blockUnload) {
            event.preventDefault();
            return 'You shall not pass.';
          }
        });

      this.putCached(this.tmp_curr.Page, this.tmp_curr);
      this.tmp_curr = null;

      enableIntercept = true;

      this.stage = 'init.2';

      this.log('init(finish)');
    };
    createRunInterface() {
      var current = this.init_comicsPage == this.init_wantPage;
      var label = 'Запуск читалки';
      if (!current) {
        label = 'Продолжить чтение (стр. ' + this.init_wantPage + ')';
      }
      var d = $('<div/>');

      $('<button/>').html('&times;').css({
        color: '#F00', fontWeight: 'bold', fontSize: '16px',
        position: 'absolute', left: '-10px', top: '-10px', width: '20px', height: '20px', padding: '1px'
      }).click((e) => {
        e.preventDefault();
        d.remove();
      }).appendTo(d);


      $('<button/>').html(label).css({
        fontSize: '14px'
      }).click((e) => {
        e.preventDefault();
        d.remove();
        this.init();
        // w.history.pushState({a:location.href}, "* Супер-читалка", location.href);
      }).appendTo(d);
      if (!current) {
        d.append('<div>Произойдет переход на стр. ' + this.init_wantPage + '</div>');
        $('<button/>').html(
          'Читать со страницы ' + this.init_comicsPage + ''
        ).css({
          fontSize: '12px'
        }).click((e) => {
          e.preventDefault();
          if (confirm(''
            + 'Вы собираетесь удалить закладку со страницы ' + this.init_wantPage
            + '\nи продолжить чтение со страницы ' + this.init_comicsPage
            + '\n\nВсё верно?'
          )) {
            this.wantPage = this.comicsPage = this.init_comicsPage;
            // this.store();
            // location.href = this.makeUrl(this.comicsPage);
            d.remove();
            this.init();
          }
        }).appendTo(d);
        d.append('<div style="font-size:10px;">Закладка со стр. ' + this.init_wantPage + ' будет удалена.</div>');
      }
      d.css({
        borderRadius: '4px',
        position: 'fixed', // absolute
        zIndex: 1e6,
        right: '3px',
        top: '3px',
        padding: '4px',
        background: 'rgba(0,0,0,0.6)',
        color: '#fff',
        border: '2px rgba(0,0,0,0.9) solid'
      }).appendTo('body');
      setTimeout(() => {
        var inner = $('header.common > div.inner');
        if (!inner.length) return;
        if (d.offset().left < (inner.offset().left + inner.width() + 55)) {
          d.css({ top: Math.round(inner.height() + 3) + 'px' });
        }
      }, 1);
    }
  }


  var enableIntercept = false;
  // disable built-in navigation with left/right arrow keys.
  let _addEventListener = document.addEventListener;
  document.addEventListener = function (en, fn, ...rest) {
    if (en == 'keydown') {
      let fnStr = fn?.toString();
      if (fnStr.indexOf(`navElement.querySelector('a').getAttribute('href')`) !== -1) {
        let origFn = fn;
        fn = function (e) {
          if (enableIntercept) return;
          return origFn(e);
        };
      }
    }
    return _addEventListener.call(this, en, fn, ...rest);
  };

  class AcomicsViewerList2 {
    static makeUrlReference(id, comicsName) {
      return '/~' + comicsName + '/' + id;
    };
    initGuard = false;
    init() {
      if (this.initGuard) return;
      this.initGuard = true;
      if (!/\/-[A-Za-z0-9_-]+\/list2/.test(location.pathname)) { return; }

      // $('div.contentSerialMargin > div.agrHolder >table.agr >tbody>tr:first-child>td.agrBody>div.numbers')

      var div = $('div.agrHolder');
      if (!div.length) { return; }
      div.find('>table.agr').css({ marginBottom: '4px' }).each(function () {
        // var a = $(this).find('>tbody>tr:first-child>td.agrBody>h3>a');
        // if(!a.length){return;}
        // a = a[0].pathname;
        // if( a.substr(0,2)!='/~' ){return;}
        // a=a.substr(2);
        var b = $(this).find('>tbody>tr:first-child>td.agrBody>div.numbers a').eq(0);
        if (!b.length) { return; }
        b = b[0].pathname;
        var m = /^\/~([^\/]+)\/(\d*)(?:\D|$)/.exec(b);
        if (!m) { return; }

        // parse page info
        var comicsName = m[1];
        var comicsPage = parseInt(m[2], 10);
        if (m[2] == '' || isNaN(comicsPage) || comicsPage < 1) { return; }

        var k = this.constants.localStorageKeyPrefix + comicsName; // stored shit
        var stored = window.localStorage.getItem(k);
        if (stored) { stored = parseInt(stored, 10) || false; } else { stored = false; }
        if (!stored) { return; }

        if (stored == comicsPage) {
          $(this).css({ backgroundColor: '#ccc', outline: '1px #999 solid' });
        } else if (stored < comicsPage) {
          $(this).css({ backgroundColor: '#cff', outline: '1px #9ff solid' });

          var link = $('<a/>').addClass('agr-today').attr(
            'href', AcomicsViewerList2.makeUrlReference(stored, comicsName)
          ).text(
            'Продолжить чтение со страницы ' + stored + ' (из ' + comicsPage + ')'
          ).css({
            fontSize: '18px'
          });
          $(this).find('>tbody>tr:first-child>td.agrBody>div.numbers').prepend('<hr/>').prepend(link);
        }

        // this.comicsName = m[1];
        // var comicsPage = parseInt(m[2],10);
      });
    }
  }

  $(function () {
    (function () {
      // $('.common .inner td.nainmenu > nav > a[href$="/list2"]').css({backgroundColor:'#f00'});
      var a = $('.common .inner td.logo > a');
      var img = a.find('> img');
      if (!img.length) { return; }
      var div = $('<div style="display:inline-block;overflow:hidden;width:55px;height:54px;height:100%;vertical-align:middle;padding-top:7px;" />');
      img.detach();
      div.append(img).appendTo(a);
      $('.common .inner td.logo').css('width', 'auto');

      var x = $('.common .inner td.nainmenu > nav > a[href$="/list2"]');

      x.css({ fontWeight: 'bold' });
      var y = $('<div/>').html('обновления').css({
        display: 'inline-block', background: '#99f', borderRadius: '4px', border: '1px #66f solid',
        position: 'absolute', marginTop: '-12px', marginLeft: '-48px',
        width: '64px', height: '14px',
        fontSize: '10px', fontFamily: 'sans-serif', textTransform: 'none', textAlign: 'center', fontWeight: 'normal'
      });
      y.insertBefore(x.find('>span'));

      var live = $('.common .inner td.nainmenu > nav > a[href$="/live"]').contents().filter(function () { return this.nodeType === 3; }).eq(0);
      if (live.length) { live[0].textContent = 'Live'; }
      var Top = $('.common .inner td.nainmenu > nav > a[href$="//top.a-comics.ru/"]').contents().filter(function () { return this.nodeType === 3; }).eq(0);
      if (Top.length) { Top[0].textContent = 'ТОП'; }
      // window.lol_y = y;
      // window.lol_x = x;
    })();

    let acomicsViewer = new AcomicsViewer();

    if (!acomicsViewer.preInit()) {
      let list2 = new AcomicsViewerList2();
      list2.init();
      return;
    }

    acomicsViewer.createRunInterface();
  });

})();