[STEAM] 创意工坊 - 优化
// ==UserScript==
// @name [STEAM] 创意工坊 - 优化
// @name:en [STEAM] Workshop - optimization
// @description 主要功能:1.美化部分界面 2.翻译模组简介 3.翻译评论区 4.备份订阅
// @description:en Major Abilities:1. Beautification partial interface 2.Introduction to the translation model 3. Translation and discussion section
// @namespace Violentmonkey Scripts
// @match *://steamcommunity.com/*
// @license GPLv3
// @version 2.6
// @author Mnaisuka
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect translate.google.com
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js
// ==/UserScript==
(async function () {
'use strict';
await SetLanguage()
await SetTranslAPI();
var Lang = {
"ui": GM_getValue('language', "zh"),//中文 = zh | english = en | 日本語 = jp
"en": {
"页面类型": "page type",
"翻译评论": "Translate comments",
"翻译失败": "Translation failed",
"错误代码": "error code",
"原文与译文并行显示": "The original and translation are displayed in parallel",
"正在翻译": "Being translated",
"网络超时": "network timeout",
"未知错误": "unknown mistake",
"备份全部订阅": "Backup ALL",
"还原备份订阅": "Restore Backup"
},
"jp": {//不再更新中英以外的语言
"页面类型": "ページタイプ",
"翻译评论": "コメントを翻訳",
"翻译失败": "翻訳に失敗しました",
"错误代码": "エラーコード",
"原文与译文并行显示": "原文と訳文が並列で表示されます",
"正在翻译": "翻訳中",
"网络超时": "ネットワークタイムアウト",
"未知错误": "未知の間違い",
}
};
var config = {
bilingual: GM_getValue("bilingual", true)
};
await SetConfigMenu()
const init = {
load: function () {
var link = window.location.href
var rules = this.rule
for (keyNmae in rules) {
var focus = rules[keyNmae]
var result = RegExp(focus[0], "i").exec(link)
if (result != null) {
console.log("页面类型", keyNmae, " | ", "使用函数", focus[1]);
for (let i = 0; i < focus[1].length; i++) {
focus[1][i].call(null, { result: result.slice(1,), rules: focus[0] })
}
}
}
},
rule: {
"browse-items": [".*://steamcommunity.com/workshop/browse/.*&browsesort=.*", [browse_items_translation]],//创意工坊 - 浏览
"filedetails": [".*://steamcommunity.com/.*/filedetails/.*", [transl_thread, transl_tntroduction]],//模组 - 简介
"mysubscriptions": [".*://steamcommunity.com/id/.*/myworkshopfiles/\\?(?=.*appid=\\d+)(?=.*&browsefilter=mysubscriptions)", [back_mysubscriptions]],
"workshop-all": [".*://steamcommunity.com/.*", [TranslSuspensionPrompt]],
"mysubscriptions": [".*://steamcommunity.com/id/.*/(?=.*browsefilter=mysubscriptions)", [TranslMysubscriptions]],
}
};
(function () { init.load() }())
//----------------------------
async function browse_items_translation() {//浏览 - 项目 - 样式\美化
var background_color = "rgb(27 40 56)"
//--------------------------------
var css = {
".workshopBrowseItems": ["grid-template-columns", "1fr"],
".workshopItem": ["margin-bottom", "0px"],
".workshopItem .ugc": ["float", "left"],
".workshopItem .fileRating": ["padding-left", "8px"],
".workshopItem .item_link .workshopItemTitle.ellipsis": ["padding-left", "8px", "max-width", "375px"],
".workshopItem .workshopItemAuthorName.ellipsis": ["padding-left", "8px"]
}
for (key in css) {
for (let i = 0; i < css[key].length; i++) {
$(key).css(css[key][i], css[key][i + 1])
}
}
var item = $(".workshopItem")
for (let index = 0; index < item.length; index++) {
var data = $(".workshopBrowseItems script")[index]
var json = JSON.parse(/({.*})/.exec(data.textContent || data.innerText)[1])
$(item[index].children[item[index].children.length - 1]).remove();
var ele = document.createElement('a')
ele.setAttribute("class", "open-text")
var introduction = document.createElement('div')
ele.appendChild(introduction)
item[index].appendChild(ele)
$(introduction).css("overflow", "hidden").css("padding-left", "8px").css("color", "#8F98A0").css("white-space", "break-spaces")
$(".fileRating").css("margin-top", "0px")
$(item[index]).css("border", "1px solid #000").css("padding", "2px").css("background-color", background_color)
var originalText = json["description"]
introduction.innerHTML = "[" + getLang("正在翻译") + "]\n" + originalText
var result = await translation(htmlDecode(originalText))
var title = $(item[index]).find('.workshopItemTitle.ellipsis')
var title_node = title[0]
var title = title[0].innerHTML
var title_transl = await translation(title)
//title_node.innerHTML = config.bilingual ? title_transl.text + " | " + title : title_transl.text
title_node.innerHTML = title_transl.text + " | " + title
if (result.error == null) {
introduction.innerHTML = result.text
} else {
introduction.innerHTML = result.error + htmlDecode(originalText)
}
}
}
function transl_thread() {//评论区翻译
if ($('.workshop_item_header').length == 0) {
return null //合集
}
var Translate_button = document.createElement('button')
Translate_button.setAttribute("class", "translate_button")
Translate_button.setAttribute("style", "padding: 0px 6px;color: rgb(0, 0, 0);font-size: 12px;line-height: 24px;margin-left: 2px;float: right;")
Translate_button.innerHTML = getLang("翻译评论")
var commentthread_count = $(".commentthread_count")
if (commentthread_count.length == 0) {
return null
}
commentthread_count[0].appendChild(Translate_button)
Translate_button.onclick = async function () {
var node = $(".commentthread_comment_text")
for (let i = 0; i < node.length; i++) {
if (node[i].getAttribute("translate")) {//避免重复翻译
reverse.call(node[i])
continue;
}
var result = await translation(node[i].innerHTML)
if (result.error == null) {
node[i].innerHTML = result.text
node[i].setAttribute("translate_state", 1)
node[i].setAttribute("translate_text", result.text)
node[i].setAttribute("original", result.original.text)
node[i].setAttribute("translate", true)
node[i].onclick = reverse
} else {
node[i].innerHTML = result.error + htmlDecode(node[i].innerHTML)
}
}
function reverse() {
if (this.getAttribute("translate_state") == 1) {
this.setAttribute("translate_state", 0)
this.innerHTML = this.getAttribute("original")
} else {
this.setAttribute("translate_state", 1)
this.innerHTML = this.getAttribute("translate_text")
}
}
}
}
async function transl_tntroduction() {//翻译简介
if ($('.workshop_item_header').length == 0) {
return null //合集
}
var bilingual = config.bilingual
//--------------------------------------
var itemTitle = $('.workshopItemTitle')
var result = await translation(itemTitle.text())
if (!result.skip) {
var sText = bilingual ? result.text + " | " + itemTitle.text() : result.text
$('.game_area_purchase_game h1').contents().each(
function () {
if (this.nodeName.toLowerCase() == "#text") {
this.nodeValue = sText
}
}
)
itemTitle.text(sText)
}
//-----------------------------------------
var merge = null
var node = Array.from($('.workshopItemDescription *,.workshopItemDescription').contents())
for (let i = 0; i < node.length; i++) {
var nodeName = node[i].nodeName.toLowerCase();
if (nodeName == "#text") {
var value = node[i].nodeValue
if (!!$.trim(value).length) {
if (merge == null) {
merge = `@${i}=${value}=@`
} else {
merge += `\n@${i}=${value}=@`
}
}
}
}
var result = await translation(merge || "")
if (!result.skip) {
var match = new RegExp('@(\\d+)=(.*)=@', 'g')
data = match.exec(result.text)
for (; data != null;) {
var focus = node[data[1]]
var sText = bilingual ? data[2] + " | " + focus.nodeValue : data[2]
focus.nodeValue = sText
data = match.exec(result.text)
}
}
}
function back_mysubscriptions() {//备份订阅
$(
back_init
)
function back_init() {
$('.menu_panel:last').append('<div class="rightSectionHolder"><div class="rightDetailsBlock"><span class="btn_grey_steamui btn_medium back"><span>' + getLang('备份全部订阅') + '</span></span></div></div>')
$('.menu_panel:last').append('<div class="rightSectionHolder"><div class="rightDetailsBlock"><span class="btn_grey_steamui btn_medium restore"><span>' + getLang('还原备份订阅') + '</span></span></div></div>')
$('.btn_grey_steamui.btn_medium.back').click(back_click)
$('.btn_grey_steamui.btn_medium.restore').click(restore_click)
async function back_click() {
var size = $('.pagelink:last').text()
var Params = getUrlParams2(window.location.href)
var id = Params['appid']
var numperpage = "&numperpage=" + (Params['numperpage'] || "10")
var url = window.location.origin + window.location.pathname
var m = Toast('共' + size + '页模组')
var save = []
for (let index = 0; index < Math.trunc(size); index++) {
var Curl = url + "?appid=" + id + "&browsefilter=mysubscriptions" + numperpage + "&p=" + (index + 1)
var doc = await new Promise(
function (load) {
GM_xmlhttpRequest({
method: 'GET',
url: Curl,
onload: load
})
}
)
var local_document = stringToDocument(doc.responseText);
var parent = $(local_document).find('.workshopItemPreviewHolder').parent()
for (let index = 0; index < parent.length; index++) {
save.push(getUrlParams2($(parent[index]).attr('href'))['id'])
}
m.innerHTML = getLang("正在获取第") + (index + 1) + "/" + size + "页"
}
m.innerHTML = "模组共有" + save.length + "个"
GM_setValue("back_" + id, save)
setTimeout(function () { m.remove() }, 3000)
}
async function restore_click() {
var Params = getUrlParams2(window.location.href)
var appid = Params['appid']
var back_mods = GM_getValue("back_" + appid, [])
var sessionid = /sessionid=([A-Za-z0-9]*)|$/.exec(document.cookie)[1]
var m = Toast('共' + back_mods.length + '个模组')
if (back_mods.length == 0) {
m.innerHTML = "找不到该游戏的相关备份,游戏ID=" + appid
setTimeout(function () { m.remove() }, 3000)
return null
}
var fail = 0
for (let index = 0; index < back_mods.length; index++) {
var result = await new Promise(
function (load) {
$.ajax({
type: "POST",
url: 'https://steamcommunity.com/sharedfiles/subscribe',
data: "id=" + back_mods[index] + "&appid=" + appid + "&sessionid=" + sessionid,
success: load,
crossDomain: true,
xhrFields: {
withCredentials: true
}
});
}
)
var success = result
success == 2 ? fail++ : "200"
console.log(fail);
m.innerHTML = `ID:${back_mods[index]} - ${index + 1}/${back_mods.length} (${success == 2 ? "失败" : "成功"})`
}
m.innerHTML = `恢复比例${back_mods.length}/${back_mods.length - fail}`
setTimeout(function () { m.remove(); window.location.reload(); }, 3000)
}
}
function getUrlParams2(url) {
let urlStr = url.split('?')[1]
const urlSearchParams = new URLSearchParams(urlStr)
const result = Object.fromEntries(urlSearchParams.entries())
return result
}
}
function TranslSuspensionPrompt() {
var node = $(".hover_box.shadow_content")[0]
if (!node) {
return null
}
var old = { title: "", content: "", };
(function ListenChange() {
let config = {
childList: true,
characterData: true,
subtree: true,
};
const mutationCallback = (mutationsList) => {
for (let mutation of mutationsList) {
let type = mutation.type;
if (type == "childList") {
(async function () {
var title = $('.hoverWorkshopItemTitle')[0].innerHTML
var content = $('.hoverWorkshopItemDesc')[0].innerHTML
if (old.title != title && old.content != content) {
old.title = title; old.content = content
old.time = Date.now();
var oid_time = old.time
TranslUI("正在翻译...", "正在翻译...")
var tl_title = await translation(title)
var tl_content = await translation(content)
if (oid_time == old.time) {
if (!tl_content.skip) {
TranslUI(tl_title.text, tl_content.text)
}
}
}
}())
}
}
};
var observe = new MutationObserver(mutationCallback);
observe.observe(node, config)
}())
function TranslUI(title, content) {
if ($('.suspension.prompt').length == 0) {
var ui = document.createElement('div')
$(ui).attr('class', 'suspension prompt')
$('div.hover_box.shadow_content').append(ui)
} else {
var ui = $('.suspension.prompt')[0]
}
ui.innerHTML = '<div class="content" id="global_hover_content"><div class="hoverWorkshopItemTitle">' + title + '</div><div class="hoverWorkshopItemDesc">' + content + '</div></div>'
return ui
}
}
async function TranslMysubscriptions() {
var title = $('.workshopItemSubscriptionDetails a .workshopItemTitle')
for (let index = 0; index < title.length; index++) {
var originalText = title[index].innerHTML
var Transl = await translation(originalText)
title[index].innerHTML = Transl.text + `<span class='color point donttranslate' style='color:#67c1f5;'> > </span>` + originalText
}
}
//----------------------------
async function translation(text, sl = "auto", tl = Lang.ui, api) {//原文,原文语言,目标语言
if (Lang.ui == "zh") {
if (Does_it_contain_ch(text)) {
return { text: text, status: 200, error: gError('检测到中文 > 跳过翻译', "216,99,72"), original: { text: text, result: {} }, skip: true }
}
}
var focus = api || GM_getValue('Transl', "sougou")
var hash = hashVal(text)
var offline = GM_getValue_local("local_data", hash, null)
if (offline != null) {
if (offline.api == focus) {
//console.log("使用离线数据加载", hash, offline.api, offline.value);
return offline.value
}
}
var consult = {
google: {
"auto": "auto",
"zh": "zh-CN",
"jp": "ja",
"en": "en"
},
sougou: {
"auto": "auto",
"zh": "zh",
"jp": "ja",
"en": "en"
}
}
var translapi = {
sougou: Sougou,
google: Google
}
var value = await translapi[focus](text, consult.sougou[sl], consult.sougou[tl])
if (value.status == 200) {//存储到本地
value.original.result = {}
GM_setValue_local("local_data", hash, { api: focus, time: new Date() * 1, value: value })
}
return value
//----------------------------
async function Google(text, sl, tl) {
var result = await new Promise(
function (end, error) {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://translate.google.com/m?hl=zh-CN&sl=' + sl + '&tl=' + tl + '&ie=UTF-8&prev=_m&q=' + encodeURIComponent(text),
onload: end,
onerror: error
})
}
).catch(
function (error) {
return error //console.log("Google - Error", error);
}
)
if (result.status == 200) {
var local_document = stringToDocument(result.responseText);
var local_translate = $(local_document).find('.result-container');
if (local_translate.length == 0) {
var error = { text: "错误代码", status: -1, translation: text };
} else {
var local_result = htmlDecode(local_translate[0].innerHTML).split('\n')
for (let index = 0; index < local_result.length; index++) {
local_result[index] = $.trim(local_result[index])
}
local_result = local_result.join('\n')
}
if (!error) {
return { text: local_result, status: 200, error: null, original: { text: text, result: result } }
} else {
return { text: error.translation, status: error.status, error: gError(error.text, "247,151,103"), original: { text: text, result: result } }
}
}
else {
return { text: null, status: result.status, error: gError('错误代码', "247,151,103", " > " + (result.status == 0 ? getLang("网络超时") : getLang("未知错误") + result.status)), original: { text: text, result: result } }
}
}
async function Sougou(text, sl, tl) {
var result = await new Promise(
function (end, error) {
GM_xmlhttpRequest({
method: 'GET',
url: `https://fanyi.sogou.com/text?fr=default&keyword=${encodeURIComponent(text)}&transfrom=${sl}&transto=${tl}&model=general&errcode=s10`,
onload: end,
onerror: error
})
}
).catch(
function (error) {
return error //console.log("Sogou - Error", error);
}
)
if (result.status == 200) {
var local_document = stringToDocument(result.responseText);
var trans_result = local_document.querySelector("#trans-result")
if (!trans_result) {
var error = { text: "解析失败", status: -1, translation: text };
m = Toast("搜狗翻译 - 解析失败\n\nPS:已切换为谷歌翻译")
setTimeout(function () { m.remove() }, 15000)
GM_setValue('Transl', "google")
return translation(text, sl, tl, "google")
} else {
var trans_result = trans_result.innerText.split('\n')
for (let index = 0; index < trans_result.length; index++) {
trans_result[index] = $.trim(trans_result[index])
}
trans_result = trans_result.join('\n')
var local_result = trans_result;
}
if (!error) {
return { text: htmlDecode(local_result), status: 200, error: null, original: { text: text, result: result } }
} else {
return { text: error.translation, status: error.status, error: gError(error.text, "247,151,103"), original: { text: text, result: result } }
}
}
else {
return { text: null, status: result.status, error: gError('错误代码', "247,151,103", " = " + result.status), original: { text: text, result: result } }
}
}
//----------------------------
function gError(text, color = '247,151,103', expansion = '') {
return `<span class='color point donttranslate' style='color:rgb(${color});'><br />[${getLang(text) + expansion}]↵<br /><br /></span>`
}
}
function htmlDecode(text) {
var temp = document.createElement("div");
temp.innerHTML = text;
var output = temp.innerText || temp.textContent;
temp = null;
return output;
}
function getLang(value) {
if (Lang["ui"] == "zh") return value;
return Lang[Lang["ui"]][value] || value
}
function stringToDocument(txt) {
try //Internet Explorer
{
xmlDoc = new ActiveXObject("Microsoft.HTMLDOM");
xmlDoc.async = "false";
xmlDoc.loadXML(txt);
return (xmlDoc);
}
catch (e) {
try //Firefox, Mozilla, Opera, etc.
{
parser = new DOMParser();
xmlDoc = parser.parseFromString(txt, "text/html");
return (xmlDoc);
}
catch (e) { alert(e.message) }
}
return (null);
}
async function SetTranslAPI() {
if (GM_getValue('Transl', null) == null) {
var m = Toast("正在检索可用翻译API")
return await new Promise(
(success, error) => {
GM_xmlhttpRequest({
method: 'GET',
url: `https://translate.google.com`,
onload: (res) => {
GM_setValue('Transl', 'google')
m.remove()
m = Toast("标记Google翻译为可用")
setTimeout(function () { m.remove() }, 5000)
success(res)
},
onerror: (res) => {
GM_setValue('Transl', 'sougou')
m.remove()
m = Toast("标记Sougou翻译为可用")
setTimeout(function () { m.remove() }, 5000)
error(res)
}
})
}
)
}
console.log('TranslAPI', GM_getValue('Transl', null));
return null
}
async function SetLanguage() {
if (!GM_getValue('language', null)) {
var language = navigator.language.toLowerCase()
var preset = "zh"
var text = "请确认当前语种是否为中文"
if (language == "ja") {
var preset = "jp"
var text = "現在の言語が日本語かどうかを確認してください"
}
else if (language == "en") {
var preset = "en"
var text = "Please confirm whether the current language is English"
}
if (confirm(text)) {
GM_setValue('language', preset)
} else {
var pr = prompt("日本語を表示する jpを入力してください。\nDisplay English, please enter EN").toLowerCase()
if (pr == "en" || pr == "jp") {
GM_setValue('language', pr)
} else {
confirm('没有指定语言,将以中文显示和翻译')
}
}
}
return null
}
async function SetConfigMenu() {//初始化油猴菜单
var name = {}
name.itme1 = getLang("原文与译文并行显示") + (config.bilingual ? " [√]" : " [×]")
GM_registerMenuCommand(name.itme1, function () {
GM_setValue("bilingual", !config.bilingual)
config.bilingual = GM_getValue("bilingual", true)
uninstallAll()
SetConfigMenu()
})
var size = parseInt(JSON.stringify(GM_getValue("local_data", {})).length / 1024)
if (size > 2048) {
uninstallAll()
var size = parseInt(JSON.stringify(GM_getValue("local_data", {})).length / 1024)
}
name.itme2 = getLang("清理翻译缓存 ") + size + "kb"
GM_registerMenuCommand(name.itme2, function () {
ClearOffline()
uninstallAll()
SetConfigMenu()
})
return null
function uninstallAll() {
for (key in name) {
GM_unregisterMenuCommand(name[key]);
}
}
}
function Does_it_contain_ch(text) {
var size = /[\u4e00-\u9fa5]/.exec(text)
if (size == null) {
return false //未知
} else {
size = /[ぁ-んァ-ヶ]/.exec(text)//匹配片假名
if (size == null) {
return true //中文
} else {
return false //日语
}
}
}
function Toast(msg) {
var m = document.createElement('div');
m.innerHTML = msg;
m.style.cssText = "z-index: 999;font-size: .32rem;color: rgb(255, 255, 255);background-color: rgba(0, 0, 0, 0.6);padding: 10px 15px;margin: 0 0 0 -60px;border-radius: 4px;position: fixed; top: 50%;left: 50%;width: 130px;text-align: center;";
document.body.appendChild(m);
return m
}
function hashVal(string) {
var hash = 0, i, chr;
if (string.length === 0) return hash;
for (i = 0; i < string.length; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
function GM_getValue_local(id, key, defaultValue) {
var data = GM_getValue(id, {});
return data[key] || defaultValue;
}
function GM_setValue_local(id, key, value) {
var data = GM_getValue(id, {});
data[key] = value;
return GM_setValue(id, data);
}
function ClearOffline() {
var value = GM_getValue("local_data", {})
var newDice = {}
var time = new Date() * 1
for (key in value) {
var disparity = parseInt((time - value[key].time) / 1000)
if (disparity < 432000) {//缓存5天
newDice[key] = value[key]
} else {
console.log("移除缓存", value[key]);
}
}
GM_setValue("local_data", newDice)
}
}())