// ==UserScript==
// @name Touhou.AI | Manga Translator
// @name:zh-CN Touhou.AI | 图片翻译器
// @namespace https://github.com/VoileLabs/imgtrans-userscript
// @version 0.7.6
// @description (WIP) Translate images on Pixiv, Twitter. Userscript version of https://touhou.ai/imgtrans/
// @description:zh-CN (WIP) 一键翻译 Pixiv、Twitter 的图片,https://touhou.ai/imgtrans/ 的用户脚本版本。
// @author QiroNT
// @license MIT
// @contributionURL https://ko-fi.com/voilelabs
// @supportURL https://github.com/VoileLabs/imgtrans-userscript/issues
// @source https://github.com/VoileLabs/imgtrans-userscript
// @require https://cdn.jsdelivr.net/combine/npm/[email protected]/dist/vue.runtime.global.prod.js,npm/@vueuse/[email protected]/index.iife.min.js,npm/@vueuse/[email protected]/index.iife.min.js
// @require https://cdn.jsdelivr.net/gh/VoileLabs/imgtrans-userscript@777037c9b1f6b734d21aa4b074d79aa73e6ba352/wasm_bg.js
// @resource wasm https://cdn.jsdelivr.net/gh/VoileLabs/imgtrans-userscript@777037c9b1f6b734d21aa4b074d79aa73e6ba352/wasm_bg.wasm
// @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
// @grant GM.getResourceUrl
// @grant GM_getResourceURL
// @grant window.onurlchange
// @run-at document-end
// ==/UserScript==
/**
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.
*/
var GMP
{
// polyfill functions
const GMPFunctionMap = {
setValue: GM_setValue,
getValue: GM_getValue,
deleteValue: GM_deleteValue,
addValueChangeListener: GM_addValueChangeListener,
removeValueChangeListener: GM_removeValueChangeListener,
getResourceUrl: GM_getResourceURL,
}
const xmlHttpRequest = GM.xmlHttpRequest.bind(GM) || GM_xmlhttpRequest
GMP = new Proxy(GM, {
get(target, prop) {
if (prop === 'xmlHttpRequest') {
return (context) => {
return new Promise((resolve, reject) => {
xmlHttpRequest({
...context,
onload(event) {
context.onload?.()
resolve(event)
},
onerror(event) {
context.onerror?.()
reject(event)
},
})
})
}
}
if (prop in target) {
const v = target[prop]
return typeof v === 'function' ? v.bind(target) : v
}
if (prop in GMPFunctionMap && typeof GMPFunctionMap[prop] === 'function') {
return GMPFunctionMap[prop]
}
},
})
}
(function (vue, wasmJsModule) {
'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var wasmJsModule__default = /*#__PURE__*/_interopDefaultLegacy(wasmJsModule);
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 GMP.getValue(key));
if (rawValue == null) {
data.value = initialValue;
}
else {
data.value = rawValue;
}
}
read();
let listener;
if (GMP.addValueChangeListener)
(async () => {
listener = await GMP.addValueChangeListener(key, (name, oldValue, newValue, remote) => {
if (name === key)
read(newValue);
});
})();
const stopWatch = vue.watch(data, async () => {
if (data.value == null) {
await GMP.deleteValue(key);
}
else {
await GMP.setValue(key, data.value);
}
});
vue.onScopeDispose(() => {
stopWatch();
if (GMP.removeValueChangeListener && listener)
GMP.removeValueChangeListener(listener);
});
return data;
}
const detectionResolution = useGMStorage('detectionResolution', 'M');
const textDetector = useGMStorage('textDetector', 'auto');
const translator$1 = useGMStorage('translator', 'youdao');
const renderTextOrientation = useGMStorage('renderTextOrientation', 'auto');
const targetLang = useGMStorage('targetLang');
const scriptLang = useGMStorage('scriptLanguage');
var data$1 = { common:{ source:{ "download-image":"正在拉取原图",
"download-image-progress":"正在拉取原图({progress})",
"download-image-error":"拉取原图出错" },
client:{ submit:"正在提交翻译",
"submit-progress":"正在提交翻译({progress})",
"submit-error":"提交翻译出错",
"download-image":"正在下载图片",
"download-image-progress":"正在下载图片({progress})",
"download-image-error":"下载图片出错",
hash:"正在哈希图片",
resize:"正在缩放图片" },
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 | 图片翻译器设置",
"inline-options-title":"设置当前翻译",
"detection-resolution":"文本扫描清晰度",
"text-detector":"文本扫描器",
"text-detector-options":{ auto:"默认" },
translator:"翻译服务",
"render-text-orientation":"渲染字体方向",
"render-text-orientation-options":{ auto:"跟随原文本",
horizontal:"仅限水平" },
"target-language":"翻译语言",
"target-language-options":{ auto:"跟随网页语言" },
"script-language":"用户脚本语言",
"script-language-options":{ auto:"跟随网页语言" },
reset:"重置所有设置",
"detection-resolution-desc":"设置检测图片文本所用的清晰度,更高的清晰度会使文本检测时间更长但精准度更高。",
"text-detector-desc":"设置使用的文本扫描器。",
"translator-desc":"设置翻译图片所用的翻译服务。",
"render-text-orientation-desc":"设置嵌字的文本方向。",
"target-language-desc":"设置图片翻译后的语言。",
"script-language-desc":"设置此用户脚本的语言。" },
sponsor:{ text:"制作不易,请考虑赞助我们!" } };
data$1.common;
data$1.settings;
data$1.sponsor;
var data = { common:{ source:{ "download-image":"Downloading original image",
"download-image-progress":"Downloading original image ({progress})",
"download-image-error":"Error during original image download" },
client:{ submit:"Submitting translation",
"submit-progress":"Submitting translation ({progress})",
"submit-error":"Error during translation submission",
"download-image":"Downloading translated image",
"download-image-progress":"Downloading translated image ({progress})",
"download-image-error":"Error during translated image download",
hash:"Hashing image",
resize:"Resizing image" },
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-orientation":"Render text orientation",
"render-text-orientation-options":{ auto:"Follow original orientation",
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" },
"inline-options-title":"Current Settings",
"detection-resolution-desc":"The resolution used to scan texts on an image, higher value will result in a longer processing time with better accuracy.",
"script-language-desc":"Language of this userscript.",
"render-text-orientation-desc":"Overwrite the orientation of texts rendered in the translated image.",
"target-language-desc":"The language that images are translated to.",
"text-detector-desc":"The detector used to scan texts in an image.",
"translator-desc":"The translate service used to translate texts." },
sponsor:{ text:"If you find this script helpful, please consider supporting us!" } };
data.common;
data.settings;
data.sponsor;
// 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';
}
}
let wasm;
let cachegetUint8Memory0 = null;
function getUint8Memory0() {
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
}
let WASM_VECTOR_LEN = 0;
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachegetInt32Memory0 = null;
function getInt32Memory0() {
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachegetInt32Memory0;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
/**
* @param {Uint8Array} rgba
* @param {number} width
* @param {number} height
* @param {number | undefined} hash_size
* @returns {string}
*/
function phash$1(rgba, width, height, hash_size) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
var ptr0 = passArray8ToWasm0(rgba, wasm.__wbindgen_export_0);
var len0 = WASM_VECTOR_LEN;
wasm.phash(retptr, ptr0, len0, width, height, !isLikeNone(hash_size), isLikeNone(hash_size) ? 0 : hash_size);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_export_1(r0, r1);
}
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {Uint8Array} rgba
* @param {number} width
* @param {number} height
* @param {number} new_width
* @param {number} new_height
* @returns {Uint8Array}
*/
function resize$1(rgba, width, height, new_width, new_height) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
var ptr0 = passArray8ToWasm0(rgba, wasm.__wbindgen_export_0);
var len0 = WASM_VECTOR_LEN;
wasm.resize(retptr, ptr0, len0, width, height, new_width, new_height);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_1(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
async function load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
async function init(input) {
if (typeof input === 'undefined') {
input = new URL('wasm_bg.wasm', (document.currentScript && document.currentScript.src || new URL('imgtrans-userscript.user.js', document.baseURI).href));
}
const imports = {};
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
input = fetch(input);
}
const { instance, module } = await load(await input, imports);
wasm = instance.exports;
init.__wbindgen_wasm_module = module;
return wasm;
}
function setWasm(w){wasm=w;}
function formatSize(bytes) {
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0)
return '0B';
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / k ** i).toFixed(2)}${sizes[i]}`;
}
function formatProgress(loaded, total) {
return `${formatSize(loaded)}/${formatSize(total)}`;
}
function phash(image) {
return phash$1(new Uint8Array(image.data), image.width, image.height);
}
function resize(image, width, height) {
const data = resize$1(new Uint8Array(image.data), image.width, image.height, width, height);
return new ImageData(new Uint8ClampedArray(data), width, height);
}
async function resizeToSubmit(blob, suffix) {
const imageData = await blobToImageData(blob);
if (imageData.width <= 4000 && imageData.height <= 4000)
return { blob, suffix };
// resize to less than 4k
const scale = Math.min(4000 / imageData.width, 4000 / imageData.height);
const width = Math.floor(imageData.width * scale);
const height = Math.floor(imageData.height * scale);
const newImageData = resize(imageData, width, height);
const newBlob = await imageDataToBlob(newImageData);
console.log(`resized from ${imageData.width}x${imageData.height}(${formatSize(blob.size)},${suffix}) to ${width}x${height}(${formatSize(newBlob.size)},png)`);
return {
blob: newBlob,
suffix: 'png',
};
}
async function submitTranslate(blob, suffix, listeners = {}, optionsOverwrite) {
var _a, _b;
const { onProgress } = listeners;
const formData = new FormData();
formData.append('file', blob, 'image.' + suffix);
formData.append('size', (_a = optionsOverwrite === null || optionsOverwrite === void 0 ? void 0 : optionsOverwrite.detectionResolution) !== null && _a !== void 0 ? _a : detectionResolution.value);
formData.append('translator', translator$1.value);
formData.append('tgt_lang', targetLang.value || BCP47ToISO639(realLang.value));
formData.append('dir', (_b = optionsOverwrite === null || optionsOverwrite === void 0 ? void 0 : optionsOverwrite.renderTextOrientation) !== null && _b !== void 0 ? _b : renderTextOrientation.value);
formData.append('detector', textDetector.value);
const result = await GMP.xmlHttpRequest({
method: 'POST',
url: 'https://touhou.ai/imgtrans/submit',
// @ts-expect-error FormData is supported
data: formData,
// supported in GM
upload: {
onprogress: onProgress
? (e) => {
if (e.lengthComputable) {
const p = formatProgress(e.loaded, e.total);
onProgress(p);
}
}
: undefined,
},
});
console.log(result.responseText);
const json = JSON.parse(result.responseText);
const id = json.task_id;
return id;
}
async function getTranslateStatus(id) {
const result = await GMP.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);
const w = img.width;
const h = img.height;
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, w, h);
});
}
async function imageDataToBlob(imageData) {
const canvas = document.createElement('canvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
const blob = await new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
}
else {
reject(new Error('Canvas toBlob failed'));
}
}, 'image/png');
});
return blob;
}
const _hoisted_1$3 = {
width: "1.2em",
height: "1.2em",
preserveAspectRatio: "xMidYMid meet",
viewBox: "0 0 32 32"
};
const _hoisted_2$3 = /*#__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$3 = [
_hoisted_2$3
];
function render$3(_ctx, _cache) {
return (vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$3, _hoisted_3$3))
}
var IconCarbonTranslate = { name: 'carbon-translate', render: render$3 };
/* vite-plugin-components disabled */
const _hoisted_1$2 = {
width: "1.2em",
height: "1.2em",
preserveAspectRatio: "xMidYMid meet",
viewBox: "0 0 32 32"
};
const _hoisted_2$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$2 = [
_hoisted_2$2
];
function render$2(_ctx, _cache) {
return (vue.openBlock(), vue.createElementBlock("svg", _hoisted_1$2, _hoisted_3$2))
}
var IconCarbonReset = { name: 'carbon-reset', render: render$2 };
/* vite-plugin-components disabled */
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: "M10 16L20 6l1.4 1.4l-8.6 8.6l8.6 8.6L20 26z"
}, 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 IconCarbonChevronLeft = { name: 'carbon-chevron-left', 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: "M22 16L12 26l-1.4-1.4l8.6-8.6l-8.6-8.6L12 6z"
}, null, -1 /* HOISTED */);
const _hoisted_3 = [
_hoisted_2
];
function render(_ctx, _cache) {
return (vue.openBlock(), vue.createElementBlock("svg", _hoisted_1, _hoisted_3))
}
var IconCarbonChevronRight = { name: 'carbon-chevron-right', render };
/* vite-plugin-components disabled */
const detectResOptionsMap = {
S: '1024px',
M: '1536px',
L: '2048px',
X: '2560px',
};
const detectResOptions = Object.keys(detectResOptionsMap);
const renderTextDirOptionsMap = {
auto: t('settings.render-text-orientation-options.auto'),
horizontal: t('settings.render-text-orientation-options.horizontal'),
};
const renderTextDirOptions = Object.keys(renderTextDirOptionsMap);
function renderSettings(options) {
const { itemOrientation = 'vertical', textStyle = {} } = options !== null && options !== void 0 ? options : {};
return vue.h('div', {
style: {
display: 'flex',
flexDirection: 'column',
gap: '8px',
},
}, [
// Sponsor
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
gap: '4px',
},
}, [
tt(t('sponsor.text')),
vue.h('a', {
href: 'https://ko-fi.com/voilelabs',
target: '_blank',
rel: 'noopener noreferrer',
style: {
color: '#2563EB',
textDecoration: 'underline',
},
}, 'ko-fi'),
vue.h('a', {
href: 'https://patreon.com/voilelabs',
target: '_blank',
rel: 'noopener noreferrer',
style: {
color: '#2563EB',
textDecoration: 'underline',
},
}, 'Patreon'),
vue.h('a', {
href: 'https://afdian.net/@voilelabs',
target: '_blank',
rel: 'noopener noreferrer',
style: {
color: '#2563EB',
textDecoration: 'underline',
},
}, '爱发电'),
]),
// Settings
...[
[
t('settings.detection-resolution'),
detectionResolution,
detectResOptionsMap,
t('settings.detection-resolution-desc'),
],
[
t('settings.text-detector'),
textDetector,
{
auto: tt(t('settings.text-detector-options.auto')),
ctd: 'CTD',
},
t('settings.text-detector-desc'),
],
[
t('settings.translator'),
translator$1,
{
youdao: 'Youdao',
baidu: 'Baidu',
google: 'Google',
deepl: 'DeepL',
},
t('settings.translator-desc'),
],
[
t('settings.render-text-orientation'),
renderTextOrientation,
{
auto: tt(t('settings.render-text-orientation-options.auto')),
horizontal: tt(t('settings.render-text-orientation-options.horizontal')),
},
t('settings.render-text-orientation-desc'),
],
[
t('settings.target-language'),
targetLang,
{
'': 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',
},
t('settings.target-language-desc'),
],
[
t('settings.script-language'),
scriptLang,
{
'': tt(t('settings.script-language-options.auto')),
'zh-CN': '简体中文',
'en-US': 'English',
},
t('settings.script-language-desc'),
],
].map(([title, opt, optMap, desc]) => vue.h('div', {
style: {
...(itemOrientation === 'horizontal'
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}
: {}),
},
}, [
vue.h('div', {
style: {
...textStyle,
},
}, tt(title)),
vue.h('div', {}, [
vue.h('select', {
value: opt.value,
onChange(e) {
opt.value = e.target.value;
},
}, Object.entries(optMap).map(([key, value]) => vue.h('option', { value: key }, value))),
desc
? vue.h('div', {
style: {
fontSize: '13px',
},
}, tt(desc))
: undefined,
]),
])),
// Reset
vue.h('div', [
vue.h('button', {
onClick: vue.withModifiers(() => {
detectionResolution.value = null;
textDetector.value = null;
translator$1.value = null;
renderTextOrientation.value = null;
targetLang.value = null;
scriptLang.value = null;
}, ['stop', 'prevent']),
}, tt(t('settings.reset'))),
]),
]);
}
function createFilterWrapper(filter, fn) {
function wrapper(...args) {
filter(() => fn.apply(this, args), { fn, thisArg: this, args });
}
return wrapper;
}
function throttleFilter(ms, trailing = true, leading = true) {
let lastExec = 0;
let timer;
let preventLeading = !leading;
const clear = () => {
if (timer) {
clearTimeout(timer);
timer = void 0;
}
};
const filter = (invoke) => {
const duration = vue.unref(ms);
const elapsed = Date.now() - lastExec;
clear();
if (duration <= 0) {
lastExec = Date.now();
return invoke();
}
if (elapsed > duration) {
lastExec = Date.now();
if (preventLeading)
preventLeading = false;
else
invoke();
}
if (trailing) {
timer = setTimeout(() => {
lastExec = Date.now();
if (!leading)
preventLeading = true;
clear();
invoke();
}, duration);
}
if (!leading && !timer)
timer = setTimeout(() => preventLeading = true, duration);
};
return filter;
}
function useThrottleFn(fn, ms = 200, trailing = true, leading = true) {
return createFilterWrapper(throttleFilter(ms, trailing, leading), fn);
}
var pixiv = () => {
const images = new Set();
const instances = new Map();
const translatedMap = new Map();
const translateEnabledMap = new Map();
function findImageNodes(node) {
return Array.from(node.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'));
});
}
function rescanImages(added, removed) {
if (added && removed) {
for (const parent of added) {
const nodes = findImageNodes(parent);
for (const node of nodes) {
if (images.has(node))
continue;
try {
instances.set(node, mountToNode(node));
images.add(node);
}
catch (e) {
// ignore
}
}
}
for (const parent of removed) {
const nodes = findImageNodes(parent);
for (const node of nodes) {
if (!instances.has(node))
continue;
const instance = instances.get(node);
instance.stop();
instances.delete(node);
images.delete(node);
}
}
return;
}
const imageNodes = findImageNodes(document.body);
const removedImages = new Set(images);
for (const node of imageNodes) {
removedImages.delete(node);
if (images.has(node))
continue;
// 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))
continue;
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);
const translateMounted = vue.ref(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);
const advancedMenuOpen = vue.ref(false);
const advDetectRes = vue.ref(detectionResolution.value);
const advDetectResIndex = vue.computed(() => detectResOptions.indexOf(advDetectRes.value));
const advRenderTextDir = vue.ref(renderTextOrientation.value);
const advRenderTextDirIndex = vue.computed(() => renderTextDirOptions.indexOf(advRenderTextDir.value));
return () =>
// container
vue.h('div', {
style: {
position: 'absolute',
zIndex: '1',
bottom: '4px',
left: '8px',
},
}, [
vue.h('div', {
style: {
position: 'relative',
},
}, [
vue.h('div', {
style: {
fontSize: '16px',
lineHeight: '16px',
padding: '2px',
paddingLeft: translateMounted.value ? '2px' : '24px',
border: '2px solid #D1D5DB',
borderRadius: '6px',
background: '#fff',
cursor: 'default',
},
}, content.value
? content.value
: !translateMounted.value
? advancedMenuOpen.value
? [
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingBottom: '2px',
},
onClick: vue.withModifiers(() => {
advancedMenuOpen.value = false;
}, ['stop', 'prevent']),
}, [
vue.h('div', {}, tt(t('settings.inline-options-title'))),
vue.h(IconCarbonChevronLeft, {
style: {
verticalAlign: 'middle',
cursor: 'pointer',
},
}),
]),
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'column',
gap: '4px',
},
}, [
[
[
t('settings.detection-resolution'),
advDetectRes,
advDetectResIndex,
detectResOptions,
detectResOptionsMap,
],
[
t('settings.render-text-orientation'),
advRenderTextDir,
advRenderTextDirIndex,
renderTextDirOptions,
Object.fromEntries(Object.entries(renderTextDirOptionsMap).map(([k, v]) => [k, tt(v)])),
],
].map(([title, opt, optIndex, opts, optMap]) => vue.h('div', {}, [
vue.h('div', {
style: {
fontSize: '12px',
},
}, tt(title)),
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
userSelect: 'none',
},
}, [
vue.h(optIndex.value <= 0 ? 'div' : IconCarbonChevronLeft, {
style: {
width: '1.2em',
cursor: 'pointer',
},
onClick: vue.withModifiers(() => {
if (optIndex.value <= 0)
return;
opt.value = opts[optIndex.value - 1];
}, ['stop', 'prevent']),
}),
vue.h('div', {}, optMap[opt.value]),
vue.h(optIndex.value >= opts.length - 1 ? 'div' : IconCarbonChevronRight, {
style: {
width: '1.2em',
cursor: 'pointer',
},
onClick: vue.withModifiers(() => {
if (optIndex.value >= opts.length - 1)
return;
opt.value = opts[optIndex.value + 1];
}, ['stop', 'prevent']),
}),
]),
])),
vue.h('div', {
style: {
width: '100%',
paddingBottom: '1px',
border: '1px solid #A1A1AA',
borderRadius: '2px',
textAlign: 'center',
},
onClick: vue.withModifiers(() => {
if (buttonDisabled)
return;
if (translateMounted.value)
return;
enable({
detectionResolution: advDetectRes.value,
renderTextOrientation: advRenderTextDir.value,
});
advancedMenuOpen.value = false;
}, ['stop', 'prevent']),
}, tt(t('common.control.translate'))),
]),
]
: vue.h(IconCarbonChevronRight, {
style: {
cursor: 'pointer',
},
onClick: vue.withModifiers(() => {
advancedMenuOpen.value = true;
}, ['stop', 'prevent']),
})
: vue.h('div', {
style: {
width: '1px',
height: '16px',
},
})),
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(() => {
if (advancedMenuOpen.value)
return;
toggle();
}, ['stop', 'prevent']),
onContextmenu: vue.withModifiers(() => {
if (translateMounted.value)
advancedMenuOpen.value = false;
else
advancedMenuOpen.value = !advancedMenuOpen.value;
}, ['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(optionsOverwrite) {
if (!optionsOverwrite && 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 GMP.xmlHttpRequest({
method: 'GET',
responseType: 'blob',
url: originalSrc,
headers: { referer: 'https://www.pixiv.net/' },
overrideMimeType: 'text/plain; charset=x-user-defined',
onprogress(e) {
if (e.lengthComputable) {
buttonText.value = t('common.source.download-image-progress', {
progress: formatProgress(e.loaded, e.total),
});
}
},
}).catch((e) => {
buttonText.value = t('common.source.download-image-error');
throw e;
});
originalImage = result.response;
}
buttonText.value = t('common.client.resize');
await vue.nextTick();
const { blob: resizedImage, suffix: resizedSuffix } = await resizeToSubmit(originalImage, originalSrcSuffix);
buttonText.value = t('common.client.hash');
await vue.nextTick();
try {
const imageData = await blobToImageData(resizedImage);
console.log('phash', phash(imageData));
}
catch (e) {
console.warn(e);
}
buttonText.value = t('common.client.submit');
const id = await submitTranslate(resizedImage, resizedSuffix, {
onProgress(progress) {
buttonText.value = t('common.client.submit-progress', { progress });
},
}, optionsOverwrite).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 GMP.xmlHttpRequest({
method: 'GET',
responseType: 'blob',
url: 'https://touhou.ai/imgtrans/result/' + id + '/final.png',
onprogress(e) {
if (e.lengthComputable) {
buttonText.value = t('common.client.download-image-progress', {
progress: formatProgress(e.loaded, e.total),
});
}
},
}).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(optionsOverwrite) {
try {
const translated = await getTranslatedImage(optionsOverwrite);
imageNode.setAttribute('data-trans', src);
imageNode.setAttribute('src', translated);
imageNode.removeAttribute('srcset');
translateMounted.value = true;
buttonTranslated.value = true;
}
catch (e) {
buttonDisabled = false;
translateMounted.value = false;
throw e;
}
}
function disable() {
imageNode.setAttribute('src', src);
if (srcset)
imageNode.setAttribute('srcset', srcset);
imageNode.removeAttribute('data-trans');
translateMounted.value = false;
buttonTranslated.value = false;
}
// called on click
function toggle() {
if (buttonDisabled)
return;
if (!translateMounted.value) {
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.value)
disable();
},
async enable() {
translateEnabledMap.set(originalSrc, true);
return await enable();
},
disable() {
translateEnabledMap.delete(originalSrc);
return disable();
},
isEnabled() {
return translateMounted.value;
},
};
}
// translate all
let removeTransAll;
function refreshTransAll() {
if (document.querySelector('.sc-emr523-2'))
return;
const section = document.querySelector('.sc-181ts2x-0');
if (section) {
if (section.querySelector('[data-transall]'))
return;
const container = document.createElement('div');
section.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();
section.removeChild(container);
};
}
}
const throttledRefreshTransAll = useThrottleFn(refreshTransAll, 200, true, false);
const imageObserver = new MutationObserver((mutations) => {
const added = [];
const removed = [];
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLElement)
added.push(node);
});
mutation.removedNodes.forEach((node) => {
if (node instanceof HTMLElement)
removed.push(node);
});
}
rescanImages(added, removed);
throttledRefreshTransAll();
});
imageObserver.observe(document.body, { childList: true, subtree: true });
rescanImages();
throttledRefreshTransAll();
return {
stop() {
imageObserver.disconnect();
instances.forEach((instance) => instance.stop());
removeTransAll === null || removeTransAll === void 0 ? void 0 : removeTransAll();
},
};
};
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, optionsOverwrite) => {
if (!optionsOverwrite && translatedMap[url])
return translatedMap[url];
translateStatusMap[url] = vue.computed(() => tt(t('common.source.download-image')));
if (!originalImageMap[url]) {
// fetch original image
const result = await GMP.xmlHttpRequest({
method: 'GET',
responseType: 'blob',
url,
headers: { referer: 'https://twitter.com/' },
overrideMimeType: 'text/plain; charset=x-user-defined',
onprogress(e) {
if (e.lengthComputable) {
translateStatusMap[url] = vue.computed(() => tt(t('common.source.download-image-progress', {
progress: formatProgress(e.loaded, e.total),
})));
}
},
}).catch((e) => {
translateStatusMap[url] = vue.computed(() => tt(t('common.source.download-image-error')));
throw e;
});
originalImageMap[url] = result.response;
}
const originalImage = originalImageMap[url];
const originalSrcSuffix = new URL(url).searchParams.get('format') || url.split('.')[1] || 'jpg';
translateStatusMap[url] = vue.computed(() => tt(t('common.client.resize')));
await vue.nextTick();
const { blob: resizedImage, suffix: resizedSuffix } = await resizeToSubmit(originalImage, originalSrcSuffix);
translateStatusMap[url] = vue.computed(() => tt(t('common.client.hash')));
await vue.nextTick();
try {
const imageData = await blobToImageData(resizedImage);
console.log('phash', phash(imageData));
}
catch (e) {
console.warn(e);
}
translateStatusMap[url] = vue.computed(() => tt(t('common.client.submit')));
const id = await submitTranslate(resizedImage, resizedSuffix, {
onProgress(progress) {
translateStatusMap[url] = vue.computed(() => tt(t('common.client.submit-progress', { progress })));
},
}, optionsOverwrite).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 GMP.xmlHttpRequest({
method: 'GET',
responseType: 'blob',
url: 'https://touhou.ai/imgtrans/result/' + id + '/final.png',
onprogress(e) {
if (e.lengthComputable) {
translateStatusMap[url] = vue.computed(() => tt(t('common.client.download-image-progress', {
progress: formatProgress(e.loaded, e.total),
})));
}
},
}).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, optionsOverwrite) => {
await getTranslatedImage(url, optionsOverwrite);
translateEnabledMap[url] = true;
};
const disable = (url) => {
translateEnabledMap[url] = false;
};
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 advancedMenuOpen = vue.ref(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.alignItems = advancedMenuOpen.value ? 'start' : 'center';
});
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 submitTranslateTest = () => {
var _a;
if (!currentImg.value)
return false;
if ((_a = translateStatusMap[currentImg.value]) === null || _a === void 0 ? void 0 : _a.value)
return false;
return true;
};
container.onclick = vue.withModifiers(() => {
// prevent misclick
if (advancedMenuOpen.value)
return;
if (!submitTranslateTest())
return;
if (translateEnabledMap[currentImg.value]) {
disable(currentImg.value);
}
else {
enable(currentImg.value);
}
}, ['stop', 'prevent']);
container.oncontextmenu = vue.withModifiers(() => {
if (currentImg.value && translateEnabledMap[currentImg.value])
advancedMenuOpen.value = false;
else
advancedMenuOpen.value = !advancedMenuOpen.value;
}, ['stop', 'prevent']);
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',
},
});
},
}));
buttonIconApp.mount(buttonIconContainer);
const buttonStatusContainer = document.createElement('div');
container.insertBefore(buttonStatusContainer, container.firstChild);
const buttonStatusApp = vue.createApp(vue.defineComponent({
setup() {
const borderRadius = vue.computed(() => (advancedMenuOpen.value || buttonContent.value ? '4px' : '16px'));
const advDetectRes = vue.ref(detectionResolution.value);
const advDetectResIndex = vue.computed(() => detectResOptions.indexOf(advDetectRes.value));
const advRenderTextDir = vue.ref(renderTextOrientation.value);
const advRenderTextDirIndex = vue.computed(() => renderTextDirOptions.indexOf(advRenderTextDir.value));
vue.watch(currentImg, (n, o) => {
if (n !== o) {
advDetectRes.value = detectionResolution.value;
advRenderTextDir.value = renderTextOrientation.value;
}
});
return () => vue.h('div', {
style: {
marginRight: '-12px',
padding: '2px',
paddingLeft: '4px',
paddingRight: '8px',
color: '#fff',
backgroundColor: backgroundColor.value,
borderRadius: '4px',
borderTopLeftRadius: borderRadius.value,
borderBottomLeftRadius: borderRadius.value,
cursor: 'default',
},
}, buttonContent.value
? vue.h('div', {
style: {
paddingRight: '8px',
},
}, buttonContent.value)
: currentImg.value && !translateEnabledMap[currentImg.value]
? advancedMenuOpen.value
? [
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
paddingRight: '8px',
paddingBottom: '2px',
},
onClick: vue.withModifiers(() => {
advancedMenuOpen.value = false;
}, ['stop', 'prevent']),
}, [
vue.h(IconCarbonChevronRight, {
style: {
verticalAlign: 'middle',
cursor: 'pointer',
},
}),
vue.h('div', {}, tt(t('settings.inline-options-title'))),
]),
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'column',
gap: '4px',
marginLeft: '18px',
},
}, [
[
[
t('settings.detection-resolution'),
advDetectRes,
advDetectResIndex,
detectResOptions,
detectResOptionsMap,
],
[
t('settings.render-text-orientation'),
advRenderTextDir,
advRenderTextDirIndex,
renderTextDirOptions,
Object.fromEntries(Object.entries(renderTextDirOptionsMap).map(([k, v]) => [k, tt(v)])),
],
].map(([title, opt, optIndex, opts, optMap]) => vue.h('div', {}, [
vue.h('div', {
style: {
fontSize: '12px',
},
}, tt(title)),
vue.h('div', {
style: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
userSelect: 'none',
},
}, [
vue.h(optIndex.value <= 0 ? 'div' : IconCarbonChevronLeft, {
style: {
width: '1.2em',
cursor: 'pointer',
},
onClick: vue.withModifiers(() => {
if (optIndex.value <= 0)
return;
opt.value = opts[optIndex.value - 1];
}, ['stop', 'prevent']),
}),
vue.h('div', {}, optMap[opt.value]),
vue.h(optIndex.value >= opts.length - 1 ? 'div' : IconCarbonChevronRight, {
style: {
width: '1.2em',
cursor: 'pointer',
},
onClick: vue.withModifiers(() => {
if (optIndex.value >= opts.length - 1)
return;
opt.value = opts[optIndex.value + 1];
}, ['stop', 'prevent']),
}),
]),
])),
vue.h('div', {
style: {
width: '100%',
paddingBottom: '1px',
border: '1px solid #A1A1AA',
borderRadius: '2px',
textAlign: 'center',
},
onClick: vue.withModifiers(() => {
if (!submitTranslateTest())
return;
if (translateEnabledMap[currentImg.value])
return;
enable(currentImg.value, {
detectionResolution: advDetectRes.value,
renderTextOrientation: advRenderTextDir.value,
});
advancedMenuOpen.value = false;
}, ['stop', 'prevent']),
}, tt(t('common.control.translate'))),
]),
]
: vue.h(IconCarbonChevronLeft, {
style: {
verticalAlign: 'middle',
paddingBottom: '3px',
cursor: 'pointer',
},
onClick: vue.withModifiers(() => {
advancedMenuOpen.value = true;
}, ['stop', 'prevent']),
})
: []);
},
}));
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 = () => {
layersObserver = new MutationObserver(useThrottleFn(() => {
rescanLayers();
}, 200, true, false));
layersObserver.observe(layers, { childList: true, subtree: true });
rescanLayers();
};
if (layers)
onLayersUpdate();
else {
initObserver = new MutationObserver(useThrottleFn(() => {
layers = document.getElementById('layers');
if (layers) {
onLayersUpdate();
initObserver === null || initObserver === void 0 ? void 0 : initObserver.disconnect();
}
}, 200, true, false));
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(useThrottleFn(() => {
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;
}
}
}, 200, true, false));
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 };
}
async function initWasm() {
const uri = await GMP.getResourceUrl('wasm');
try {
if (/^data:.+;base64,/.test(uri)) {
const data = window.atob(uri.split(';base64,', 2)[1]);
const buffer = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
buffer[i] = data.charCodeAt(i);
}
await init(buffer);
}
else {
await init(uri);
}
}
catch (e) {
setWasm(wasmJsModule__default["default"]);
}
}
Promise.allSettled =
Promise.allSettled ||
((promises) => Promise.all(promises.map((p) => p
.then((value) => ({
status: 'fulfilled',
value,
}))
.catch((reason) => ({
status: 'rejected',
reason,
})))));
let currentURL;
let translator;
let settingsInjector;
const onUpdate = () => {
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);
}
}
}
};
Promise.allSettled([initWasm()]).then((results) => {
for (const result of results) {
if (result.status === 'rejected')
console.warn(result.reason);
}
// @ts-expect-error Tampermonkey specific
if (window.onurlchange === null) {
window.addEventListener('urlchange', onUpdate);
}
else {
const installObserver = new MutationObserver(useThrottleFn(onUpdate, 200, true, false));
installObserver.observe(document.body, { childList: true, subtree: true });
}
onUpdate();
});
})(Vue, wasmJsModule);