New Tab Image Zoom

Allows the image opened in the new tab to zoom independently without affecting the original page

// ==UserScript==
// @name                New Tab Image Zoom
// @name:zh-CN          新标签页图片缩放
// @namespace           https://greasyfork.org/zh-CN/users/193133-pana
// @homepage            https://greasyfork.org/zh-CN/users/193133-pana
// @version             1.0.1
// @description         Allows the image opened in the new tab to zoom independently without affecting the original page
// @description:zh-CN   允许在新标签页中打开的图片独立缩放,而不影响原网页
// @author              pana
// @license             GNU General Public License v3.0 or later
// @match               *://*/*
// @compatible          chrome
// @compatible          firefox
// @grant               GM_addStyle
// @grant               GM_getValue
// @grant               GM_setValue
// @grant               GM_registerMenuCommand
// @noframes
// ==/UserScript==

(function () {
  'use strict';
  class Picture {
    constructor(imgObj, shortcuts = false) {
      this.imgObj = imgObj;
      this.imgSrc = imgObj.src;
      this.shortcuts = shortcuts;
      this.wrapper = null;
      this.tip = null;
      this.ratio = 1;
      this.width = 0;
      this.height = 0;
      this.naturalWidth = 0;
      this.naturalHeight = 0;
      this.innerWidth = window.innerWidth;
      this.innerHeight = window.innerHeight;
      this.init();
    }
    sleep(ms, callback) {
      setTimeout(() => {
        console.info(ms / 1000 + ' second later.');
        if (typeof callback === 'function') {
          callback();
        }
      }, ms);
    }
    resetImgUrl(maxErrorNum) {
      if (maxErrorNum > 0) {
        this.imgObj.onerror = () => {
          console.warn('Image load error: ' + this.imgSrc + '\nReset count: ' + (12 - maxErrorNum));
          this.resetImgUrl(maxErrorNum - 1);
        };
        this.sleep(500, () => {
          this.imgObj.src = this.imgSrc;
        });
      } else {
        this.imgObj.onerror = null;
      }
    }
    tips() {
      const tip = document.createElement('div');
      tip.className = 'new-tab-image-zoom-tips';
      tip.title = 'Left-Click: Restore the original size(100%).\nRight-Click: Adapt to window size.';
      tip.addEventListener('click', event => {
        event.stopPropagation();
        this.restore();
      });
      tip.addEventListener('contextmenu', event => {
        event.preventDefault();
        this.adapt();
      });
      this.tip = tip;
      this.setTips();
      this.observer();
    }
    setTips() {
      if (this.tip) {
        this.tip.textContent = Math.round(this.ratio * 100) + '%';
      }
    }
    observer() {
      const ob = new ResizeObserver(() => {
        this.handler();
      });
      ob.observe(this.imgObj);
    }
    wrap() {
      this.tips();
      const wrapDiv = document.createElement('div');
      wrapDiv.className = 'new-tab-image-zoom-wrapper';
      document.body.insertBefore(wrapDiv, this.imgObj);
      document.body.removeChild(this.imgObj);
      wrapDiv.appendChild(this.imgObj);
      this.tip && wrapDiv.appendChild(this.tip);
      wrapDiv.addEventListener('wheel', event => {
        if (event.ctrlKey) {
          event.preventDefault();
          if (event.wheelDelta > 0) {
            this.zoomIn(0.1);
          } else if (event.wheelDelta < 0) {
            this.zoomOut(0.1);
          }
          return false;
        }
      });
      this.wrapper = wrapDiv;
    }
    handler() {
      this.width = this.imgObj.width || 0;
      this.height = this.imgObj.height || 0;
      this.naturalWidth = this.imgObj.naturalWidth || 0;
      this.naturalHeight = this.imgObj.naturalHeight || 0;
      if (this.width > 0 && this.height > 0 && this.naturalWidth > 0 && this.naturalHeight > 0) {
        this.ratio = this.width / this.naturalWidth || this.height / this.naturalHeight;
        this.setTips();
      }
    }
    init() {
      this.wrap();
      this.imgObj.classList.add('new-tab-image-zoom-image');
      if (this.imgObj.complete) {
        this.handler();
      } else {
        this.imgObj.onload = () => {
          this.handler();
        };
        this.imgObj.onerror = () => {
          console.warn('Image load error: ' + this.src + '\nReset count: 1');
          this.resetImgUrl(10);
        };
      }
      if (this.shortcuts) {
        document.onkeydown = e => {
          const ev = e || window.event;
          const eKeyCode = ev.keycode || ev.which || ev.charCode;
          const eCtrl = ev.ctrlKey || ev.metaKey;
          const eAlt = ev.altKey;
          if (eKeyCode === 187 && eCtrl) {
            e.preventDefault();
            this.zoomIn();
            return false;
          }
          if (eKeyCode === 189 && eCtrl) {
            e.preventDefault();
            this.zoomOut();
            return false;
          }
          if (eKeyCode === 48 && eCtrl && eAlt) {
            e.preventDefault();
            this.restore();
            return false;
          }
          if (eKeyCode === 48 && eCtrl) {
            e.preventDefault();
            this.adapt();
            return false;
          }
        };
      }
    }
    zoom() {
      this.imgObj.width = this.naturalWidth * this.ratio;
      this.imgObj.height = this.naturalHeight * this.ratio;
    }
    zoomIn(ratio = 0.1) {
      this.ratio += ratio;
      this.zoom();
    }
    zoomOut(ratio = 0.1) {
      if (this.ratio > ratio) {
        this.ratio -= ratio;
        this.zoom();
      }
    }
    restore() {
      this.ratio = 1;
      this.zoom();
    }
    adapt() {
      this.innerWidth = window.innerWidth;
      this.innerHeight = window.innerHeight;
      if (this.naturalWidth / this.naturalHeight >= this.innerWidth / this.innerHeight) {
        this.ratio = this.innerWidth / this.naturalWidth;
      } else {
        this.ratio = this.innerHeight / this.naturalHeight;
      }
      this.zoom();
    }
  }
  if (document.body.children.length === 1 && document.body.children[0].nodeName === 'IMG') {
    const zoomStyle = `
        .new-tab-image-zoom-wrapper {
          display: flex;
          background-color: #0e0e0e;
          width: 100%;
          height: 100%;
          justify-content: center;
          align-items: center;
        }
        .new-tab-image-zoom-image {
          margin: auto;
        }
        .new-tab-image-zoom-tips {
          position: fixed;
          z-index: 99;
          top: 1vh;
          right: 1vw;
          padding: 3px 6px;
          text-align: center;
          color: #eeeeee;
          font-size: 14px;
          line-height: 1;
          background-color: #666666;
          border: 1px solid #333333;
          box-shadow: 1px 1px 2px #030303;
          border-radius: 5px;
          box-sizing: border-box;
          overflow: hidden;
          user-select: none;
          cursor: pointer;
        }
        .new-tab-image-zoom-tips:hover {
          color: #449a46;
          background-color: #f0f9eb;
          border: 1px solid #d6e9c6;
        }
      `;
    GM_addStyle(zoomStyle);
    const { shortcuts } = GM_getValue('newTabConfig', { shortcuts: false });
    const _picture = new Picture(document.body.children[0], shortcuts);
    GM_registerMenuCommand(`Keyboard shortcuts [${shortcuts ? 'Enabled' : 'Disabled'}]`, () => {
      GM_setValue('newTabConfig', {
        shortcuts: !shortcuts,
      });
      location.reload();
    });
  }
})();