// ==UserScript== // @name Touhou.AI | Manga Translator // @name:zh-CN Touhou.AI | 图片翻译器 // @namespace https://github.com/VoileLabs/imgtrans-userscript // @version 0.7.8 // @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/vue@3.2.31/dist/vue.runtime.global.prod.js,npm/@vueuse/shared@8.2.2/index.iife.min.js,npm/@vueuse/core@8.2.2/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 // @downloadURL none // ==/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. */ /* eslint-disable no-undef */ var GMP { // polyfill functions const GMPFunctionMap = { xmlHttpRequest: typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : undefined, setValue: typeof GM_setValue !== 'undefined' ? GM_setValue : undefined, getValue: typeof GM_getValue !== 'undefined' ? GM_getValue : undefined, deleteValue: typeof GM_deleteValue !== 'undefined' ? GM_deleteValue : undefined, addValueChangeListener: typeof GM_addValueChangeListener !== 'undefined' ? GM_addValueChangeListener : undefined, removeValueChangeListener: typeof GM_removeValueChangeListener !== 'undefined' ? GM_removeValueChangeListener : undefined, getResourceUrl: typeof GM_getResourceURL !== 'undefined' ? GM_getResourceURL : undefined, } const xmlHttpRequest = GM.xmlHttpRequest.bind(GM) || GMPFunctionMap.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] } console.error( `[Touhou.AI | Manga Translator] GM.${prop} isn't supported in your userscript engine and it's required by this script. This may lead to unexpected behavior.` ) }, }) } (function (vue, shared, 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 : ''; }); }; const untt = (state) => { if (typeof state === 'string') return state; else return tt(state); }; 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; } } async function downloadResultBlob(id, listeners = {}) { const { onProgress } = listeners; const res = await GMP.xmlHttpRequest({ method: 'GET', responseType: 'blob', url: `https://touhou.ai/imgtrans/result/${id}/final.png`, onprogress: onProgress ? (e) => { if (e.lengthComputable) { const p = formatProgress(e.loaded, e.total); onProgress(p); } } : undefined, }); return res.response; } 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 = { preserveAspectRatio: "xMidYMid meet", viewBox: "0 0 32 32", width: "1.2em", height: "1.2em" }; 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 = { preserveAspectRatio: "xMidYMid meet", viewBox: "0 0 32 32", width: "1.2em", height: "1.2em" }; 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 = { preserveAspectRatio: "xMidYMid meet", viewBox: "0 0 32 32", width: "1.2em", height: "1.2em" }; 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 = { preserveAspectRatio: "xMidYMid meet", viewBox: "0 0 32 32", width: "1.2em", height: "1.2em" }; 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); const textDetectorOptionsMap = { auto: t('settings.text-detector-options.auto'), ctd: 'CTD', }; const textDetectorOptions = Object.keys(textDetectorOptionsMap); const translatorOptionsMap = { youdao: 'Youdao', baidu: 'Baidu', google: 'Google', deepl: 'DeepL', }; const translatorOptions = Object.keys(translatorOptionsMap); 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, textDetectorOptionsMap, t('settings.text-detector-desc')], [t('settings.translator'), translator$1, translatorOptionsMap, t('settings.translator-desc')], [ t('settings.render-text-orientation'), renderTextOrientation, renderTextDirOptionsMap, 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 }, untt(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'))), ]), ]); } 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() { 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)); const advTextDetector = vue.ref(textDetector.value); const advTextDetectorIndex = vue.computed(() => textDetectorOptions.indexOf(advTextDetector.value)); const advTranslator = vue.ref(translator$1.value); const advTranslatorIndex = vue.computed(() => translatorOptions.indexOf(advTranslator.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.text-detector'), advTextDetector, advTextDetectorIndex, textDetectorOptions, textDetectorOptionsMap, ], [ t('settings.translator'), advTranslator, advTranslatorIndex, translatorOptions, translatorOptionsMap, ], [ 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', {}, untt(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 downloadResultBlob(id, { onProgress(progress) { buttonText.value = t('common.client.download-image-progress', { progress }); }, }).catch((e) => { buttonText.value = t('common.client.download-image-error'); throw e; }); const imageUri = URL.createObjectURL(image); 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 imageObserver = new MutationObserver(shared.useThrottleFn(() => { rescanImages(); refreshTransAll(); }, 200, true, false)); imageObserver.observe(document.body, { childList: true, subtree: true }); rescanImages(); refreshTransAll(); 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 downloadResultBlob(id, { onProgress(progress) { }, }).catch((e) => { translateStatusMap[url] = vue.computed(() => tt(t('common.client.download-image-error'))); throw e; }); const imageUri = URL.createObjectURL(image); 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)); const advTextDetector = vue.ref(textDetector.value); const advTextDetectorIndex = vue.computed(() => textDetectorOptions.indexOf(advTextDetector.value)); const advTranslator = vue.ref(translator$1.value); const advTranslatorIndex = vue.computed(() => translatorOptions.indexOf(advTranslator.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.text-detector'), advTextDetector, advTextDetectorIndex, textDetectorOptions, textDetectorOptionsMap, ], [ t('settings.translator'), advTranslator, advTranslatorIndex, translatorOptions, translatorOptionsMap, ], [ 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', {}, untt(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(shared.useThrottleFn(() => { rescanLayers(); }, 200, true, false)); layersObserver.observe(layers, { childList: true, subtree: true }); rescanLayers(); }; if (layers) onLayersUpdate(); else { initObserver = new MutationObserver(shared.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(shared.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/ if (url.hostname.endsWith('pixiv.net') && url.pathname.match(/\/artworks\//)) { translator = createScopedInstance(pixiv); } // https://twitter.com//status/ 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/ 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(shared.useThrottleFn(onUpdate, 200, true, false)); installObserver.observe(document.body, { childList: true, subtree: true }); } onUpdate(); }); })(Vue, VueUse, wasmJsModule);