vjudge++

为vJudge设置背景,并汉化部分界面

  1. // ==UserScript==
  2. // @name vjudge++
  3. // @namespace vjudge-plus-v2
  4. // @version 1.8.4b12.6
  5. // @description 为vJudge设置背景,并汉化部分界面
  6. // @author axototl (original by Suntra)
  7. // @match https://vjudge.net/*
  8. // @noframes
  9. // @icon https://vjudge.net/favicon.ico
  10. // @license AGPLv3 or later
  11. // @grant GM_addStyle
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_info
  16. // @run-at document-end
  17. // ==/UserScript==
  18.  
  19. 'use strict';
  20.  
  21. // license text: https://www.gnu.org/licenses/agpl-3.0.txt
  22. (() => {
  23. // 在基于 Blink 浏览器上检测是否为正常返回
  24. if (GM_info.platform.browserName == "chrome" && performance.getEntries()[0].responseStatus != 200) return;
  25.  
  26. let config = {};
  27.  
  28. function dbgopt(...txt) {
  29. if (config.debug) console.debug(txt.join(' '));
  30. }
  31.  
  32.  
  33. function reloader() {
  34. alert("设置成功,刷新生效");
  35. if (!navigator.onLine) {
  36. alert("离线状态,无法重加载。\n修改无法即刻生效");
  37. return;
  38. }
  39. location.reload();
  40. }
  41.  
  42.  
  43. // 初始化Getter / Setter
  44. (() => {
  45. const def_props = {
  46. experimental: false,
  47. debug: true,
  48. ads: true,
  49. enable_style: true,
  50. back: "https://cdn.luogu.com.cn/images/bg/fe/Day_And_Night_1.jpg",
  51. col: "#fff",
  52. collink: "#ff4c8c"
  53. }
  54.  
  55. function getVal(key) {
  56. let gg = GM_getValue(key);
  57. if ('' === gg || undefined === gg) {
  58. let def = def_props[key];
  59. GM_setValue(key, def);
  60. gg = def;
  61. }
  62. return gg;
  63. }
  64. for (let prop in def_props) {
  65. Object.defineProperty(config, prop, {
  66. get: () => getVal(prop),
  67. set: val => GM_setValue(prop, val)
  68. });
  69. }
  70. })();
  71.  
  72.  
  73. // 获取环境配置(不得异步处理) -Begin-
  74. (() => {
  75. function reg_command(name, prompts, need_reload = true) {
  76. let flag = true;
  77. GM_registerMenuCommand(prompts[config[name] | 0], () => {
  78. if (flag) {
  79. config[name] = !config[name];
  80. flag = false;
  81. }
  82. if (need_reload) reloader();
  83. })
  84. }
  85. // 设置实验性功能
  86. reg_command("experimental", ["× 点击启用实验性功能(界面汉化等)", "✔ 点击关闭实验性功能"]);
  87. // 设置debug
  88. reg_command("debug", ["已禁用 debug 输出", "已启用debug输出"], false);
  89. reg_command("ads", ["× 屏蔽广告(点击启用)", "✔ 屏蔽广告(点击禁用)"]);
  90. reg_command("enable_style", ["✖ 点击开启美化功能", '✔ 点击关闭美化功能']);
  91. // 禁用美化功能
  92. if (!config.enable_style) return;
  93. // 设置背景
  94. GM_registerMenuCommand("设置背景URL", () => {
  95. let tmp = window.prompt("请输入背景URL", config.back);
  96. if (null === tmp) {
  97. alert("未更改背景图片URL");
  98. return;
  99. }
  100. config.back = tmp;
  101. GM_setValue("background", config.back);
  102. reloader();
  103. });
  104.  
  105. // 设置文字颜色
  106. const tester = /^#([0-9a-f]{3,6})$/i;
  107.  
  108. function getColor(t) {
  109. let tmp;
  110. do {
  111. tmp = window.prompt("请输入颜色的Hexcode\n(比如#b93e3e)\n建议选择背景主色调的反差色", t);
  112. } while (null !== tmp && !tester.test(tmp) && '' !== tmp);
  113. if (null === tmp) tmp = t;
  114. return tmp;
  115. }
  116. GM_registerMenuCommand("设置文字颜色", () => {
  117. config.col = getColor(config.col);
  118. reloader();
  119. });
  120. GM_registerMenuCommand("设置链接背景颜色", () => {
  121. config.collink = getColor(config.collink);
  122. reloader();
  123. });
  124. })();
  125. // 获取环境配置 -End-
  126.  
  127. // 界面美化程序 -Begin-
  128. (async () => {
  129. if (!config.enable_style) return;
  130. document.body.innerHTML = "<div style='height: 60px'></div>" + document.body.innerHTML; // 防止顶栏和页面内容重叠
  131. // User defined style
  132. GM_addStyle("body {background: url(" + config.back + ") no-repeat center top fixed;background-size: 100% 100%;-moz-background-size: 100% 100%;color: " + config.col + ";}" +
  133. "a:focus, a:hover, .active {&:not(.nav-link){color: " + config.collink + " !important;text-decoration: underline;}}");
  134. // Global Style
  135. GM_addStyle(
  136. ".navbar {border-radius:0rem;background-color: rgba(0,0,0,65%) !important;position: fixed;top: 0;left: 0;z-index: 1000;width: 100%;}" +
  137. "scrollbar-width: none" +
  138. ".modal-content {background-color: rgba(255,255,255,90%);}" +
  139. ".form-control {background-color: rgba(255,255,255,50%);}" +
  140. ".tab-content {background-color: rgba(255,255,255,50%);border: 2px solid #eceeef;border-radius: 0.25rem;padding: 20px;}" +
  141. "table {background-color: rgba(255,255,255,70%);border-radius: 0.25rem;color: #000;}"
  142. );
  143. GM_addStyle(".card-block, .card, .list-group-item, .btn-secondary, .page-link, .page-item.disabled .page-link, .dropdown-menu {background-color: rgba(255,255,255,0%) !important;}");
  144. document.querySelector("body > div.body-footer").innerHTML += '<p style="color: #3fb98b">Theme powered by vjudge++ (original <a href="https://greasyfork.org/scripts/448801">vjudge+</a>)</p>';
  145. })();
  146. // 界面美化程序 -End-
  147.  
  148. (async () => {
  149. if (!config.ads) return;
  150. let arr = document.querySelectorAll(".social, #prob-ads, #img-support");
  151. for (let x of arr) x.remove();
  152. })(); // 广告移除
  153.  
  154. // 界面汉化程序 -Begin-
  155. (async () => {
  156. if (!config.experimental) return;
  157. console.warn("未来版本将分离JSON文件,请注意");
  158. const basicTranslateTable = {
  159. "#nav-problem > a": "问题列表",
  160. "#nav-status > a": "提交记录",
  161. "#nav-contest > a": "比赛",
  162. "#nav-workbook > a": "题单",
  163. "#nav-user > a": "用户",
  164. "#nav-group > a": "小组",
  165. "#nav-comment > a": "留言板",
  166. ".login": "登录",
  167. ".register": "注册",
  168. ".logout": "登出",
  169. ".user-dropdown > a:nth-child(1)": "个人主页",
  170. ".update-profile": "更新个人信息",
  171. ".message": "消息"
  172. };
  173.  
  174. const basicDynTransTable = {
  175. ".previous > a": "上一页",
  176. ".next > a": "下一页",
  177. "#filter": "应用过滤器", // 无法工作
  178. "#reset": "重置过滤器", // 无法工作
  179. };
  180. const loginBoxTranslate = {
  181. "#loginModalLabel": "登录",
  182. "#btn-forget-password": "忘记密码",
  183. "#btn-login": "登录",
  184. ".btn[data-dismiss]": "取消"
  185. };
  186.  
  187. const registerBoxTrans = {
  188. "#registerModalLabel": "注册",
  189. "[for=register-username]": "用户名\n(必填)",
  190. "[for=register-password]": "密码\n(必填)",
  191. "[for=register-repeat-password]": "重复密码\n(必填)",
  192. "[for=register-nickname]": "昵称\n(可修改)",
  193. "[for=register-school]": "学校",
  194. "[for=register-email]": "邮箱\n(必填)",
  195. "[for=register-introduction]": "自我介绍",
  196. "[for=register-captcha]": "验证码\n(必填)",
  197. "#btn-register": "注册",
  198. ".btn[data-dismiss]": "取消"
  199. };
  200.  
  201. const updateProfileTrans = {
  202. "#updateModalLabel": "更新个人信息",
  203. "[for=update-username]": "用户名",
  204. "[for=update-orig-password]": "原密码(必填)",
  205. "[for=update-password]": "新密码\n(可选)",
  206. "[for=update-repeat-password]": "重复新密码",
  207. "[for=update-nickname]": "昵称",
  208. "[for=update-school]": "学校",
  209. "[for=update-captcha]": "验证码",
  210. "[for=update-email]": "邮箱",
  211. "[for=update-introduction]": "个人简介",
  212. "#btn-update-profile": "更新",
  213. ".btn[data-dismiss]": "取消"
  214. };
  215.  
  216. function upd_trans(tr, flag = false) {
  217. for (let prop in tr) {
  218. let k = document.querySelector(prop);
  219. if (null != k)
  220. if (flag && k.childNodes.length >= 1) k.childNodes[0].data = tr[prop];
  221. else {
  222. k.innerText = tr[prop];
  223. }
  224. else dbgopt(prop, "is null");
  225. }
  226. }
  227.  
  228. function dynamic_trans(table, triggerDOM = null) {
  229. let ev = "click";
  230. if (null == triggerDOM)
  231. ev = "load", triggerDOM = window;
  232. triggerDOM.addEventListener(ev, () => setTimeout(() => upd_trans(table), 200));
  233. }
  234.  
  235. function reg_box_trans(triggerElem_selector, table) {
  236. let s = document.querySelector(triggerElem_selector);
  237. if (null != s) dynamic_trans(table, s);
  238. else dbgopt(triggerElem_selector, "is null");
  239. }
  240.  
  241. (async () => {
  242. document.querySelector(".navbar-brand").childNodes[2].data = " 首页";
  243. upd_trans(basicTranslateTable);
  244. reg_box_trans(".login", loginBoxTranslate);
  245. reg_box_trans(".register", registerBoxTrans);
  246. reg_box_trans(".update-profile", updateProfileTrans);
  247. dynamic_trans(basicDynTransTable);
  248. })(); //基本汉化
  249.  
  250. // 静态内容汉化
  251. /* -Begin- */
  252. const staticTransTable = {
  253. "/": [{
  254. "#index-intro > div > div > p": "Vritual Judge(以下简称vj)并不是一个真实的在线评测网站(以下简称OJ),\
  255. 而是整合了各大OJ平台的题目形成的虚拟OJ平台。你提交的所有代码都会被发回原平台进行评测。\n\
  256. vj可以让你轻松开展比赛,不再为测试数据发愁\n\n\
  257. 目前我们支持以下平台的题库:"
  258. }, 0],
  259. "/problem": [{
  260. "[data-category=all]": "全部问题",
  261. "[data-category=solved]": "已解决问题",
  262. '[data-category=favorites]': "收藏的问题",
  263. "[data-category=attempted]": "未通过/正在评测的问题"
  264. }, 1],
  265. "/status": [{
  266. "[data-owner=all]": "所有提交",
  267. "[data-owner=mine]": "我的提交",
  268. ".username": "用户名",
  269. ".oj": "测评平台",
  270. ".prob_num": "问题编号",
  271. ".status": "状态",
  272. ".runtime": "运行时长",
  273. ".memory": "运行内存",
  274. ".length": "代码长度",
  275. ".language": "语言",
  276. ".date": "提交时间"
  277. }, 1],
  278. };
  279. (async () => {
  280. for (const path in staticTransTable) {
  281. if (path == location.pathname) {
  282. const tr = staticTransTable[path]
  283. upd_trans(tr[0], tr[1]);
  284. break;
  285. }
  286. }
  287. })();
  288. /* -End- */
  289. })();
  290. // 界面汉化程序 -End-
  291. })();