Touhou.AI | Manga Translator

(WIP) Userscript for https://touhou.ai/imgtrans/, translate images on Pixiv, Twitter.

20.02.2022 itibariyledir. En son verisyonu görün.

// ==UserScript==
// @name         Touhou.AI | Manga Translator
// @name:zh-CN   Touhou.AI | 图片翻译器
// @namespace    https://github.com/VoileLabs/imgtrans-userscript
// @version      0.5.3
// @description  (WIP) Userscript for https://touhou.ai/imgtrans/, translate images on Pixiv, Twitter.
// @description:zh-CN (WIP) https://touhou.ai/imgtrans/ 的用户脚本版本,一键翻译 Pixiv、Twitter 的图片
// @author       QiroNT
// @license      MIT
// @supportURL   https://github.com/VoileLabs/imgtrans-userscript/issues
// @require      https://unpkg.com/[email protected]/dist/vue.runtime.global.prod.js
// @include      http*://www.pixiv.net/*
// @match        http://www.pixiv.net/
// @include      http*://twitter.com/*
// @match        http://twitter.com/
// @connect      i.pximg.net
// @connect      i-f.pximg.net
// @connect      i-cf.pximg.net
// @connect      pbs.twimg.com
// @connect      touhou.ai
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @grant        GM.setValue
// @grant        GM_setValue
// @grant        GM.getValue
// @grant        GM_getValue
// @grant        GM.deleteValue
// @grant        GM_deleteValue
// @grant        GM.addValueChangeListener
// @grant        GM_addValueChangeListener
// @grant        GM.removeValueChangeListener
// @grant        GM_removeValueChangeListener
// @run-at       document-end
// ==/UserScript==

// polyfill functions
if (GM === undefined) {
  GM = {};
}
GM.xmlHttpRequest = GM.xmlHttpRequest || GM_xmlhttpRequest;
GM.setValue = GM.setValue || GM_setValue;
GM.getValue = GM.getValue || GM_getValue;
GM.deleteValue = GM.deleteValue || GM_deleteValue;
GM.addValueChangeListener = GM.addValueChangeListener || GM_addValueChangeListener;
GM.removeValueChangeListener = GM.removeValueChangeListener || GM_removeValueChangeListener;

/**
MIT License

Copyright (c) 2020-2022, VoileLabs

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
The MIT License (MIT)

Copyright (c) 2014 Commons Machinery

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
Copyright (c) 2013-2015, Johannes Buchner
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

*/

