Enhancement Userscript for LIHKG

An Enhancement Userscript for LIHKG

As of 11.06.2021. See апошняя версія.

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.5.8
// @description  An Enhancement Userscript for LIHKG
// @include        /https?\:\/\/lihkg\.com/
// @icon         https://www.google.com/s2/favicons?domain=lihkg.com
// @grant        GM_addStyle
// @namespace https://greasyfork.org/users/371179
// ==/UserScript==
(function() {
    'use strict';

    GM_addStyle([
        // drag & drop area for image upload
        `.EGBBkGyEbfIEpHMLTW84H:not([dragmode]),.EGBBkGyEbfIEpHMLTW84H[dragmode="text"]{position:fixed;left:-9999px;top:-9999px;width:2px;height:2px;}`,
        // copy reply content from reply preview
        `div[contenteditable] p>img~br:last-child,div[contenteditable] p>a~br:last-child,div[contenteditable] p>div~br:last-child{content:'';}`,
        // 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);
}
}`

    ].map(x => x.trim()).join('\n'))


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

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

                default:
                    return false;

            }

        }
    }

    var getElementText = function(el) {
        var text = '';
        // Text node (3) or CDATA node (4) - return its text
        if ((el.nodeType === 3) || (el.nodeType === 4)) {
            text = el.nodeValue;
            // If node is an element (1) and an img, input[type=image], or area element, return its alt text
        } else if ((el.nodeType === 1) && (
                (el.tagName.toLowerCase() == 'img') ||
                (el.tagName.toLowerCase() == 'area') ||
                ((el.tagName.toLowerCase() == 'input') && el.getAttribute('type') && (el.getAttribute('type').toLowerCase() == 'image'))
            )) {
            text = el.getAttribute('alt') || '';
            if (el.tagName.toLowerCase() == 'img' && text == '' && el.getAttribute('data-original')) {
                text = '[img]' + el.getAttribute('data-original') + '[/img]';
            } else if (el.tagName.toLowerCase() == 'img' && text == '' && el.getAttribute('src')) {
                text = '[img]' + el.getAttribute('src') + '[/img]';
            }
            // Traverse children unless this is a script or style element
        } else if ((el.nodeType === 1) && (
                (el.tagName.toLowerCase() == 'br')
            )) {
            text = '\n';
            // Traverse children unless this is a script or style element
        } else if ((el.nodeType === 1) && !el.tagName.match(/^(script|style)$/i)) {
            var children = el.childNodes;

            if (el && testBlockElm(el) && el.previousSibling ? testBlockElm(el.previousSibling) : false) {
                text += '\n'
            }
            if (el.tagName.toLowerCase() == 'blockquote') {
                text += '[quote]'
            }
            for (var i = 0, l = children.length; i < l; i++) {
                text += getElementText(children[i]);
            }
            if (el.tagName.toLowerCase() == 'blockquote') {
                text += '[/quote]'
            }

        }
        return text;
    };


    document.cssAll = function() {

        var s = document.querySelectorAll.apply(this, arguments)

        s = Array.prototype.slice.call(s, 0)
        return s
    }

    function urlConvert(url) {
        var src = url.replace(/\w+\:\/\//, '')
        var 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;

    }

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

    setInterval(() => {

        document.cssAll('img[src*="lihkg.com"][alt]:not([title])').forEach(function(imgElm) {
            var src = imgElm.getAttribute('src');
            var erc = urlConvert(src)
            var 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) {
            var src = imgElm.getAttribute('src');
            var erc = urlConvert(src)
            var text = emoji[erc] ? emoji[erc] : "[img]" + erc + "[/img]"
            imgElm.parentNode.setAttribute('title', text)
            imgElm.setAttribute('alt', text)


        })

        document.cssAll('a[href*="local.lihkg.com"]>img:not([anchored])').forEach(function(img) {
            img.setAttribute('anchored', 'true')


            var originalSrc = img.getAttribute('src') || img.getAttribute('data-original') || ""
            var newSrc = originalSrc.replace('local.lihkg.com', 'cdn.lihkg.com');

            if (newSrc && originalSrc != newSrc) {

                // console.log(originalSrc, newSrc)

                var fx = function() {
                    if (img.complete == false) return setTimeout(fx, 33);

                    if (img.currentSrc == "") {

                        var b = img.cloneNode(false);
                        b.removeAttribute('data-original');
                        b.removeAttribute('data-src');

                        if (b.getAttribute('alt') == "") b.removeAttribute('alt')
                        if (b.getAttribute('title') == "") b.removeAttribute('title')

                        b.setAttribute('src', newSrc);
                        img.parentNode.replaceChild(b, img)

                        if (b.parentNode.getAttribute('href')) {
                            b.parentNode.setAttribute('href', b.parentNode.getAttribute('href').replace(originalSrc, newSrc));
                            if (b.nextElementSibling && b.nextElementSibling.hasAttribute('data-error')) b.nextElementSibling.parentNode.removeChild(b.nextElementSibling);
                            if (b.nextElementSibling && b.nextElementSibling.outerHTML.toLocaleLowerCase() == '<ins></ins>') b.nextElementSibling.parentNode.removeChild(b.nextElementSibling);



                        }

                        b.removeAttribute('anchored')


                    }

                }
                fx();


            }

        })


        document.cssAll('div[contenteditable] div[data-ic][contenteditable="false"]').forEach((elm) => {

            elm.removeAttribute('contenteditable')
        })


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

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

            var 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');
            var post_id = el.getAttribute('data-post-id');
            if (!post_id) return;

            //console.log(post_id, postDetails)
            var 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';
            })



        }


    }


    var cid_refreshingThread = 0;

    function refreshingThreadRunning() {

        if (!cid_refreshingThread) return;


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

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


                if (pendingRefreshThread) {

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

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


                }


            }
        }

    }




    var makePlain = false;
    document.addEventListener('drop', function(evt) {
        // console.log(evt, makePlain, evt.target)

        var p = evt.target;
        var contenteditable = false;
        while (p) {
            if (p.hasAttribute('contenteditable') && p.getAttribute('contenteditable') != 'false') {
                contenteditable = true;
                break;
            }
            p = p.parentNode;

        }

        if (contenteditable) {

            if (makePlain && evt.dataTransfer.getData('text/html')) {

                var text = evt.dataTransfer.getData('text/html');


                setTimeout(function() {
                    var sel = window.getSelection();
                    if (sel.getRangeAt && sel.rangeCount) {


                        var range = sel.getRangeAt(0);

                        var cloneContents = range.cloneContents();


                        var hh = document.createElement('html')
                        hh.innerHTML = text;
                        console.log(hh.cloneNode(true))
                        var hhPlain = getElementText(hh)

                        console.log(hhPlain)
                        hh.innerText = hhPlain


                        if (cloneContents.textContent !== hhPlain) {

                            range.deleteContents();

                            var trueHTML = '<p>' + hh.innerHTML.toLowerCase().replace(/<br\s*\/?>/g, "<br></p><p>") + "</p>"

                            console.log(trueHTML)

                            document.execCommand('insertHTML', false, trueHTML);
                        }

                    }

                }, 10);
            }

        }


    }, true)


    document.addEventListener('dragstart', function(evt) {
        var editable = document.querySelector('div[contenteditable]')
        var dragFrom = evt.target
        if (editable && dragFrom) {

            if (editable == dragFrom || editable.contains(dragFrom)) {

            } else {
                makePlain = true;

            }

        }
        // console.log(evt)
    }, false);


    document.addEventListener('dragend', function(evt) {
        if (Event.prototype._preventDefault) {
            Event.prototype.preventDefault = Event.prototype._preventDefault
            Event.prototype._preventDefault = null

        }
        setTimeout(function() {
            makePlain = false;
        }, 77);
    }, false);

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

        setTimeout(function() {
            makePlain = false;
        }, 77);
        if (Event.prototype._preventDefault) {
            Event.prototype.preventDefault = Event.prototype._preventDefault
            Event.prototype._preventDefault = null
        }

        var dropZone = document.querySelector('.EGBBkGyEbfIEpHMLTW84H');
        if (!dropZone) return;
        dropZone.removeAttribute('dragmode');


    }, true)



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

        var dropZone = document.querySelector('.EGBBkGyEbfIEpHMLTW84H');



        var isFileTransfer = false;
        if (evt.dataTransfer.types) {
            for (var i = 0; i < evt.dataTransfer.types.length; i++) {
                if (evt.dataTransfer.types[i] == "Files") {
                    isFileTransfer = true;
                    break;
                }
            }
        }

        if (!isFileTransfer) {

            if (dropZone)
                dropZone.setAttribute('dragmode', 'text');
            //  evt.dataTransfer.effectAllowed='copy';
            if (!Event.prototype._preventDefault) {
                Event.prototype._preventDefault = Event.prototype.preventDefault;
                Event.prototype.preventDefault = function() {
                    if (this.type == 'dragover') {

                    } else {
                        return this._preventDefault();
                    }
                    //console.log(this)
                };
            }



        } else {
            makePlain = false;
            if (dropZone)
                dropZone.setAttribute('dragmode', 'file');


        }
        //        console.log('dragenter',!isFileTransfer)



    }, false)



    var injection = function() {


        function extractRawURL(thumbnailURL) {

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

                if (u0) {

                    var 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')
                }
                */
                var res = JSON._parse.apply(this, arguments)

                var contentFix = (resObj) => {
                    if(!resObj || typeof resObj!='object')return;
                    for (var 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') {

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

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

                                    /*
                                    console.log(v0, b)

                                    var 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) {

                                    var b = extractRawURL(a)
                                    if (b) {
                                        replace = true
                                        return _.replace(a,b).replace(a,b)
                                        /*
                                        var a01 = encodeURIComponent(a)
                                        var a02 = a01.replace(/\%26amp\%3B/gi, '%26')
                                        var b01 = encodeURIComponent(b)
                                        var 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;
            }
        }

        var 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() {
                            var resText = this.responseText;
                            var jsonObj = null;
                            if (resText && typeof resText == 'string') {
                                try {
                                    jsonObj = JSON.parse(resText);
                                } catch (e) {}
                            }

                            if (jsonObj) {
                                //like_count

                                var 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);
                                var 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)

    }

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

    var api_callback = "uleccyqjstui"
    //data-post-id="5226a9cb7b395fbc182d183a6ee9b35c8adfd2fe"
    document.addEventListener(api_callback, function(e) {
        if (!e || !e.detail) return;
        console.log("API_CALLBACK", e.detail)
        var jsonObj;
        var 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"]) {
                    var reply_post_fx = (reply_item) => {
                        if ('dislike_count' in reply_item && 'like_count' in reply_item && reply_item["post_id"]) {

                            var like_count = +reply_item['like_count']
                            var dislike_count = +reply_item['dislike_count']
                            var 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) {
                    var thread_fx = (thread_item) => {
                        if ('like_count' in thread_item && 'dislike_count' in thread_item && thread_item["thread_id"]) {

                            var like_count = +thread_item['like_count']
                            var dislike_count = +thread_item['dislike_count']
                            var 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:
        }

    });


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