Enhancement Userscript for LIHKG

An Enhancement Userscript for LIHKG

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Enhancement Userscript for LIHKG
// @version      0.6.3
// @description  An Enhancement Userscript for LIHKG
// @include        /https?\:\/\/lihkg\.com/
// @icon         https://www.google.com/s2/favicons?domain=lihkg.com
// @grant        GM_addStyle
// @license MIT
// @namespace https://greasyfork.org/users/371179
// ==/UserScript==
(function() {
    'use strict';

    GM_addStyle([

`
/*
html.skip-drag-upload .EGBBkGyEbfIEpHMLTW84H{
    display:none !important;
}
*/

/* main textarea */
._2ME7dqW8n0YSe687nDnGvI ._3ILx6bqxXMKzVd1P5DRS9F{
    padding-bottom: 40px;
}


/* div 放開圖片即可上載*/
.EGBBkGyEbfIEpHMLTW84H{
    opacity: 1 !important;
    pointer-events: all !important;
    left: 5%;
    right: 5%;
    top: calc(100% - 35px);
    bottom: 0;
    width: auto;
    height: auto;
}

/* div Preview  ~ div + div 放開圖片即可上載*/
._1TdM0E7HcWQwmxXpfa6yJg ~ ._2ME7dqW8n0YSe687nDnGvI .EGBBkGyEbfIEpHMLTW84H{
    visibility: collapse;
}

`,

// css fix for thread posts positioning
`
body ._21IQKhlBjN2jlHS_TVgI3l:after {left:0.4rem}
body ._21IQKhlBjN2jlHS_TVgI3l .vv9keWAXpwoonDah6rSIU ._3D2lzCKDMcdgEkexZrTSUh{margin-left: -6px;width: 16px;}
`,

// css fix for like and dislike due to js hack of like count and dislike count (reply posts)
`
body label[for*="-dislike-like"] {display:inline-block !important;}
body label[for*="-like-like"] {display:inline-block !important;}
body ._3ExaynSI6tUp5h1U50MHtI ._3imUf8qB9LmLpk_t5PjDm4>div:first-child+div:last-child {margin-left:-6px;}
`,

// css fix for like and dislike due to js hack of like count and dislike count (main thread)
// empty full space char for maintaining padding when the count is not yet shown
`
span[data-tip="正評"]:not([data-score])::after{
    content: " ";
    font-size: .6rem;
    font-weight: 400;
    margin-top: .3rem;
}
span[data-tip="負評"]:not([data-score])::after{
    content: " ";
    font-size: .6rem;
    font-weight: 400;
    margin-top: .3rem;
}
span[data-tip="正評"],span[data-tip="負評"]{
    padding-top:0px !important;
}
`,

// kiwi browser css fix
`
@supports not (padding-bottom: env(safe-area-inset-bottom)){
  ._3dwGLtjqTgI2gc9wpc7FuT {
    padding: 1rem .6rem calc(1rem + 0px) calc(.7rem + 0px);
  }
}
`,

// po reply - userselect for icons
`
html body ._1Ku9qL4qhkBDwAgVLYcQdi[class], html body ._1Ku9qL4qhkBDwAgVLYcQdi[class]:hover {
  user-select:none !important;
}
`
].map(x => x.trim()).join('\n'))


    let isNumCheck = function(n) {
        return n > 0 || n < 0 || n === 0
    }
    let postDetails = {}
    let threadDetails = {}
    let pendingRefreshThread = false;

    let testBlockElm = function(elm) {
        if (elm && elm.nodeType == 1) {
            switch (elm.tagName) {
                case 'DIV':
                case 'P':
                case 'BLOCKQUOTE':
                    return true;

                default:
                    return false;

            }

        }
    }


    document.cssAll = function() {
        return [...document.querySelectorAll.apply(this, arguments)]
    }

    function urlConvert(url) {
        let src = url.replace(/\w+\:\/\//, '')
        let replacements = [...src.matchAll(/[\w\.]+/g)].filter((t) => /\./.test(t))
        if (replacements.length > 1) {
            replacements.length--;

        }
        replacements.forEach((s) => {
            src = src.replace(s, '')
        })

        src = src.replace(/\/+/g, '/')

        return src;

    }

    let emoji = {};
    setTimeout(function() {
        console.log(emoji)
    }, 1500)

    setInterval(() => {

        document.cssAll('img[src*="lihkg.com"][alt]:not([title])').forEach(function(imgElm) {
            let src = imgElm.getAttribute('src');
            let erc = urlConvert(src)
            let imgAlt = imgElm.getAttribute('alt') || "";
            if (/^[\x20-\x7E]+$/.test(imgAlt) && /\W/.test(imgAlt)) {
                emoji[erc] = imgAlt.trim()
            }

            imgElm.setAttribute('title', imgAlt)

        })


        document.cssAll('a[href*="profile/"]:not([href*="//"]):not([title])').forEach(function(aElm) {
            aElm.setAttribute('title', aElm.getAttribute('href'))
        })

        document.cssAll('[data-ic~="hkgmoji"]:not([title])>img[src*="lihkg.com"]:not([alt])').forEach(function(imgElm) {
            let src = imgElm.getAttribute('src');
            let erc = urlConvert(src)
            let text = emoji[erc] ? emoji[erc] : "[img]" + erc + "[/img]"
            imgElm.parentNode.setAttribute('title', text)
            imgElm.setAttribute('alt', text)


        })



        document.cssAll('img[src]:not([alt]),img[src][alt=""]').forEach((el) => {

            if (el.getAttribute('alt') || el.getAttribute('title')) return;

            let text = '';
            if (el.tagName.toLowerCase() == 'img' && el.getAttribute('data-original')) {
                text = '[img]' + el.getAttribute('data-original') + '[/img]';
            } else if (el.tagName.toLowerCase() == 'img' && el.getAttribute('src')) {
                text = '[img]' + el.getAttribute('src') + '[/img]';
            }
            if (text) el.setAttribute('alt', text)
            if (text) el.setAttribute('title', text)

        })




        document.cssAll('[data-post-id]:not([hacked])').forEach((el) => {

            el.setAttribute('hacked', 'true');
            let post_id = el.getAttribute('data-post-id');
            if (!post_id) return;

            //console.log(post_id, postDetails)
            let post_detail = postDetails[post_id]
            if (post_detail) {
                // console.log(55,post_detail)

            }

        })



    }, 33)



    function refreshingThreadEvent(thread_id) {


        console.log("refreshingThreadEvent", threadDetails[thread_id])
        if (thread_id && threadDetails[thread_id]) {


            document.cssAll('span[data-tip="正評"]').forEach((elm) => {

                elm.setAttribute('data-score', threadDetails[thread_id]["like_count"]);
                elm.style.paddingTop = '0px';
            })


            document.cssAll('span[data-tip="負評"]').forEach((elm) => {

                elm.setAttribute('data-score', threadDetails[thread_id]["dislike_count"]);
                elm.style.paddingTop = '0px';
            })



        }


    }


    let cid_refreshingThread = 0;

    function refreshingThreadRunning() {

        if (!cid_refreshingThread) return;


        let titlespan = document.cssAll('a[href^="/category/"]+span');
        if (titlespan.length == 1) {
            let titlespanElm = titlespan[0]

            if (!titlespanElm.querySelector('noscript')) {
                titlespanElm.appendChild(document.createElement('noscript'))


                if (pendingRefreshThread) {

                    let thread_id = pendingRefreshThread === true ? (/thread\/(\d+)\//.exec(location + "") || [null, null])[1] : pendingRefreshThread

                    pendingRefreshThread = false;
                    clearInterval(cid_refreshingThread);
                    cid_refreshingThread = 0;
                    refreshingThreadEvent(thread_id)


                }


            }
        }

    }




    let makePlain = false;


    document.addEventListener("dragstart", function(evt) {
        console.log(evt.target)
        if(!evt || !evt.target) return;

        let type = 0
        if(evt.target.nodeType!==1 && evt.target.parentElement/* && evt.target.parentElement.closest('[data-post-id]')*/){
            type=1;
        }

        if(evt.target.nodeType ===1){
        if(!evt.target.matches('img[alt][src][title]'))return;
        let alt = evt.target.getAttribute('alt')+'';
          if(!alt)return;
        if(/https?\:\/\//.test(alt))return;
        if(/^[a-zA-Z0-9]+$/.test(alt))return;
          if(/[\u0100-\uFFFF]/.test(alt))return;
        type = 2;

            console.log(alt)

            evt.dataTransfer.setData('text/plain', alt);

            evt.stopPropagation()
            evt.stopImmediatePropagation();

        }
        if(type>0){
        evt.dropEffect='copy';
            evt.effectAllowed = "all";

        document.documentElement.classList.add('skip-drag-upload')
        }
    }, true);



function makeRangeFromXY(evt){

    let range=null;
    if (document.caretRangeFromPoint) { // Chrome
        range=document.caretRangeFromPoint(evt.clientX,evt.clientY);
    }
    else if (evt.rangeParent) { // Firefox
        range=document.createRange(); range.setStart(evt.rangeParent,evt.rangeOffset);
    }
return range;
}
    document.addEventListener("drop", function(evt) {

        if(!evt || !evt.target)return;

        let node = evt.target;

        if(node.nodeType!==1 && node.parentNode && node.parentNode.nodeType===1) node= node.parentNode;

        if(node.nodeType===1 &&node.closest('div.ProseMirror[contenteditable]')){
            evt.preventDefault();
let range = makeRangeFromXY(evt)
    var sel = window.getSelection();
    sel.removeAllRanges(); sel.addRange(range);

    let p = sel.anchorNode
    while(p&&p.parentNode){
        if(p.nodeType===1&&p.matches('div.ProseMirror[contenteditable]')){
            p.focus();
            p.classList.add('ProseMirror-focused');
        break;
        }
        p=p.parentNode;
    }

            range.collapse(true);

            let text = evt.dataTransfer.getData('text/plain')
    document.execCommand('insertHTML',false,text);

    //sel.removeAllRanges();

            let mRange = window.getSelection().getRangeAt(0);

            mRange.setStart(mRange.endContainer,mRange.endOffset-text.length);
            mRange.setEnd(mRange.endContainer,mRange.endOffset);

         //   nRange.collapse(true);
   // sel.addRange(nRange)


        }

        document.documentElement.classList.remove('skip-drag-upload')

    },true)


    document.addEventListener("dragend", function(evt) {

        document.documentElement.classList.remove('skip-drag-upload')

        let p = document.querySelector('div.EGBBkGyEbfIEpHMLTW84H[style]')
        if(p){
            p.style.opacity='0';
            p.style.pointerEvents='none';
        }

    },true)

/*
    document.addEventListener("dragover", function(evt) {
        if(!evt || !evt.target)return;
        try{

        if(!evt.target.matches('[contenteditable], textarea, div.EGBBkGyEbfIEpHMLTW84H, div._1xaNo-2jhq5KooKoBBRKwe '))return;
        }catch(e){return;}
        console.log(evt)
        evt.stopPropagation();
    }, true);
    */


    let injection = function() {


        function extractRawURL(thumbnailURL) {

            let u = [...thumbnailURL.matchAll(/[\?\&]\w+\=([\x21-\x25\x27-\x3E\x40-\x7E]+)/g)].map(d => d[1])
            if (u.length) {
                let uMaxT = Math.max(...u.map(t => t.length))
                let u0 = u.filter(t => t.length == uMaxT)[0]

                if (u0) {

                    let v0 = null
                    try {
                        v0 = decodeURIComponent(u0)
                    } catch (e) {}
                    //console.log(v0,u0)
                    if (v0) {
                        return v0
                    }

                }
            }
            return null
        }

        if (!JSON._parse && JSON.parse) {
            JSON._parse = JSON.parse
            JSON.parse = function(text, r) {
/*
                if (text && typeof text == "string" && text.indexOf('display_vote') > 0) {
                    text = text.replace(/([\'\"])display_vote[\'\"]\s*:\s*false/gi, '$1display_vote$1:true')
                }
                */
                let res = JSON._parse.apply(this, arguments)

                let contentFix = (resObj) => {
                    if(!resObj || typeof resObj!='object')return;
                    for (let k in resObj) {
                        if (typeof resObj[k] == 'object') contentFix(resObj[k]);
                        else if (k=='display_vote' && resObj[k]===false){
                        resObj[k]=true;
                        }
                        else if (k == 'msg' && typeof resObj[k] == 'string') {

                            let msg = resObj[k];
                            let replace = false;
                            let bmsg=msg
                            msg = msg.replace(/(\<img\s+src\=\")(https?\:\/\/i\.lih\.kg\/thumbnail\?[^\"]+)(\"[^\>]+\>)/g, function(s, a, b, c) {

                                let v0 = extractRawURL(b)
                                if (v0) {
                                    replace = true;
return s.replace(b,v0).replace(b,v0).replace(b,v0)

                                    /*
                                    console.log(v0, b)

                                    let v1 = '<img src="' + v0 + '" data-thumbnail-src="' + b + '" />';

                                    return v1
*/
                                }

                                return a + b + c

                            })

                            msg = msg.replace(

                                /<a\s+href=\"(https\:\/\/i\.lih\.kg\/thumbnail\?u=[^\?\s\x00-\x20\x7F-\xFF\"]+)\"[^<>]+>\1<\/a>/g,
                                function(_, a) {

                                    let b = extractRawURL(a)
                                    if (b) {
                                        replace = true
                                        return _.replace(a,b).replace(a,b)
                                        /*
                                        let a01 = encodeURIComponent(a)
                                        let a02 = a01.replace(/\%26amp\%3B/gi, '%26')
                                        let b01 = encodeURIComponent(b)
                                        let c = _.replace(a, b).replace(a, b).replace(a02, b01).replace(a01, b01)

                                        return c*/
                                    }
                                    return _
                                })


                            if (replace) {
                                console.log(333,bmsg,msg)
                                resObj[k] = msg;

                            }
                        }
                    }
                }

                contentFix(res)

                return res;
            }
        }

        let api_callback = "uleccyqjstui"

        ;
        ((xmlhr, xmlhr_pt) => {
            if (!xmlhr_pt._open) {
                xmlhr_pt._open = xmlhr_pt.open;


                xmlhr_pt.open = function() {
                    // console.log('xmlhr_open', arguments)
                    if (/https?\:\/\/[\x20-2E\x30-5B\x5D-\x7E]*lihkg\.com\/[\x20-\x7E]*api[\x20-\x7E]+/.test(arguments[1])) {
                        this._url = arguments[1];

                        console.log('_url', this._url)
                    }
                    this._open.apply(this, arguments)
                }
            }



            if (!xmlhr_pt._send) {
                xmlhr_pt._send = xmlhr_pt.send;


                xmlhr_pt.send = function() {
                    if (this._url) {
                        this.addEventListener('load', function() {
                            let resText = this.responseText;
                            let jsonObj = null;
                            if (resText && typeof resText == 'string') {
                                try {
                                    jsonObj = JSON.parse(resText);
                                } catch (e) {}
                            }

                            if (jsonObj) {
                                //like_count

                                let code_num = 0;

                                if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.item_data && jsonObj.response.item_data.length >= 1 && jsonObj.response.item_data[0]["post_id"]) {
                                    code_num |= 16;
                                }
                                if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.thread_id) {
                                    code_num |= 8;
                                }
                                // console.log('code', code_num);
                                let event = new CustomEvent(api_callback, {
                                    detail: {
                                        code: code_num,
                                        responseJSON: jsonObj
                                    }
                                });
                                document.dispatchEvent(event);



                                //console.log(jsonObj)
                            }

                        })
                    }
                    // console.log('xmlhr_send', arguments)
                    this._send.apply(this, arguments)
                }
            }


        })(XMLHttpRequest, XMLHttpRequest.prototype)

    }

    let jsscript = document.createElement('script');
    jsscript.type = 'text/javascript';
    jsscript.innerHTML = '(' + injection + ')()';
    document.documentElement.appendChild(jsscript)

    let api_callback = "uleccyqjstui"
    //data-post-id="5226a9cb7b395fbc182d183a6ee9b35c8adfd2fe"
    document.addEventListener(api_callback, function(e) {
        if (!e || !e.detail) return;
        console.log("API_CALLBACK", e.detail)
        let jsonObj;
        let code_num = e.detail.code
        switch (true) {

            case (code_num & 8) == 8: //main thread

            case (code_num & 16) == 16: //posts


                jsonObj = e.detail.responseJSON;


                if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.item_data && jsonObj.response.item_data.length >= 1 && jsonObj.response.item_data[0]["post_id"]) {
                    let reply_post_fx = (reply_item) => {
                        if ('dislike_count' in reply_item && 'like_count' in reply_item && reply_item["post_id"]) {

                            let like_count = +reply_item['like_count']
                            let dislike_count = +reply_item['dislike_count']
                            let post_id = reply_item['post_id']

                            if (isNumCheck(like_count) && isNumCheck(dislike_count) && post_id) {
                                postDetails[post_id] = {
                                    'like_count': like_count,
                                    'dislike_count': dislike_count
                                }
                            }

                        }
                    };
                    jsonObj.response.item_data.forEach(reply_post_fx)
                    if (jsonObj.response.pinned_post && jsonObj.response.pinned_post["post_id"]) reply_post_fx(jsonObj.response.pinned_post)

                }



                if (jsonObj.success == 1 && jsonObj.response && jsonObj.response.thread_id) {
                    let thread_fx = (thread_item) => {
                        if ('like_count' in thread_item && 'dislike_count' in thread_item && thread_item["thread_id"]) {

                            let like_count = +thread_item['like_count']
                            let dislike_count = +thread_item['dislike_count']
                            let thread_id = thread_item['thread_id']

                            if (isNumCheck(like_count) && isNumCheck(dislike_count) && thread_id) {
                                threadDetails[thread_id] = {
                                    'like_count': like_count,
                                    'dislike_count': dislike_count
                                }
                                pendingRefreshThread = thread_id;
                                if (!cid_refreshingThread) cid_refreshingThread = setInterval(refreshingThreadRunning, 1);
                            }

                        }
                    };
                    thread_fx(jsonObj.response)
                    //console.log(99, threadDetails)

                }

                //console.log(jsonObj)
                break;


            default:
        }

    });

    const makePopup=(obj)=>{
        const {header, content1,content2, placeholder, remarks, btn1, btn2, closeFn, btn1Fn, btn2Fn}=obj;

        let mcontent2=content2?`<br>${content2}<br>`:'';

        let df = document.createElement('div');


        df.innerHTML=
     `
<div class="_34dVbr5A8khk2N65H9Nl-j  ">
  <div class="_27su4Zj_qATokwVdWIbEWB  ">
    <div class="_1nqRVNQ2PyO3vnAwZIISAJ ">
      <div class="_2b5VMoBy8yIXlX-wC8v57F">${header}</div>
      <div class="_10tWW0o-L-5oSH8lCBl9ai"><i class="i-close"></i></div>
    </div>
    <div class="_27SmIe4FGDNnK7apcCB3W7">
      <div class="_3dbMg7zkkTIVJ5VZ3ygu4-">
        <div style="text-align: center; width: 240px;"><span>${content1}</span>${mcontent2}
          <div><input type="text" placeholder="${placeholder}" class=" _2SVsmVSmKfAWFlZGk_5_L8" value=""
              style="margin: 0.5rem 0px 0px;"><span
              style="font-size: 0.8rem; color: rgb(136, 136, 136);">${remarks}</span></div>
        </div>
      </div>
    </div>
    <div class="_2c5AwJ_0ePFIYub8OFE97J _2F7zIQl_1y5nHpDllTwX17"><a href="#">${btn1}</a><a
        href="#">${btn2}</a></div>
  </div>
</div>
`
df.querySelector('._10tWW0o-L-5oSH8lCBl9ai').addEventListener('click', closeFn)
        let btnFns = [btn1Fn, btn2Fn];
for(const [idx,a] of [...df.querySelectorAll('a[href*="#"]')].entries()) a.addEventListener('click', btnFns[idx])
        return df.querySelector('div');

    }


    document.addEventListener('keyup',function(evt){

        if(evt.code=='KeyB'){

            let memberID = getSelection()+""

            let m = null
            if(m=/\s*\#(\d+)\s*/.exec(memberID)){



            let mPopup = makePopup({header:'Block', content1:`Memeber ID:${m[1]}`,content2:'', placeholder:'Type the Reason Here', remarks:'Please be careful', btn1:'Cancel', btn2:'Block',
                                   closeFn:function(){
                                   mPopup.remove()
                                   },
                                    btn1Fn:function(){
                                   mPopup.remove()
                                   },
                                    btn2Fn:function(){
                                        let reason = mPopup.querySelector('input[type="text"]').value || '';

                                  fetch(`https://lihkg.com/api_v2/user/${m[1]}/block?reason=${reason}`)
    .then(response => response.json())
    .then(data => {
                                  if(data&& 'success' in data) alert( data.success?'成功':'失敗');
                          console.log(data)
                                  });

                                   mPopup.remove()
                                   },

                                   })
            document.querySelector('body').appendChild(mPopup)

            }




        }else if(evt.code=='KeyU'){

            let memberID = getSelection()+""

            let m = null
            if(m=/\s*\#(\d+)\s*/.exec(memberID)){
                                  fetch(`https://lihkg.com/api_v2/user/${m[1]}/unblock`)
    .then(response => response.json())
    .then(data => {
                                  if(data&& 'success' in data) alert( data.success?'成功':'失敗');
                          console.log(data)
                                  });
            }





        }

    })


    // Your code here...
})();