/**
The MIT License (MIT)

Copyright (c) 2015 Vail Systems

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

/**
MIT License

Copyright (c) 2018 Nik Coughlin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

(function (vue) {
  'use strict';

  const css = `
@keyframes imgtrans-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
`;
  const cssEl = document.createElement('style');
  cssEl.innerHTML = css;
  function checkCSS() {
      if (!document.head.contains(cssEl)) {
          document.head.appendChild(cssEl);
      }
  }

  function useGMStorage(key, initialValue) {
      const data = vue.ref(initialValue);
      async function read(newValue) {
          const rawValue = newValue !== null && newValue !== void 0 ? newValue : (await GM.getValue(key));
          if (rawValue == null) {
              data.value = initialValue;
          }
          else {
              data.value = rawValue;
          }
      }
      read();
      let listener;
      if (GM.addValueChangeListener) {
          (async () => {
              listener = await GM.addValueChangeListener(key, (name, oldValue, newValue, remote) => {
                  if (name === key)
                      read(newValue);
              });
          })();
      }
      const stopWatch = vue.watch(data, async () => {
          if (data.value == null) {
              await GM.deleteValue(key);
          }
          else {
              await GM.setValue(key, data.value);
          }
      });
      vue.onScopeDispose(() => {
          stopWatch();
          if (GM.removeValueChangeListener && listener)
              GM.removeValueChangeListener(listener);
      });
      return data;
  }
  const detectionResolution = useGMStorage('detectionResolution', 'M');
  const textDetector = useGMStorage('textDetector', 'auto');
  const translator$1 = useGMStorage('translator', 'baidu');
  const renderTextDirection = useGMStorage('renderTextDirection', 'auto');
  const targetLang = useGMStorage('targetLang');
  const scriptLang = useGMStorage('scriptLanguage');

  var data$1 = { common:{ source:{ "download-image":"正在拉取原图",
        "download-image-error":"拉取原图出错" },
      client:{ submit:"正在提交翻译",
        "submit-error":"提交翻译出错",
        "download-image":"正在下载图片",
        "download-image-error":"下载图片出错" },
      status:{ "default":"未知状态",
        pending:"正在等待",
        pending_pos:"正在等待,列队还有 {pos} 张图片",
        detection:"正在检测文本",
        ocr:"正在识别文本",
        mask_generation:"正在生成文本掩码",
        inpainting:"正在修补图片",
        translating:"正在翻译文本",
        render:"正在渲染",
        error:"翻译出错",
        "error-lang":"不支持的语言" },
      control:{ translate:"翻译",
        batch:"翻译全部",
        reset:"还原" },
      batch:{ progress:"翻译中({count}/{total})",
        finish:"翻译完成",
        error:"翻译完成(有失败)" } },
    settings:{ title:"Touhou.AI | 图片翻译器设置",
      "detection-resolution":"文本扫描清晰度",
      "text-detector":"文本扫描器",
      "text-detector-options":{ auto:"默认" },
      translator:"翻译服务",
      "render-text-direction":"渲染字体方向",
      "render-text-direction-options":{ auto:"跟随原文本",
        horizontal:"仅限水平" },
      "target-language":"翻译语言",
      "target-language-options":{ auto:"跟随网页语言" },
      "script-language":"用户脚本语言",
      "script-language-options":{ auto:"跟随网页语言" },
      reset:"重置所有设置" } };
  data$1.common;
  data$1.settings;

  var data = { common:{ source:{ "download-image":"Downloading original image",
        "download-image-error":"Error during original image download" },
      client:{ submit:"Submitting translation",
        "submit-error":"Error during translation submission",
        "download-image":"Downloading translated image",
        "download-image-error":"Error during translated image download" },
      status:{ "default":"Unknown status",
        pending:"Pending",
        pending_pos:"Pending, {pos} in queue",
        detection:"Detecting text",
        ocr:"Scanning text",
        mask_generation:"Generating mask",
        inpainting:"Inpainting",
        translating:"Translating",
        render:"Rendering",
        error:"Error during translation",
        "error-lang":"Unsupported language" },
      control:{ translate:"Translate",
        batch:"Translate all",
        reset:"Reset" },
      batch:{ progress:"Translating ({count}/{total} finished)",
        finish:"Translation finished",
        error:"Translation finished with errors" } },
    settings:{ "detection-resolution":"Text detection resolution",
      "render-text-direction":"Render text direction",
      "render-text-direction-options":{ auto:"Follow original direction",
        horizontal:"Horizontal only" },
      reset:"Reset Settings",
      "target-language":"Translate target language",
      "target-language-options":{ auto:"Follow website language" },
      "text-detector":"Text detector",
      "text-detector-options":{ auto:"Default" },
      title:"Touhou.AI | Manga Translator Settings",
      translator:"Translator",
      "script-language":"Userscript language",
      "script-language-options":{ auto:"Follow website language" } } };
  data.common;
  data.settings;

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const messages = {
      'zh-CN': data$1,
      'en-US': data,
  };
  function tryMatchLang(lang) {
      if (lang.startsWith('zh'))
          return 'zh-CN';
      if (lang.startsWith('en'))
          return 'en-US';
      return 'zh-CN';
  }
  const realLang = vue.ref(navigator.language);
  const lang = vue.computed(() => scriptLang.value || tryMatchLang(realLang.value));
  vue.watch(lang, (o, n) => {
      if (o === n)
          return;
      console.log('lang changed: ' + lang.value, 'real: ' + realLang.value);
  });
  const t = (key, props = {}) => {
      return { key, props };
  };
  const tt = ({ key, props }) => {
      const msg = key.split('.').reduce((obj, k) => obj[k], messages[lang.value]) ||
          key.split('.').reduce((obj, k) => obj[k], messages['zh-CN']);
      if (!msg)
          return key;
      return msg.replace(/\{([^}]+)\}/g, (_, k) => {
          var _a;
          return (_a = String(props[k])) !== null && _a !== void 0 ? _a : '';
      });
  };
  let langEL;
  let langObserver;
  const changeLangEl = (el) => {
      if (langEL === el)
          return;
      if (langObserver)
          langObserver.disconnect();
      const observer = new MutationObserver((mutations) => {
          for (const mutation of mutations) {
              if (mutation.type === 'attributes' && mutation.attributeName === 'lang') {
                  const target = mutation.target;
                  if (target.lang) {
                      realLang.value = target.lang;
                  }
                  break;
              }
          }
      });
      observer.observe(el, { attributes: true });
      langObserver = observer;
      langEL = el;
      realLang.value = el.lang;
  };
  function BCP47ToISO639(code) {
      try {
          const lo = new Intl.Locale(code);
          switch (lo.language) {
              case 'zh': {
                  switch (lo.script) {
                      case 'Hans':
                          return 'CHS';
                      case 'Hant':
                          return 'CHT';
                  }
                  switch (lo.region) {
                      case 'CN':
                          return 'CHS';
                      case 'HK':
                      case 'TW':
                          return 'CHT';
                  }
                  return 'CHS';
              }
              case 'ja':
                  return 'JPN';
              case 'en':
                  return 'ENG';
              case 'ko':
                  return 'KOR';
              case 'vi':
                  return 'VIE';
              case 'cs':
                  return 'CSY';
              case 'nl':
                  return 'NLD';
              case 'fr':
                  return 'FRA';
              case 'de':
                  return 'DEU';
              case 'hu':
                  return 'HUN';
              case 'it':
                  return 'ITA';
              case 'pl':
                  return 'PLK';
              case 'pt':
                  return 'PTB';
              case 'ro':
                  return 'ROM';
              case 'ru':
                  return 'RUS';
              case 'es':
                  return 'ESP';
              case 'tr':
                  return 'TRK';
          }
          return 'CHS';
      }
      catch (e) {
          return 'CHS';
      }
  }

  async function submitTranslate(blob, suffix) {
      const formData = new FormData();
      formData.append('file', blob, 'image.' + suffix);
      formData.append('size', detectionResolution.value);
      formData.append('translator', translator$1.value);
      formData.append('tgt_lang', targetLang.value || BCP47ToISO639(realLang.value));
      formData.append('dir', renderTextDirection.value);
      formData.append('detector', textDetector.value);
      const result = await GM.xmlHttpRequest({
          method: 'POST',
          url: 'https://touhou.ai/imgtrans/submit',
          // @ts-expect-error FormData is supported
          data: formData,
      });
      const json = JSON.parse(result.responseText);
      const id = json.task_id;
      return id;
  }
  async function getTranslateStatus(id) {
      const result = await GM.xmlHttpRequest({
          method: 'GET',
          url: 'https://touhou.ai/imgtrans/task-state?taskid=' + id,
      });
      const data = JSON.parse(result.responseText);
      return {
          state: data.state,
          waiting: (data.waiting || 0),
      };
  }
  function getStatusText(status) {
      switch (status.state) {
          case 'pending':
              if (status.waiting > 0) {
                  return t('common.status.pending_pos', { pos: status.waiting });
              }
              else {
                  return t('common.status.pending');
              }
          case 'detection':
              return t('common.status.detection');
          case 'ocr':
              return t('common.status.ocr');
          case 'mask_generation':
              return t('common.status.mask_generation');
          case 'inpainting':
              return t('common.status.inpainting');
          case 'translating':
              return t('common.status.translating');
          case 'render':
              return t('common.status.render');
          case 'error':
              return t('common.status.error');
          case 'error-lang':
              return t('common.status.error-lang');
          default:
              return t('common.status.default');
      }
  }
  async function pullTransStatusUntilFinish(id, cb) {
      for (;;) {
          const timer = new Promise((resolve) => setTimeout(resolve, 500));
          const status = await getTranslateStatus(id);
          if (status.state === 'finished') {
              return;
          }
          else if (status.state === 'error') {
              throw t('common.status.error');
          }
          else if (status.state === 'error-lang') {
              throw t('common.status.error-lang');
          }
          else {
              cb(status);
          }
          await timer;
      }
  }
  function blobToImageData(blob) {
      const blobUrl = URL.createObjectURL(blob);
      return new Promise((resolve, reject) => {
          const img = new Image();
          img.onload = () => resolve(img);
          img.onerror = (err) => reject(err);
          img.src = blobUrl;
      }).then((img) => {
          URL.revokeObjectURL(blobUrl);
          let w = img.width;
          let h = img.height;
          const factor = Math.max(w, h) / 256;
          w = w / factor;
          h = h / factor;
          const canvas = document.createElement('canvas');
          canvas.width = w;
          canvas.height = h;
          const ctx = canvas.getContext('2d');
          ctx.drawImage(img, 0, 0, w, h);
          return ctx.getImageData(0, 0, w, h);
      });
  }

  // Copyright 2014 Commons Machinery http://commonsmachinery.se/. Distributed under an MIT license, please see LICENSE.
  function median$1(data) {
      const mdarr = data.slice(0);
      mdarr.sort((a, b) => a - b);
      if (mdarr.length % 2 === 0) {
          const middle = mdarr.length / 2;
          return (mdarr[middle - 1] + mdarr[middle]) / 2;
      }
      return mdarr[Math.floor(mdarr.length / 2)];
  }
  function translateBlocksToBits(blocks, pixels_per_block) {
      const halfBlockValue = (pixels_per_block * 256 * 3) / 2;
      const bandsize = blocks.length / 4;
      // Compare medians across four horizontal bands
      for (let i = 0; i < 4; i++) {
          const m = median$1(blocks.slice(i * bandsize, (i + 1) * bandsize));
          for (let j = i * bandsize; j < (i + 1) * bandsize; j++) {
              const v = blocks[j];
              // Output a 1 if the block is brighter than the median.
              // With images dominated by black or white, the median may
              // end up being 0 or the max value, and thus having a lot
              // of blocks of value equal to the median.  To avoid
              // generating hashes of all zeros or ones, in that case output
              // 0 if the median is in the lower value space, 1 otherwise
              blocks[j] = Number(v > m || (Math.abs(v - m) < 1 && m > halfBlockValue));
          }
      }
  }
  function bitsToHexhash(bitsArray) {
      const hex = [];
      for (let i = 0; i < bitsArray.length; i += 4) {
          const nibble = bitsArray.slice(i, i + 4);
          hex.push(parseInt(nibble.join(''), 2).toString(16));
      }
      return hex.join('');
  }
  function bmvbhashEven(data, bits) {
      const blocksizeX = Math.floor(data.width / bits);
      const blocksizeY = Math.floor(data.height / bits);
      const result = [];
      for (let y = 0; y < bits; y++) {
          for (let x = 0; x < bits; x++) {
              let total = 0;
              for (let iy = 0; iy < blocksizeY; iy++) {
                  for (let ix = 0; ix < blocksizeX; ix++) {
                      const cx = x * blocksizeX + ix;
                      const cy = y * blocksizeY + iy;
                      const ii = (cy * data.width + cx) * 4;
                      const alpha = data.data[ii + 3];
                      if (alpha === 0) {
                          total += 765;
                      }
                      else {
                          total += data.data[ii] + data.data[ii + 1] + data.data[ii + 2];
                      }
                  }
              }
              result.push(total);
          }
      }
      translateBlocksToBits(result, blocksizeX * blocksizeY);
      return bitsToHexhash(result);
  }
  function bmvbhash(data, bits) {
      const evenX = data.width % bits === 0;
      const evenY = data.height % bits === 0;
      if (evenX && evenY) {
          return bmvbhashEven(data, bits);
      }
      // initialize blocks array with 0s
      const blocks = new Array(bits).fill(0).map(() => new Array(bits).fill(0));
      const blockWidth = data.width / bits;
      const blockWeight = data.height / bits;
      for (let y = 0; y < data.height; y++) {
          let blockTop, blockBottom, blockLeft, blockRight, weightTop, weightBottom, weightLeft, weightRight;
          if (evenY) {
              // don't bother dividing y, if the size evenly divides by bits
              blockTop = blockBottom = Math.floor(y / blockWeight);
              weightTop = 1;
              weightBottom = 0;
          }
          else {
              const yMod = (y + 1) % blockWeight;
              const yFrac = yMod - Math.floor(yMod);
              const yInt = yMod - yFrac;
              weightTop = 1 - yFrac;
              weightBottom = yFrac;
              // yInt will be 0 on bottom/right borders and on block boundaries
              if (yInt > 0 || y + 1 === data.height) {
                  blockTop = blockBottom = Math.floor(y / blockWeight);
              }
              else {
                  blockTop = Math.floor(y / blockWeight);
                  blockBottom = Math.ceil(y / blockWeight);
              }
          }
          for (let x = 0; x < data.width; x++) {
              const ii = (y * data.width + x) * 4;
              const alpha = data.data[ii + 3];
              let avgvalue;
              if (alpha === 0) {
                  avgvalue = 765;
              }
              else {
                  avgvalue = data.data[ii] + data.data[ii + 1] + data.data[ii + 2];
              }
              if (evenX) {
                  blockLeft = blockRight = Math.floor(x / blockWidth);
                  weightLeft = 1;
                  weightRight = 0;
              }
              else {
                  const xMod = (x + 1) % blockWidth;
                  const xFrac = xMod - Math.floor(xMod);
                  const xInt = xMod - xFrac;
                  weightLeft = 1 - xFrac;
                  weightRight = xFrac;
                  // xInt will be 0 on bottom/right borders and on block boundaries
                  if (xInt > 0 || x + 1 === data.width) {
                      blockLeft = blockRight = Math.floor(x / blockWidth);
                  }
                  else {
                      blockLeft = Math.floor(x / blockWidth);
                      blockRight = Math.ceil(x / blockWidth);
                  }
              }
              // add weighted pixel value to relevant blocks
              blocks[blockTop][blockLeft] += avgvalue * weightTop * weightLeft;
              blocks[blockTop][blockRight] += avgvalue * weightTop * weightRight;
              blocks[blockBottom][blockLeft] += avgvalue * weightBottom * weightLeft;
              blocks[blockBottom][blockRight] += avgvalue * weightBottom * weightRight;
          }
      }
      const result = blocks.flat();
      translateBlocksToBits(result, blockWidth * blockWeight);
      return bitsToHexhash(result);
  }
  function blockhash(imgData, bits = 16, method = 2) {
      if (method === 1) {
          return bmvbhashEven(imgData, bits);
      }
      else if (method === 2) {
          return bmvbhash(imgData, bits);
      }
      else {
          throw new Error('Bad hashing method');
      }
  }

  var dist$2 = {};

  var dist$1 = {};

  Object.defineProperty(dist$1, "__esModule", { value: true });
  dist$1.copy = void 0;
  const copy = (source, dest, sx = 0, sy = 0, sw = source.width - sx, sh = source.height - sy, dx = 0, dy = 0) => {
      sx = sx | 0;
      sy = sy | 0;
      sw = sw | 0;
      sh = sh | 0;
      dx = dx | 0;
      dy = dy | 0;
      if (sw <= 0 || sh <= 0)
          return;
      const sourceData = new Uint32Array(source.data.buffer);
      const destData = new Uint32Array(dest.data.buffer);
      for (let y = 0; y < sh; y++) {
          const sourceY = sy + y;
          if (sourceY < 0 || sourceY >= source.height)
              continue;
          const destY = dy + y;
          if (destY < 0 || destY >= dest.height)
              continue;
          for (let x = 0; x < sw; x++) {
              const sourceX = sx + x;
              if (sourceX < 0 || sourceX >= source.width)
                  continue;
              const destX = dx + x;
              if (destX < 0 || destX >= dest.width)
                  continue;
              const sourceIndex = sourceY * source.width + sourceX;
              const destIndex = destY * dest.width + destX;
              destData[destIndex] = sourceData[sourceIndex];
          }
      }
  };
  dist$1.copy = copy;

  var dist = {};

  (function (exports) {
  Object.defineProperty(exports, "__esModule", { value: true });
  exports.CreateImageFactory = (fill = [0, 0, 0, 0], channels = 4) => {
      channels = Math.floor(channels);
      if (isNaN(channels) || channels < 1) {
          throw TypeError('channels should be a positive non-zero number');
      }
      if (!('length' in fill) || fill.length < channels) {
          throw TypeError(`fill should be iterable with at least ${channels} members`);
      }
      fill = (new Uint8ClampedArray(fill)).slice(0, channels);
      const allZero = fill.every(v => v === 0);
      const createImage = (width, height, data) => {
          if (width === undefined || height === undefined) {
              throw TypeError('Not enough arguments');
          }
          width = Math.floor(width);
          height = Math.floor(height);
          if (isNaN(width) || width < 1 || isNaN(height) || height < 1) {
              throw TypeError('Index or size is negative or greater than the allowed amount');
          }
          const length = width * height * channels;
          if (data === undefined) {
              data = new Uint8ClampedArray(length);
          }
          if (data instanceof Uint8ClampedArray) {
              if (data.length !== length) {
                  throw TypeError('Index or size is negative or greater than the allowed amount');
              }
              if (!allZero) {
                  for (let y = 0; y < height; y++) {
                      for (let x = 0; x < width; x++) {
                          const index = (y * width + x) * channels;
                          for (let c = 0; c < channels; c++) {
                              data[index + c] = fill[c];
                          }
                      }
                  }
              }
              return {
                  get width() { return width; },
                  get height() { return height; },
                  get data() { return data; }
              };
          }
          throw TypeError('Expected data to be Uint8ClampedArray or undefined');
      };
      return createImage;
  };
  exports.createImage = exports.CreateImageFactory();

  }(dist));

  var filters$1 = {};

  Object.defineProperty(filters$1, "__esModule", { value: true });
  filters$1.filters = void 0;
  const fixedFracBits$1 = 14;
  const filterValue = (x, a) => {
      if (x <= -a || x >= a)
          return 0;
      if (x == 0)
          return 0;
      // appears to do nothing?
      // if ( x > -1.19209290e-07 && x < 1.19209290e-07 ) return 1
      const xPi = x * Math.PI;
      return (Math.sin(xPi) / xPi) * Math.sin(xPi / a) / (xPi / a);
  };
  const toFixedPoint = (value) => Math.round(value * ((1 << fixedFracBits$1) - 1));
  const filters = (srcSize, destSize, scale, offset, use2) => {
      const a = use2 ? 2 : 3;
      const scaleInverted = 1 / scale;
      const scaleClamped = Math.min(1, scale); // For upscale
      // Filter window (averaging interval), scaled to src image
      const srcWindow = a / scaleClamped;
      const maxFilterElementSize = Math.floor((srcWindow + 1) * 2);
      const packedFilter = new Int16Array((maxFilterElementSize + 2) * destSize);
      let packedFilterPtr = 0;
      // For each destination pixel calculate source range and built filter values
      for (let destPixel = 0; destPixel < destSize; destPixel++) {
          // Scaling should be done relative to central pixel point
          const sourcePixel = (destPixel + 0.5) * scaleInverted + offset;
          const sourceFirst = Math.max(0, Math.floor(sourcePixel - srcWindow));
          const sourceLast = Math.min(srcSize - 1, Math.ceil(sourcePixel + srcWindow));
          const filterElementSize = sourceLast - sourceFirst + 1;
          const floatFilter = new Float32Array(filterElementSize);
          const fxpFilter = new Int16Array(filterElementSize);
          let total = 0;
          // Fill filter values for calculated range
          let index = 0;
          for (let pixel = sourceFirst; pixel <= sourceLast; pixel++) {
              const floatValue = filterValue(((pixel + 0.5) - sourcePixel) * scaleClamped, a);
              total += floatValue;
              floatFilter[index] = floatValue;
              index++;
          }
          // Normalize filter, convert to fixed point and accumulate conversion error
          let filterTotal = 0;
          for (let index = 0; index < floatFilter.length; index++) {
              const filterValue = floatFilter[index] / total;
              filterTotal += filterValue;
              fxpFilter[index] = toFixedPoint(filterValue);
          }
          // Compensate normalization error, to minimize brightness drift
          fxpFilter[destSize >> 1] += toFixedPoint(1 - filterTotal);
          //
          // Now pack filter to useable form
          //
          // 1. Trim heading and tailing zero values, and compensate shitf/length
          // 2. Put all to single array in this format:
          //
          //    [ pos shift, data length, value1, value2, value3, ... ]
          //
          let leftNotEmpty = 0;
          while (leftNotEmpty < fxpFilter.length && fxpFilter[leftNotEmpty] === 0) {
              leftNotEmpty++;
          }
          let rightNotEmpty = fxpFilter.length - 1;
          while (rightNotEmpty > 0 && fxpFilter[rightNotEmpty] === 0) {
              rightNotEmpty--;
          }
          const filterShift = sourceFirst + leftNotEmpty;
          const filterSize = rightNotEmpty - leftNotEmpty + 1;
          packedFilter[packedFilterPtr++] = filterShift; // shift
          packedFilter[packedFilterPtr++] = filterSize; // size
          packedFilter.set(fxpFilter.subarray(leftNotEmpty, rightNotEmpty + 1), packedFilterPtr);
          packedFilterPtr += filterSize;
      }
      return packedFilter;
  };
  filters$1.filters = filters;

  var convolve$1 = {};

  Object.defineProperty(convolve$1, "__esModule", { value: true });
  convolve$1.convolve = void 0;
  const fixedFracBits = 14;
  const convolve = (source, dest, sw, sh, dw, filters) => {
      let srcOffset = 0;
      let destOffset = 0;
      // For each row
      for (let sourceY = 0; sourceY < sh; sourceY++) {
          let filterPtr = 0;
          // Apply precomputed filters to each destination row point
          for (let destX = 0; destX < dw; destX++) {
              // Get the filter that determines the current output pixel.
              const filterShift = filters[filterPtr++];
              let srcPtr = (srcOffset + (filterShift * 4)) | 0;
              let r = 0;
              let g = 0;
              let b = 0;
              let a = 0;
              // Apply the filter to the row to get the destination pixel r, g, b, a
              for (let filterSize = filters[filterPtr++]; filterSize > 0; filterSize--) {
                  const filterValue = filters[filterPtr++];
                  r = (r + filterValue * source[srcPtr]) | 0;
                  g = (g + filterValue * source[srcPtr + 1]) | 0;
                  b = (b + filterValue * source[srcPtr + 2]) | 0;
                  a = (a + filterValue * source[srcPtr + 3]) | 0;
                  srcPtr = (srcPtr + 4) | 0;
              }
              // Bring this value back in range. All of the filter scaling factors
              // are in fixed point with fixedFracBits bits of fractional part.
              //
              // (!) Add 1/2 of value before clamping to get proper rounding. In other
              // case brightness loss will be noticeable if you resize image with white
              // border and place it on white background.
              //
              dest[destOffset] = (r + (1 << 13)) >> fixedFracBits;
              dest[destOffset + 1] = (g + (1 << 13)) >> fixedFracBits;
              dest[destOffset + 2] = (b + (1 << 13)) >> fixedFracBits;
              dest[destOffset + 3] = (a + (1 << 13)) >> fixedFracBits;
              destOffset = (destOffset + sh * 4) | 0;
          }
          destOffset = ((sourceY + 1) * 4) | 0;
          srcOffset = ((sourceY + 1) * sw * 4) | 0;
      }
  };
  convolve$1.convolve = convolve;

  Object.defineProperty(dist$2, "__esModule", { value: true });
  dist$2.lanczos2 = lanczos_1 = dist$2.lanczos = void 0;
  const copy_1 = dist$1;
  const create_image_1 = dist;
  const filters_1 = filters$1;
  const convolve_1 = convolve$1;
  const resize = (source, dest, use2 = false) => {
      const xRatio = dest.width / source.width;
      const yRatio = dest.height / source.height;
      const filtersX = filters_1.filters(source.width, dest.width, xRatio, 0, use2);
      const filtersY = filters_1.filters(source.height, dest.height, yRatio, 0, use2);
      const tmp = new Uint8ClampedArray(dest.width * source.height * 4);
      convolve_1.convolve(source.data, tmp, source.width, source.height, dest.width, filtersX);
      convolve_1.convolve(tmp, dest.data, source.height, dest.width, dest.height, filtersY);
  };
  const lanczos = (source, dest, sx = 0, sy = 0, sw = source.width - sx, sh = source.height - sy, dx = 0, dy = 0, dw = dest.width - dx, dh = dest.height - dy) => {
      sx = sx | 0;
      sy = sy | 0;
      sw = sw | 0;
      sh = sh | 0;
      dx = dx | 0;
      dy = dy | 0;
      dw = dw | 0;
      dh = dh | 0;
      if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0)
          return;
      if (sx === 0 && sy === 0 && sw === source.width && sh === source.height && dx === 0 && dy === 0 && dw === dest.width && dh === dest.height) {
          resize(source, dest);
          return;
      }
      /*
        this is more expensive than the way we do in other rgba-lib functions, but
        I don't understand the pica code that I based this on well enough to work
        out how to use regions without doing crops first
    
        however copy is pretty fast compared to lanczos, so the difference is slight
      */
      const croppedSource = create_image_1.createImage(sw, sh);
      const croppedDest = create_image_1.createImage(dw, dh);
      copy_1.copy(source, croppedSource, sx, sy);
      resize(croppedSource, croppedDest);
      copy_1.copy(croppedDest, dest, 0, 0, croppedDest.width, croppedDest.height, dx, dy);
  };
  var lanczos_1 = dist$2.lanczos = lanczos;
  const lanczos2 = (source, dest, sx = 0, sy = 0, sw = source.width - sx, sh = source.height - sy, dx = 0, dy = 0, dw = dest.width - dx, dh = dest.height - dy) => {
      sx = sx | 0;
      sy = sy | 0;
      sw = sw | 0;
      sh = sh | 0;
      dx = dx | 0;
      dy = dy | 0;
      dw = dw | 0;
      dh = dh | 0;
      if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0)
          return;
      if (sx === 0 && sy === 0 && sw === source.width && sh === source.height && dx === 0 && dy === 0 && dw === dest.width && dh === dest.height) {
          resize(source, dest, true);
          return;
      }
      /*
        this is more expensive than the way we do in other rgba-lib functions, but
        I don't understand the pica code that I based this on well enough to work
        out how to use regions without doing crops first
    
        however copy is pretty fast compared to lanczos, so the difference is slight
      */
      const croppedSource = create_image_1.createImage(sw, sh);
      const croppedDest = create_image_1.createImage(dw, dh);
      copy_1.copy(source, croppedSource, sx, sy);
      resize(croppedSource, croppedDest, true);
      copy_1.copy(croppedDest, dest, 0, 0, croppedDest.width, croppedDest.height, dx, dy);
  };
  dist$2.lanczos2 = lanczos2;

  const cosMap = new Map();
  function memoizeCosines(n) {
      const cos = new Array(n ** 2);
      const piN = Math.PI / n;
      for (let i = 0; i < n; i++) {
          for (let j = 0; j < n; j++) {
              cos[j + i * n] = Math.cos(piN * (j + 0.5) * i);
          }
      }
      cosMap.set(n, cos);
  }
  // https://github.com/vail-systems/node-dct/blob/master/src/dct.js
  function dct(signal, scale = 2) {
      const l = signal.length;
      if (!cosMap.has(l))
          memoizeCosines(l);
      const cos = cosMap.get(l);
      return signal.map((_, ix) => scale * signal.reduce((pv, cv, i) => pv + cv * cos[i + ix * l], 0));
  }
  function flipAxis(arr) {
      const len = arr.length;
      const na = new Array(len).fill(0).map(() => new Array(len));
      for (let i = 0; i < len; i++) {
          for (let j = 0; j < len; j++) {
              na[i][j] = arr[j][i];
          }
      }
      return na;
  }
  function median(arr) {
      const na = arr.slice();
      const len = na.length;
      const mid = Math.floor(len / 2);
      na.sort((a, b) => a - b);
      return len % 2 ? na[mid] : (na[mid] + na[mid - 1]) / 2;
  }
  /**
   * Perceptual Hash computation.
   * Implementation follows http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html
   * Converted to TypeScript from https://github.com/JohannesBuchner/imagehash/blob/0abd4878bdb3c2b7bd0a5ec58d1ffca530e70cec/imagehash.py#L197
   */
  function phash(image, hashSize = 16, highfreqFactor = 4) {
      const imageSize = hashSize * highfreqFactor;
      // convert image to grayscale
      const data = image.data;
      const dataLength = data.length;
      for (let i = 0; i < dataLength; i += 4) {
          const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
          data[i] = data[i + 1] = data[i + 2] = gray;
      }
      // resize image to imageSize x imageSize
      const resizedImage = new ImageData(imageSize, imageSize);
      lanczos_1(image, resizedImage);
      // convert resizedImage to array of pixels
      // since we are using a grayscale image, we'll just read the red channel
      const resizedImageData = resizedImage.data;
      const resizedImageDataLength = resizedImageData.length;
      const pixels = new Array(resizedImageDataLength / 4);
      for (let i = 0; i < resizedImageDataLength; i += 4) {
          pixels[i / 4] = resizedImageData[i];
      }
      // format pixels into a 2d array
      const pixels2d = [];
      for (let i = 0; i < pixels.length; i += imageSize) {
          pixels2d.push(pixels.slice(i, i + imageSize));
      }
      // apply dct by column
      const dctCol = flipAxis(flipAxis(pixels2d).map((col) => dct(col)));
      // apply dct by row
      const dctRow = dctCol.map((row) => dct(row));
      // slice and join
      const dctLowFreq = dctRow.slice(0, hashSize).reduce((pv, cv) => pv.concat(cv.slice(0, hashSize)), []);
      const med = median(dctLowFreq);
      const hash = dctLowFreq.map((v) => (v > med ? 1 : 0));
      // convert binary array hash to hex string
      let hashHex = '';
      for (let i = 0; i < hash.length; i += 4) {
          const h = (hash[i] << 3) | (hash[i + 1] << 2) | (hash[i + 2] << 1) | hash[i + 3];
          hashHex += h.toString(16);
      }
      return hashHex;
  }

  const _hoisted_1$1 = {
    width: "1.2em",
    height: "1.2em",
    preserveAspectRatio: "xMidYMid meet",
    viewBox: "0 0 32 32"
  };
  const _hoisted_2$1 = /*#__PURE__*/vue.createElementVNode("path", {
    fill: "currentColor",
    d: "M27.85 29H30l-6-15h-2.35l-6 15h2.15l1.6-4h6.85zm-7.65-6l2.62-6.56L25.45 23zM18 7V5h-7V2H9v3H2v2h10.74a14.71 14.71 0 0 1-3.19 6.18A13.5 13.5 0 0 1 7.26 9h-2.1a16.47 16.47 0 0 0 3 5.58A16.84 16.84 0 0 1 3 18l.75 1.86A18.47 18.47 0 0 0 9.53 16a16.92 16.92 0 0 0 5.76 3.84L16 18a14.48 14.48 0 0 1-5.12-3.37A17.64 17.64 0 0 0 14.8 7z"
  }, null, -1 /* HOISTED */);
  const _hoisted_3$1 = [
    _hoisted_2$1
  ];

  function render$1(_ctx, _cache) {
    return (vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$1, _hoisted_3$1))
  }

  var IconCarbonTranslate = { name: 'carbon-translate', render: render$1 };
  /* vite-plugin-components disabled */

  const _hoisted_1 = {
    width: "1.2em",
    height: "1.2em",
    preserveAspectRatio: "xMidYMid meet",
    viewBox: "0 0 32 32"
  };
  const _hoisted_2 = /*#__PURE__*/vue.createElementVNode("path", {
    fill: "currentColor",
    d: "M18 28A12 12 0 1 0 6 16v6.2l-3.6-3.6L1 20l6 6l6-6l-1.4-1.4L8 22.2V16a10 10 0 1 1 10 10Z"
  }, null, -1 /* HOISTED */);
  const _hoisted_3 = [
    _hoisted_2
  ];

  function render(_ctx, _cache) {
    return (vue.openBlock(), vue.createElementBlock("svg", _hoisted_1, _hoisted_3))
  }

  var IconCarbonReset = { name: 'carbon-reset', render };
  /* vite-plugin-components disabled */

  var pixiv = () => {
      const images = new Set();
      const instances = new Map();
      const translatedMap = new Map();
      const translateEnabledMap = new Map();
      function rescanImages() {
          const imageNodes = Array.from(document.querySelectorAll('img')).filter((node) => {
              var _a;
              return node.hasAttribute('srcset') ||
                  node.hasAttribute('data-trans') ||
                  ((_a = node.parentElement) === null || _a === void 0 ? void 0 : _a.classList.contains('sc-1pkrz0g-1'));
          });
          const removedImages = new Set(images);
          for (const node of imageNodes) {
              removedImages.delete(node);
              if (!images.has(node)) {
                  // new image
                  // console.log('new', node)
                  try {
                      instances.set(node, mountToNode(node));
                      images.add(node);
                  }
                  catch (e) {
                      // ignore
                  }
              }
          }
          for (const node of removedImages) {
              // removed image
              // console.log('remove', node)
              if (instances.has(node)) {
                  const instance = instances.get(node);
                  instance.stop();
                  instances.delete(node);
                  images.delete(node);
              }
          }
      }
      function mountToNode(imageNode) {
          // get current displayed image
          const src = imageNode.getAttribute('src');
          const srcset = imageNode.getAttribute('srcset');
          // get original image
          const parent = imageNode.parentElement;
          if (!parent)
              throw new Error('no parent');
          const originalSrc = parent.getAttribute('href') || src;
          const originalSrcSuffix = originalSrc.split('.').pop();
          // console.log(src, originalSrc)
          let originalImage;
          let translatedImage = translatedMap.get(originalSrc);
          let translateMounted = false;
          let buttonDisabled = false;
          const buttonProcessing = vue.ref(false);
          const buttonTranslated = vue.ref(false);
          const buttonText = vue.ref();
          const buttonHint = vue.ref('');
          // create a translate botton
          parent.style.position = 'relative';
          const container = document.createElement('div');
          parent.appendChild(container);
          const buttonApp = vue.createApp(vue.defineComponent({
              setup() {
                  const content = vue.computed(() => (buttonText.value ? tt(buttonText.value) : '') + buttonHint.value);
                  return () => 
                  // container
                  vue.h('div', {
                      style: {
                          position: 'absolute',
                          zIndex: '1',
                          bottom: '8px',
                          right: content.value ? '4px' : '26px',
                      },
                  }, [
                      vue.h('div', {
                          style: {
                              position: 'relative',
                          },
                      }, [
                          vue.h('div', {
                              style: {
                                  fontSize: '16px',
                                  lineHeight: '16px',
                                  height: '16px',
                                  padding: '3px',
                                  paddingLeft: content.value ? '28px' : '2px',
                                  border: '2px solid #D1D5DB',
                                  borderRadius: '6px',
                                  background: '#fff',
                              },
                          }, [content.value]),
                          vue.h('div', {
                              style: {
                                  position: 'absolute',
                                  left: '-5px',
                                  top: '-2px',
                                  background: '#fff',
                                  borderRadius: '24px',
                              },
                          }, [
                              // button
                              vue.h(buttonTranslated.value ? IconCarbonReset : IconCarbonTranslate, {
                                  style: {
                                      fontSize: '18px',
                                      lineHeight: '18px',
                                      width: '18px',
                                      height: '18px',
                                      padding: '6px',
                                      cursor: 'pointer',
                                  },
                                  onClick: vue.withModifiers(() => {
                                      toggle();
                                  }, ['stop', 'prevent']),
                              }),
                              vue.h('div', {
                                  style: {
                                      position: 'absolute',
                                      top: '0',
                                      left: '0',
                                      right: '0',
                                      bottom: '0',
                                      border: '2px solid #D1D5DB',
                                      ...(buttonProcessing.value
                                          ? {
                                              borderTop: '2px solid #7DD3FC',
                                              animation: 'imgtrans-spin 1s linear infinite',
                                          }
                                          : {}),
                                      borderRadius: '24px',
                                      pointerEvents: 'none',
                                  },
                              }),
                          ]),
                      ]),
                  ]);
              },
          }));
          buttonApp.mount(container);
          async function getTranslatedImage() {
              if (translatedImage)
                  return translatedImage;
              buttonDisabled = true;
              const text = buttonText.value;
              buttonHint.value = '';
              buttonProcessing.value = true;
              buttonText.value = t('common.source.download-image');
              if (!originalImage) {
                  // fetch original image
                  const result = await GM.xmlHttpRequest({
                      method: 'GET',
                      responseType: 'blob',
                      url: originalSrc,
                      headers: { referer: 'https://www.pixiv.net/' },
                      overrideMimeType: 'text/plain; charset=x-user-defined',
                  }).catch((e) => {
                      buttonText.value = t('common.source.download-image-error');
                      throw e;
                  });
                  originalImage = result.response;
              }
              const imageData = await blobToImageData(originalImage);
              console.log('blockhash', blockhash(imageData), 'phash', phash(imageData));
              buttonText.value = t('common.client.submit');
              const id = await submitTranslate(originalImage, originalSrcSuffix).catch((e) => {
                  buttonText.value = t('common.client.submit-error');
                  throw e;
              });
              buttonText.value = t('common.status.pending');
              await pullTransStatusUntilFinish(id, (status) => {
                  buttonText.value = getStatusText(status);
              }).catch((e) => {
                  buttonText.value = e;
                  throw e;
              });
              buttonText.value = t('common.client.download-image');
              const image = await GM.xmlHttpRequest({
                  method: 'GET',
                  responseType: 'blob',
                  url: 'https://touhou.ai/imgtrans/result/' + id + '/final.png',
              }).catch((e) => {
                  buttonText.value = t('common.client.download-image-error');
                  throw e;
              });
              const imageUri = URL.createObjectURL(image.response);
              translatedImage = imageUri;
              translatedMap.set(originalSrc, translatedImage);
              buttonText.value = text;
              buttonProcessing.value = false;
              buttonDisabled = false;
              return imageUri;
          }
          async function enable() {
              translateMounted = true;
              try {
                  const translated = await getTranslatedImage();
                  imageNode.setAttribute('data-trans', src);
                  imageNode.setAttribute('src', translated);
                  imageNode.removeAttribute('srcset');
                  buttonTranslated.value = true;
              }
              catch (e) {
                  buttonDisabled = false;
                  translateMounted = false;
                  throw e;
              }
          }
          function disable() {
              translateMounted = false;
              imageNode.setAttribute('src', src);
              if (srcset)
                  imageNode.setAttribute('srcset', srcset);
              imageNode.removeAttribute('data-trans');
              buttonTranslated.value = false;
          }
          // called on click
          function toggle() {
              if (buttonDisabled)
                  return;
              if (!translateMounted) {
                  translateEnabledMap.set(originalSrc, true);
                  enable();
              }
              else {
                  translateEnabledMap.delete(originalSrc);
                  disable();
              }
          }
          // enable if enabled
          if (translateEnabledMap.get(originalSrc))
              enable();
          return {
              imageNode,
              stop: () => {
                  buttonApp.unmount();
                  parent.removeChild(container);
                  if (translateMounted)
                      disable();
              },
              async enable() {
                  translateEnabledMap.set(originalSrc, true);
                  return await enable();
              },
              disable() {
                  translateEnabledMap.delete(originalSrc);
                  return disable();
              },
              isEnabled() {
                  return translateMounted;
              },
          };
      }
      // translate all
      let removeTransAll;
      function refreshTransAll() {
          if (document.querySelector('.sc-emr523-2'))
              return;
          const bookmark = document.querySelector('.gtm-main-bookmark');
          if (bookmark) {
              const parent = bookmark.parentElement.parentElement;
              if (parent.querySelector('[data-transall]'))
                  return;
              const container = document.createElement('div');
              parent.appendChild(container);
              const buttonApp = vue.createApp(vue.defineComponent({
                  setup() {
                      const started = vue.ref(false);
                      const total = vue.ref(0);
                      const finished = vue.ref(0);
                      const erred = vue.ref(false);
                      return () => vue.h('div', {
                          'data-transall': 'true',
                          style: {
                              display: 'inline-block',
                              marginRight: '13px',
                              padding: '0',
                              color: 'inherit',
                              height: '32px',
                              lineHeight: '32px',
                              cursor: 'pointer',
                              fontWeight: '700',
                          },
                          onClick: vue.withModifiers(() => {
                              if (started.value)
                                  return;
                              started.value = true;
                              total.value = instances.size;
                              const inc = () => {
                                  finished.value++;
                              };
                              const err = () => {
                                  erred.value = true;
                                  finished.value++;
                              };
                              for (const instance of instances.values()) {
                                  if (instance.isEnabled())
                                      inc();
                                  else
                                      instance.enable().then(inc).catch(err);
                              }
                          }, ['stop', 'prevent']),
                      }, [
                          tt(started.value
                              ? finished.value === total.value
                                  ? erred.value
                                      ? t('common.batch.error')
                                      : t('common.batch.finish')
                                  : t('common.batch.progress', {
                                      count: finished.value,
                                      total: total.value,
                                  })
                              : t('common.control.batch')),
                      ]);
                  },
              }));
              buttonApp.mount(container);
              removeTransAll = () => {
                  buttonApp.unmount();
                  parent.removeChild(container);
              };
          }
      }
      const imageObserver = new MutationObserver((mutations) => {
          rescanImages();
          refreshTransAll();
      });
      imageObserver.observe(document.body, { childList: true, subtree: true });
      return {
          stop() {
              instances.forEach((instance) => instance.stop());
              removeTransAll === null || removeTransAll === void 0 ? void 0 : removeTransAll();
          },
      };
  };

  function renderSettings(options) {
      const { itemOrientation = 'vertical', textStyle = {} } = options !== null && options !== void 0 ? options : {};
      return vue.h('div', {
          style: {
              display: 'flex',
              flexDirection: 'column',
              gap: '8px',
          },
      }, [
          // Detection resolution
          vue.h('div', {
              style: {
                  ...(itemOrientation === 'horizontal'
                      ? {
                          display: 'flex',
                          flexDirection: 'row',
                          alignItems: 'center',
                      }
                      : {}),
              },
          }, [
              vue.h('div', {
                  style: {
                      ...textStyle,
                  },
              }, tt(t('settings.detection-resolution'))),
              vue.h('select', {
                  value: detectionResolution.value,
                  onChange(e) {
                      detectionResolution.value = e.target.value;
                  },
              }, [
                  vue.h('option', { value: 'S' }, '1024px'),
                  vue.h('option', { value: 'M' }, '1536px'),
                  vue.h('option', { value: 'L' }, '2048px'),
                  vue.h('option', { value: 'X' }, '2560px'),
              ]),
          ]),
          // Text detector
          vue.h('div', {
              style: {
                  ...(itemOrientation === 'horizontal'
                      ? {
                          display: 'flex',
                          flexDirection: 'row',
                          alignItems: 'center',
                      }
                      : {}),
              },
          }, [
              vue.h('div', {
                  style: {
                      ...textStyle,
                  },
              }, tt(t('settings.text-detector'))),
              vue.h('select', {
                  value: textDetector.value,
                  onChange(e) {
                      textDetector.value = e.target.value;
                  },
              }, [
                  vue.h('option', { value: 'auto' }, tt(t('settings.text-detector-options.auto'))),
                  vue.h('option', { value: 'ctd' }, 'CTD'),
              ]),
          ]),
          // Translator
          vue.h('div', {
              style: {
                  ...(itemOrientation === 'horizontal'
                      ? {
                          display: 'flex',
                          flexDirection: 'row',
                          alignItems: 'center',
                      }
                      : {}),
              },
          }, [
              vue.h('div', {
                  style: {
                      ...textStyle,
                  },
              }, tt(t('settings.translator'))),
              vue.h('select', {
                  value: translator$1.value,
                  onChange(e) {
                      translator$1.value = e.target.value;
                  },
              }, [
                  vue.h('option', { value: 'baidu' }, 'Baidu'),
                  vue.h('option', { value: 'google' }, 'Google'),
                  vue.h('option', { value: 'deepl' }, 'DeepL'),
              ]),
          ]),
          // Render text direction
          vue.h('div', {
              style: {
                  ...(itemOrientation === 'horizontal'
                      ? {
                          display: 'flex',
                          flexDirection: 'row',
                          alignItems: 'center',
                      }
                      : {}),
              },
          }, [
              vue.h('div', {
                  style: {
                      ...textStyle,
                  },
              }, tt(t('settings.render-text-direction'))),
              vue.h('select', {
                  value: renderTextDirection.value,
                  onChange(e) {
                      renderTextDirection.value = e.target.value;
                  },
              }, [
                  vue.h('option', { value: 'auto' }, tt(t('settings.render-text-direction-options.auto'))),
                  vue.h('option', { value: 'horizontal' }, tt(t('settings.render-text-direction-options.horizontal'))),
              ]),
          ]),
          // Target language
          vue.h('div', {
              style: {
                  ...(itemOrientation === 'horizontal'
                      ? {
                          display: 'flex',
                          flexDirection: 'row',
                          alignItems: 'center',
                      }
                      : {}),
              },
          }, [
              vue.h('div', {
                  style: {
                      ...textStyle,
                  },
              }, tt(t('settings.target-language'))),
              vue.h('select', {
                  value: targetLang.value,
                  onChange(e) {
                      targetLang.value = e.target.value;
                  },
              }, [
                  vue.h('option', { value: '' }, tt(t('settings.target-language-options.auto'))),
                  ...[
                      ['CHS', '简体中文'],
                      ['CHT', '繁體中文'],
                      ['JPN', '日本語'],
                      ['ENG', 'English'],
                      ['KOR', '한국어'],
                      ['VIN', 'Tiếng Việt'],
                      ['CSY', 'čeština'],
                      ['NLD', 'Nederlands'],
                      ['FRA', 'français'],
                      ['DEU', 'Deutsch'],
                      ['HUN', 'magyar nyelv'],
                      ['ITA', 'italiano'],
                      ['PLK', 'polski'],
                      ['PTB', 'português'],
                      ['ROM', 'limba română'],
                      ['RUS', 'русский язык'],
                      ['ESP', 'español'],
                      ['TRK', 'Türk dili'],
                  ].map(([value, text]) => vue.h('option', { value }, text)),
              ]),
          ]),
          // Script language
          vue.h('div', {
              style: {
                  ...(itemOrientation === 'horizontal'
                      ? {
                          display: 'flex',
                          flexDirection: 'row',
                          alignItems: 'center',
                      }
                      : {}),
              },
          }, [
              vue.h('div', {
                  style: {
                      ...textStyle,
                  },
              }, tt(t('settings.script-language'))),
              vue.h('select', {
                  value: scriptLang.value,
                  onChange(e) {
                      scriptLang.value = e.target.value;
                  },
              }, [
                  vue.h('option', { value: '' }, tt(t('settings.script-language-options.auto'))),
                  vue.h('option', { value: 'zh-CN' }, '简体中文'),
                  vue.h('option', { value: 'en-US' }, 'English'),
              ]),
          ]),
          // Reset
          vue.h('div', [
              vue.h('button', {
                  onClick: vue.withModifiers(() => {
                      detectionResolution.value = null;
                      textDetector.value = null;
                      translator$1.value = null;
                      renderTextDirection.value = null;
                      targetLang.value = null;
                      scriptLang.value = null;
                  }, ['stop', 'prevent']),
              }, tt(t('settings.reset'))),
          ]),
      ]);
  }

  var pixivSettings = () => {
      const wrapper = document.getElementById('wrapper');
      if (!wrapper)
          return {};
      const adFooter = wrapper.querySelector('.ad-footer');
      if (!adFooter)
          return {};
      const settingsContainer = document.createElement('div');
      const settingsApp = vue.createApp(vue.defineComponent({
          setup() {
              return () => vue.h('div', {
                  style: {
                      paddingTop: '10px',
                      paddingLeft: '20px',
                      paddingRight: '20px',
                      paddingBottom: '15px',
                      marginBottom: '10px',
                      background: '#fff',
                      border: '1px solid #d6dee5',
                  },
              }, [
                  vue.h('h2', {
                      style: {
                          fontSize: '18px',
                          fontWeight: 'bold',
                      },
                  }, tt(t('settings.title'))),
                  vue.h('div', {
                      style: {
                          width: '665px',
                          margin: '10px auto',
                      },
                  }, renderSettings({
                      itemOrientation: 'horizontal',
                      textStyle: {
                          width: '185px',
                          fontWeight: 'bold',
                      },
                  })),
              ]);
          },
      }));
      settingsApp.mount(settingsContainer);
      wrapper.insertBefore(settingsContainer, adFooter);
      return {
          stop() {
              settingsApp.unmount();
              settingsContainer.remove();
          },
      };
  };

  var twitter = () => {
      var _a;
      const statusId = (_a = location.pathname.match(/\/status\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1];
      const translatedMap = vue.reactive({});
      const translateStatusMap = vue.shallowReactive({});
      const translateEnabledMap = vue.reactive({});
      const originalImageMap = {};
      let initObserver;
      let layersObserver;
      let layers = document.getElementById('layers');
      let dialog;
      const createDialogInstance = () => {
          const active = vue.ref(0);
          const updateRef = vue.ref();
          const buttonParent = dialog.querySelector('[aria-labelledby="modal-header"][role="dialog"]').firstChild
              .firstChild;
          const images = vue.computed(() => {
              updateRef.value;
              return Array.from(buttonParent.firstChild.querySelectorAll('img'));
          });
          const currentImg = vue.computed(() => {
              const img = images.value[active.value];
              if (!img)
                  return undefined;
              return img.getAttribute('data-transurl') || img.src;
          });
          const stopImageWatch = vue.watch([images, translateEnabledMap, translatedMap], () => {
              for (const img of images.value) {
                  const div = img.previousSibling;
                  if (img.hasAttribute('data-transurl')) {
                      const transurl = img.getAttribute('data-transurl');
                      if (!translateEnabledMap[transurl]) {
                          if (div)
                              div.style.backgroundImage = `url("${transurl}")`;
                          img.src = transurl;
                          img.removeAttribute('data-transurl');
                      }
                  }
                  else if (translateEnabledMap[img.src] && translatedMap[img.src]) {
                      const ori = img.src;
                      img.setAttribute('data-transurl', ori);
                      img.src = translatedMap[ori];
                      if (div)
                          div.style.backgroundImage = `url("${translatedMap[ori]}")`;
                  }
              }
          });
          const getTranslatedImage = async (url) => {
              if (translatedMap[url])
                  return translatedMap[url];
              translateStatusMap[url] = vue.computed(() => tt(t('common.source.download-image')));
              if (!originalImageMap[url]) {
                  // fetch original image
                  const result = await GM.xmlHttpRequest({
                      method: 'GET',
                      responseType: 'blob',
                      url,
                      headers: { referer: 'https://twitter.com/' },
                      overrideMimeType: 'text/plain; charset=x-user-defined',
                  }).catch((e) => {
                      translateStatusMap[url] = vue.computed(() => tt(t('common.source.download-image-error')));
                      throw e;
                  });
                  originalImageMap[url] = result.response;
              }
              const originalImage = originalImageMap[url];
              const imageData = await blobToImageData(originalImage);
              console.log('blockhash', blockhash(imageData), 'phash', phash(imageData));
              translateStatusMap[url] = vue.computed(() => tt(t('common.client.submit')));
              const originalSrcSuffix = url.split('.').pop();
              const id = await submitTranslate(originalImage, originalSrcSuffix).catch((e) => {
                  translateStatusMap[url] = vue.computed(() => tt(t('common.client.submit-error')));
                  throw e;
              });
              translateStatusMap[url] = vue.computed(() => tt(t('common.status.pending')));
              await pullTransStatusUntilFinish(id, (status) => {
                  translateStatusMap[url] = vue.computed(() => tt(getStatusText(status)));
              }).catch((e) => {
                  translateStatusMap[url] = vue.computed(() => tt(e));
                  throw e;
              });
              translateStatusMap[url] = vue.computed(() => tt(t('common.client.download-image')));
              const image = await GM.xmlHttpRequest({
                  method: 'GET',
                  responseType: 'blob',
                  url: 'https://touhou.ai/imgtrans/result/' + id + '/final.png',
              }).catch((e) => {
                  translateStatusMap[url] = vue.computed(() => tt(t('common.client.download-image-error')));
                  throw e;
              });
              const imageUri = URL.createObjectURL(image.response);
              translatedMap[url] = imageUri;
              // https://github.com/vuejs/core/blob/1574edd490bd5cc0a213bc9f48ff41a1dc43ab22/packages/reactivity/src/baseHandlers.ts#L153
              translateStatusMap[url] = vue.computed(() => undefined);
              return imageUri;
          };
          const enable = async (url) => {
              await getTranslatedImage(url);
              translateEnabledMap[url] = true;
          };
          const disable = (url) => {
              translateEnabledMap[url] = false;
          };
          const referenceEl = buttonParent.children[2];
          const container = referenceEl.cloneNode(true);
          container.style.top = '48px';
          // container.style.display = 'flex'
          const stopDisplayWatch = vue.watchEffect(() => {
              container.style.display = currentImg.value ? 'flex' : 'none';
          });
          container.style.flexDirection = 'row';
          container.style.flexWrap = 'nowrap';
          const child = container.firstChild;
          const referenceChild = referenceEl.firstChild;
          const backgroundColor = vue.ref(referenceChild.style.backgroundColor);
          buttonParent.appendChild(container);
          const buttonProcessing = vue.computed(() => { var _a; return currentImg.value && !!((_a = translateStatusMap[currentImg.value]) === null || _a === void 0 ? void 0 : _a.value); });
          const buttonTranslated = vue.computed(() => currentImg.value && !!translateEnabledMap[currentImg.value]);
          const buttonContent = vue.computed(() => { var _a; return (currentImg.value ? (_a = translateStatusMap[currentImg.value]) === null || _a === void 0 ? void 0 : _a.value : ''); });
          const spinnerContainer = container.firstChild;
          const processingSpinner = document.createElement('div');
          processingSpinner.style.position = 'absolute';
          processingSpinner.style.top = '0';
          processingSpinner.style.left = '0';
          processingSpinner.style.bottom = '0';
          processingSpinner.style.right = '0';
          processingSpinner.style.borderTop = '1px solid #A1A1AA';
          processingSpinner.style.animation = 'imgtrans-spin 1s linear infinite';
          processingSpinner.style.borderRadius = '9999px';
          const stopSpinnerWatch = vue.watch(buttonProcessing, (p, o) => {
              if (p === o)
                  return;
              if (p && !spinnerContainer.contains(processingSpinner))
                  spinnerContainer.appendChild(processingSpinner);
              else if (spinnerContainer.contains(processingSpinner))
                  spinnerContainer.removeChild(processingSpinner);
          }, { immediate: true });
          const svg = container.querySelector('svg');
          const svgParent = svg.parentElement;
          const buttonIconContainer = document.createElement('div');
          svgParent.insertBefore(buttonIconContainer, svg);
          svgParent.removeChild(svg);
          const buttonIconApp = vue.createApp(vue.defineComponent({
              setup() {
                  return () => vue.h(buttonTranslated.value ? IconCarbonReset : IconCarbonTranslate, {
                      style: {
                          width: '20px',
                          height: '20px',
                          marginTop: '4px',
                      },
                      onClick: vue.withModifiers(() => {
                          var _a;
                          if (!currentImg.value)
                              return;
                          if ((_a = translateStatusMap[currentImg.value]) === null || _a === void 0 ? void 0 : _a.value)
                              return;
                          if (translateEnabledMap[currentImg.value]) {
                              disable(currentImg.value);
                          }
                          else {
                              enable(currentImg.value);
                          }
                      }, ['stop', 'prevent']),
                  });
              },
          }));
          buttonIconApp.mount(buttonIconContainer);
          const buttonStatusContainer = document.createElement('div');
          container.insertBefore(buttonStatusContainer, container.firstChild);
          const buttonStatusApp = vue.createApp(vue.defineComponent({
              setup() {
                  return () => vue.h('div', {
                      style: {
                          display: buttonContent.value ? '' : 'none',
                          marginRight: '-12px',
                          padding: '2px',
                          paddingLeft: '4px',
                          paddingRight: '16px',
                          color: '#fff',
                          backgroundColor: backgroundColor.value,
                          borderRadius: '4px',
                      },
                  }, [buttonContent.value]);
              },
          }));
          buttonStatusApp.mount(buttonStatusContainer);
          return {
              active,
              update() {
                  vue.triggerRef(updateRef);
                  if (referenceChild.style.backgroundColor)
                      child.style.backgroundColor = backgroundColor.value = referenceChild.style.backgroundColor;
              },
              stop() {
                  stopDisplayWatch();
                  stopSpinnerWatch();
                  stopImageWatch();
                  buttonIconApp.unmount();
                  buttonStatusApp.unmount();
                  buttonParent.removeChild(container);
                  for (const img of images.value) {
                      if (img.hasAttribute('data-transurl')) {
                          const transurl = img.getAttribute('data-transurl');
                          img.src = transurl;
                          img.removeAttribute('data-transurl');
                      }
                  }
              },
          };
      };
      let dialogInstance;
      const rescanLayers = () => {
          var _a;
          const [newDialog] = Array.from(layers.children).filter((el) => { var _a, _b, _c; return (_c = (_b = (_a = el.querySelector('[aria-labelledby="modal-header"][role="dialog"]')) === null || _a === void 0 ? void 0 : _a.firstChild) === null || _b === void 0 ? void 0 : _b.firstChild) === null || _c === void 0 ? void 0 : _c.childNodes[2]; });
          if (newDialog !== dialog || !newDialog) {
              dialogInstance === null || dialogInstance === void 0 ? void 0 : dialogInstance.stop();
              dialogInstance = undefined;
              dialog = newDialog;
              if (!dialog)
                  return;
              dialogInstance = createDialogInstance();
          }
          const newIndex = Number((_a = location.pathname.match(/\/status\/\d+\/photo\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]) - 1;
          if (newIndex !== dialogInstance.active.value) {
              dialogInstance.active.value = newIndex;
          }
          dialogInstance.update();
      };
      const onLayersUpdate = () => {
          rescanLayers();
          layersObserver = new MutationObserver((mutations) => {
              rescanLayers();
          });
          layersObserver.observe(layers, { childList: true, subtree: true });
      };
      if (layers)
          onLayersUpdate();
      else {
          initObserver = new MutationObserver((mutations) => {
              layers = document.getElementById('layers');
              if (layers) {
                  onLayersUpdate();
                  initObserver === null || initObserver === void 0 ? void 0 : initObserver.disconnect();
              }
          });
          initObserver.observe(document.body, { childList: true, subtree: true });
      }
      return {
          canKeep(url) {
              var _a;
              const urlStatusId = (_a = url.match(/\/status\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1];
              return urlStatusId === statusId;
          },
          stop() {
              layersObserver === null || layersObserver === void 0 ? void 0 : layersObserver.disconnect();
              initObserver === null || initObserver === void 0 ? void 0 : initObserver.disconnect();
          },
      };
  };

  var twitterSettings = () => {
      let settingsTab;
      let textApp;
      const checkTab = () => {
          const tablist = document.querySelector('[role="tablist"]') || document.querySelector('[data-testid="loggedOutPrivacySection"]');
          if (!tablist) {
              if (textApp) {
                  textApp.unmount();
                  textApp = undefined;
              }
              return;
          }
          if (tablist.querySelector('div[data-imgtrans-settings]'))
              return;
          const inactiveRefrenceEl = Array.from(tablist.children).find((el) => el.children.length < 2 && el.querySelector('a'));
          if (!inactiveRefrenceEl)
              return;
          settingsTab = inactiveRefrenceEl.cloneNode(true);
          settingsTab.setAttribute('data-imgtrans-settings', 'true');
          const textEl = settingsTab.querySelector('span');
          if (textEl) {
              textApp = vue.createApp(vue.defineComponent({
                  render() {
                      return tt(t('settings.title'));
                  },
              }));
              textApp.mount(textEl);
          }
          const linkEl = settingsTab.querySelector('a');
          if (linkEl)
              linkEl.href = '/settings/__imgtrans';
          tablist.appendChild(settingsTab);
      };
      let settingsApp;
      const checkSettings = () => {
          var _a, _b;
          const section = (_b = (_a = document.querySelector('[data-testid="error-detail"]')) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.parentElement;
          if (!(section === null || section === void 0 ? void 0 : section.querySelector('[data-imgtrans-settings-section]'))) {
              if (settingsApp) {
                  settingsApp.unmount();
                  settingsApp = undefined;
              }
              if (!section)
                  return;
          }
          const title = tt(t('settings.title')) + ' / Twitter';
          if (document.title !== title)
              document.title = title;
          if (settingsApp)
              return;
          const errorPage = section.firstChild;
          errorPage.style.display = 'none';
          const settingsContainer = document.createElement('div');
          settingsContainer.setAttribute('data-imgtrans-settings-section', 'true');
          section.appendChild(settingsContainer);
          settingsApp = vue.createApp(vue.defineComponent({
              setup() {
                  vue.onUnmounted(() => {
                      errorPage.style.display = '';
                  });
                  return () => 
                  // container
                  vue.h('div', {
                      style: {
                          paddingLeft: '16px',
                          paddingRight: '16px',
                      },
                  }, [
                      // title
                      vue.h('div', {
                          style: {
                              display: 'flex',
                              height: '53px',
                              alignItems: 'center',
                          },
                      }, vue.h('h2', {
                          style: {
                              fontSize: '20px',
                              lineHeight: '24px',
                          },
                      }, tt(t('settings.title')))),
                      renderSettings(),
                  ]);
              },
          }));
          settingsApp.mount(settingsContainer);
      };
      const listObserver = new MutationObserver(() => {
          checkTab();
          if (location.pathname.match(/\/settings\/__imgtrans/)) {
              if (settingsTab && settingsTab.children.length < 2) {
                  settingsTab.style.backgroundColor = '#F7F9F9';
                  const activeIndicator = document.createElement('div');
                  activeIndicator.style.position = 'absolute';
                  activeIndicator.style.zIndex = '1';
                  activeIndicator.style.top = '0';
                  activeIndicator.style.left = '0';
                  activeIndicator.style.bottom = '0';
                  activeIndicator.style.right = '0';
                  activeIndicator.style.borderRight = '2px solid #1D9Bf0';
                  activeIndicator.style.pointerEvents = 'none';
                  settingsTab.appendChild(activeIndicator);
              }
              checkSettings();
          }
          else {
              if (settingsTab && settingsTab.children.length > 1) {
                  settingsTab.style.backgroundColor = '';
                  settingsTab.removeChild(settingsTab.lastChild);
              }
              if (settingsApp) {
                  settingsApp.unmount();
                  settingsApp = undefined;
              }
          }
      });
      listObserver.observe(document.body, { childList: true, subtree: true });
      return {
          canKeep(url) {
              return url.includes('twitter.com') && url.includes('settings/');
          },
          stop() {
              settingsApp === null || settingsApp === void 0 ? void 0 : settingsApp.unmount();
              listObserver.disconnect();
          },
      };
  };

  function createScopedInstance(cb) {
      const scope = vue.effectScope();
      const i = scope.run(cb);
      scope.run(() => {
          vue.onScopeDispose(() => {
              var _a;
              (_a = i.stop) === null || _a === void 0 ? void 0 : _a.call(i);
          });
      });
      return { scope, i };
  }
  let currentURL;
  let translator;
  let settingsInjector;
  const installObserver = new MutationObserver(() => {
      var _a, _b, _c, _d;
      if (currentURL !== location.href) {
          currentURL = location.href;
          // there is a navigation in the page
          /* ensure css is loaded */
          checkCSS();
          /* update i18n element */
          changeLangEl(document.documentElement);
          /* update translator */
          // only if the translator needs to be updated
          if (!((_b = translator === null || translator === void 0 ? void 0 : (_a = translator.i).canKeep) === null || _b === void 0 ? void 0 : _b.call(_a, currentURL))) {
              // unmount previous translator
              translator === null || translator === void 0 ? void 0 : translator.scope.stop();
              translator = undefined;
              // check if the page is a image page
              const url = new URL(location.href);
              // https://www.pixiv.net/(en/)artworks/<id>
              if (url.hostname.endsWith('pixiv.net') && url.pathname.match(/\/artworks\//)) {
                  translator = createScopedInstance(pixiv);
              }
              // https://twitter.com/<user>/status/<id>
              else if (url.hostname.endsWith('twitter.com') && url.pathname.match(/\/status\//)) {
                  translator = createScopedInstance(twitter);
              }
          }
          /* update settings page */
          if (!((_d = settingsInjector === null || settingsInjector === void 0 ? void 0 : (_c = settingsInjector.i).canKeep) === null || _d === void 0 ? void 0 : _d.call(_c, currentURL))) {
              // unmount previous settings injector
              settingsInjector === null || settingsInjector === void 0 ? void 0 : settingsInjector.scope.stop();
              settingsInjector = undefined;
              // check if the page is a settings page
              const url = new URL(location.href);
              // https://www.pixiv.net/setting_user.php
              if (url.hostname.endsWith('pixiv.net') && url.pathname.match(/\/setting_user\.php/)) {
                  settingsInjector = createScopedInstance(pixivSettings);
              }
              // https://twitter.com/settings/<tab>
              if (url.hostname.endsWith('twitter.com') && url.pathname.match(/\/settings\//)) {
                  settingsInjector = createScopedInstance(twitterSettings);
              }
          }
      }
  });
  installObserver.observe(document.body, { childList: true, subtree: true });

})(Vue);