Greasy Fork 支持简体中文。

MCBBS Extender Core

MCBBS模块化优化框架

  1. // ==UserScript==
  2. // @name MCBBS Extender Core
  3. // @namespace https://i.zapic.cc
  4. // @version v2.0.3
  5. // @description MCBBS模块化优化框架
  6. // @author Zapic
  7. // @match https://*.mcbbs.net/*
  8. // @run-at document-body
  9. // ==/UserScript==
  10.  
  11. //Core
  12. const MExt_version = "2.0.3";
  13. const MExt_vercode = "121043";
  14. (() => {
  15. //夹带私货
  16. console.log(" %c Zapic's Homepage %c https://i.zapic.cc ", "color: #ffffff; background: #E91E63; padding:5px;", "background: #000; padding:5px; color:#ffffff");
  17. // jQuery检查
  18. if (typeof jQuery == "undefined") {
  19. console.error("This page does NOT contain JQuery,MCBBS Extender will not work.");
  20. return;
  21. }
  22. //在手机页面主动禁用
  23. if (document.getElementsByTagName('meta').viewport) {
  24. console.log("MCBBS Extender not fully compatible with Moblie page,exit manually");
  25. return;
  26. }
  27. const selfMd = {
  28. "meta": {
  29. "id": "MExt_Core",
  30. "name": "MCBBS Extender Core Loader",
  31. "version": "2.0.3",
  32. "updateInfo":[]
  33. }
  34. }
  35.  
  36. // 初始化配置
  37. let valueList = null;
  38. const configList = [];
  39. const moduleList = {};
  40. // 加载ValueStorage
  41. try {
  42. valueList = JSON.parse(localStorage.getItem("MExt_config"));
  43. if (typeof valueList != "object" || valueList == null) {
  44. valueList = {};
  45. localStorage.setItem("MExt_config", "{}")
  46. }
  47. } catch (ig) {
  48. valueList = {};
  49. localStorage.setItem("MExt_config", "{}")
  50. }
  51. // 导出模块
  52. const exportModule = (...modules) => {
  53. for (let m of modules) {
  54. try {
  55. moduleLoader(m);
  56. dispatchEvent(new CustomEvent("MExtModuleLoaded",{"detail":m.meta}));
  57. } catch (e) {
  58. console.error("Error occurred while try to load a module:\n" + e);
  59. }
  60. }
  61. }
  62. const dlg = (m) => {
  63. console.debug("[MCBBS Extender]" + m);
  64. };
  65. const setValue = (name, val) => {
  66. valueList[name] = val;
  67. localStorage.setItem("MExt_config", JSON.stringify(valueList));
  68. }
  69. const getValue = (name) => {
  70. return valueList[name];
  71. }
  72. const deleteValue = (name) => {
  73. delete valueList[name];
  74. localStorage.setItem("MExt_config", JSON.stringify(valueList));
  75. }
  76. const appendStyle = (style) => {
  77. let s = document.createElement("style");
  78. s.className = "MExtStyle";
  79. s.innerHTML = style;
  80. document.head.appendChild(s);
  81. };
  82. const getRequest = (variable, url = "") => {
  83. let query = url ? /\?(.*)/.exec(url)[1] : window.location.search.substring(1);
  84. let vars = query.split("&");
  85. for (let i = 0; i < vars.length; i++) {
  86. let pair = vars[i].split("=");
  87. if (pair[0] == variable) {
  88. return pair[1];
  89. }
  90. }
  91. return (false);
  92. }
  93. // 模块加载器
  94. const moduleLoader = (module) => {
  95. // 载入配置项
  96. if (typeof module.meta == "undefined" || typeof module.meta.id !== "string") {
  97. throw new Error("Invalid module meta");
  98. }
  99. moduleList[module.meta.id] = module.meta;
  100. if (typeof module.config !== "undefined") {
  101. module.config.forEach((v) => {
  102. if (typeof getValue(v.id) == "undefined") {
  103. setValue(v.id, v.default);
  104. }
  105. let config = v;
  106. v.value = getValue(v.id);
  107. configList.push(config);
  108. });
  109. }
  110. // 判断是否应该运行
  111. if (typeof module.case == "function") {
  112. if (!module.case()) {
  113. return;
  114. }
  115. }
  116. // 加载模块CSS
  117. if (typeof module.style == 'string') {
  118. appendStyle(module.style);
  119. }
  120. // 运行模块Core
  121. if (typeof module.core == "function") {
  122. module.core();
  123. }
  124. }
  125.  
  126. // 对外暴露API
  127. const MExt = {
  128. "exportModule": exportModule,
  129. "jQuery": unsafeWindow.jQuery,
  130. "configList": configList,
  131. "moduleList": moduleList,
  132. "versionName": MExt_version,
  133. "versionCode": MExt_vercode,
  134. "Storage": {
  135. "get": getValue,
  136. "set": setValue,
  137. "delete": deleteValue
  138. },
  139. "Units": {
  140. "appendStyle": appendStyle,
  141. "getRequest": getRequest,
  142. "debugLog": dlg
  143. }
  144. };
  145. unsafeWindow.MExt = MExt;
  146. unsafeWindow.dispatchEvent(new CustomEvent("MExtLoaded",{bubbles: true}));
  147. exportModule(selfMd);
  148. })();
  149.  
  150. // Discuz UI Operate Event Dispatcher
  151. (async ()=>{
  152. await new Promise(_ => { !unsafeWindow.MExt ? unsafeWindow.addEventListener("MExtLoaded", __ => { _(unsafeWindow.MExt) }) : _(unsafeWindow.MExt)});
  153. const removeHandler = (r) => {
  154. switch (r.target.nodeName) {
  155. case "TBODY":
  156. if (typeof r.target.id != "undefined") {
  157. if (r.target.id.lastIndexOf("normalthread_") >= 0) {
  158. r.target.dispatchEvent(new CustomEvent("ThreadPreviewClosed",{bubbles: true}));
  159. }
  160. }
  161. break;
  162. case "DIV":
  163. if (typeof r.target.id != 'undefined' && r.target.id.lastIndexOf("threadPreview_") >= 0) {
  164. if (r.removedNodes[0].nodeName == "SPAN" && r.removedNodes[0].innerText == " 请稍候...") {
  165. r.target.dispatchEvent(new CustomEvent("ThreadPreviewOpened",{bubbles: true}));
  166. }
  167. } else if (r.removedNodes.length >= 3 && r.target.id.lastIndexOf("post_") >= 0) {
  168. if (r.removedNodes[0].nodeName == "A" && r.removedNodes[0].name == "newpost" && r.removedNodes[0].parentNode != null) {
  169. r.target.dispatchEvent(new CustomEvent("ThreadFlushStarted",{bubbles: true}));
  170. }
  171. } else if (r.target.id == "append_parent") {
  172. if (r.removedNodes[0].nodeName == "DIV") {
  173. if (r.removedNodes[0].id == "fwin_rate") {
  174. r.target.dispatchEvent(new CustomEvent("RateWindowClosed",{bubbles: true}));
  175. } else if (r.removedNodes[0].id == "fwin_reply") {
  176. r.target.dispatchEvent(new CustomEvent("ReplyWindowClosed",{bubbles: true}));
  177. } else if (typeof r.removedNodes[0].id != 'undefined' && r.removedNodes[0].id.lastIndexOf("fwin_miscreport") >= 0) {
  178. r.target.dispatchEvent(new CustomEvent("ReportWindowClosed",{bubbles: true}));
  179. }
  180. }
  181. }
  182. break;
  183. }
  184. }
  185. const addHandler = (r) => {
  186. switch (r.target.nodeName) {
  187. case "DIV":
  188. if (typeof r.target.id != "undefined") {
  189. if (r.target.id.lastIndexOf("threadPreview_") >= 0) {
  190. if (r.addedNodes[0].nodeName == "SPAN" && r.addedNodes[0].innerText == " 请稍候...") {
  191. r.target.dispatchEvent(new CustomEvent("ThreadPreviewPreOpen",{bubbles: true}));
  192. }
  193. } else if (r.addedNodes.length >= 3 && r.target.id.lastIndexOf("post_") >= 0) {
  194. if (r.addedNodes[0].nodeName == "A" && r.addedNodes[0].name == "newpost" && r.addedNodes[0].parentNode != null) {
  195. r.target.dispatchEvent(new CustomEvent("ThreadFlushFinished",{bubbles: true}));
  196. }
  197. } else if (r.target.id == "append_parent") {
  198. if (r.addedNodes[0].nodeName == "DIV") {
  199. if (r.addedNodes[0].id == "fwin_rate") {
  200. r.addedNodes[0].dispatchEvent(new CustomEvent("RateWindowPreOpen",{bubbles: true}));
  201. } else if (r.addedNodes[0].id == "fwin_reply") {
  202. r.addedNodes[0].dispatchEvent(new CustomEvent("ReplyWindowPreOpen",{bubbles: true}));
  203. } else if (typeof r.addedNodes[0].id != 'undefined' && r.addedNodes[0].id.lastIndexOf("fwin_miscreport") >= 0) {
  204. r.addedNodes[0].dispatchEvent(new CustomEvent("ReportWindowPreOpen",{bubbles: true}));
  205. }
  206. }
  207. } else if (r.target.id === "") {
  208. if (r.target.parentElement != null && r.target.parentElement == "postlistreply") {
  209. r.target.dispatchEvent(new CustomEvent("NewReplyAppended",{bubbles: true}));
  210. }
  211. }
  212. }
  213. break;
  214. case "A":
  215. if (r.addedNodes[0].nodeName == "#text" && typeof tid == "undefined") {
  216. if (r.addedNodes[0].nodeValue == "正在加载, 请稍后...") {
  217. r.target.dispatchEvent(new CustomEvent("ThreadsListLoadStart",{bubbles: true}));
  218. } else if (r.addedNodes[0].nodeValue == "下一页 »") {
  219. r.target.dispatchEvent(new CustomEvent("ThreadsListLoadFinished",{bubbles: true}));
  220. }
  221. }
  222. break;
  223. case "TD":
  224. if (r.target.id == "fwin_content_rate" && r.addedNodes[0].nodeName == "DIV" && r.addedNodes[0].id == "floatlayout_topicadmin") {
  225. r.target.dispatchEvent(new CustomEvent("RateWindowOpened",{bubbles: true}));
  226. }
  227. if (r.target.id == "fwin_content_reply" && r.addedNodes[0].nodeName == "H3" && r.addedNodes[0].id == "fctrl_reply") {
  228. r.target.dispatchEvent(new CustomEvent("ReplyWindowOpened",{bubbles: true}));
  229. }
  230. if (typeof r.target.id != 'undefined' && r.target.id.lastIndexOf("fwin_content_miscreport") >= 0 && r.addedNodes[0].nodeName == "H3" && r.addedNodes[0].id.lastIndexOf("fctrl_miscreport") >= 0) {
  231. r.target.dispatchEvent(new CustomEvent("ReportWindowOpened",{bubbles: true}));
  232. }
  233. break;
  234. }
  235. }
  236. const mainHandler = (r) => {
  237. if (r.type == "childList") {
  238. if (r.addedNodes.length > 0) {
  239. addHandler(r);
  240. }
  241. if (r.removedNodes.length > 0) {
  242. removeHandler(r);
  243. }
  244. }
  245. }
  246. let O = new MutationObserver((e) => {
  247. for (let record of e) {
  248. mainHandler(record);
  249. }
  250. });
  251. document.addEventListener("DOMContentLoaded",()=>{
  252. O.observe(document.body, { childList: true, subtree: true });
  253. });
  254. // 钩住DiscuzAjax函数,使其触发全局事件
  255. const __ajaxpost = unsafeWindow.ajaxpost;
  256. unsafeWindow.ajaxpost = (formid, showid, waitid, showidclass, submitbtn, recall) => {
  257. let relfunc = () => {
  258. if (typeof recall == 'function') {
  259. recall();
  260. } else {
  261. eval(recall);
  262. }
  263. this.dispatchEvent(new CustomEvent("DiscuzAjaxPostFinished",{bubbles: true}));
  264. }
  265. __ajaxpost(formid, showid, waitid, showidclass, submitbtn, relfunc);
  266. }
  267. const __ajaxget = unsafeWindow.ajaxget;
  268. unsafeWindow.ajaxget = (url, showid, waitid, loading, display, recall) => {
  269. let relfunc = () => {
  270. if (typeof recall == 'function') {
  271. recall();
  272. } else {
  273. eval(recall);
  274. }
  275. this.dispatchEvent(new CustomEvent("DiscuzAjaxGetFinished",{bubbles: true}));
  276. }
  277. __ajaxget(url, showid, waitid, loading, display, relfunc);
  278. }
  279. })();
  280.  
  281. // Config Panel
  282. (async () => {
  283. const MExt = await new Promise(_ => { !unsafeWindow.MExt ? unsafeWindow.addEventListener("MExtLoaded", __ => { _(unsafeWindow.MExt) }) : _(unsafeWindow.MExt)});
  284. const $ = MExt.jQuery;
  285. const Md = {
  286. "meta": {
  287. "id": "MExt_Config",
  288. "name": "MCBBS Extender 设置",
  289. "version": "2.0.3",
  290. "updateInfo": []
  291. },
  292. "style": `.conf_contain {
  293. max-height: 45vh;
  294. overflow-y: auto;
  295. padding-right: 5px;
  296. overflow-x: hidden;
  297. scrollbar-color: rgba(0, 0, 0, 0.17) #f7f7f7;
  298. scrollbar-width: thin;
  299. }
  300.  
  301. .alert_info ::-webkit-scrollbar {
  302. background: #f7f7f7;
  303. height: 7px;
  304. width: 7px
  305. }
  306.  
  307. .alert_info ::-webkit-scrollbar-thumb:hover {
  308. background: rgba(0, 0, 0, 0.35);
  309. }
  310.  
  311. .alert_info ::-webkit-scrollbar-thumb {
  312. background: rgba(0, 0, 0, 0.17);
  313. }
  314.  
  315. .conf_item {
  316. line-height: 1.2;
  317. margin-bottom: 5px;
  318. }
  319.  
  320. .conf_title {
  321. font-weight: 1000;
  322. }
  323.  
  324. .conf_subtitle {
  325. font-size: 10px;
  326. color: rgba(0, 0, 0, 0.5);
  327. padding-right: 40px;
  328. display: block;
  329. }
  330.  
  331. .conf_check {
  332. float: right;
  333. margin-top: -25px;
  334. }
  335.  
  336. .conf_input {
  337. float: right;
  338. width: 30px;
  339. margin-top: -27px;
  340. }
  341.  
  342. .conf_longinput {
  343. width: 100%;
  344. margin-top: 5px;
  345. }
  346.  
  347. .conf_textarea {
  348. width: calc(100% - 4px);
  349. margin-top: 5px;
  350. resize: vertical;
  351. min-height: 50px;
  352. }`
  353. };
  354. MExt.exportModule(Md);
  355. const getRequest = MExt.Units.getRequest;
  356. $(() => {
  357. // 发送警告
  358. if (location.pathname == "/forum.php" && getRequest('mod') == "post" && getRequest('action') == "newthread" && getRequest('fid') == "246") {
  359. const alertWin = document.createElement("div");
  360. alertWin.style = "max-width:430px;position: fixed; left: 20px; top: 80px; z-index: 9999; transform: matrix3d(1, 0, 0, 0.0001, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1.025) translateX(-120%); background: rgba(228, 0, 0, 0.81); color: white; padding: 15px; transition-duration: 0.3s; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.66) 2px 2px 5px 0px;";
  361. alertWin.innerHTML = `<h1 style="font-size: 3em;float: left;margin-right: 12px;font-weight: 500;margin-top: 6px;">警告</h1><span style="font-size: 1.7em;">您正在向反馈与投诉版发表新的帖子</span><br>如果您正在向论坛报告论坛内的Bug,请先关闭此脚本再进行一次复现,以确保Bug不是由MCBBS Extender造成的.`;
  362. document.body.appendChild(alertWin);
  363. setTimeout(() => { alertWin.style.transform = "matrix3d(1, 0, 0, 0.0001, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1.025)"; }, 10);
  364. setTimeout(() => { alertWin.style.transform = "none"; }, 300);
  365. setTimeout(() => { alertWin.style.transform = "translateX(-120%)"; }, 10000);
  366. }
  367. // 设置界面初始化
  368. const btnContainer = document.createElement("li");
  369. const btnMExt = document.createElement("a");
  370. btnMExt.href = "javascript: void(0);";
  371. btnMExt.id = "MExt_config";
  372. btnMExt.innerHTML = "MCBBS Extender 设置";
  373. btnContainer.appendChild(btnMExt);
  374. const target = document.querySelector("#user_info_menu .user_info_menu_btn");
  375. if(target == null) return;
  376. target.appendChild(btnContainer);
  377. btnMExt.addEventListener("click", () => {
  378. let confwinContent = '<style>body{overflow:hidden}.altw{width:700px;max-width:95vh;}.alert_info {background-image: unset;padding-left: 20px;padding-right: 17px;}</style><div class="conf_contain">';
  379. const inputType = {
  380. "check": '',
  381. "num": '',
  382. "text": '',
  383. "textarea": ''
  384. };
  385. MExt.configList.forEach((v) => {
  386. switch (v.type) {
  387. case "check":
  388. inputType.check += '<p class="conf_item"><span class="conf_title">' + v.name + '</span><br><span class="conf_subtitle">' + v.desc + '</span><input class="conf_check" type="checkbox" id="in_' + v.id + '"></input></p>';
  389. break;
  390. case "num":
  391. inputType.num += '<p class="conf_item"><span class="conf_title">' + v.name + '</span><br><span class="conf_subtitle">' + v.desc + '</span><input type="number" class="conf_input" id="in_' + v.id + '"></input></p>';
  392. break;
  393. case "text":
  394. inputType.text += '<p class="conf_item"><span class="conf_title">' + v.name + '</span><br><span class="conf_subtitle">' + v.desc + '</span><input type="text" class="conf_longinput" id="in_' + v.id + '"></input></p>';
  395. break;
  396. case "textarea":
  397. inputType.textarea += '<p class="conf_item"><span class="conf_title">' + v.name + '</span><br><span class="conf_subtitle">' + v.desc + '</span><textarea class="conf_textarea" id="in_' + v.id + '"></textarea></p>';
  398. break;
  399. default:
  400. inputType.check += '<p class="conf_item"><span class="conf_title">' + v.name + '</span><br><span class="conf_subtitle">' + v.desc + '</span><input class="conf_check" type="checkbox" id="in_' + v.id + '"></input></p>';
  401. break;
  402. }
  403. });
  404. confwinContent += inputType.check + inputType.num + inputType.text + inputType.textarea + '</div>';
  405. unsafeWindow.showDialog(
  406. confwinContent,
  407. "confirm",
  408. "MCBBS Extender 设置",
  409. () => {
  410. MExt.configList.forEach((v) => {
  411. let val = '';
  412. if (v.type == "num" || v.type == "text" || v.type == "textarea") {
  413. val = $("#in_" + v.id).val();
  414. } else {
  415. val = $("#in_" + v.id).prop("checked");
  416. }
  417. MExt.ValueStorage.set(v.id, val);
  418. });
  419. setTimeout(() => {
  420. unsafeWindow.showDialog("设置已保存,刷新生效<style>.alert_info{background:url(https://www.mcbbs.net/template/mcbbs/image/right.gif) no-repeat 8px 8px}</style>", "confirm", "", () => { location.reload() }, true, () => { }, "", "刷新", "确定");
  421. });
  422. },
  423. true,
  424. () => { },
  425. "MCBBS Extender " + MExt.versionName + " - <s>世界第二委屈公主殿下</s>"
  426. );
  427. MExt.configList.forEach((v) => {
  428. if (v.type == "num" || v.type == "text" || v.type == "textarea") {
  429. $("#in_" + v.id).val(MExt.ValueStorage.get(v.id));
  430. } else {
  431. $("#in_" + v.id).prop("checked", MExt.ValueStorage.get(v.id));
  432. }
  433. });
  434. });
  435. });
  436. })();
  437.  
  438. // Update Manager
  439. (async () => {
  440. const MExt = await new Promise(_ => { !unsafeWindow.MExt ? unsafeWindow.addEventListener("MExtLoaded", __ => { _(unsafeWindow.MExt) }) : _(unsafeWindow.MExt)});
  441. MExt.exportModule({
  442. "meta": {
  443. "id": "MExt_updateManager",
  444. "name": "MCBBS Extender Update Manager",
  445. "version": "2.0.3",
  446. "updateInfo": []
  447. }
  448. });
  449. if (localStorage.getItem("MExt_UpdateMgr") == null) {
  450. localStorage.setItem("MExt_UpdateMgr", JSON.stringify(MExt.moduleList));
  451. unsafeWindow.showDialog("<b>欢迎使用MCBBS Extender</b>.<br>脚本本身不包含任何功能,请到<a style=\"color: #E91E63\" href=\"https://github.com/Proj-MExt/Modules-Repo\">模块仓库</a>寻找模块.<br>设置按钮已经放进入了您的个人信息菜单里,如需调整设置请在个人信息菜单里查看.", "right", "欢迎", () => {
  452. unsafeWindow.showMenu('user_info');
  453. unsafeWindow.MExt.jQuery("#MExt_config").css("background-color", "#E91E63").css("color", "#fff");
  454. setTimeout(() => {
  455. unsafeWindow.hideMenu('user_info_menu');
  456. unsafeWindow.MExt.jQuery("#MExt_config").css("background-color", "").css("color", "");
  457. }, 3000);
  458. });
  459. return;
  460. }
  461. let updateContent = '';
  462. let source = null;
  463. try {
  464. source = JSON.parse(localStorage.getItem("MExt_UpdateMgr"));
  465. } catch(e){
  466. localStorage.setItem("MExt_UpdateMgr", JSON.stringify(MExt.moduleList));
  467. return;
  468. }finally {
  469. localStorage.setItem("MExt_UpdateMgr", JSON.stringify(MExt.moduleList));
  470. }
  471. const compareVer = (b,a) => {
  472. return [b,a][0] != [b,a].sort()[0];
  473. }
  474. for (let m in MExt.moduleList ){
  475. if(typeof source[m] != "undefined" && typeof MExt.moduleList[m].version != "undefined"){
  476. if(compareVer(MExt.moduleList[m].version,source[m].version)){
  477. if(typeof MExt.moduleList[m].updateInfo !="undefined" && MExt.moduleList[m].updateInfo.length > 0){
  478. updateContent += "<b>" + (typeof MExt.moduleList[m].name == "undefinded" ? MExt.moduleList[m].id : MExt.moduleList[m].name) + "</b> " + source[m].version + " &gt; " + MExt.moduleList[m].version + "<br>";
  479. for(let info of MExt.moduleList[m].updateInfo){
  480. updateContent += info + "<br>"
  481. }
  482. }
  483. }
  484. }
  485. }
  486. if(updateContent == "") return;
  487. unsafeWindow.showDialog("<b>模块已更新</b>" + updateContent, "right");
  488. })();
  489.