// ==UserScript==
// @name hipda-ID笔记
// @namespace http://tampermonkey.net/
// @version 0.3.0
// @description 来自地板带着爱,记录上网冲浪的美好瞬间
// @author 屋大维
// @license MIT
// @match https://www.hi-pda.com/forum/viewthread.php?tid*
// @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
// ==/UserScript==
(function() {
'use strict';
// 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}");
// Your code here...
// helpers
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));
}
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>
</div>
</div>
`);
$("body").append(dialog);
function updateNoteStat() {
let note_stat = _notebook.getNotebookStat();
$(`#noteStat`).text(`共${note_stat.note_number}条ID笔记,大小为${(note_stat.size_kb).toFixed(2)}KB`);
}
$(document).ready( function () {
$(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("导入成功!");
}
});
$(document).on ("click", "#noteButton_export", function() {
let r = confirm("确定要导出ID笔记吗?");
if (!r) {
return;
}
let a = document.createElement("a");
a.href = "data:text," + _notebook.exportNotebook();
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 () {
console.log("open notebook management dialog");
// update statistics
updateNoteStat();
$(`#noteDialog_management`).dialog({
title: "ID笔记:管理面板",
height: 200,
width: 300,
closeOnEscape: true,
});
});
});
// 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()}`;
}
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}`, function () {
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: 350,
width: 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();
_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 Notebook {
constructor(user_uid) {
// initialization
this._name = "hipda-notebook";
this._user_uid = user_uid;
this._notebook = {};
return (async () => {
this.loadFromLocalStorage();
return this;
})();
}
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));
}
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();
}
}
exportNotebook() {
// can add meta data here
let output = {
notebook: this._notebook,
version: GM_info.script.version,
timestamp: + new Date()
};
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
};
}
}
async function main() {
// get a thread object
var THIS_THREAD = new HpThread();
// get tid and uid; uid for future extension
var tid = THIS_THREAD.getThreadTid();
var uid = THIS_THREAD.getUserUid();
var notebook = await new Notebook(uid);
// 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");
}
}
// management panel
THIS_THREAD.addNoteManagementUI(notebook);
}
main();
})();