hipda-ID笔记

来自地板带着爱,记录上网冲浪的美好瞬间

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         hipda-ID笔记
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  来自地板带着爱,记录上网冲浪的美好瞬间
// @author       屋大维
// @license      MIT
// @match        https://www.hi-pda.com/forum/*
// @match        https://www.4d4y.com/forum/*
// @resource     IMPORTED_CSS https://code.jquery.com/ui/1.13.0/themes/base/jquery-ui.css
// @require      https://code.jquery.com/jquery-3.4.1.min.js
// @require      https://code.jquery.com/ui/1.13.0/jquery-ui.js
// @icon         https://icons.iconarchive.com/icons/iconshock/real-vista-project-managment/64/task-notes-icon.png
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.deleteValue
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// ==/UserScript==
(function() {
    'use strict';
    // CONST
    const BROWSER_KEY = 'alt+I';
    const MANAGEMENT_KEY = "alt+U";

    // CSS
    const my_css = GM_getResourceText("IMPORTED_CSS");
    GM_addStyle(my_css);
    GM_addStyle(".no-close .ui-dialog-titlebar-close{display:none} textarea{height:100%;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box} .card{box-shadow:0 4px 8px 0 rgba(0,0,0,.2);transition:.3s;width:100%;overflow-y: scroll;}.card:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,.2)}.container{padding:2px 16px}");
    GM_addStyle(".flex-container{display:flex;flex-wrap: wrap;}.flex-container>div{background-color:#f1f1f1;width:500px;max-height:500px;margin:15px; padding:5px;text-align:left;}");
    GM_addStyle(`.lds-roller{display:inline-block;position:fixed;top:50vh;left:50vh;width:80px;height:80px}.lds-roller div{animation:1.2s cubic-bezier(.5,0,.5,1) infinite lds-roller;transform-origin:40px 40px}.lds-roller div:after{content:" ";display:block;position:absolute;width:7px;height:7px;border-radius:50%;background:#bfa1cf;margin:-4px 0 0 -4px}.lds-roller div:first-child{animation-delay:-36ms}.lds-roller div:first-child:after{top:63px;left:63px}.lds-roller div:nth-child(2){animation-delay:-72ms}.lds-roller div:nth-child(2):after{top:68px;left:56px}.lds-roller div:nth-child(3){animation-delay:-108ms}.lds-roller div:nth-child(3):after{top:71px;left:48px}.lds-roller div:nth-child(4){animation-delay:-144ms}.lds-roller div:nth-child(4):after{top:72px;left:40px}.lds-roller div:nth-child(5){animation-delay:-.18s}.lds-roller div:nth-child(5):after{top:71px;left:32px}.lds-roller div:nth-child(6){animation-delay:-216ms}.lds-roller div:nth-child(6):after{top:68px;left:24px}.lds-roller div:nth-child(7){animation-delay:-252ms}.lds-roller div:nth-child(7):after{top:63px;left:17px}.lds-roller div:nth-child(8){animation-delay:-288ms}.lds-roller div:nth-child(8):after{top:56px;left:12px}@keyframes lds-roller{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}`);

    // Your code here...
    // helpers
    function showLoader() {
        let loader = $(`<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>`);
        $("body").append(loader);
    }

    function hideLoader() {
        $(".lds-roller").remove();
    }

    function getKeys(e) { // keycode 转换
        var codetable = {
            '96': 'Numpad 0',
            '97': 'Numpad 1',
            '98': 'Numpad 2',
            '99': 'Numpad 3',
            '100': 'Numpad 4',
            '101': 'Numpad 5',
            '102': 'Numpad 6',
            '103': 'Numpad 7',
            '104': 'Numpad 8',
            '105': 'Numpad 9',
            '106': 'Numpad *',
            '107': 'Numpad +',
            '108': 'Numpad Enter',
            '109': 'Numpad -',
            '110': 'Numpad .',
            '111': 'Numpad /',
            '112': 'F1',
            '113': 'F2',
            '114': 'F3',
            '115': 'F4',
            '116': 'F5',
            '117': 'F6',
            '118': 'F7',
            '119': 'F8',
            '120': 'F9',
            '121': 'F10',
            '122': 'F11',
            '123': 'F12',
            '8': 'BackSpace',
            '9': 'Tab',
            '12': 'Clear',
            '13': 'Enter',
            '16': 'Shift',
            '17': 'Ctrl',
            '18': 'Alt',
            '20': 'Cape Lock',
            '27': 'Esc',
            '32': 'Spacebar',
            '33': 'Page Up',
            '34': 'Page Down',
            '35': 'End',
            '36': 'Home',
            '37': '←',
            '38': '↑',
            '39': '→',
            '40': '↓',
            '45': 'Insert',
            '46': 'Delete',
            '144': 'Num Lock',
            '186': ';:',
            '187': '=+',
            '188': ',<',
            '189': '-_',
            '190': '.>',
            '191': '/?',
            '192': '`~',
            '219': '[{',
            '220': '\|',
            '221': ']}',
            '222': '"'
        };
        var Keys = '';
        e.shiftKey && (e.keyCode != 16) && (Keys += 'shift+');
        e.ctrlKey && (e.keyCode != 17) && (Keys += 'ctrl+');
        e.altKey && (e.keyCode != 18) && (Keys += 'alt+');
        return Keys + (codetable[e.keyCode] || String.fromCharCode(e.keyCode) || '');
    };

    function addHotKey(codes, func) { // 监视并执行快捷键对应的函数
        document.addEventListener('keydown', function(e) {
            if ((e.target.tagName != 'INPUT') && (e.target.tagName != 'TEXTAREA') && getKeys(e) == codes) {
                func();
                e.preventDefault();
                e.stopPropagation();
            }
        }, false);
    };

    function htmlToElement(html) {
        var template = document.createElement('template');
        html = html.trim(); // Never return a text node of whitespace as the result
        template.innerHTML = html;
        return template.content.firstChild;
    }

    function getEpoch(date_str, time_str) {
        let [y, m, d] = date_str.split("-").map(x => parseInt(x));
        let [H, M] = time_str.split(":").map(x => parseInt(x));
        return new Date(y, m - 1, d, H, M, 0).getTime() / 1000;
    }

    // classes
    class HpThread {
        constructor() {}

        getThreadTid() {
            return location.href.match(/tid=(\d+)/) ? parseInt(location.href.match(/tid=(\d+)/)[1]) : -999;
        }

        getUserUid() {
            return parseInt($("cite > a").attr("href").split("uid=")[1]);
        }

        getThreadTitle() {
            let l = $('#nav').text().split(" » ");
            return l[l.length - 1];
        }

        getHpPosts() {
            let threadTid = this.getThreadTid();
            let threadTitle = this.getThreadTitle();
            let divs = $('#postlist > div').get();
            return divs.map(d => new HpPost(threadTid, threadTitle, d));
        }

        addNoteBrowserUI(_notebook) {
            $('#menu>ul').append($(`<li class="menu_2"><a href="javascript:void(0)" hidefocus="true" id="noteButton_browser">搜索笔记</a></li>`));
            var that = this;
            // create dialog
            let dialog = htmlToElement(`
              <div id="noteDialog_browser" style="display: none;">
                <div id="noteDialog_browser_search_bar" style="width: 80%; margin: 20px auto 20px auto;">
                    <select style="display: inline-block;" name="searchMethod" id="noteDialog_browser_search_method">
                        <option value="content">笔记内容</option>
                        <option value="userName">用户名</option>
                    </select>
                    <input type="text" autofocus="true" style="display: inline-block; width: 300px;" id="noteDialog_browser_search_input">
                </div>
                <div id="noteDialog_browser_note_list" style="width: 95%; margin: 10px auto 10px auto;" class="flex-container">
                </div>
              </div>
            `);
            $("body").append(dialog);

            function updateNoteList() {
                $('#noteDialog_browser_note_list').empty(); // remove all notes from the list
                var notes;
                var searchMethod = $('#noteDialog_browser_search_method').val();
                var searchInput = $('#noteDialog_browser_search_input').val();
                if (searchMethod === "userName") {
                    notes = _notebook.getNotesByUsername(searchInput);
                } else if (searchMethod === "content") {
                    notes = _notebook.getNotesByKeyword(searchInput);
                } else {
                    return;
                }
                console.log(notes.length)
                for (let i = 0; i < notes.length; i++) {
                    let element = noteToHtmlElement(notes[i]);
                    $('#noteDialog_browser_note_list').append(element);
                }
            }

            function noteToHtmlElement(note) {
                var searchMethod = $('#noteDialog_browser_search_method').val();
                var searchInput = $('#noteDialog_browser_search_input').val();
                var userName = note.userName;
                var uid = note.uid;
                var content = note.note;
                if (searchMethod === 'userName') {
                    userName = userName.replaceAll(searchInput, '<mark class="highlight">$&</mark>');
                }
                if (searchMethod === 'content') {
                    content = content.replaceAll(searchInput, '<mark class="highlight">$&</mark>');
                }
                // highlight all URLs
                var expression = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
                var regex = new RegExp(expression);
                content = content.replace(regex, '<a style="color:blue;" href="$&" target="_blank">$&</a>')

                var html = `
                <div class="card">
                    <div style="font-size: 2em; float: left; margin: 10px;">${userName}</div>
                    <div style="float: right;">
                        <button class="noteEditButton">编辑</button>
                        <button class="noteDeleteButton" style="margin-right: 2px;">删除</button>
                    </div>
                    <div class="container" style="word-break: break-all; white-space: pre-wrap;">
                    ${content}
                    </div>
                </div>
                `;
                var element = $(html);
                // delete
                element.find("button.noteDeleteButton").click(function() {
                    let r = confirm(`确定要删除 ${note.userName} 的ID笔记吗?`);
                    if (!r) {
                        return;
                    }
                    _notebook.delete(uid);
                    updateNoteList();
                });
                // edit
                element.find("button.noteEditButton").click(function() {
                    // note dialog (this will be different from the one opened in posts)
                    let dialog = htmlToElement(`
                        <div id="noteDialog_${uid}" style="display: none;">
                        <textarea rows="10" wrap="hard" placeholder="暂时没有笔记">
                        </div>
                    `);
                    $("body").append(dialog);

                    // bind event listener
                    console.log("open note for", userName);
                    // freshly fetched from DB
                    $(`#noteDialog_${uid}`).find('textarea').first().val(_notebook.get(uid));
                    $(`#noteDialog_${uid}`).dialog({
                        title: `ID笔记:${userName}`,
                        dialogClass: "no-close",
                        closeText: "hide",
                        closeOnEscape: true,
                        height: Math.max(parseInt($(window).height() * 0.4), 350),
                        width: Math.max(parseInt($(window).width() * 0.4), 600),
                        buttons: [{
                                text: "确认",
                                click: function() {
                                    // save the new note before close
                                    let newNote = $(`#noteDialog_${uid}`).find('textarea').first().val();
                                    if (newNote.length === 0) {
                                        _notebook.delete(uid);
                                    } else {
                                        _notebook.put(uid, userName, newNote);
                                    }
                                    $(this).dialog("close");
                                    // update the Note List
                                    updateNoteList();
                                }
                            },
                            {
                                text: "取消",
                                click: function() {
                                    // close without saving
                                    $(this).dialog("close");
                                }
                            }
                        ]
                    });

                });
                return element;
            }

            function openBrowser() {
                $('#menu>ul>li').first().removeClass("current");
                $('#menu>ul>li').first().addClass("menu_2");
                $('#noteButton_browser').parent().removeClass("menu_2");
                $('#noteButton_browser').parent().addClass("current");
                console.log("open notebook browser dialog");

                $(`#noteDialog_browser`).dialog({
                    title: "ID笔记:浏览器",
                    modal: true,
                    height: parseInt($(window).height() * 0.8),
                    width: parseInt($(window).width() * 0.8),
                    closeOnEscape: true,
                    open: function(event, ui) {
                        $('.ui-widget-overlay').css("background-color", "black");
                        $('.ui-widget-overlay').css("opacity", "0.6");
                    },
                    close: function(event, ui) {
                        $('#menu>ul>li').first().removeClass("menu_2");
                        $('#menu>ul>li').first().addClass("current");
                        $('#noteButton_browser').parent().removeClass("current");
                        $('#noteButton_browser').parent().addClass("menu_2");
                    }
                });
            }

            $(document).ready(function() {
                $('#noteDialog_browser_search_input').on("input", () => {
                    updateNoteList();
                });
                $('#noteDialog_browser_search_method').change(() => {
                    updateNoteList();
                });
                $(document).on("click", `#noteButton_browser`, function() {
                    openBrowser();
                });
                // HOTKEY
                addHotKey(BROWSER_KEY, openBrowser);
            });

        }

        addNoteManagementUI(_notebook) {
            var that = this;
            var button = htmlToElement(`
              <button id="noteButton_management">
                <span><img src="https://icons.iconarchive.com/icons/iconshock/real-vista-project-managment/32/task-notes-icon.png"></img></span>
              </button>
            `);

            // create dialog
            let dialog = htmlToElement(`
              <div id="noteDialog_management" style="display: none;">
                <h3>hipda-ID笔记 v${GM_info.script.version}</h3>
                <p style="margin: 10px auto 10px auto;">来自地板带着爱</p>
                <p id="noteStat" style="margin: 10px auto 10px auto;"></p>
                <div>
                  <button id="noteButton_import">导入</button>
                  <button id="noteButton_export">导出</button>
                  <button id="noteButton_reset">重置</button>
                  <button id="noteButton_migrate">4d4y</button>
                  <button id="noteButton_server">服务器</button>
                  <input type="hidden" autofocus="true" />
                </div>
              </div>
            `);
            $("body").append(dialog);

            function updateNoteStat() {
                let note_stat = _notebook.getNotebookStat();
                let synced = _notebook._synced;
                $(`#noteStat`).text(`共${note_stat.note_number}条ID笔记,大小为${(note_stat.size_kb).toFixed(2)}KB${synced ? " (已同步)" : ""}`);
            }

            function openManagement() {
                console.log("open notebook management dialog");
                // update statistics
                updateNoteStat();

                $(`#noteDialog_management`).dialog({
                    title: "ID笔记:管理面板",
                    height: 200,
                    width: 300,
                    closeOnEscape: true,
                });
            }

            $(document).ready(function() {
                $(document).on("click", "#noteButton_server", async function() {
                    let apiKey = await _notebook.getApiKey();
                    let data = prompt("请将 API链接 输入文本框:", apiKey ? apiKey : "");
                    if (data !== null) {
                        // try to load
                        try {
                            _notebook.setApiKey(data);
                        } catch (err) {
                            alert("格式错误!" + err);
                            return;
                        }
                        alert("导入成功!");
                    }
                });
                $(document).on("click", "#noteButton_migrate", function() {
                    let r = confirm("确定要从hi-pda迁移到4d4y吗?");
                    if (!r) {
                        return;
                    }
                    _notebook.migrate();
                    alert("迁移成功!");
                });
                $(document).on("click", "#noteButton_import", function() {
                    let r = confirm("确定要导入ID笔记吗?现有笔记将会被覆盖!");
                    if (!r) {
                        return;
                    }

                    // prompt cannot handle large file, extend it in the future
                    let data = prompt("请将 id笔记.json 中的文本复制粘贴入文本框:");
                    if (data !== null) {
                        // try to load
                        try {
                            let j = JSON.parse(data);
                            _notebook.importNotebook(j);
                        } catch (err) {
                            alert("格式错误!" + err);
                            return;
                        }
                        alert("导入成功!");
                        updateNoteStat();
                    }
                });
                $(document).on("click", "#noteButton_export", async function() {
                    let r = confirm("确定要导出ID笔记吗?");
                    if (!r) {
                        return;
                    }
                    let a = document.createElement("a");
                    let data = await _notebook.exportNotebook();
                    a.href = "data:text," + encodeURIComponent(data);
                    a.download = "id笔记.json";
                    a.click();
                });
                $(document).on("click", "#noteButton_reset", function() {
                    let r = confirm("确定要清空ID笔记吗?");
                    if (!r) {
                        return;
                    }
                    _notebook.resetNotebook();
                    alert("ID笔记已经清空!");
                    updateNoteStat();
                });
                $(document).on("click", `#noteButton_management`, function() {
                    openManagement();
                });
                // HOTKEY
                addHotKey(MANAGEMENT_KEY, openManagement);
            });

            // add UI
            let d = $("td.modaction").last();
            d.append(button);

        }

    }

    class HpPost {
        constructor(threadTid, threadTitle, postDiv) {
            this.threadTid = threadTid;
            this.threadTitle = threadTitle;
            this._post_div = postDiv;
        }

        getPostAuthorName() {
            return $(this._post_div).find("div.postinfo > a").first().text();
        }

        getPostAuthorUid() {
            return parseInt($(this._post_div).find("div.postinfo > a").first().attr("href").split("uid=")[1]);
        }

        getPostPid() {
            return parseInt($(this._post_div).attr("id").split("_")[1]);
        }

        getGotoUrl() {
            // return `https://www.hi-pda.com/forum/redirect.php?goto=findpost&ptid=${this.threadTid}&pid=${this.getPostPid()}`;
            return `https://www.4d4y.com/forum/redirect.php?goto=findpost&ptid=${this.threadTid}&pid=${this.getPostPid()}`;
        }

        getPostContent() {
            // get text without quotes
            let t = $(this._post_div).find("td.t_msgfont").first().clone();
            t.find('.quote').replaceWith("<p>【引用内容】</p>");
            t.find('.t_attach').replaceWith("<p>【附件】</p>");
            t.find('img').remove();
            let text = t.text().replace(/\n+/g, "\n").trim();
            return text;
        }

        getPostBrief(n) {
            let content = this.getPostContent();
            if (content.length <= n) {
                return content;
            }
            return content.slice(0, n) + "\n\n【以上为截取片段】";
        }

        getOriginalTimestamp(use_string = false) {
            let dt = $(this._post_div).find("div.authorinfo > em").text().trim().split(" ").slice(1, 3);
            if (use_string) {
                return dt.join(" ");
            }
            return getEpoch(dt[0], dt[1]);
        }

        getLastTimestamp(use_string = false) {
            let ele = $(this._post_div).find("i.pstatus").get();
            if (ele.length !== 0) {
                let dt = $(this._post_div).find("i.pstatus").text().trim().split(" ").slice(3, 5);
                if (use_string) {
                    return dt.join(" ");
                }
                return getEpoch(dt[0], dt[1]);
            }
            return null;
        }

        getTimestamp(use_string = false) {
            // get last edit time
            let lastTimestamp = this.getLastTimestamp(use_string);
            return lastTimestamp ? lastTimestamp : this.getOriginalTimestamp(use_string);
        }

        addNoteUI(_notebook) {
            let uid = this.getPostAuthorUid();
            let index = $(this._post_div).index();
            let userName = this.getPostAuthorName();

            var that = this;
            // create an UI element which contains data and hooks
            // button
            let button = htmlToElement(`
              <button id="noteButton_${index}" style="color:grey; margin-left:20px;">
                ID笔记
              </button>
            `);
            // note dialog
            let dialog = htmlToElement(`
              <div id="noteDialog_${index}" style="display: none;">
                <textarea rows="10" wrap="hard" placeholder="暂时没有笔记">
              </div>
            `);
            $("body").append(dialog);

            // add event to button
            $(document).ready(function() {
                $(document).on("click", `#noteButton_${index}`, async function() {
                    // try to sync DB
                    if (!_notebook._synced) {
                        try {
                            await _notebook.sync_server(uid);
                        } catch (err) {
                            console.log(err);
                        }
                    }
                    console.log("open note for", userName);
                    // freshly fetched from DB
                    $(`#noteDialog_${index}`).find('textarea').first().val(_notebook.get(uid));
                    $(`#noteDialog_${index}`).dialog({
                        title: `ID笔记:${userName}`,
                        dialogClass: "no-close",
                        closeText: "hide",
                        closeOnEscape: true,
                        height: Math.max(parseInt($(window).height() * 0.4), 350),
                        width: Math.max(parseInt($(window).width() * 0.4), 600),
                        buttons: [{
                                text: "插入当前楼层",
                                click: function() {
                                    let txt = $(`#noteDialog_${index}`).find('textarea').first();
                                    var caretPos = txt[0].selectionStart;
                                    var textAreaTxt = txt.val();
                                    var txtToAdd = `\n====\n引用: ${that.getGotoUrl()}\n【${that.getTimestamp(true)}】\n${that.getPostAuthorName()} 在《${that.threadTitle}》中说:\n ${that.getPostBrief(200)}\n====\n`;
                                    txt.val(textAreaTxt.substring(0, caretPos) + txtToAdd + textAreaTxt.substring(caretPos));
                                }
                            },
                            {
                                text: "确认",
                                click: function() {
                                    // save the new note before close
                                    let newNote = $(`#noteDialog_${index}`).find('textarea').first().val();
                                    if (newNote.length === 0) {
                                        _notebook.delete(uid);
                                    } else {
                                        _notebook.put(uid, userName, newNote);
                                    }
                                    $(this).dialog("close");
                                }
                            },
                            {
                                text: "取消",
                                click: function() {
                                    // close without saving
                                    $(this).dialog("close");
                                }
                            }
                        ]
                    });
                });
            });

            // add UI
            let d = $(this._post_div).find("td[rowspan='2'].postauthor").first();
            d.append(button);
        }

    }

    class NotebookClient {
        // used to connect to the server
        constructor(UID, apiKey) {
            this.UID = String(UID);
            this.apiKey = apiKey;
        }

        get() {
            return new Promise((resolve, reject) => {
                GM.xmlHttpRequest({
                    method: "GET",
                    url: `${this.apiKey}`,
                    onload: function(response) {
                        let data = response.responseText;
                        if (response.status === 200) {
                            resolve(data);
                        } else {
                            reject(data);
                        }
                    }
                });
            });
        }

        put(payload) {
            return new Promise((resolve, reject) => {
                let d = {
                    note: payload
                };
                GM.xmlHttpRequest({
                    method: "POST",
                    url: `${this.apiKey}`,
                    data: JSON.stringify(d),
                    headers: {
                        "Content-Type": "application/json"
                    },
                    onload: function(response) {
                        let data = response.responseText;
                        if (response.status === 200) {
                            resolve(data);
                        } else {
                            reject(data);
                        }
                    }
                });
            });

        }

    }

    class Notebook {
        // notebook data structure:
        //     this._notebook[uid] = {uid, userName, note};
        constructor(UID) {
            // initialization
            this._name = "hipda-notebook";
            this._keyname = "hipda-notebook-key";
            this._timestamp_name = "hipda-notebook-timestamp";
            this._uid = UID;
            this._key = null;
            this._client = null;
            this._notebook = {};
            this._synced = false;
            return (async () => {
                this.loadFromLocalStorage();
                this._key = await this.getApiKey();
                return this;
            })();
        }

        async sync_server() {
            showLoader();
            await this._sync_server();
            hideLoader();
        }

        async _sync_server() {
            if (GM.xmlHttpRequest === undefined) {
                console.log("浏览器不支持连接服务器");
                return;
            }
            if (this._key === null) {
                return;
            }
            let client = new NotebookClient(this._uid, this._key);
            let data;
            try {
                data = await client.get();
            } catch (err) {
                console.log(err);
                this._synced = true;
            }
            function isServerDataValid(data) {
                if (data === undefined || data === '') {
                    return false;
                }
                try {
                    let serverVal = JSON.parse(JSON.parse(data).note)
                    if (serverVal.timestamp === undefined) {
                        return false
                    }
                } catch {
                    return false;
                }
                return true;
            }
            if (!isServerDataValid(data)) {
                // initialize in server
                let payload = await this.exportNotebook();
                let data = await client.put(payload);
                console.log("initialize record in server");
                console.log("server:", data);
            } else {
                // check timestamp
                let serverVal = JSON.parse(JSON.parse(data).note)
                let serverTimestamp = serverVal.timestamp;
                let localTimestamp = await this.getTimestamp();
                if (localTimestamp === null || localTimestamp < serverTimestamp) {
                    // import from server
                    this.importNotebook(serverVal);
                    console.log("import record from server");
                } else if (localTimestamp > serverTimestamp) {
                    // push to server
                    let payload = await this.exportNotebook();
                    let data = await client.put(payload);
                    console.log("update record in server");
                    console.log("server:", data);
                } else {
                    console.log("already up-to-date");
                }
            }
            this._synced = true;
        }

        async getTimestamp() {
            let data = await GM.getValue(this._timestamp_name, null);
            return data;
        }

        async setTimestamp() {
            await GM.setValue(this._timestamp_name, +new Date());
        }

        async getApiKey() {
            console.log("load ID Notebook API key from Local Storage");
            let data = await GM.getValue(this._keyname, null);
            return data;
        }

        async setApiKey(apiKey) {
            console.log("save ID Notebook API key to Local Storage");
            if (apiKey === "") {
                await GM.deleteValue(this._keyname);
                this._key = null;
            } else {
                await GM.setValue(this._keyname, apiKey);
                this._key = apiKey;
            }
        }

        async loadFromLocalStorage() {
            console.log("load ID Notebook from Local Storage");
            let data = await GM.getValue(this._name, null);
            if (data !== null) {
                this._notebook = JSON.parse(data);
            }
        }

        async saveToLocalStorage() {
            console.log("save ID Notebook to Local Storage");
            await GM.setValue(this._name, JSON.stringify(this._notebook));
            await this.setTimestamp();
            await this.sync_server();
        }

        put(uid, userName, note) {
            // we need userName here, so user can analyze notes even after export
            this._notebook[uid] = {
                uid,
                userName,
                note
            };
            this.saveToLocalStorage();
        }

        get(uid) {
            if (uid in this._notebook) {
                return this._notebook[uid].note;
            }
            return "";
        }

        delete(uid) {
            if (uid in this._notebook) {
                delete this._notebook[uid];
                this.saveToLocalStorage();
            }
        }

        getNotesByUsername(userName) {
            if (userName.length === 0) {
                return [];
            }

            function compareFn(a, b) {
                if (a.userName < b.userName) {
                    return -1;
                }
                if (a.userName > b.userName) {
                    return 1;
                }
                return 0;

            }
            return Object.values(this._notebook).filter(x => x.userName.toLocaleLowerCase().indexOf(userName.toLocaleLowerCase()) !== -1).sort(compareFn);
        }

        getNotesByKeyword(keyword) {
            if (keyword.length === 0) {
                return [];
            }

            function compareFn(a, b) {
                if (a.note < b.userName) {
                    return -1;
                }
                if (a.userName > b.userName) {
                    return 1;
                }
                return 0;

            }
            return Object.values(this._notebook).filter(x => x.note.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) !== -1).sort(compareFn);
        }

        async exportNotebook() {
            // can add meta data here
            let timestamp = await this.getTimestamp()
            let output = {
                notebook: this._notebook,
                version: GM_info.script.version,
                timestamp: timestamp
            };
            return JSON.stringify(output);
        }

        importNotebook(input) {
            let attrs = ['notebook', 'version', 'timestamp'];
            for (let i = 0; i < attrs.length; i++) {
                if (!input.hasOwnProperty(attrs[i])) {
                    throw (`bad format: ${attrs[i]} does not exist`);
                }
            }
            this._notebook = {
                ...input.notebook
            };
            this.saveToLocalStorage();
        }

        resetNotebook() {
            this._notebook = {};
            this.saveToLocalStorage();
        }

        getNotebookStat() {
            return {
                'note_number': Object.keys(this._notebook).length,
                'size_kb': (new TextEncoder().encode(this.exportNotebook())).length / 1024
            };
        }

        migrate() {
            // update all hi-pda urls to 4d4y urls
            Object.keys(this._notebook).forEach(uid => {
                let oldVal = this._notebook[uid].note;
                let newVal = oldVal.replace('www.hi-pda.com/forum/', 'www.4d4y.com/forum/');
                this._notebook[uid].note = newVal;
            });
        }
    }

    async function main() {

        // get a thread object
        var THIS_THREAD = new HpThread();
        var notebook = await new Notebook(THIS_THREAD.getUserUid());

        // notebook browser
        THIS_THREAD.addNoteBrowserUI(notebook);
        // management panel
        THIS_THREAD.addNoteManagementUI(notebook);

        // render UI below
        // ID notes
        var hp_posts = THIS_THREAD.getHpPosts();
        for (let i = 0; i < hp_posts.length; i++) {
            let hp_post = hp_posts[i];
            try {
                hp_post.addNoteUI(notebook);
            } catch (e) {
                // deleted post, simply pass it
                console.log("unable to parse the post, pass");
            }

        }



    }

    main();


})();