/* eslint-disable no-multi-spaces */
// ==UserScript==
// @name Kemono中文翻译按钮
// @namespace Kemono-Translate
// @version 0.6.1
// @description 一键翻译Content内容到中文
// @author PY-DNG
// @license GPL-v3
// @match https://kemono.party/fanbox/user/*/post/*
// @match https://kemono.party/patreon/user/*/post/*
// @match https://kemono.su/fanbox/user/*/post/*
// @match https://kemono.su/patreon/user/*/post/*
// @require https://greasyfork.org/scripts/456034-basic-functions-for-userscripts/code/script.js?version=1226884
// @require https://greasyfork.org/scripts/452362-baidu-translate/code/Baidu%20Translate.js?version=1175971
// @connect baidu.com
// @icon data:image/png;base64,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
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager */
/* global bdTransReady baidu_translate */
(function __MAIN__() {
'use strict';
// Constances
const CONST = {
TextAllLang: {
DEFAULT: 'zh-CN',
'zh-CN': {
Initing: '初始化...',
Translate: '翻译',
Translating: '翻译中...',
ShowOriginal: '显示原文',
ShowTrans: '显示译文'
}
}
};
// Init language
const i18n = !Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT;
CONST.Text = CONST.TextAllLang[i18n];
detectDom('.post__footer', content => detectDom('.post__actions>*:nth-child(2)', fav => main()));
function main() {
const content = $('.post__content');
if (!content) {return false;}
// Content
const container = $('.post__actions');
const button = container.children[1].cloneNode(true);
button.children[0].remove();
const button_text = button.children[0];
button_text.innerText = CONST.Text.Initing;
container.appendChild(button);
transButton(button, button_text, content);
// Comment
const comments = $All('.comment');
for (const comment of comments) {
const header = $(comment, '.comment__header');
const btn = $$CrE({
tagName: 'span',
styles: {
float: 'right',
cursor: 'pointer'
},
props: {
innerText: CONST.Text.Initing
}
});
const content = $(comment, '.comment__body');
header.appendChild(btn);
transButton(btn, btn, content);
}
function transButton(button, button_text, content) {
let instantTrans = false;
$AEL(button, 'click', function() {
instantTrans = true;
}, {once: true});
bdTransReady(function() {
button_text.innerText = CONST.Text.Translate;
const Translator = new NodeTranslator();
let tranlated = false;
let hasTrans = false;
$AEL(button, 'click', switchTrans);
instantTrans && switchTrans();
function switchTrans() {
(tranlated ? Translator.recover : Translator.translate)(content, onTrans);
function onTrans(status) {
const node = this;
if (node === content) {
switch (status) {
case -1: {
button_text.innerText = hasTrans ? CONST.Text.ShowTrans : CONST.Text.Translate;
tranlated = false;
break;
}
case 0: {
button_text.innerText = CONST.Text.Translating;
break;
}
case 1: {
button_text.innerText = CONST.Text.ShowOriginal;
tranlated = true;
hasTrans = true;
break;
}
}
}
}
}
});
}
}
function NodeTranslator() {
const NT = this;
NT.transMap = new Map();
NT.translate = translate;
NT.recover = recover;
// Translate whole node tree
// callback: [all nodes in tree].forEach(node => callback.call(node, status));
// status = {-1: untranslated, 0: translating, 1: translated}
// Returns a boolean repersents whether translate progress can start. (if already translated or during translating, retuns false)
function translate(node, callback) {
// Get node text obj
const text = getTextObj(node);
// Translate
if (text.status !== 0) {
if (node.childNodes.length) {
const AM = new AsyncManager();
for (const childNode of node.childNodes) {
translate(childNode, function(status) {
status === 1 && AM.finish();
callback.apply(this, arguments);
if (text.status === -1 && status === 0) {
text.status = status;
callback.call(node, text.status);
}
}) && AM.add();
}
AM.onfinish = function() {
text.status = 1;
callback.call(node, 1);
}
AM.finishEvent = true;
return true;
} else {
return transTerminalNode(node, callback);
}
} else {
return false;
}
}
// Show original text for whole node tree
// callback: [all nodes in tree].forEach(node => callback.call(node, status));
// status = {-1: untranslated, 0: translating, 1: translated}
// Returns a boolean repersents whether recover progress can start. (if not translated or during translating, retuns false)
function recover(node, callback) {
// Get node text obj
const text = getTextObj(node);
// Recover
if (text.status !== 0) {
if (node.childNodes.length) {
const AM = new AsyncManager();
for (const childNode of node.childNodes) {
recover(childNode, function(status) {
status === -1 && AM.finish();
callback.apply(this, arguments);
}) && AM.add();
}
AM.onfinish = function() {
text.status = -1;
callback.call(node, text.status);
}
AM.finishEvent = true;
return true;
} else {
return recoverTerminalNode(node, callback);
}
} else {
return false;
}
}
// Translate single terminal node
// callback: callback.call(node, status=1);
// status = {-1: untranslated, 0: translating, 1: translated}
// Returns a boolean repersents whether translate progress can start. (if already translated or during translating, retuns false)
function transTerminalNode(node, callback) {
// No need to translate if nothing more than whitespaces
if (!node.nodeValue || !node.nodeValue.trim()) {return false;}
// No translating for links
if (node.nodeValue.trim().match(/^https?:\/\//)) {return false;}
// Get node text obj
const text = getTextObj(node);
// Returns true only when untranslated
if (text.status === -1) {
if (text.hasTrans) {
node.nodeValue = text.trans;
text.status = 1;
// translated, callback
setTimeout(callback.bind(node, text.status), 0);
} else {
baidu_translate({
text: text.ori,
dst: 'zh',
callback: function(result_text) {
text.trans = node.nodeValue = result_text;
text.status = 1;
text.hasTrans = true;
// translated, callback
callback.call(node, text.status);
},
onerror: function(reason) {
DoLog(LogLevel.Error, 'translate error');
}
});
text.status = 0;
// translating, callback
callback.call(node, text.status);
}
return true;
} else {
return false;
}
}
// Show original text of single terminal node
// callback.call(node, status=-1);
// status = {-1: untranslated, 0: translating, 1: translated}
// Returns a boolean repersents whether recover progress can start. (if not translated or during translating, retuns false)
function recoverTerminalNode(node, callback) {
// Get node text obj
const text = getTextObj(node);
// Returns true only when translated
if (text.status === 1) {
node.nodeValue = text.ori;
text.status = -1;
// recovered, callback
setTimeout(callback.bind(node, text.status), 0);
return true;
} else {
return false;
}
}
function getTextObj(node) {
// Init node text obj
!NT.transMap.has(node) && NT.transMap.set(node, {
ori: node.nodeValue,
trans: null,
status: -1,
hasTrans: false
});
// Get node text obj
return NT.transMap.get(node);
}
}
})();