Greasy Fork is available in English.

Enhancement Userscript for LIHKG

An Enhancement Userscript for LIHKG

// ==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...
})();