Enhanced viewer for acomics.ru

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
  });

})();