// ==UserScript== // @name hipda-ID笔记 // @namespace http://tampermonkey.net/ // @version 0.7.0 // @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 // @downloadURL none // ==/UserScript== (function() { 'use strict'; // CONST const SERVER_ENDPOINT = 'https://hp-notebook-server.onrender.com'; 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;}"); // Your code here... // helpers 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($(`
【引用内容】
" ); t.find('.t_attach').replaceWith( "【附件】
" ); 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(` `); // note dialog let dialog = htmlToElement(` `); $("body").append(dialog); // add event to button $(document).ready( function () { $(document).on ("click", `#noteButton_${index}`, async function () { // try to sync DB if (!_notebook._synced) { await _notebook.sync_server(uid); } 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.endpoint = SERVER_ENDPOINT; this.UID = String(UID); this.apiKey = apiKey; } get() { return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: "GET", url: `${this.endpoint}/get?UID=${this.UID}&key=${this.apiKey}`, onload: function(response) { let data = response.responseText; resolve(data); } }); }); } put(payload) { return new Promise((resolve, reject) => { let d = {UID: this.UID, key: this.apiKey, note: payload}; GM.xmlHttpRequest({ method: "POST", url: `${this.endpoint}/put`, data: JSON.stringify(d), headers: { "Content-Type": "application/json" }, onload: function(response) { let data = response.responseText; resolve(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() { if (this._key === null) { return; } let client = new NotebookClient(this._uid, this._key); let data = await client.get(); if (data === undefined) { // initialize in server let data = await client.put(this.exportNotebook()); console.log("initialize record in server"); console.log("server:", data); } else { // check timestamp let serverVal = JSON.parse(data); 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"); 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