🏠 返回首頁 

Greasy Fork is available in English.

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