NGA Author Jump

Help jump to original thread when use author-only mode

// ==UserScript==
// @name         NGA Author Jump
// @namespace    Hieuzest
// @version      0.2
// @description  Help jump to original thread when use author-only mode
// @author       Hieuzest
// @match        *://nga.178.com/read.php*
// @match        *://nga.178.com/thread.php*
// @match        *://ngabbs.com/read.php*
// @match        *://ngabbs.com/thread.php*
// @match        *://bbs.nga.cn/read.php*
// @match        *://bbs.nga.cn/thread.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @inject-into content
// ==/UserScript==

(function() {
    'use strict';

    /*
    function setCookie(c_name, value, expiredays) {
        let exdate = new Date();
        exdate.setDate(exdate.getDate()+expiredays);
        document.cookie = c_name + "=" + escape(value) + ((expiredays==null) ?
                                                          "" :
                                                          ";expires="+exdate.toUTCString());
    }

    function getCookie(name) {
        let nameEQ = name + "=";
        let ca = document.cookie.split(';');
        for(let i=0;i < ca.length;i++) {
            let c = ca[i];
            while (c.charAt(0)==' ') c = c.substring(1,c.length);
            if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
        }
        return null;
    }
    */

    function save(key, value) {
        GM_setValue(key, value);
    }

    function load(key) {
        return GM_getValue(key);
    }

    class Dep {
        constructor(name){
            this.id = new Date()
            this.subs = []
        }
        defined(){
            Dep.watch.add(this);
        }
        notify() {
            this.subs.forEach((e, i) => {
                if(typeof e.update === 'function'){
                    try {
                        e.update.apply(e)
                    } catch(err){
                        console.warr(err)
                    }
                }
            })
        }
    }
    Dep.watch = null;
    class Watch {
        constructor(name, fn){
            this.name = name;
            this.id = new Date();
            this.callBack = fn;
        }
        add(dep) {
            dep.subs.push(this);
        }
        update() {
            var cb = this.callBack;
            cb(this.name);
        }
    }


    var addHistoryMethod = (function(){
        var historyDep = new Dep();
        return function(name) {
            if(name === 'historychange'){
                return function(name, fn){
                    var event = new Watch(name, fn)
                    Dep.watch = event;
                    historyDep.defined();
                    Dep.watch = null;
                }
            } else if(name === 'pushState' || name === 'replaceState') {
                var method = history[name];
                return function(){
                    method.apply(history, arguments);
                    historyDep.notify();
                }
            }
        }
    }())

    window.addHistoryListener = addHistoryMethod('historychange');
    history.pushState = addHistoryMethod('pushState');
    history.replaceState = addHistoryMethod('replaceState');


    function GetRequestParamValue(url, paras) {
        var paraString = url.substring(url.indexOf("?") + 1, url.length).split("&");
        var paraObj = {}
        paraString.forEach(para => {
            paraObj[para.substring(0, para.indexOf("=")).toLowerCase()] = para.substring(para.indexOf("=") + 1, para.length);
        })
        var returnValue = paraObj[paras.toLowerCase()];
        if (typeof (returnValue) == "undefined") {
            return "";
        } else {
            return returnValue;
        }
    }


    function GetFirstThreadTimestampOfPage(doc) {
        let flt = doc.querySelectorAll('.postInfo')[0].children[0].textContent;
        let flts = Date.parse(flt);
        return flts;
    }

    async function GetThreadPageNum(tid) {
        let html = await fetch(`/read.php?tid=${tid}`).then(r => r.text());
        let parser = new DOMParser();
        let doc = parser.parseFromString(html, 'text/html');
        let el = doc.querySelector('#pagebbtm script');
        let script = el.text;
        script = script.slice(script.indexOf('=') + 1, script.indexOf(';'));
        script = script.slice(script.indexOf(','));
        script = script.slice(script.indexOf(':') + 1);
        script = script.slice(0, script.indexOf(','));
        return parseInt(script);
    }

    async function main() {
        if (document.URL.indexOf('/read.php') === -1) return;

        let tid = GetRequestParamValue(document.URL, 'tid');
        let pageCurrent = GetRequestParamValue(document.URL, 'page');

        let pageMax = await GetThreadPageNum(tid);
        let pageStart = 1;
        let pageEnd = pageMax;
        let tsOfPages = new Array(pageMax + 1);
        tsOfPages[0] = 0;

        console.log('Initializing NGA Author Jump');
        await GetThreadPageNum(tid);

        let cached = false;

        /*if (getCookie(`_nga_jump_ts_pages_${tid}`)) {
            console.log('Loading cookie ...');
            tsOfPages = JSON.parse(unescape(getCookie(`_nga_jump_ts_pages_${tid}`)));
            cached = true;
        } else if (window._nga_tsOfPages !== undefined && window._nga_tid !== undefined && window._nga_tid === tid) {
            console.log('Loading cache ...');
            tsOfPages = window._nga_tsOfPages;
            cached = true;
        } else */
        if (load(`_nga_jump_ts_pages_${tid}`)) {
            console.log('Loading saved data ...');
            tsOfPages = JSON.parse(unescape(load(`_nga_jump_ts_pages_${tid}`)));
            cached = true;
        }
        console.log(tsOfPages);
        if (!cached || tsOfPages.length <= pageMax || !tsOfPages.slice(1).every(x => x)) {

            if (tsOfPages.length <= pageMax) pageStart = tsOfPages.length;

            console.log(`Process ${tid}:${pageCurrent}:${pageMax} through ${pageStart}:${pageEnd}`);

            while (tsOfPages.length <= pageMax) { tsOfPages.push(null); }

            let promises = [];

            for (let p = pageStart; p <= pageEnd; p++) {
                promises.push(fetch(`/read.php?tid=${tid}&page=${p}`)
                              .then(r => r.text())
                              .then(html => {
                    let parser = new DOMParser();
                    let doc = parser.parseFromString(html, 'text/html');
                    let ts = GetFirstThreadTimestampOfPage(doc);
                    console.log(p, ts);
                    tsOfPages[p] = ts;
                }));
            }

            await Promise.all(promises);

            window._nga_tid = tid;
            window._nga_tsOfPages = tsOfPages;

            save(`_nga_jump_ts_pages_${tid}`, JSON.stringify(tsOfPages))
            console.log('Thread data saved.');
        }

        const JUMP_ID = 'jump_link';

        document.querySelectorAll('span[class^=posterinfo] span.right').forEach(el => {

            for (let child of el.children) {
                if (child.id === JUMP_ID) child.remove()
            }

            let index = parseInt(el.children[0].name.slice(1));
            let pidIndex = el.children[0].href.lastIndexOf('#');
            let pidString = pidIndex === -1 ? "" : el.children[0].href.slice(pidIndex);

            let elJump = document.createElement('a');
            elJump.id = JUMP_ID;
            elJump.className = el.children[0].className;
            elJump.text = "→";
            let ts = Date.parse(el.parentElement.parentElement.parentElement.parentElement.querySelector('span[id^=postdate]').textContent);
            let page = tsOfPages.findIndex(tsP => ts < tsP) - 1;
            if (page < 0) page = pageMax;
            elJump.href = `/read.php?tid=${tid}&page=${page}${pidString}`;
            el.appendChild(elJump);
        })

    }

    window.addHistoryListener('history', main);

    main();

})();