Enhancement Userscript for LIHKG

An Enhancement Userscript for LIHKG

As of 2022-05-09. See the latest version.

  1. // ==UserScript==
  2. // @name Enhancement Userscript for LIHKG
  3. // @version 0.6.2
  4. // @description An Enhancement Userscript for LIHKG
  5. // @include /https?\:\/\/lihkg\.com/
  6. // @icon https://www.google.com/s2/favicons?domain=lihkg.com
  7. // @grant GM_addStyle
  8. // @license MIT
  9. // @namespace https://greatest.deepsurf.us/users/371179
  10. // ==/UserScript==
  11. (function() {
  12. 'use strict';
  13.  
  14. GM_addStyle([
  15.  
  16. `
  17. html.skip-drag-upload .EGBBkGyEbfIEpHMLTW84H{
  18. display:none !important;
  19. }
  20.  
  21. ._3ILx6bqxXMKzVd1P5DRS9F{
  22.  
  23. padding-bottom: 40px;
  24. }
  25.  
  26. .EGBBkGyEbfIEpHMLTW84H{
  27.  
  28. opacity: 1 !important;
  29. pointer-events: all !important;
  30. left: 5%;
  31. right: 5%;
  32. top: calc(100% - 35px);
  33. bottom: 0;
  34. width: auto;
  35. height: auto;
  36. }
  37.  
  38. `,
  39.  
  40.  
  41. // css fix for thread posts positioning
  42. `
  43. body ._21IQKhlBjN2jlHS_TVgI3l:after {left:0.4rem}
  44. body ._21IQKhlBjN2jlHS_TVgI3l .vv9keWAXpwoonDah6rSIU ._3D2lzCKDMcdgEkexZrTSUh{margin-left: -6px;width: 16px;}
  45. `,
  46. // css fix for like and dislike due to js hack of like count and dislike count (reply posts)
  47. `
  48. body label[for*="-dislike-like"]{display:inline-block !important;}
  49. body label[for*="-like-like"]{display:inline-block !important;}
  50. body ._3ExaynSI6tUp5h1U50MHtI ._3imUf8qB9LmLpk_t5PjDm4>div:first-child+div:last-child{margin-left:-6px;}
  51. `,
  52. // css fix for like and dislike due to js hack of like count and dislike count (main thread)
  53. // empty full space char for maintaining padding when the count is not yet shown
  54. `
  55. span[data-tip="正評"]:not([data-score])::after{content: " ";
  56. font-size: .6rem;
  57. font-weight: 400;
  58. margin-top: .3rem;}
  59. span[data-tip="負評"]:not([data-score])::after{content: " ";
  60. font-size: .6rem;
  61. font-weight: 400;
  62. margin-top: .3rem;}
  63. span[data-tip="正評"],span[data-tip="負評"]{padding-top:0px !important;}
  64. `,
  65. // kiwi browser css fix
  66.  
  67. `
  68. @supports not (padding-bottom: env(safe-area-inset-bottom)){
  69. ._3dwGLtjqTgI2gc9wpc7FuT {
  70. padding: 1rem .6rem calc(1rem + 0px) calc(.7rem + 0px);
  71. }
  72. }`
  73.  
  74. ].map(x => x.trim()).join('\n'))
  75.  
  76.  
  77. let isNumCheck = function(n) {
  78. return n > 0 || n < 0 || n === 0
  79. }
  80. let postDetails = {}
  81. let threadDetails = {}
  82. let pendingRefreshThread = false;
  83.  
  84. let testBlockElm = function(elm) {
  85. if (elm && elm.nodeType == 1) {
  86. switch (elm.tagName) {
  87. case 'DIV':
  88. case 'P':
  89. case 'BLOCKQUOTE':
  90. return true;
  91.  
  92. default:
  93. return false;
  94.  
  95. }
  96.  
  97. }
  98. }
  99.  
  100.  
  101. document.cssAll = function() {
  102. return [...document.querySelectorAll.apply(this, arguments)]
  103. }
  104.  
  105. function urlConvert(url) {
  106. let src = url.replace(/\w+\:\/\//, '')
  107. let replacements = [...src.matchAll(/[\w\.]+/g)].filter((t) => /\./.test(t))
  108. if (replacements.length > 1) {
  109. replacements.length--;
  110.  
  111. }
  112. replacements.forEach((s) => {
  113. src = src.replace(s, '')
  114. })
  115.  
  116. src = src.replace(/\/+/g, '/')
  117.  
  118. return src;
  119.  
  120. }
  121.  
  122. let emoji = {};
  123. setTimeout(function() {
  124. console.log(emoji)
  125. }, 1500)
  126.  
  127. setInterval(() => {
  128.  
  129. document.cssAll('img[src*="lihkg.com"][alt]:not([title])').forEach(function(imgElm) {
  130. let src = imgElm.getAttribute('src');
  131. let erc = urlConvert(src)
  132. let imgAlt = imgElm.getAttribute('alt') || "";
  133. if (/^[\x20-\x7E]+$/.test(imgAlt) && /\W/.test(imgAlt)) {
  134. emoji[erc] = imgAlt.trim()
  135. }
  136.  
  137. imgElm.setAttribute('title', imgAlt)
  138.  
  139. })
  140.  
  141.  
  142. document.cssAll('a[href*="profile/"]:not([href*="//"]):not([title])').forEach(function(aElm) {
  143. aElm.setAttribute('title', aElm.getAttribute('href'))
  144. })
  145.  
  146. document.cssAll('[data-ic~="hkgmoji"]:not([title])>img[src*="lihkg.com"]:not([alt])').forEach(function(imgElm) {
  147. let src = imgElm.getAttribute('src');
  148. let erc = urlConvert(src)
  149. let text = emoji[erc] ? emoji[erc] : "[img]" + erc + "[/img]"
  150. imgElm.parentNode.setAttribute('title', text)
  151. imgElm.setAttribute('alt', text)
  152.  
  153.  
  154. })
  155.  
  156.  
  157.  
  158. document.cssAll('img[src]:not([alt]),img[src][alt=""]').forEach((el) => {
  159.  
  160. if (el.getAttribute('alt') || el.getAttribute('title')) return;
  161.  
  162. let text = '';
  163. if (el.tagName.toLowerCase() == 'img' && el.getAttribute('data-original')) {
  164. text = '[img]' + el.getAttribute('data-original') + '[/img]';
  165. } else if (el.tagName.toLowerCase() == 'img' && el.getAttribute('src')) {
  166. text = '[img]' + el.getAttribute('src') + '[/img]';
  167. }
  168. if (text) el.setAttribute('alt', text)
  169. if (text) el.setAttribute('title', text)
  170.  
  171. })
  172.  
  173.  
  174.  
  175.  
  176. document.cssAll('[data-post-id]:not([hacked])').forEach((el) => {
  177.  
  178. el.setAttribute('hacked', 'true');
  179. let post_id = el.getAttribute('data-post-id');
  180. if (!post_id) return;
  181.  
  182. //console.log(post_id, postDetails)
  183. let post_detail = postDetails[post_id]
  184. if (post_detail) {
  185. // console.log(55,post_detail)
  186.  
  187. }
  188.  
  189. })
  190.  
  191.  
  192.  
  193. }, 33)
  194.  
  195.  
  196.  
  197. function refreshingThreadEvent(thread_id) {
  198.  
  199.  
  200. console.log("refreshingThreadEvent", threadDetails[thread_id])
  201. if (thread_id && threadDetails[thread_id]) {
  202.  
  203.  
  204. document.cssAll('span[data-tip="正評"]').forEach((elm) => {
  205.  
  206. elm.setAttribute('data-score', threadDetails[thread_id]["like_count"]);
  207. elm.style.paddingTop = '0px';
  208. })
  209.  
  210.  
  211. document.cssAll('span[data-tip="負評"]').forEach((elm) => {
  212.  
  213. elm.setAttribute('data-score', threadDetails[thread_id]["dislike_count"]);
  214. elm.style.paddingTop = '0px';
  215. })
  216.  
  217.  
  218.  
  219. }
  220.  
  221.  
  222. }
  223.  
  224.  
  225. let cid_refreshingThread = 0;
  226.  
  227. function refreshingThreadRunning() {
  228.  
  229. if (!cid_refreshingThread) return;
  230.  
  231.  
  232. let titlespan = document.cssAll('a[href^="/category/"]+span');
  233. if (titlespan.length == 1) {
  234. let titlespanElm = titlespan[0]
  235.  
  236. if (!titlespanElm.querySelector('noscript')) {
  237. titlespanElm.appendChild(document.createElement('noscript'))
  238.  
  239.  
  240. if (pendingRefreshThread) {
  241.  
  242. let thread_id = pendingRefreshThread === true ? (/thread\/(\d+)\//.exec(location + "") || [null, null])[1] : pendingRefreshThread
  243.  
  244. pendingRefreshThread = false;
  245. clearInterval(cid_refreshingThread);
  246. cid_refreshingThread = 0;
  247. refreshingThreadEvent(thread_id)
  248.  
  249.  
  250. }
  251.  
  252.  
  253. }
  254. }
  255.  
  256. }
  257.  
  258.  
  259.  
  260.  
  261. let makePlain = false;
  262.  
  263.  
  264. document.addEventListener("dragstart", function(evt) {
  265. console.log(evt.target)
  266. if(!evt || !evt.target) return;
  267.  
  268. let type = 0
  269. if(evt.target.nodeType!==1 && evt.target.parentElement/* && evt.target.parentElement.closest('[data-post-id]')*/){
  270. type=1;
  271. }
  272.  
  273. if(evt.target.nodeType ===1){
  274. if(!evt.target.matches('img[alt][src][title]'))return;
  275. let alt = evt.target.getAttribute('alt')+'';
  276. if(!alt)return;
  277. if(/https?\:\/\//.test(alt))return;
  278. if(/^[a-zA-Z0-9]+$/.test(alt))return;
  279. if(/[\u0100-\uFFFF]/.test(alt))return;
  280. type = 2;
  281.  
  282. console.log(alt)
  283.  
  284. evt.dataTransfer.setData('text/plain', alt);
  285.  
  286. evt.stopPropagation()
  287. evt.stopImmediatePropagation();
  288.  
  289. }
  290. if(type>0){
  291. evt.dropEffect='copy';
  292. evt.effectAllowed = "all";
  293.  
  294. document.documentElement.classList.add('skip-drag-upload')
  295. }
  296. }, true);
  297.  
  298.  
  299.  
  300. function makeRangeFromXY(evt){
  301.  
  302. let range=null;
  303. if (document.caretRangeFromPoint) { // Chrome
  304. range=document.caretRangeFromPoint(evt.clientX,evt.clientY);
  305. }
  306. else if (evt.rangeParent) { // Firefox
  307. range=document.createRange(); range.setStart(evt.rangeParent,evt.rangeOffset);
  308. }
  309. return range;
  310. }
  311. document.addEventListener("drop", function(evt) {
  312.  
  313. if(!evt || !evt.target)return;
  314.  
  315. let node = evt.target;
  316.  
  317. if(node.nodeType!==1 && node.parentNode && node.parentNode.nodeType===1) node= node.parentNode;
  318.  
  319. if(node.nodeType===1 &&node.closest('div.ProseMirror[contenteditable]')){
  320. evt.preventDefault();
  321. let range = makeRangeFromXY(evt)
  322. var sel = window.getSelection();
  323. sel.removeAllRanges(); sel.addRange(range);
  324.  
  325. let p = sel.anchorNode
  326. while(p&&p.parentNode){
  327. if(p.nodeType===1&&p.matches('div.ProseMirror[contenteditable]')){
  328. p.focus();
  329. p.classList.add('ProseMirror-focused');
  330. break;
  331. }
  332. p=p.parentNode;
  333. }
  334.  
  335. range.collapse(true);
  336.  
  337. let text = evt.dataTransfer.getData('text/plain')
  338. document.execCommand('insertHTML',false,text);
  339.  
  340. //sel.removeAllRanges();
  341.  
  342. let mRange = window.getSelection().getRangeAt(0);
  343.  
  344. mRange.setStart(mRange.endContainer,mRange.endOffset-text.length);
  345. mRange.setEnd(mRange.endContainer,mRange.endOffset);
  346.  
  347. // nRange.collapse(true);
  348. // sel.addRange(nRange)
  349.  
  350.  
  351. }
  352.  
  353. document.documentElement.classList.remove('skip-drag-upload')
  354.  
  355. },true)
  356.  
  357.  
  358. document.addEventListener("dragend", function(evt) {
  359.  
  360. document.documentElement.classList.remove('skip-drag-upload')
  361.  
  362. let p = document.querySelector('div.EGBBkGyEbfIEpHMLTW84H[style]')
  363. if(p){
  364. p.style.opacity='0';
  365. p.style.pointerEvents='none';
  366. }
  367.  
  368. },true)
  369.  
  370. /*
  371. document.addEventListener("dragover", function(evt) {
  372. if(!evt || !evt.target)return;
  373. try{
  374.  
  375. if(!evt.target.matches('[contenteditable], textarea, div.EGBBkGyEbfIEpHMLTW84H, div._1xaNo-2jhq5KooKoBBRKwe '))return;
  376. }catch(e){return;}
  377. console.log(evt)
  378. evt.stopPropagation();
  379. }, true);
  380. */
  381.  
  382.  
  383. let injection = function() {
  384.  
  385.  
  386. function extractRawURL(thumbnailURL) {
  387.  
  388. let u = [...thumbnailURL.matchAll(/[\?\&]\w+\=([\x21-\x25\x27-\x3E\x40-\x7E]+)/g)].map(d => d[1])
  389. if (u.length) {
  390. let uMaxT = Math.max(...u.map(t => t.length))
  391. let u0 = u.filter(t => t.length == uMaxT)[0]
  392.  
  393. if (u0) {
  394.  
  395. let v0 = null
  396. try {
  397. v0 = decodeURIComponent(u0)
  398. } catch (e) {}
  399. //console.log(v0,u0)
  400. if (v0) {
  401. return v0
  402. }
  403.  
  404. }
  405. }
  406. return null
  407. }
  408.  
  409. if (!JSON._parse && JSON.parse) {
  410. JSON._parse = JSON.parse
  411. JSON.parse = function(text, r) {
  412. /*
  413. if (text && typeof text == "string" && text.indexOf('display_vote') > 0) {
  414. text = text.replace(/([\'\"])display_vote[\'\"]\s*:\s*false/gi, '$1display_vote$1:true')
  415. }
  416. */
  417. let res = JSON._parse.apply(this, arguments)
  418.  
  419. let contentFix = (resObj) => {
  420. if(!resObj || typeof resObj!='object')return;
  421. for (let k in resObj) {
  422. if (typeof resObj[k] == 'object') contentFix(resObj[k]);
  423. else if (k=='display_vote' && resObj[k]===false){
  424. resObj[k]=true;
  425. }
  426. else if (k == 'msg' && typeof resObj[k] == 'string') {
  427.  
  428. let msg = resObj[k];
  429. let replace = false;
  430. let bmsg=msg
  431. msg = msg.replace(/(\<img\s+src\=\")(https?\:\/\/i\.lih\.kg\/thumbnail\?[^\"]+)(\"[^\>]+\>)/g, function(s, a, b, c) {
  432.  
  433. let v0 = extractRawURL(b)
  434. if (v0) {
  435. replace = true;
  436. return s.replace(b,v0).replace(b,v0).replace(b,v0)
  437.  
  438. /*
  439. console.log(v0, b)
  440.  
  441. let v1 = '<img src="' + v0 + '" data-thumbnail-src="' + b + '" />';
  442.  
  443. return v1
  444. */
  445. }
  446.  
  447. return a + b + c
  448.  
  449. })
  450.  
  451. msg = msg.replace(
  452.  
  453. /<a\s+href=\"(https\:\/\/i\.lih\.kg\/thumbnail\?u=[^\?\s\x00-\x20\x7F-\xFF\"]+)\"[^<>]+>\1<\/a>/g,
  454. function(_, a) {
  455.  
  456. let b = extractRawURL(a)
  457. if (b) {
  458. replace = true
  459. return _.replace(a,b).replace(a,b)
  460. /*
  461. let a01 = encodeURIComponent(a)
  462. let a02 = a01.replace(/\%26amp\%3B/gi, '%26')
  463. let b01 = encodeURIComponent(b)
  464. let c = _.replace(a, b).replace(a, b).replace(a02, b01).replace(a01, b01)
  465.  
  466. return c*/
  467. }
  468. return _
  469. })
  470.  
  471.  
  472. if (replace) {
  473. console.log(333,bmsg,msg)
  474. resObj[k] = msg;
  475.  
  476. }
  477. }
  478. }
  479. }
  480.  
  481. contentFix(res)
  482.  
  483. return res;
  484. }
  485. }
  486.  
  487. let api_callback = "uleccyqjstui"
  488.  
  489. ;
  490. ((xmlhr, xmlhr_pt) => {
  491. if (!xmlhr_pt._open) {
  492. xmlhr_pt._open = xmlhr_pt.open;
  493.  
  494.  
  495. xmlhr_pt.open = function() {
  496. // console.log('xmlhr_open', arguments)
  497. if (/https?\:\/\/[\x20-2E\x30-5B\x5D-\x7E]*lihkg\.com\/[\x20-\x7E]*api[\x20-\x7E]+/.test(arguments[1])) {
  498. this._url = arguments[1];
  499.  
  500. console.log('_url', this._url)
  501. }
  502. this._open.apply(this, arguments)
  503. }
  504. }
  505.  
  506.  
  507.  
  508. if (!xmlhr_pt._send) {
  509. xmlhr_pt._send = xmlhr_pt.send;
  510.  
  511.  
  512. xmlhr_pt.send = function() {
  513. if (this._url) {
  514. this.addEventListener('load', function() {
  515. let resText = this.responseText;
  516. let jsonObj = null;
  517. if (resText && typeof resText == 'string') {
  518. try {
  519. jsonObj = JSON.parse(resText);
  520. } catch (e) {}
  521. }
  522.  
  523. if (jsonObj) {
  524. //like_count
  525.  
  526. let code_num = 0;
  527.  
  528. if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.item_data && jsonObj.response.item_data.length >= 1 && jsonObj.response.item_data[0]["post_id"]) {
  529. code_num |= 16;
  530. }
  531. if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.thread_id) {
  532. code_num |= 8;
  533. }
  534. // console.log('code', code_num);
  535. let event = new CustomEvent(api_callback, {
  536. detail: {
  537. code: code_num,
  538. responseJSON: jsonObj
  539. }
  540. });
  541. document.dispatchEvent(event);
  542.  
  543.  
  544.  
  545. //console.log(jsonObj)
  546. }
  547.  
  548. })
  549. }
  550. // console.log('xmlhr_send', arguments)
  551. this._send.apply(this, arguments)
  552. }
  553. }
  554.  
  555.  
  556. })(XMLHttpRequest, XMLHttpRequest.prototype)
  557.  
  558. }
  559.  
  560. let jsscript = document.createElement('script');
  561. jsscript.type = 'text/javascript';
  562. jsscript.innerHTML = '(' + injection + ')()';
  563. document.documentElement.appendChild(jsscript)
  564.  
  565. let api_callback = "uleccyqjstui"
  566. //data-post-id="5226a9cb7b395fbc182d183a6ee9b35c8adfd2fe"
  567. document.addEventListener(api_callback, function(e) {
  568. if (!e || !e.detail) return;
  569. console.log("API_CALLBACK", e.detail)
  570. let jsonObj;
  571. let code_num = e.detail.code
  572. switch (true) {
  573.  
  574. case (code_num & 8) == 8: //main thread
  575.  
  576. case (code_num & 16) == 16: //posts
  577.  
  578.  
  579. jsonObj = e.detail.responseJSON;
  580.  
  581.  
  582. if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.item_data && jsonObj.response.item_data.length >= 1 && jsonObj.response.item_data[0]["post_id"]) {
  583. let reply_post_fx = (reply_item) => {
  584. if ('dislike_count' in reply_item && 'like_count' in reply_item && reply_item["post_id"]) {
  585.  
  586. let like_count = +reply_item['like_count']
  587. let dislike_count = +reply_item['dislike_count']
  588. let post_id = reply_item['post_id']
  589.  
  590. if (isNumCheck(like_count) && isNumCheck(dislike_count) && post_id) {
  591. postDetails[post_id] = {
  592. 'like_count': like_count,
  593. 'dislike_count': dislike_count
  594. }
  595. }
  596.  
  597. }
  598. };
  599. jsonObj.response.item_data.forEach(reply_post_fx)
  600. if (jsonObj.response.pinned_post && jsonObj.response.pinned_post["post_id"]) reply_post_fx(jsonObj.response.pinned_post)
  601.  
  602. }
  603.  
  604.  
  605.  
  606. if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.thread_id) {
  607. let thread_fx = (thread_item) => {
  608. if ('like_count' in thread_item && 'dislike_count' in thread_item && thread_item["thread_id"]) {
  609.  
  610. let like_count = +thread_item['like_count']
  611. let dislike_count = +thread_item['dislike_count']
  612. let thread_id = thread_item['thread_id']
  613.  
  614. if (isNumCheck(like_count) && isNumCheck(dislike_count) && thread_id) {
  615. threadDetails[thread_id] = {
  616. 'like_count': like_count,
  617. 'dislike_count': dislike_count
  618. }
  619. pendingRefreshThread = thread_id;
  620. if (!cid_refreshingThread) cid_refreshingThread = setInterval(refreshingThreadRunning, 1);
  621. }
  622.  
  623. }
  624. };
  625. thread_fx(jsonObj.response)
  626. //console.log(99, threadDetails)
  627.  
  628. }
  629.  
  630. //console.log(jsonObj)
  631. break;
  632.  
  633.  
  634. default:
  635. }
  636.  
  637. });
  638.  
  639.  
  640. // Your code here...
  641. })();