PttChrome+term.ptt.cc Add-on

new features for PttChrome+term.ptt.cc (show flags features code by osk2/ptt-comment-flag)

  1. // ==UserScript==
  2. // @name PttChrome+term.ptt.cc Add-on
  3. // @license MIT
  4. // @namespace https://gf.qytechs.cn/zh-TW/scripts/372391-pttchrome-add-on-ptt
  5. // @description new features for PttChrome+term.ptt.cc (show flags features code by osk2/ptt-comment-flag)
  6. // @version 1.7.1
  7. // @author avan
  8. // @match https://iamchucky.github.io/PttChrome/*
  9. // @match term.ptt.cc/*
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/tippy.js/2.5.4/tippy.min.js
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.min.js
  13. // @require https://update.gf.qytechs.cn/scripts/480183/1282331/GM_config_sync.js
  14. // @require https://gf.qytechs.cn/scripts/372760-gm-config-lz-string/code/GM_config_lz-string.js?version=634230
  15. // @require https://gf.qytechs.cn/scripts/372675-flags-css/code/Flags-CSS.js?version=632757
  16. // @grant GM_getValue
  17. // @grant GM_setValue
  18. // @grant unsafeWindow
  19. // ==/UserScript==
  20. "use strict";
  21. //===================================
  22. const pageUrl = window.location.href;
  23. const isTerm = pageUrl.match(/term.ptt.cc/);
  24. let configStatus = false, configBlackStatus = false, flagMap = {};
  25. let fields = { // Fields object
  26. 'isAddFloorNum': {
  27. 'label': '是否顯示推文樓層', // Appears next to field
  28. 'type': 'checkbox', // Makes this setting a checkbox input
  29. 'default': true // Default value if user doesn't change it
  30. },
  31. 'isShowFlags': {
  32. 'label': '看板內若有IP(ex.Gossiping),是否依IP顯示國旗', // Appears next to field
  33. 'type': 'checkbox', // Makes this setting a checkbox input
  34. 'default': true // Default value if user doesn't change it
  35. },
  36. 'whenShowFlagsIgnoreSpecificCountrys': {
  37. 'label': '指定國家不顯示 ex.「tw;jp」(ISO 3166-1 alpha-2)', // Appears next to field
  38. 'type': 'text', // Makes this setting a text input
  39. 'size': 35, // Limit length of input (default is 25)
  40. 'default': '' // Default value if user doesn't change it
  41. },
  42. 'isHndleAuthor': {
  43. 'label': '是否合併相同作者連續留言的ID名稱', // Appears next to field
  44. 'type': 'checkbox', // Makes this setting a checkbox input
  45. 'default': false // Default value if user doesn't change it
  46. },
  47. 'isShowDebug': {
  48. 'label': '是否顯示DeBug紀錄', // Appears next to field
  49. 'type': 'checkbox', // Makes this setting a checkbox input
  50. 'default': false // Default value if user doesn't change it
  51. },
  52. };
  53. if (isTerm) {
  54. fields = Object.assign({
  55. 'isAutoLogin': {
  56. 'label': '是否自動登入', // Appears next to field
  57. 'type': 'checkbox', // Makes this setting a checkbox input
  58. 'default': false // Default value if user doesn't change it
  59. },
  60. 'autoUser': {
  61. 'label': '帳號', // Appears next to field
  62. 'type': 'text', // Makes this setting a text input
  63. 'size': 25, // Limit length of input (default is 25)
  64. 'default': '' // Default value if user doesn't change it
  65. },
  66. 'autoPassWord': {
  67. 'label': '密碼', // Appears next to field
  68. 'type': 'password', // Makes this setting a text input
  69. 'size': 25, // Limit length of input (default is 25)
  70. 'default': '' // Default value if user doesn't change it
  71. },
  72. 'isAutoSkipInfo1': {
  73. 'label': '是否自動跳過登入後歡迎畫面', // Appears next to field
  74. 'type': 'checkbox', // Makes this setting a checkbox input
  75. 'default': false // Default value if user doesn't change it
  76. },
  77. 'isAutoToFavorite': {
  78. 'label': '是否自動進入 Favorite 我的最愛', // Appears next to field
  79. 'type': 'checkbox', // Makes this setting a checkbox input
  80. 'default': false // Default value if user doesn't change it
  81. },
  82. 'isEnableDeleteDupLogin': {
  83. 'label': '當被問到是否刪除其他重複登入的連線,回答:', // Appears next to field
  84. 'type': 'select', // Makes this setting a dropdown
  85. 'options': ['N/A', 'Y', 'N'], // Possible choices
  86. 'default': 'N/A' // Default value if user doesn't change it
  87. },
  88. 'Button': {
  89. 'label': '編輯黑名單', // Appears on the button
  90. 'type': 'button', // Makes this setting a button input
  91. 'size': 100, // Control the size of the button (default is 25)
  92. 'click': function() { // Function to call when button is clicked
  93. if (configBlackStatus) gmcBlack.close();
  94. else if (!configBlackStatus) gmcBlack.open();
  95. }
  96. },
  97. 'isHideViewImg': {
  98. 'label': '是否隱藏黑名單圖片預覽', // Appears next to field
  99. 'type': 'checkbox', // Makes this setting a checkbox input
  100. 'default': true // Default value if user doesn't change it
  101. },
  102. 'isHideViewVideo': {
  103. 'label': '是否隱藏黑名單影片預覽', // Appears next to field
  104. 'type': 'checkbox', // Makes this setting a checkbox input
  105. 'default': true // Default value if user doesn't change it
  106. },
  107. "previewPbsTwimg": {
  108. 'label': '是否預覽推特圖片',
  109. 'type': 'checkbox',
  110. 'default': true
  111. },
  112. "previewMeeeimg": {
  113. 'label': '是否預覽Meee圖片',
  114. 'type': 'checkbox',
  115. 'default': true
  116. },
  117. "previewYoutube": {
  118. 'label': '是否預覽Youtube影片',
  119. 'type': 'checkbox',
  120. 'default': true
  121. },
  122. /*
  123. 'isHideAll': {
  124. 'label': '是否隱藏黑名單推文', // Appears next to field
  125. 'type': 'checkbox', // Makes this setting a checkbox input
  126. 'default': false // Default value if user doesn't change it
  127. },
  128. 'whenHideAllShowInfo': {
  129. 'label': '當隱藏黑名單推文顯示提示訊息', // Appears next to field
  130. 'type': 'text', // Makes this setting a text input
  131. 'size': 35, // Limit length of input (default is 25)
  132. 'default': '<本文作者已被列黑名單>' // Default value if user doesn't change it
  133. },
  134. 'whenHideAllShowInfoColor': {
  135. 'label': '上述提示訊息之顏色', // Appears next to field
  136. 'type': 'text', // Makes this setting a text input
  137. 'class':'jscolor',
  138. 'data-jscolor': '{hash:true}',
  139. 'size': 10, // Limit length of input (default is 25)
  140. 'default': '#c0c0c0' // Default value if user doesn't change it
  141. },
  142. 'isReduceHeight': {
  143. 'label': '是否調降黑名單推文高度', // Appears next to field
  144. 'type': 'checkbox', // Makes this setting a checkbox input
  145. 'default': true // Default value if user doesn't change it
  146. },
  147. 'reduceHeight': {
  148. 'label': '設定高度值(單位em)', // Appears next to field
  149. 'type': 'float', // Makes this setting a text input
  150. 'min': 0, // Optional lower range limit
  151. 'max': 10, // Optional upper range limit
  152. 'size': 10, // Limit length of input (default is 25)
  153. 'default': 0.4 // Default value if user doesn't change it
  154. },
  155. 'isReduceOpacity': {
  156. 'label': '是否調降黑名單推文透明值', // Appears next to field
  157. 'type': 'checkbox', // Makes this setting a checkbox input
  158. 'default': false // Default value if user doesn't change it
  159. },
  160. 'reduceOpacity': {
  161. 'label': '設定透明值', // Appears next to field
  162. 'type': 'float', // Makes this setting a text input
  163. 'min': 0, // Optional lower range limit
  164. 'max': 1, // Optional upper range limit
  165. 'size': 10, // Limit length of input (default is 25)
  166. 'default': 0.05 // Default value if user doesn't change it
  167. },
  168. 'isDisableClosePrompt': {
  169. 'label': '是否停用關閉頁面提示', // Appears next to field
  170. 'type': 'checkbox', // Makes this setting a checkbox input
  171. 'default': true // Default value if user doesn't change it
  172. },
  173. */
  174. }, fields);
  175. } else {
  176. fields = Object.assign({
  177. 'isHideAll': {
  178. 'label': '是否隱藏黑名單推文', // Appears next to field
  179. 'type': 'checkbox', // Makes this setting a checkbox input
  180. 'default': false // Default value if user doesn't change it
  181. },
  182. 'whenHideAllShowInfo': {
  183. 'label': '當隱藏黑名單推文顯示提示訊息', // Appears next to field
  184. 'type': 'text', // Makes this setting a text input
  185. 'size': 35, // Limit length of input (default is 25)
  186. 'default': '<本文作者已被列黑名單>' // Default value if user doesn't change it
  187. },
  188. 'whenHideAllShowInfoColor': {
  189. 'label': '上述提示訊息之顏色', // Appears next to field
  190. 'type': 'text', // Makes this setting a text input
  191. 'class':'jscolor',
  192. 'data-jscolor': '{hash:true}',
  193. 'size': 10, // Limit length of input (default is 25)
  194. 'default': '#c0c0c0' // Default value if user doesn't change it
  195. },
  196. 'isHideViewImg': {
  197. 'label': '是否隱藏黑名單圖片預覽', // Appears next to field
  198. 'type': 'checkbox', // Makes this setting a checkbox input
  199. 'default': true // Default value if user doesn't change it
  200. },
  201. 'isHideViewVideo': {
  202. 'label': '是否隱藏黑名單影片預覽', // Appears next to field
  203. 'type': 'checkbox', // Makes this setting a checkbox input
  204. 'default': true // Default value if user doesn't change it
  205. },
  206. 'isReduceHeight': {
  207. 'label': '是否調降黑名單推文高度', // Appears next to field
  208. 'type': 'checkbox', // Makes this setting a checkbox input
  209. 'default': false // Default value if user doesn't change it
  210. },
  211. 'reduceHeight': {
  212. 'label': '設定高度值(單位em)', // Appears next to field
  213. 'type': 'float', // Makes this setting a text input
  214. 'min': 0, // Optional lower range limit
  215. 'max': 10, // Optional upper range limit
  216. 'size': 10, // Limit length of input (default is 25)
  217. 'default': 0.4 // Default value if user doesn't change it
  218. },
  219. 'isReduceOpacity': {
  220. 'label': '是否調降黑名單推文透明值', // Appears next to field
  221. 'type': 'checkbox', // Makes this setting a checkbox input
  222. 'default': false // Default value if user doesn't change it
  223. },
  224. 'reduceOpacity': {
  225. 'label': '設定透明值', // Appears next to field
  226. 'type': 'float', // Makes this setting a text input
  227. 'min': 0, // Optional lower range limit
  228. 'max': 1, // Optional upper range limit
  229. 'size': 10, // Limit length of input (default is 25)
  230. 'default': 0.05 // Default value if user doesn't change it
  231. },
  232. 'isAutoGotoAIDPage': {
  233. 'label': '是否自動跳至AID文章,而非開啟www.ptt.cc網站', // Appears next to field
  234. 'type': 'checkbox', // Makes this setting a checkbox input
  235. 'default': true // Default value if user doesn't change it
  236. },
  237. }, fields);
  238. };
  239.  
  240. // Convert number to it's base-64 representation, and reverse it. https://gist.github.com/meeDamian/5749143
  241. const Number64 = {
  242. _rixits: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
  243. toHash: (n) => {
  244. if(isNaN(Number(n)) || n === null || n === Number.POSITIVE_INFINITY || n < 0 ) throw "The input(" + n + ") is not valid";
  245. n = Math.floor(n);
  246. let result = '';
  247. do result = Number64._rixits.charAt(n%64) + result;
  248. while(n = Math.floor(n/64));
  249. return result; // String
  250. },
  251. toNumber: (h) => {
  252. let result = 0;
  253. for(let i = 0; i < h.length; i++) result = (result*64) + Number64._rixits.indexOf(h.charAt(i));
  254. return result; // Integer
  255. }
  256. }
  257.  
  258. //M.timestamp.A.random{0xfff}
  259. //https://www.ptt.cc/man/C_Chat/DE98/DFF5/DB61/M.1419434423.A.DF0.html
  260. unsafeWindow.AID = {
  261. patternEncode: (name) => {
  262. const pattern = /([\w-]*)\/{0,1}M\.(\d+)\.A\.(\w{3})/g;
  263. const match = pattern.exec(name); //['GroupName/M.1234567890.A.DEF', 'GroupName', '1234567890', 'DEF', index: 23, input:...]
  264. return match;
  265. },
  266. patternDecode: (aid) => {
  267. const pattern = /(?<![\w/'"<>;])#([\w-]{6})([\w-]{2})(?: *\(([\w-]+)\)){0,1}/g;
  268. const match = pattern.exec(aid); //['#19bWBItl (GroupName)', '19bWBI', 'tl', 'GroupName', index: 0, input: '#19bWBItl (GroupName)']
  269. return match;
  270. },
  271. patternDecodeAll: (text) => {
  272. const pattern = /(?<![\w/'"<>;])#([\w-]{6})([\w-]{2})(?: *\(([\w-]+)\)){0,1}/g;
  273. const matchs = [...new Set(text.matchAll(pattern))]//[...text.matchAll(pattern)]; //matchAll compatibility support in Chrome 73
  274. return matchs;
  275. },
  276. encode: (name) => {
  277. const match = unsafeWindow.AID.patternEncode(name);
  278. if (!match || match.length < 4) throw "The input(" + name + ") is not valid";
  279. const hash1 = Number64.toHash(match[2])
  280. const hash2 = Number64.toHash(parseInt("0x" + match[3]))
  281. let result = "#" + hash1 + hash2;
  282. if (match[1].length > 0) result += " (" + match[1] + ")"
  283. return result;
  284. },
  285. decode: (decodeMatch, defaultGroup) => {
  286. if (!decodeMatch || decodeMatch.length < 3) throw "The input is not valid";
  287. const timestamp = Number64.toNumber(decodeMatch[1]);
  288. const random = Number64.toNumber(decodeMatch[2]).toString(16).toUpperCase().padStart(3, '0');
  289. let result = "M." + timestamp + ".A." + random;
  290. if (decodeMatch[3] && decodeMatch[3].length > 0) defaultGroup = decodeMatch[3];
  291. result = "https://www.ptt.cc/bbs/" + defaultGroup + "/" + result + ".html";
  292. return result;
  293. },
  294. goto: (aid) => {
  295. if (isTerm) return
  296. if (unsafeWindow.pttchrome.app.view.bbscore.buf.pageState === 3) unsafeWindow.pttchrome.app.conn.send("")
  297. if (unsafeWindow.pttchrome.app.view.bbscore.buf.pageState === 5) unsafeWindow.pttchrome.app.conn.send("\r")
  298. unsafeWindow.pttchrome.app.buf.cancelPageDownAndResetPrevPageState()
  299.  
  300. const aidArr = aid.split(" ", 2)
  301. if (aidArr.length > 1) {
  302. aid = aidArr[0]
  303. unsafeWindow.pttchrome.app.conn.send("s")
  304. unsafeWindow.pttchrome.app.conn.send(aidArr[1] + "\r\r[1~")
  305. }
  306. unsafeWindow.pttchrome.app.buf.cancelPageDownAndResetPrevPageState()
  307.  
  308. if (unsafeWindow.pttchrome.app.view.bbscore.buf.BBSWin.innerText.includes("【看板列表】")) return
  309.  
  310. unsafeWindow.pttchrome.app.conn.send(aid + "\r\r[1~")
  311. }
  312. }
  313.  
  314. const queryConfigEl = (configSelectors, selectors, callback) => {
  315. let configEl = document.querySelector(configSelectors);
  316. if (!configEl) {
  317. setTimeout(queryConfigEl.bind(null, configSelectors, selectors, callback), 1000);
  318. return;
  319. }
  320. configEl = configEl.contentWindow.document.querySelector(selectors);
  321. if (!configEl) {
  322. setTimeout(queryConfigEl.bind(null, configSelectors, selectors, callback), 1000);
  323. return;
  324. }
  325. callback(configEl);
  326. };
  327.  
  328. const addCssLink = (id, cssStr) => {
  329. let checkEl = document.querySelector(`#${id}`);
  330. if (checkEl) {
  331. checkEl.remove();
  332. }
  333. const cssLinkEl = document.createElement('link');
  334. cssLinkEl.setAttribute('rel', 'stylesheet');
  335. cssLinkEl.setAttribute('id', id);
  336. cssLinkEl.setAttribute('type', 'text/css');
  337. cssLinkEl.setAttribute('href', 'data:text/css;charset=UTF-8,' + encodeURIComponent(cssStr));
  338. document.head.appendChild(cssLinkEl);
  339. };
  340. const gmc = new ConfigLzString({
  341. 'id': 'PttChromeAddOnConfig', // The id used for this instance of GM_config
  342. 'title': 'PttChrome Add-on Settings', // Panel Title
  343. 'fields': fields,
  344. 'events': { // Callback functions object
  345. 'open': function() {
  346. this.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 23em; height: 35em; position: fixed; top: 2.5em; right: 0.5em; z-index: 900;");
  347.  
  348. configStatus = true;
  349. },
  350. 'close': () => { configStatus = false;},
  351. },
  352. 'css': `#PttChromeAddOnConfig * { color: #999 !important;background-color: #111 !important; } body#PttChromeAddOnConfig { background-color: #111}`,
  353. 'src':`https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.0.4/jscolor.js`,
  354. });
  355. const gmcDebug = new ConfigLzString({
  356. 'id': 'PttChromeAddOnConfigDebug', // The id used for this instance of GM_config
  357. 'title': 'PttChrome Add-on DeBugLog', // Panel Title
  358. 'fields': { // Fields object
  359. 'showLog': {
  360. 'label': 'Show log of debug text',
  361. 'type': 'textarea',
  362. 'default': ''
  363. },
  364. },
  365. 'events': { // Callback functions object
  366. 'open': () => {
  367. gmcDebug.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 26em; height: 35em; position: fixed; top: 2.5em; left: 0.5em; z-index: 900;");
  368. },
  369. },
  370. 'css': `#PttChromeAddOnConfigDebug * { color: #999 !important;background-color: #111 !important; } body#PttChromeAddOnConfigDebug { background-color: #111} #PttChromeAddOnConfigDebug_field_showLog { width:26em; height: 24em;}`
  371. });
  372. const addBlackStyle = (blackList) => {
  373. if (blackList && blackList.trim().length === 0) return;
  374. blackList = blackList.replace(/\n$/g, '').replace(/\n\n/g, '\n');
  375.  
  376. let opacityStyle = blackList.replace(/([^\n]+)/g, '.blu_$1').replace(/\n/g, ',');
  377. addCssLink('opacityStyle', `${opacityStyle} {opacity: 0.2;}`);
  378.  
  379. if (gmc.get('isHideViewImg')) {
  380. let imgStyle = blackList.replace(/([^\n]+)/g, '.blu_$1 + div > .easyReadingImg').replace(/\n/g, ',');
  381. addCssLink('imgStyle', `${imgStyle} {display: none;}`);
  382. }
  383. if (gmc.get('isHideViewVideo')) {
  384. let videoStyle = blackList.replace(/([^\n]+)/g, '.blu_$1 + div > .easyReadingVideo').replace(/\n/g, ',');
  385. addCssLink('videoStyle', `${videoStyle} {display: none;}`);
  386. }
  387. }
  388. const gmcBlack = new ConfigLzString({
  389. 'id': 'PttChromeAddOnConfigBlack', // The id used for this instance of GM_config
  390. 'title': 'PttChrome Add-on Black List', // Panel Title
  391. 'fields': { // Fields object
  392. 'blackList': {
  393. 'label': 'Black List',
  394. 'type': 'textarea',
  395. 'default': ''
  396. },
  397. },
  398. 'events': { // Callback functions object
  399. 'init': function() {
  400. addBlackStyle(this.get('blackList'));
  401. },
  402. 'open': function() {
  403. gmcBlack.frame.setAttribute('style', "border: 1px solid #AAA;color: #999;background-color: #111; width: 26em; height: 35em; position: fixed; top: 2.5em; left: 0.5em; z-index: 900;");
  404. configBlackStatus = true;
  405. },
  406. 'save': function() {
  407. addBlackStyle(this.get('blackList'));
  408. },
  409. 'close': function() { configBlackStatus = false;},
  410. },
  411. 'css': `#PttChromeAddOnConfigBlack * { color: #999 !important;background-color: #111 !important; } body#PttChromeAddOnConfigBlack { background-color: #111} #PttChromeAddOnConfigBlack_field_blackList { width:26em; height: 24em;}`
  412. });
  413. const HOST = 'https://osk2.me:9977',
  414. ipValidation = /(\d{1,3}\.){3}\d{1,3}/,
  415. timerArray = [];
  416.  
  417. let timestamp = Math.floor(Date.now() / 1000);
  418. const execInterval = () => {
  419. if (timerArray.length === 0) {
  420. timerArray.push(setInterval(excute, 1000));
  421. }
  422. }
  423. const stopInterval = () => {
  424. while (timerArray.length > 0) {
  425. clearInterval(timerArray .shift());
  426. }
  427. }
  428. let currentNum, currentPage, pageData = {}, currentGroup;
  429. const excute = async () => {
  430. //console.log("do excute");
  431. const css = (elements, styles) => {
  432. elements = elements.length ? elements : [elements];
  433. elements.forEach(element => {
  434. for (var property in styles) {
  435. element.style[property] = styles[property];
  436. }
  437. });
  438. }
  439. const findAll = (elements, selectors) => {
  440. let rtnElements = [];
  441. elements = elements.length ? elements : [elements];
  442. elements.forEach(element => rtnElements.push.apply(rtnElements, element.querySelectorAll(selectors)));
  443. return rtnElements;
  444. }
  445. const innerHTMLAll = (elements) => {
  446. let rtn = "";
  447. elements = elements.length ? elements : [elements];
  448. elements.forEach(element => {element.innerHTML ? rtn += element.innerHTML : ""});
  449. return rtn;
  450. }
  451. const show = (elements, specifiedDisplay = 'block') => {
  452. elements = elements.length ? elements : [elements];
  453. elements.forEach(element => {
  454. if (!element.style) return;
  455. element.style.display = specifiedDisplay;
  456. });
  457. }
  458. const hide = (elements) => {
  459. elements = elements.length ? elements : [elements];
  460. elements.forEach(element => {
  461. if (!element.style) return;
  462. element.style.display = 'none';
  463. });
  464. }
  465. const generateImageHTML = (ip, flag) => {
  466. if (!flag) return;
  467. flag.countryCode = flag.countryCode ? flag.countryCode : "unknown";
  468. const ignoreCountrys = gmc.get('whenShowFlagsIgnoreSpecificCountrys').match(new RegExp(flag.countryCode, 'i'));
  469. if (ignoreCountrys && ignoreCountrys.length > 0) return;
  470. const imageTitile = `${flag.locationName || 'N/A'}<br><a href='https://www.google.com/search?q=${ip}' target='_blank'>${ip}</a>`;
  471.  
  472. return `<div data-flag title="${imageTitile}" class="flag-${flag.countryCode}" style="background-repeat:no-repeat;background-position:left;float:right;height:0.8em;width:0.8em;cursor:pointer !important;"></div>`;
  473. }
  474. const chkBlackSpan = (isListPage) => {
  475. if (isTerm && isListPage) {
  476. let allNode = document.querySelectorAll('span[data-type="bbsline"]');
  477. if (allNode && allNode.length > 0) {
  478. allNode = [].filter.call(allNode, (element, index) => {
  479. if (element.dataset.type === 'bbsline') { //for term.ptt.cc
  480. let user = element.querySelectorAll('span[class^="q7"]')
  481. if (user && user.length > 1 && user[1].innerHTML.length > 10) {
  482. user = user[1].innerHTML.replace(/ +/g, ' ').split(' ');
  483. user = user && user.length > 3 ? user[1].toLowerCase() : "";
  484. user && user.match(/^[^\d][^ ]+$/) ? element.classList.add(`blu_${user}`) : null;
  485. }
  486. user = element.querySelector('span[class^="q15"]')
  487. if (user && user.innerHTML.trim().match(/^[^\d][^ ]+$/)) {
  488. user = user ? user.innerText.trim().toLowerCase() : "";
  489. user ? element.classList.add(`blu_${user}`) : null;
  490. }
  491. }
  492. });
  493. }
  494. }
  495. let blackSpan = document.querySelectorAll('span[style="opacity:0.2"]');
  496. let whenHideAllShowInfoCss = document.querySelector('#whenHideAllShowInfo');
  497. if (blackSpan.length > 0) {
  498. writeDebugLog(`黑名單筆數:${blackSpan.length}`);
  499. if (whenHideAllShowInfoCss) whenHideAllShowInfoCss.remove();
  500. gmc.get('isHideViewImg') && hide(findAll(blackSpan, 'img:not([style*="display: none"])'));
  501. gmc.get('isHideViewVideo') && hide(findAll(blackSpan, '.easyReadingVideo:not([style*="display: none"])'));
  502. if (gmc.get('isHideAll')) {
  503. if (gmc.get('whenHideAllShowInfo').length > 0 || isListPage) {
  504. addCssLink('whenHideAllShowInfo', `
  505. span[type="bbsrow"][style="opacity:0.2"] {opacity:1 !important;visibility: hidden;}
  506. span[type="bbsrow"][style="opacity:0.2"]:before {
  507. visibility: visible;color: ${gmc.get('whenHideAllShowInfoColor')};
  508. content: ' - ${gmc.get('whenHideAllShowInfo')}';
  509. }`);
  510. } else {
  511. hide(blackSpan);
  512. }
  513. } else {
  514. !isListPage && gmc.get('isReduceHeight') && css(blackSpan, {
  515. 'height': gmc.get('reduceHeight') + 'em',
  516. 'font-size': (gmc.get('reduceHeight')/2) + 'em',
  517. 'line-height': gmc.get('reduceHeight') + 'em'
  518. });
  519. gmc.get('isReduceOpacity') && css(blackSpan, {'opacity': gmc.get('reduceOpacity')});
  520. }
  521. }
  522. }
  523. const findPrevious = (element, selectors) => {
  524. if (!element) return;
  525. if (element.dataset.type === 'bbsline') { //for term.ptt.cc
  526. element = element.closest('span[type="bbsrow"]');
  527. element = element.parentElement;
  528. }
  529. element = element.previousElementSibling;
  530. if (!element) return;
  531. let rtnElement = element.querySelectorAll(selectors)
  532. if (rtnElement && rtnElement.length > 0) {
  533. return rtnElement;
  534. } else {
  535. return findPrevious(element, selectors);
  536. }
  537. }
  538. const firstEl = (element) => {
  539. if (!element) return;
  540. if (element.dataset.type === 'bbsline') { //for term.ptt.cc
  541. element = element.closest('span[type="bbsrow"]');
  542. element = element.parentElement;
  543. }
  544. element = element.nextElementSibling;
  545. if (!element) return;
  546. if (!element.textContent.startsWith("※")) {
  547. if (element.querySelector('span[data-type="bbsline"]')) { //for term.ptt.cc
  548. return element.querySelector('span[data-type="bbsline"]');
  549. } else if (element.classList.toString().match(/blu_[^ ]+/)) {
  550. return element;
  551. }
  552. } else {
  553. return firstEl(element);
  554. }
  555. }
  556. const queryPage = (node) => {
  557. let rtnPage;
  558. if (node && node.length > 0) {
  559. rtnPage = node[node.length -1].querySelector('span');
  560. if (!rtnPage) return;
  561. rtnPage = rtnPage.innerText.match(/瀏覽[^\d]+(\d+)\/(\d+)/);
  562. if (rtnPage && rtnPage.length === 3) {
  563. rtnPage = rtnPage[1];
  564. writeDebugLog(`警告:未啟用文章好讀模式,結果會不正確`);
  565. return rtnPage;
  566. }
  567. }
  568. }
  569.  
  570. const adjAllNode = (allNode) => {
  571. let bluName = null;
  572. allNode.some((comment, index) => {
  573. try {
  574. const test = comment.innerHTML.match(/^[ \t]*\d+/);
  575. if (test && test.length > 0) return true;
  576.  
  577. if (gmc.get('isAddFloorNum') && comment.classList && comment.classList.toString().match(/blu_[^ ]+/) && !comment.innerHTML.match(/data-floor/)) {
  578. //確認該行是否為"轉錄者"的節點
  579. if (isHasTranscriber && comment === transcriberNode) {
  580. currentNum = 1;
  581. }
  582. else if (currentNum > 0) {
  583. let upstairs = findPrevious(allNode[index], 'div[data-floor]');
  584. if (upstairs && upstairs.length > 0) {
  585. let upstairsNum = Number(upstairs[0].innerHTML);
  586. if (upstairsNum) {
  587. currentNum = Number(upstairs[0].innerHTML) + 1;
  588. }
  589. } else if (currentPage) { //非好讀模式才有頁數
  590. if (!pageData[currentPage]) pageData[currentPage] = currentNum;
  591. currentNum = pageData[currentPage];
  592. } else {
  593. currentNum = 1;
  594. }
  595. } else if (isHasFirst && comment === firstNode) {
  596. currentNum = 1;
  597. } else if (!isHasFirst) {
  598. currentNum = 1;
  599. }
  600. if (currentNum > 0) {
  601. count.commentCnt++
  602. const divCnt = `<div data-floor style="float:left;margin-left: 2.2%;height: 0em;width: 1.5em;font-size: 0.4em;font-weight:bold;text-align: right;">${currentNum}</div>`;
  603. comment.innerHTML = divCnt + comment.innerHTML.trim();
  604. } else {
  605. const divCnt = `<div data-floor></div>`;
  606. comment.innerHTML = divCnt + comment.innerHTML.trim();
  607. }
  608. } else if ((gmc.get('isAddFloorNum') && comment.classList && !comment.querySelector('.q2') && !comment.classList.toString().match(/blu_[^ ]+/))) {
  609. writeDebugLog(`警告 推文資料格式錯誤:${comment.innerHTML}`);
  610. } else if (comment.innerHTML.match(/data-floor/)) {
  611. count.completed++;
  612. }
  613.  
  614. if (gmc.get("isHndleAuthor") && comment.classList && comment.classList.toString().match(/blu_[^ ]+/) ) {
  615. if (bluName && bluName === comment.classList.toString()) {
  616. const spans = comment.querySelectorAll("span[class]")
  617. if (spans.length >= 3) {
  618. spans[1].textContent = spans[1].textContent.replace(/./g, ' ') //將同一人的第二行的ID置換為空白
  619. spans[2].innerHTML = spans[2].innerHTML.replace(/^:/g, ' ')
  620. }
  621. }
  622. bluName = comment.classList.toString()
  623. }
  624.  
  625. if (!gmc.get('isShowFlags')) return;
  626.  
  627. const ip = comment.innerHTML.match(ipValidation);
  628.  
  629. if (!ip) return;
  630. if (comment.innerHTML.match(/data-flag/)) return;
  631. const imageHTML = generateImageHTML(ip[0], flagMap[ip[0]]);
  632. if (!imageHTML) return;
  633.  
  634. const authorNode = comment.querySelector("span.q2");
  635. if (authorNode) {
  636. count.authorIp++;
  637. authorNode.innerHTML = imageHTML + authorNode.innerHTML.trim()
  638. } else {
  639. count.commentIp++;
  640. comment.innerHTML = imageHTML + comment.innerHTML.trim();
  641. }
  642. timestamp = Math.floor(Date.now() / 1000);
  643. } catch (e) {
  644. console.log("AdjAllNode failed, err:" + e);
  645. }
  646. });
  647. if (document.querySelectorAll("[data-flag]").length >= allNode.length) {
  648. const instance = tippy('[data-flag]', {
  649. arrow: true,
  650. size: 'large',
  651. placement: 'left',
  652. interactive: true
  653. });
  654. }
  655. }
  656. const currentTS = Math.floor(Date.now() / 1000);
  657.  
  658. if ((currentTS - timestamp) > 2) stopInterval();
  659.  
  660. const checkNodes = document.querySelectorAll('span.q2');
  661. let invalid = true;
  662. if (checkNodes.length > 0) {
  663. for (let spanQ2 of document.querySelectorAll('span.q2')) {
  664. if (invalid && spanQ2.innerHTML.length > 10) {
  665. invalid = false;
  666. break;
  667. }
  668. }
  669. }
  670. if (!invalid) chkBlackSpan();
  671. else {
  672. chkBlackSpan(true);
  673. const spanBBSrowSrow0 = document.querySelector('span[type="bbsrow"][srow="0"]');
  674. if (spanBBSrowSrow0) {
  675. const match = / +看板《([\w-]+)》/g.exec(spanBBSrowSrow0.textContent);
  676. if (match && match.length > 1 && match[1].length > 0) currentGroup = match[1];
  677. }
  678. return;
  679. }
  680.  
  681. let currentURL, firstNode, isHasFirst, transcriberNode, isHasTranscriber, author, signGroup, allNode = document.querySelectorAll('span[type="bbsrow"]'), bbsline = document.querySelectorAll('span[data-type="bbsline"]');
  682. bbsline && bbsline.length > 0 ? allNode = bbsline : null; //for term.ptt.cc
  683.  
  684. currentPage = queryPage(allNode);
  685.  
  686. let count = {author:0, comment:0, authorCnt:0, commentCnt:0, authorIp:0, commentIp:0, completed: 0};
  687. allNode = [].filter.call(allNode, (element, index) => {
  688. if (element.dataset.type === 'bbsline') { //for term.ptt.cc
  689. let user = element.querySelector('span[class^="q11"]'); //ex.1.<span class="q11 b0">USERNAME</span> 2.<span class="q11 b0">USERNAME </span>
  690. let name = user ? user.innerHTML.match(/^([^ ]+)[ ]*$/) : "";
  691. name && name.length > 0 ? element.classList.add(`blu_${name[1]}`) : null;
  692. }
  693. if (index === 0) { //從文章第一行中取得作者(author)與看板(signGroup)
  694. let articleInfos = element.textContent.match("作者 +(.*) +看板 +(.*) +")
  695. if (articleInfos && articleInfos.length > 2) {
  696. author = articleInfos[1].trim()
  697. signGroup = articleInfos[2].trim()
  698. }
  699. }
  700.  
  701. const textLink = element.querySelector("a[href]:not([previewProcessed])")
  702. let previewElements = "";
  703. if (isTerm) {
  704. if (gmc.get("previewPbsTwimg")) {
  705. let imgUrls = element.textContent.match(/https{0,1}:\/\/pbs.twimg.com\/media\/[^ ]+/g);
  706. if (imgUrls && imgUrls.length) {
  707. for (let url of imgUrls) {
  708. if (url) previewElements += `<img class="easyReadingImg hyperLinkPreview" src="${url}">`
  709. }
  710. }
  711. }
  712. if (gmc.get("previewMeeeimg")) {
  713. let imgUrls = element.textContent.match(/https{0,1}:\/\/[^ ]*meee\.com\.tw\/[^ ]+/g);
  714. if (imgUrls && imgUrls.length) {
  715. for (let url of imgUrls) {
  716. if (url) {
  717. const url_ = new URL(url)
  718. if (!url_.pathname.includes(".")) url += ".jpg"
  719. previewElements += `<img class="easyReadingImg hyperLinkPreview" src="${url}">`
  720. }
  721. }
  722. }
  723. }
  724. if (gmc.get("previewYoutube")) {
  725. let youtubeUrls = element.textContent.match(/https{0,1}:\/\/[^ ]*youtu\.{0,1}be[^ ]*\/[^ ]*/g)
  726. if (youtubeUrls && youtubeUrls.length) {
  727. for (let url of youtubeUrls) {
  728. let VIDEO_ID = ""
  729. const url_ = new URL(url)
  730. const urlParams = url_.searchParams
  731. if (urlParams.get("v")) VIDEO_ID = urlParams.get("v")
  732. else if (url_.pathname) VIDEO_ID = url_.pathname.replace(/^\//,"")
  733. if (VIDEO_ID) previewElements += `<iframe width="50%" height="400px" class="easyReadingVideo hyperLinkPreview" src="https://www.youtube.com/embed/${VIDEO_ID}" frameborder="0" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>`
  734. }
  735. }
  736. }
  737. }
  738. else {
  739. let imgUrls = element.textContent.match(/https{0,1}:\/\/pbs.twimg.com\/media\/[^ ]+format=[^ ]+/g);
  740. if (imgUrls && imgUrls.length) {
  741. for (let url of imgUrls) {
  742. if (url) previewElements += `<img class="easyReadingImg hyperLinkPreview" src="${url}">`
  743. }
  744. }
  745. }
  746. if (textLink && previewElements !== "") {
  747. textLink.setAttribute("previewProcessed","")
  748. element.outerHTML += previewElements
  749. }
  750.  
  751. let node = element.innerHTML.match('※ 文章網址:');
  752. if (node && node.length > 0) {
  753. isHasFirst = true;
  754. const currentLink = element.querySelector("a");
  755. if (currentLink) {
  756. currentURL = currentLink.href;
  757. const match = unsafeWindow.AID.patternEncode(currentURL);
  758. if (match && match.length > 1 && match[1].length > 0) currentGroup = match[1];
  759. }
  760. firstNode = firstEl(element);
  761. if (firstNode && !firstNode.innerHTML.match(/data-floor/)) {
  762. pageData = [];
  763. currentNum = -1;
  764. }
  765. }
  766. //Verify transcriber
  767. let transcriberElement = element.innerHTML.match('※ 轉錄者:');
  768. if (transcriberElement && transcriberElement.length > 0) {
  769. isHasTranscriber= true;
  770. transcriberNode = firstEl(element);
  771. }
  772.  
  773. const elementSpan = isTerm ? element.querySelectorAll("span span[class]") : null
  774. const matchs = unsafeWindow.AID.patternDecodeAll(elementSpan && elementSpan.length === 1 ? elementSpan[0].innerHTML : element.innerHTML);
  775. if (matchs && matchs.length > 0) {
  776. matchs.forEach((match) => {
  777. if (match && match.length > 3) {
  778. let name = "";
  779. const subMatch = /本文轉錄自 +([\w-]+) +看板/g.exec(match.input);
  780. if (subMatch && subMatch.length > 1 && subMatch[1].length > 0) {
  781. if (!isHasTranscriber) name = unsafeWindow.AID.decode(match, subMatch[1]);
  782. else name = unsafeWindow.AID.decode(match, signGroup);
  783. } else {
  784. if (!isHasTranscriber) name = unsafeWindow.AID.decode(match, currentGroup);
  785. else name = unsafeWindow.AID.decode(match, signGroup);
  786. }
  787. if (!isTerm && gmc.get("isAutoGotoAIDPage")) element.innerHTML = element.innerHTML.replace(match[0], '<a href="' + name + '" target="_blank" onclick=\'AID.goto("' + match[0] + '");return false;\'>' + match[0] + '</a>')
  788. else element.innerHTML = element.innerHTML.replace(match[0], '<a href="' + name + '" target="_blank">' + match[0] + '</a>');
  789. }
  790. });
  791. }
  792.  
  793. if (innerHTMLAll(findAll(element, "span.q2")).match(ipValidation)) {
  794. count.author++;
  795. return true;
  796. }
  797. if (element.classList && element.classList.toString().match(/blu_[^ ]+/)) {
  798. count.comment++;
  799. return true;
  800. }
  801. });
  802. writeDebugLog(`偵測 作者筆數:${count.author}、留言筆數:${count.comment}`);
  803. adjAllNode(allNode);
  804. if (count.comment !== count.completed) {
  805. writeDebugLog(`寫入 作者IP數:${count.authorIp}、留言樓層:${count.commentCnt}、留言IP數:${count.commentIp}`);
  806. }
  807. let allIpList = allNode.map(c => {
  808. const ip = c.innerHTML.match(ipValidation);
  809. if (ip && !flagMap[ip[0]]) return ip[0];
  810. });
  811. allIpList = new Set(allIpList);
  812. allIpList.delete(undefined);
  813. allIpList.delete(null);
  814. allIpList = Array.from(allIpList);
  815. if (allIpList && allIpList.length > 0 && allIpList[0]) {
  816. try {
  817. const flagsResponse = await axios.post(`${HOST}/ip`, { ip: allIpList}, {headers: {'Content-Type': 'application/json',}}),
  818. flags = flagsResponse.data;
  819. if (flags && flags.length > 0) {
  820. flags.forEach((flag, index) => {
  821. const ip = allIpList[index];
  822. if (!flag) {
  823. flag = [];
  824. } else if (flag.imagePath) {
  825. flag.countryCode = flag.imagePath.toLowerCase().replace('assets/','').replace('.png','');;
  826. }
  827. flag.ip = ip;
  828. flagMap[ip] = flag;
  829. });
  830. adjAllNode(allNode);
  831. }
  832. } catch (ex) {
  833. writeDebugLog(`查詢IP失敗...${ex}`);
  834. console.log(`查詢IP失敗...${ex}`);
  835. }
  836. }
  837. }
  838.  
  839. const chkBeforeunloadEvents = () => {
  840. if (gmc.get('isDisableClosePrompt')) {
  841. window.addEventListener("beforeunload", function f() {
  842. window.removeEventListener("beforeunload", f, true);
  843. }, true);
  844. unsafeWindow.addEventListener("beforeunload", function beforeunload() {
  845. unsafeWindow.removeEventListener("beforeunload", beforeunload, true);
  846. }, true);
  847. if (window.getEventListeners) {
  848. window.getEventListeners(window).beforeunload.forEach((e) => {
  849. window.removeEventListener('beforeunload', e.listener, true);
  850. })
  851. } else if (unsafeWindow.getEventListeners) {
  852. unsafeWindow.getEventListeners(unsafeWindow).beforeunload.forEach((e) => {
  853. unsafeWindow.removeEventListener('beforeunload', e.listener, true);
  854. })
  855. } else {
  856. setTimeout(chkBeforeunloadEvents, 2000);
  857. }
  858. }
  859. }
  860.  
  861. const CreateMutationObserver = () => {
  862. const container = document.querySelector('#mainContainer');
  863. if (!container) {
  864. setTimeout(CreateMutationObserver, 2000);
  865. return;
  866. }
  867.  
  868. if (isTerm) {
  869. autoLogin(container);
  870. const reactAlert = document.querySelector('#reactAlert');
  871. const observerTerm = new MutationObserver(mutations => {
  872. mutations.forEach(mutation => {
  873. if (reactAlert.querySelector('p button')) {
  874. reactAlert.querySelector('p button').addEventListener("click", function(event) {
  875. autoLogin(container);
  876. });
  877. }
  878. });
  879. })
  880. observerTerm.observe(reactAlert, {childList: true,});
  881. }
  882. const observer = new MutationObserver(mutations => {
  883. mutations.forEach(mutation => execInterval());
  884. })
  885. observer.observe(container, {childList: true,});
  886.  
  887. //chkBeforeunloadEvents();
  888. }
  889.  
  890. const writeDebugLog = (log) => {
  891. if (gmc.get('isShowDebug')) {
  892. queryConfigEl('#PttChromeAddOnConfigDebug', 'textarea', el => {
  893. el.value = `${log}\n` + el.value;
  894. });
  895. }
  896. }
  897. const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));
  898. const autoLogin = async (container) => {
  899. const checkAndWait = async (container, keyword) => {
  900. if (container && container.innerText.match(keyword)) {
  901. await sleep(1000);
  902. return checkAndWait(container, keyword);
  903. }
  904. }
  905. const pasteInputArea = async (str) => {
  906. let inputArea = document.querySelector('#t');
  907. if (!inputArea) {
  908. await sleep(1000);
  909. return pasteInputArea(str);
  910. }
  911.  
  912. const pasteE = new CustomEvent('paste');
  913. pasteE.clipboardData = { getData: () => str };
  914. inputArea.dispatchEvent(pasteE);
  915. }
  916. const autoSkip = async (node, regexp, pasteKey, isReCheck) => {
  917. if (node.innerText.match(regexp)) {
  918. await pasteInputArea(pasteKey);
  919. await checkAndWait(node, regexp);
  920. } else if (isReCheck) {
  921. await sleep(1000);
  922. return autoSkip(node, regexp, pasteKey, isReCheck)
  923. }
  924. }
  925. if (gmc.get('isAutoLogin')) {
  926. if (container.innerText.trim().length < 10) {
  927. await sleep(1000);
  928. return autoLogin(container);
  929. }
  930. const list = [];
  931. if (gmc.get('autoUser') && gmc.get('autoPassWord')) {
  932. list.push({regexp: /請輸入代號,或以/, pasteKey: `${gmc.get('autoUser')}\n${gmc.get('autoPassWord')}\n`, isReCheck: true});
  933. }
  934.  
  935. if (gmc.get('isEnableDeleteDupLogin') !== "N/A") {
  936. list.push({regexp: /您有其它連線已登入此帳號/, pasteKey: `${gmc.get('isEnableDeleteDupLogin')}\n`, isReCheck: true});
  937. }
  938.  
  939. if (gmc.get('isAutoSkipInfo1')) {
  940. list.push(
  941. {regexp: /正在更新與同步線上使用者及好友名單,系統負荷量大時會需時較久.../, pasteKey: '\n'},
  942. {regexp: /歡迎您再度拜訪,上次您是從/, pasteKey: '\n'},
  943. {regexp: /─+名次─+範本─+次數/, pasteKey: 'q'},
  944. {regexp: /發表次數排行榜/, pasteKey: 'q'},
  945. {regexp: /大富翁 排行榜/, pasteKey: 'q'},
  946. {regexp: /本日十大熱門話題/, pasteKey: 'q'},
  947. {regexp: /本週五十大熱門話題/, pasteKey: 'q'},
  948. {regexp: /每小時上站人次統計/, pasteKey: 'qq'},
  949. {regexp: /程式開始啟用/, pasteKey: 'q'},
  950. {regexp: /排名 +看 *板 +目錄數/, pasteKey: 'q'},
  951. );
  952.  
  953. }
  954. if (gmc.get('isAutoToFavorite')) {
  955. list.push({regexp: /【主功能表】 +批踢踢實業坊/, pasteKey: `f\n`, isReCheck: true});
  956. }
  957. let isMatch = false;
  958. for (let idx=0;idx < list.length; idx++) {
  959. if (container.innerText.match(list[idx].regexp)) {
  960. isMatch = true;
  961. await autoSkip(container, list[idx].regexp, list[idx].pasteKey, list[idx].isReCheck);
  962. }
  963. if (idx == list.length-1 && !isMatch) {
  964. idx = 0;
  965. await sleep(500);
  966. }
  967. }
  968. }
  969. }
  970.  
  971. (function() {
  972. try {
  973. window.addEventListener("load", function(event) {
  974. CreateMutationObserver();
  975. });
  976. } catch (ex) {
  977. writeDebugLog(`出現錯誤...${ex}`);
  978. console.error(ex);
  979. }
  980.  
  981. const _button = document.createElement("div");
  982. _button.innerHTML = 'Settings';
  983. _button.onclick = event => {
  984. event.preventDefault();
  985. event.stopPropagation();
  986. if (!configStatus) {
  987. configStatus = true;
  988. if (gmc) gmc.open();
  989. if (gmc.get('isShowDebug') && gmcDebug) gmcDebug.open();
  990. } else if (configStatus) {
  991. configStatus = false;
  992. if (gmc.isOpen) gmc.close();
  993. if (gmcDebug.isOpen) gmcDebug.close();
  994. if (gmcBlack.isOpen) gmcBlack.close();
  995. }
  996. }
  997. _button.style = "border: 1px solid #AAA;color: #999;background-color: #111;position: fixed; top: 0.5em; right: 0.5em; z-index: 900;cursor:pointer !important;"
  998.  
  999. document.body.appendChild(_button)
  1000.  
  1001. const el = document.createElement('link');
  1002. el.rel = 'stylesheet';
  1003. el.type = 'text/css';
  1004. el.href = "https://cdnjs.cloudflare.com/ajax/libs/tippy.js/2.5.4/tippy.css";
  1005. document.head.appendChild(el);
  1006. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址