xeHentai Helper

Become a hentai

// ==UserScript==
// @name        xeHentai Helper
// @version     0.41
// @description Become a hentai
// @namespace 	https://yooooo.us
// @include     http*://*e-hentai.org/*
// @include     http*://exhentai.org/*
// @license     GNU General Public License (GPL)
// @run-at      document-end
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_xmlHttpRequest
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM.deleteValue
// @grant       GM.xmlHttpRequest
// @homepageURL  https://github.com/fffonion/xeHentaiHelper.user.js
// @supportURL   https://github.com/fffonion/xeHentaiHelper.user.js/issues
// ==/UserScript==

// ==== ARIA2 class taken from Binux's ThunderLixianExported === //
(function () {
    var JSONRPC = (function () {
        var jsonrpc_version = '2.0';

        function get_auth(url) {
            return url.match(/^(?:(?![^:@]+:[^:@\/]*@)[^:\/?#.]+:)?(?:\/\/)?(?:([^:@]*(?::[^:@]*)?)?@)?/)[1];
        };

        function request(jsonrpc_path, method, params) {
            var auth = get_auth(jsonrpc_path);
            jsonrpc_path = jsonrpc_path.replace(/^((?![^:@]+:[^:@\/]*@)[^:\/?#.]+:)?(\/\/)?(?:(?:[^:@]*(?::[^:@]*)?)?@)?(.*)/, '$1$2$3'); // auth string not allowed in url for firefox

            var request_obj = {
                jsonrpc: jsonrpc_version,
                method: method,
                id: (new Date()).getTime().toString(),
            };
            if (params) request_obj.params = params;
            if (auth && auth.indexOf('token:') == 0) params.unshift(auth);

            var headers = {
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
            }
            if (auth && auth.indexOf('token:') != 0) {
                headers["Authorization"] = "Basic " + btoa(auth);
            }
            var err = function () {
                console.error(method, params[params.length-2], params[params.length-1], "=>")
            };
            var r;
            if (jsonrpc_path.match(/\/\/localhost:/)) {
                // there's not CORS on both sites, we only need GM_xmlHttpRequest
                // to bypass insecure http downgrade error
                // but chrome is happy to send to http://localhost
                // use original XHR gives us better error message
                r = GM_xmlHttpRequest_fallback;
            } else {
                r = GM_xmlHttpRequest;
            }
            new Promise((_, reject) => {
                try {
                    r({
                        method: "POST",
                        url: jsonrpc_path + "?tm=" + (new Date()).getTime().toString(),
                        headers: headers,
                        data: JSON.stringify(request_obj),
                        onerror: err,
                        onabort: err,
                        ontimeout: err,
                        onload: function (r) {
                            console.info(method, params[params.length-2], params[params.length-1]
                                    , "=>", JSON.parse(r.responseText))
                        },
                    })
                } catch (error) {
                    reject(error);
                }
            });
        };

        return function (jsonrpc_path) {
            this.jsonrpc_path = jsonrpc_path;
            this.addTask = function (uri, options) {
                request(this.jsonrpc_path, 'xeH.addTask', [
                    [uri, ], options
                ]);
            };
            this.setCookie = function (cookie) {
                request(this.jsonrpc_path, 'xeH.setCookie', [
                    [cookie, ], {}
                ]);
            };
            return this;
        };
    })();

    // dom helper functions
    function newWrapperDiv(label, dom) {
        var grp = document.createElement("div");
        grp.innerHTML = label + ":";
        grp.appendChild(dom);
        return grp;
    }

    function newInput() {
        var input = document.createElement("input");
        input.type = "text";
        input.size = 50;
        input.style = "margin-bottom: 5px;"
        return input;
    }

    function newButton(label, style, f) {
        var btn = document.createElement("div");
        btn.innerHTML = "<a href='javascript:void(0)' style='text-decoration: none'>" + label + "</a>";
        btn.style = "position: absolute; " + (style || "");
        btn.onclick = f;
        btn.className = "gt";
        return btn;
    }

    if (GM !== undefined) {
        this.GM_getValue = GM.getValue;
        this.GM_setValue = GM.setValue;
        this.GM_deleteValue = GM.deleteValue;
        this.GM_xmlHttpRequest = GM.xmlHttpRequest;
    }
    if (!this.GM_getValue || (this.GM_getValue.toString && this.GM_getValue.toString().indexOf("not supported") > -1)) {
        console.info("[XEH] using fallback set/getValue")
        this.GM_getValue = function (key, def) {
            return localStorage[key] || def;
        };
        this.GM_setValue = function (key, value) {
            return localStorage[key] = value;
        };
        this.GM_deleteValue = function (key) {
            return delete localStorage[key];
        };
    }
    var GM_xmlHttpRequest_fallback = function (opts) {
        var xhr = new XMLHttpRequest();
        xhr.open(opts.method, opts.url, !opts.synchronous);
        for (let key in opts.headers || {}) {
            xhr.setRequestHeader(key, opts.headers[key]);
        }
        xhr.onerror = opts.onerror;
        xhr.onabort = opts.onabort;
        xhr.onload = function () {
            if (xhr.readyState === xhr.DONE) {
                opts.onload(xhr);
            }
        }
        xhr.send(opts.data);
    }
    if (!this.GM_xmlHttpRequest) {
        console.info("[XEH] using fallback XHR")
        this.GM_xmlHttpRequest = GM_xmlHttpRequest_fallback;
    }

    XEH = {
        config_keys: ["host", "port", "token", "name"],
    };
    (function (XEH) {
        // JSONRPC client, will be initlized in the config area
        var jr;
        var gd5 = document.getElementById("gd5");
        if (gd5) { // gallery page
            var p = document.createElement("p");
            p.innerHTML = '<img src="https://ehgt.org/g/mr.gif"> <a id="xeh_addtask" href="#">添加到xeHentai</a>';
            p.className = "g2";
            p.onclick = function () {
                if (document.cookie.indexOf("ipb_pass_hash") != -1 && document.cookie.indexOf("ipb_member_id") != -1) {
                    jr.setCookie(document.cookie);
                }
                jr.addTask(location.protocol + '//' + location.host + location.pathname, {});
            };
            gd5.childNodes[gd5.childNodes.length - 1].className = "g2";
            gd5.appendChild(p);
        }
        var glnames = document.getElementsByClassName("glname");
        if (glnames && glnames.length) { // index page
            function saveInputState() {
                var checked = {};
                for (var i = 0; i < allinps.length; ++i) {
                    if (allinps[i].checked) {
                        checked[allinps[i].value] = 1;
                    }
                }
                checked.ts = new Date().getTime();
                GM_setValue("xeh_checked", JSON.stringify(checked));
            }

            function loadInputState() {
                var checked = JSON.parse(GM_getValue("xeh_checked", "0"));
                if (!checked) return;
                // ignore saved states longer than 30 minutes
                if (checked.ts && new Date().getTime() - checked.ts > 10 * 60 * 1000) {
                    return;
                }
                for (var i = 0; i < allinps.length; ++i) {
                    if (checked[allinps[i].value] === 1) {
                        allinps[i].checked = true;
                    }
                }
            }
            var inp_onclick = function (e) {
                e.stopPropagation();
                saveInputState();
            };

            var inputSize = "0.7em";
            if (glnames[0].className.search(/gl\dm/) != -1) { // minimal, minimal+
            } else if (glnames[0].className.search(/gl\dc/) != -1) { // compact
                inputSize = "0.8em";
            } else if (glnames[0].className.search(/gl\d[te]/) != 1) { // extended or thumbail mode
                inputSize = "0.9em";
            }

            var allinps = [];
            for (var i = 0; i < glnames.length; ++i) {
                var glname = glnames[i];
                var ip = document.createElement("input");
                ip.type = "checkbox";
                ip.style = "float:left;font-size:20px;width:" + inputSize + ";height:" + inputSize + ";top:0;";
                var href;
                var href_dom = glname;
                for (var j = 0; j < 3; j++) {
                    href = href_dom.innerHTML.match(/\/g\/\d+\/[a-f0-9]+\//);
                    if (href) break;
                    href_dom = href_dom.parentNode;
                }
                ip.value = decodeURIComponent(location.protocol + "//" + location.hostname + href[0]);
                ip.onclick = inp_onclick;
                var doms = href_dom.childNodes;
                for (var k = 0; k < doms.length; k++) {
                    if (doms[k].tagName === "A") {
                        doms[k].insertBefore(ip, doms[k].childNodes[0]);
                    }
                }
                allinps.push(ip);
            }
            var titlebar = document.getElementsByClassName("itg")[0].childNodes[0].childNodes[0];
            var titleInnerHTML = '<input type="checkbox" id="xeh_toogle" style="margin-left:5px;top:0;font-size: 20px;width:##SIZE##;height:##SIZE##;">反选</input>' +
                '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a id="xeh_clear" href="javascript:void(0)">清空</a>' +
                '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a id="xeh_export" href="javascript:void(0)">导出选中项到xeHentai</a>';

            titleInnerHTML = titleInnerHTML.replaceAll("##SIZE##", inputSize);

            if (glnames[0].className.search(/gl\d[te]/) != -1) { // extended or thumbail mode
                var el = document.createElement("tr");
                el.style = "padding:0;font-size:14px;";
                var tbl = document.getElementsByClassName("itg")[0];
                if (glnames[0].className.search(/gl\dt/) != -1) { // thumbnail is not a table
                    var tt = document.createElement("table");
                    tt.className = "itg";
                    tbl.parentNode.insertBefore(tt, tbl);
                    tbl = tt;
                    titleInnerHTML += "<tbody></tbody>";
                } else { // extended, shift right by one cell
                    titleInnerHTML = "<td></td><td><div style='padding-left: 124px;'><a>" + titleInnerHTML + "</a></div></td>"
                }
                el.innerHTML = titleInnerHTML;
                tbl.insertBefore(el, tbl.childNodes[0]);
            } else {
                for (var n = 0; n < titlebar.childNodes.length; n++) {
                    if (titlebar.childNodes[n].innerHTML.search(/title/i) !== -1) {
                        titlebar.childNodes[n].innerHTML = titleInnerHTML;
                    }
                }
            }

            loadInputState();

            document.getElementById("xeh_toogle").onclick = function (s) {
                for (var i = 0; i < allinps.length; ++i) {
                    allinps[i].checked = !allinps[i].checked;
                }
                saveInputState();
            };
            document.getElementById("xeh_clear").onclick = function (s) {
                for (var i = 0; i < allinps.length; ++i) {
                    allinps[i].checked = false;
                }
                saveInputState();
            };
            document.getElementById("xeh_export").onclick = function (s) {
                if (document.cookie.indexOf("ipb_pass_hash") != -1 && document.cookie.indexOf("ipb_member_id") != -1) {
                    jr.setCookie(document.cookie);
                }
                for (var i = 0; i < allinps.length; ++i) {
                    if (allinps[i].checked) {
                        jr.addTask(allinps[i].value, {});
                    }
                }
            };
        }

        // config
        var titleBar = document.getElementById("nb");
        if (titleBar) {
            // increase width for exhentai
            if (titleBar.childElementCount < 10) {
                titleBar.style.maxWidth = "750px";
            }
            var div = document.createElement("div");
            div.innerHTML = '<div><a>xeHentai</a></div>';
            div.onclick = function () {
                configBox.style.display = "block";
            };
            titleBar.appendChild(div);

            // the config box
            var configBox = document.createElement("div");
            configBox.className = "gm"
            configBox.innerHTML = '<h1 id="gn" style="text-align: center;">xeHentai 配置</h1>';
            configBox.style = "height: 300px; position: absolute; top: 200px; z-index: 999; left: 50%; margin-left: -250px; min-width: 320px;";
            configBox.style.display = "none";
            document.body.appendChild(configBox)

            /****************** starts top buttons ************************/
            var closeBtn = newButton("关闭", "right: 5px; top: 5px;", function () {
                configBox.style.display = "none";
            });
            configBox.appendChild(closeBtn);
            /****************** ends top buttons ************************/

            /****************** starts input areas ************************/
            var controlsGrp = document.createElement("div");
            controlsGrp.style = "padding: 10px;"

            XEH.configs = undefined;
            var inputs = {};

            function saveConfigSet(who) {
                GM_setValue("xeh_configs", JSON.stringify(XEH.configs));
                GM_setValue("xeh_config_idx", who.selectedIndex);
            }

            function loadConfigSet(i) {
                if (XEH.configs === undefined) {
                    XEH.configs = JSON.parse(GM_getValue("xeh_configs", "[]"));
                }
                if (XEH.configs.length === 0) {
                    XEH.configs = [{
                        "host": "localhost",
                        "port": 8010,
                        "name": "<默认(点右上角配置)>"
                    }];
                }
                if (i === undefined) {
                    i = parseInt(GM_getValue("xeh_config_idx", "0"));
                }
                if (i >= XEH.configs.length) {
                    i = 0
                }

                var cfg = XEH.configs[i];
                for (var j = 0; j < XEH.config_keys.length; j++) {
                    var k = XEH.config_keys[j];
                    inputs[k].value = cfg[k] || "";
                }

                return i;
            }

            function initJSONRPC(i) {
                var cfg = XEH.configs[i]
                jr = JSONRPC("http://" + (cfg.token ? ("token:" + cfg.token + "@") : "") +
                    cfg.host + ":" + cfg.port + "/jsonrpc");
            }

            var hasNewConfigUnsaved = false;

            // add <select> first, its options will be filled later
            var configSet = document.createElement("select");
            var configSetOnChangeHandler = function () {
                if (hasNewConfigUnsaved) {
                    this.remove(this.length - 1)
                    hasNewConfigUnsaved = false;
                }
                var i = this.selectedIndex
                loadConfigSet(i)
                initJSONRPC(i)
            }
            configSet.onchange = configSetOnChangeHandler;
            controlsGrp.appendChild(newWrapperDiv("当前配置", configSet));

            configBox.appendChild(controlsGrp);

            var __input_labels = ["地址", "端口", "密钥", "名称"];
            for (var i = 0; i < XEH.config_keys.length; i++) {
                var k = XEH.config_keys[i];
                inputs[k] = newInput();
                controlsGrp.appendChild(newWrapperDiv(__input_labels[i], inputs[k]));
            }
            inputs.port.size = 6;

            // load input boxes etc.
            var shouldSelectedIdx = loadConfigSet();

            for (var i = 0; i < XEH.configs.length; i++) {
                var c = document.createElement("option");
                c.text = XEH.configs[i].name;
                configSet.options.add(c)
            }
            configSet.selectedIndex = shouldSelectedIdx;
            initJSONRPC(shouldSelectedIdx)
            /****************** ends input areas ************************/

            var webuiBtn = newButton("打开WebUI", "left: 20px; bottom: 60px; font-size: 12px;", function () {
                window.open("https://xehentai.yooooo.us/#host=" + inputs.host.value + 
                            ",port=" + inputs.port.value + ",token=" + inputs.token.value + 
                            ",https=no",
                            '_blank').focus();
                win;
            });
            configBox.appendChild(webuiBtn);

            /****************** starts bottom buttons ************************/
            var addBtn = newButton("新建", "left: 20px; bottom: 5px;", function () {
                for (var i = 0; i < XEH.config_keys.length; i++) {
                    var k = XEH.config_keys[i];
                    inputs[k].value = "";
                }
                var c = document.createElement("option");
                c.text = "<新配置>";
                configSet.options.add(c);
                configSet.selectedIndex = configSet.options.length - 1;
                hasNewConfigUnsaved = true;
            });
            configBox.appendChild(addBtn);

            var delBtn = newButton("删除", "left: 70px; bottom: 5px;", function () {
                var i = configSet.selectedIndex;
                configSet.remove(i);
                configSet.selectedIndex = Math.max(i - 1, 0);
                hasNewConfigUnsaved = false;
                XEH.configs.splice(i);
                saveConfigSet(configSet);
            });
            configBox.appendChild(delBtn);

            var ojbkBtn = newButton("保存", "left: 150px; bottom: 5px;", function (e) {
                var idx = configSet.selectedIndex;
                var cfg;
                if (inputs.host.value == "" || parseInt(inputs.port.value) === undefined) {
                    alert("地址不能为空,端口必须为数字");
                    e.stopImmediatePropagation();
                    return;
                }
                if (hasNewConfigUnsaved) {
                    hasNewConfigUnsaved = false
                    cfg = {}
                    XEH.configs.push(cfg)
                } else {
                    cfg = XEH.configs[idx]
                }
                inputs.name.value = inputs.name.value || (inputs.host.value + ":" + inputs.port.value);
                for (var i = 0; i < XEH.config_keys.length; i++) {
                    var k = XEH.config_keys[i];
                    cfg[k] = inputs[k].value;
                }
                configSet.options.item(idx).text = cfg.name;
                saveConfigSet(configSet);
                initJSONRPC(idx);
            });
            configBox.appendChild(ojbkBtn);

            var resetBtn = newButton("重置", "right: 5px; bottom: 5px;", function () {
                loadConfigSet(configSet.selectedIndex)
            });
            configBox.appendChild(resetBtn);
            /****************** ends bottom buttons ************************/

            /****************** starts duplicate ui ************************/
            var closeBtn2 = closeBtn.cloneNode(true);
            closeBtn2.style = "position: absolute; left: 200px; bottom: 5px;";
            closeBtn2.onclick = closeBtn.onclick;
            configBox.appendChild(closeBtn2);

            var xehExportAnchor = document.getElementById("xeh_export");
            if (xehExportAnchor) {
                var configSet2 = configSet.cloneNode(true);
                configSet2.style = configSet2.style + "; margin-left: 10px;width:auto;"
                configSet2.addEventListener("change", function () {
                    configSet.selectedIndex = this.selectedIndex;
                    saveConfigSet(this);
                });
                configSet2.addEventListener("change", configSetOnChangeHandler);

                configSet.addEventListener("change", function () {
                    configSet2.selectedIndex = this.selectedIndex;
                })

                delBtn.addEventListener("click", function () {
                    var i = configSet2.selectedIndex;
                    configSet2.remove(i);
                    configSet2.selectedIndex = Math.max(i - 1, 0);
                });
                ojbkBtn.addEventListener("click", function () {
                    var i = configSet.selectedIndex;
                    if (i >= configSet2.options.length) {
                        var c = document.createElement("option");
                        c.text = XEH.configs[i].name;
                        configSet2.options.add(c);
                    } else {
                        configSet2.options.item(i).text = XEH.configs[i].name;
                    }
                    configSet2.selectedIndex = i;
                });
                xehExportAnchor.parentNode.appendChild(configSet2);
                configSet2.selectedIndex = shouldSelectedIdx;
            }
            /****************** ends duplicate ui ************************/
        }
    })(XEH);
})();