hipda-ID笔记

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

目前为 2024-07-27 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name hipda-ID笔记
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.1
  5. // @description 来自地板带着爱,记录上网冲浪的美好瞬间
  6. // @author 屋大维
  7. // @license MIT
  8. // @match https://www.hi-pda.com/forum/*
  9. // @match https://www.4d4y.com/forum/*
  10. // @resource IMPORTED_CSS https://code.jquery.com/ui/1.13.0/themes/base/jquery-ui.css
  11. // @require https://code.jquery.com/jquery-3.4.1.min.js
  12. // @require https://code.jquery.com/ui/1.13.0/jquery-ui.js
  13. // @icon https://icons.iconarchive.com/icons/iconshock/real-vista-project-managment/64/task-notes-icon.png
  14. // @grant GM.setValue
  15. // @grant GM.getValue
  16. // @grant GM.deleteValue
  17. // @grant GM_getResourceText
  18. // @grant GM_addStyle
  19. // @grant GM.xmlHttpRequest
  20. // ==/UserScript==
  21. (function() {
  22. 'use strict';
  23. // CONST
  24. const BROWSER_KEY = 'alt+I';
  25. const MANAGEMENT_KEY = "alt+U";
  26.  
  27. // CSS
  28. const my_css = GM_getResourceText("IMPORTED_CSS");
  29. GM_addStyle(my_css);
  30. 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}");
  31. 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;}");
  32. 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)}}`);
  33.  
  34. // Your code here...
  35. // helpers
  36. function showLoader() {
  37. let loader = $(`<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>`);
  38. $("body").append(loader);
  39. }
  40.  
  41. function hideLoader() {
  42. $(".lds-roller").remove();
  43. }
  44.  
  45. function getKeys(e) { // keycode 转换
  46. var codetable = {
  47. '96': 'Numpad 0',
  48. '97': 'Numpad 1',
  49. '98': 'Numpad 2',
  50. '99': 'Numpad 3',
  51. '100': 'Numpad 4',
  52. '101': 'Numpad 5',
  53. '102': 'Numpad 6',
  54. '103': 'Numpad 7',
  55. '104': 'Numpad 8',
  56. '105': 'Numpad 9',
  57. '106': 'Numpad *',
  58. '107': 'Numpad +',
  59. '108': 'Numpad Enter',
  60. '109': 'Numpad -',
  61. '110': 'Numpad .',
  62. '111': 'Numpad /',
  63. '112': 'F1',
  64. '113': 'F2',
  65. '114': 'F3',
  66. '115': 'F4',
  67. '116': 'F5',
  68. '117': 'F6',
  69. '118': 'F7',
  70. '119': 'F8',
  71. '120': 'F9',
  72. '121': 'F10',
  73. '122': 'F11',
  74. '123': 'F12',
  75. '8': 'BackSpace',
  76. '9': 'Tab',
  77. '12': 'Clear',
  78. '13': 'Enter',
  79. '16': 'Shift',
  80. '17': 'Ctrl',
  81. '18': 'Alt',
  82. '20': 'Cape Lock',
  83. '27': 'Esc',
  84. '32': 'Spacebar',
  85. '33': 'Page Up',
  86. '34': 'Page Down',
  87. '35': 'End',
  88. '36': 'Home',
  89. '37': '←',
  90. '38': '↑',
  91. '39': '→',
  92. '40': '↓',
  93. '45': 'Insert',
  94. '46': 'Delete',
  95. '144': 'Num Lock',
  96. '186': ';:',
  97. '187': '=+',
  98. '188': ',<',
  99. '189': '-_',
  100. '190': '.>',
  101. '191': '/?',
  102. '192': '`~',
  103. '219': '[{',
  104. '220': '\|',
  105. '221': ']}',
  106. '222': '"'
  107. };
  108. var Keys = '';
  109. e.shiftKey && (e.keyCode != 16) && (Keys += 'shift+');
  110. e.ctrlKey && (e.keyCode != 17) && (Keys += 'ctrl+');
  111. e.altKey && (e.keyCode != 18) && (Keys += 'alt+');
  112. return Keys + (codetable[e.keyCode] || String.fromCharCode(e.keyCode) || '');
  113. };
  114.  
  115. function addHotKey(codes, func) { // 监视并执行快捷键对应的函数
  116. document.addEventListener('keydown', function(e) {
  117. if ((e.target.tagName != 'INPUT') && (e.target.tagName != 'TEXTAREA') && getKeys(e) == codes) {
  118. func();
  119. e.preventDefault();
  120. e.stopPropagation();
  121. }
  122. }, false);
  123. };
  124.  
  125. function htmlToElement(html) {
  126. var template = document.createElement('template');
  127. html = html.trim(); // Never return a text node of whitespace as the result
  128. template.innerHTML = html;
  129. return template.content.firstChild;
  130. }
  131.  
  132. function getEpoch(date_str, time_str) {
  133. let [y, m, d] = date_str.split("-").map(x => parseInt(x));
  134. let [H, M] = time_str.split(":").map(x => parseInt(x));
  135. return new Date(y, m - 1, d, H, M, 0).getTime() / 1000;
  136. }
  137.  
  138. // classes
  139. class HpThread {
  140. constructor() {}
  141.  
  142. getThreadTid() {
  143. return location.href.match(/tid=(\d+)/) ? parseInt(location.href.match(/tid=(\d+)/)[1]) : -999;
  144. }
  145.  
  146. getUserUid() {
  147. return parseInt($("cite > a").attr("href").split("uid=")[1]);
  148. }
  149.  
  150. getThreadTitle() {
  151. let l = $('#nav').text().split(" » ");
  152. return l[l.length - 1];
  153. }
  154.  
  155. getHpPosts() {
  156. let threadTid = this.getThreadTid();
  157. let threadTitle = this.getThreadTitle();
  158. let divs = $('#postlist > div').get();
  159. return divs.map(d => new HpPost(threadTid, threadTitle, d));
  160. }
  161.  
  162. addNoteBrowserUI(_notebook) {
  163. $('#menu>ul').append($(`<li class="menu_2"><a href="javascript:void(0)" hidefocus="true" id="noteButton_browser">搜索笔记</a></li>`));
  164. var that = this;
  165. // create dialog
  166. let dialog = htmlToElement(`
  167. <div id="noteDialog_browser" style="display: none;">
  168. <div id="noteDialog_browser_search_bar" style="width: 80%; margin: 20px auto 20px auto;">
  169. <select style="display: inline-block;" name="searchMethod" id="noteDialog_browser_search_method">
  170. <option value="content">笔记内容</option>
  171. <option value="userName">用户名</option>
  172. </select>
  173. <input type="text" autofocus="true" style="display: inline-block; width: 300px;" id="noteDialog_browser_search_input">
  174. </div>
  175. <div id="noteDialog_browser_note_list" style="width: 95%; margin: 10px auto 10px auto;" class="flex-container">
  176. </div>
  177. </div>
  178. `);
  179. $("body").append(dialog);
  180.  
  181. function updateNoteList() {
  182. $('#noteDialog_browser_note_list').empty(); // remove all notes from the list
  183. var notes;
  184. var searchMethod = $('#noteDialog_browser_search_method').val();
  185. var searchInput = $('#noteDialog_browser_search_input').val();
  186. if (searchMethod === "userName") {
  187. notes = _notebook.getNotesByUsername(searchInput);
  188. } else if (searchMethod === "content") {
  189. notes = _notebook.getNotesByKeyword(searchInput);
  190. } else {
  191. return;
  192. }
  193. console.log(notes.length)
  194. for (let i = 0; i < notes.length; i++) {
  195. let element = noteToHtmlElement(notes[i]);
  196. $('#noteDialog_browser_note_list').append(element);
  197. }
  198. }
  199.  
  200. function noteToHtmlElement(note) {
  201. var searchMethod = $('#noteDialog_browser_search_method').val();
  202. var searchInput = $('#noteDialog_browser_search_input').val();
  203. var userName = note.userName;
  204. var uid = note.uid;
  205. var content = note.note;
  206. if (searchMethod === 'userName') {
  207. userName = userName.replaceAll(searchInput, '<mark class="highlight">$&</mark>');
  208. }
  209. if (searchMethod === 'content') {
  210. content = content.replaceAll(searchInput, '<mark class="highlight">$&</mark>');
  211. }
  212. // highlight all URLs
  213. var expression = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi;
  214. var regex = new RegExp(expression);
  215. content = content.replace(regex, '<a style="color:blue;" href="$&" target="_blank">$&</a>')
  216.  
  217. var html = `
  218. <div class="card">
  219. <div style="font-size: 2em; float: left; margin: 10px;">${userName}</div>
  220. <div style="float: right;">
  221. <button class="noteEditButton">编辑</button>
  222. <button class="noteDeleteButton" style="margin-right: 2px;">删除</button>
  223. </div>
  224. <div class="container" style="word-break: break-all; white-space: pre-wrap;">
  225. ${content}
  226. </div>
  227. </div>
  228. `;
  229. var element = $(html);
  230. // delete
  231. element.find("button.noteDeleteButton").click(function() {
  232. let r = confirm(`确定要删除 ${note.userName} ID笔记吗?`);
  233. if (!r) {
  234. return;
  235. }
  236. _notebook.delete(uid);
  237. updateNoteList();
  238. });
  239. // edit
  240. element.find("button.noteEditButton").click(function() {
  241. // note dialog (this will be different from the one opened in posts)
  242. let dialog = htmlToElement(`
  243. <div id="noteDialog_${uid}" style="display: none;">
  244. <textarea rows="10" wrap="hard" placeholder="暂时没有笔记">
  245. </div>
  246. `);
  247. $("body").append(dialog);
  248.  
  249. // bind event listener
  250. console.log("open note for", userName);
  251. // freshly fetched from DB
  252. $(`#noteDialog_${uid}`).find('textarea').first().val(_notebook.get(uid));
  253. $(`#noteDialog_${uid}`).dialog({
  254. title: `ID笔记:${userName}`,
  255. dialogClass: "no-close",
  256. closeText: "hide",
  257. closeOnEscape: true,
  258. height: Math.max(parseInt($(window).height() * 0.4), 350),
  259. width: Math.max(parseInt($(window).width() * 0.4), 600),
  260. buttons: [{
  261. text: "确认",
  262. click: function() {
  263. // save the new note before close
  264. let newNote = $(`#noteDialog_${uid}`).find('textarea').first().val();
  265. if (newNote.length === 0) {
  266. _notebook.delete(uid);
  267. } else {
  268. _notebook.put(uid, userName, newNote);
  269. }
  270. $(this).dialog("close");
  271. // update the Note List
  272. updateNoteList();
  273. }
  274. },
  275. {
  276. text: "取消",
  277. click: function() {
  278. // close without saving
  279. $(this).dialog("close");
  280. }
  281. }
  282. ]
  283. });
  284.  
  285. });
  286. return element;
  287. }
  288.  
  289. function openBrowser() {
  290. $('#menu>ul>li').first().removeClass("current");
  291. $('#menu>ul>li').first().addClass("menu_2");
  292. $('#noteButton_browser').parent().removeClass("menu_2");
  293. $('#noteButton_browser').parent().addClass("current");
  294. console.log("open notebook browser dialog");
  295.  
  296. $(`#noteDialog_browser`).dialog({
  297. title: "ID笔记:浏览器",
  298. modal: true,
  299. height: parseInt($(window).height() * 0.8),
  300. width: parseInt($(window).width() * 0.8),
  301. closeOnEscape: true,
  302. open: function(event, ui) {
  303. $('.ui-widget-overlay').css("background-color", "black");
  304. $('.ui-widget-overlay').css("opacity", "0.6");
  305. },
  306. close: function(event, ui) {
  307. $('#menu>ul>li').first().removeClass("menu_2");
  308. $('#menu>ul>li').first().addClass("current");
  309. $('#noteButton_browser').parent().removeClass("current");
  310. $('#noteButton_browser').parent().addClass("menu_2");
  311. }
  312. });
  313. }
  314.  
  315. $(document).ready(function() {
  316. $('#noteDialog_browser_search_input').on("input", () => {
  317. updateNoteList();
  318. });
  319. $('#noteDialog_browser_search_method').change(() => {
  320. updateNoteList();
  321. });
  322. $(document).on("click", `#noteButton_browser`, function() {
  323. openBrowser();
  324. });
  325. // HOTKEY
  326. addHotKey(BROWSER_KEY, openBrowser);
  327. });
  328.  
  329. }
  330.  
  331. addNoteManagementUI(_notebook) {
  332. var that = this;
  333. var button = htmlToElement(`
  334. <button id="noteButton_management">
  335. <span><img src="https://icons.iconarchive.com/icons/iconshock/real-vista-project-managment/32/task-notes-icon.png"></img></span>
  336. </button>
  337. `);
  338.  
  339. // create dialog
  340. let dialog = htmlToElement(`
  341. <div id="noteDialog_management" style="display: none;">
  342. <h3>hipda-ID笔记 v${GM_info.script.version}</h3>
  343. <p style="margin: 10px auto 10px auto;">来自地板带着爱</p>
  344. <p id="noteStat" style="margin: 10px auto 10px auto;"></p>
  345. <div>
  346. <button id="noteButton_import">导入</button>
  347. <button id="noteButton_export">导出</button>
  348. <button id="noteButton_reset">重置</button>
  349. <button id="noteButton_migrate">4d4y</button>
  350. <button id="noteButton_server">服务器</button>
  351. <input type="hidden" autofocus="true" />
  352. </div>
  353. </div>
  354. `);
  355. $("body").append(dialog);
  356.  
  357. function updateNoteStat() {
  358. let note_stat = _notebook.getNotebookStat();
  359. let synced = _notebook._synced;
  360. $(`#noteStat`).text(`共${note_stat.note_number}条ID笔记,大小为${(note_stat.size_kb).toFixed(2)}KB${synced ? " (已同步)" : ""}`);
  361. }
  362.  
  363. function openManagement() {
  364. console.log("open notebook management dialog");
  365. // update statistics
  366. updateNoteStat();
  367.  
  368. $(`#noteDialog_management`).dialog({
  369. title: "ID笔记:管理面板",
  370. height: 200,
  371. width: 300,
  372. closeOnEscape: true,
  373. });
  374. }
  375.  
  376. $(document).ready(function() {
  377. $(document).on("click", "#noteButton_server", async function() {
  378. let apiKey = await _notebook.getApiKey();
  379. let data = prompt("请将 API链接 输入文本框:", apiKey ? apiKey : "");
  380. if (data !== null) {
  381. // try to load
  382. try {
  383. _notebook.setApiKey(data);
  384. } catch (err) {
  385. alert("格式错误!" + err);
  386. return;
  387. }
  388. alert("导入成功!");
  389. }
  390. });
  391. $(document).on("click", "#noteButton_migrate", function() {
  392. let r = confirm("确定要从hi-pda迁移到4d4y吗?");
  393. if (!r) {
  394. return;
  395. }
  396. _notebook.migrate();
  397. alert("迁移成功!");
  398. });
  399. $(document).on("click", "#noteButton_import", function() {
  400. let r = confirm("确定要导入ID笔记吗?现有笔记将会被覆盖!");
  401. if (!r) {
  402. return;
  403. }
  404.  
  405. // prompt cannot handle large file, extend it in the future
  406. let data = prompt("请将 id笔记.json 中的文本复制粘贴入文本框:");
  407. if (data !== null) {
  408. // try to load
  409. try {
  410. let j = JSON.parse(data);
  411. _notebook.importNotebook(j);
  412. } catch (err) {
  413. alert("格式错误!" + err);
  414. return;
  415. }
  416. alert("导入成功!");
  417. updateNoteStat();
  418. }
  419. });
  420. $(document).on("click", "#noteButton_export", async function() {
  421. let r = confirm("确定要导出ID笔记吗?");
  422. if (!r) {
  423. return;
  424. }
  425. let a = document.createElement("a");
  426. let data = await _notebook.exportNotebook();
  427. a.href = "data:text," + encodeURIComponent(data);
  428. a.download = "id笔记.json";
  429. a.click();
  430. });
  431. $(document).on("click", "#noteButton_reset", function() {
  432. let r = confirm("确定要清空ID笔记吗?");
  433. if (!r) {
  434. return;
  435. }
  436. _notebook.resetNotebook();
  437. alert("ID笔记已经清空!");
  438. updateNoteStat();
  439. });
  440. $(document).on("click", `#noteButton_management`, function() {
  441. openManagement();
  442. });
  443. // HOTKEY
  444. addHotKey(MANAGEMENT_KEY, openManagement);
  445. });
  446.  
  447. // add UI
  448. let d = $("td.modaction").last();
  449. d.append(button);
  450.  
  451. }
  452.  
  453. }
  454.  
  455. class HpPost {
  456. constructor(threadTid, threadTitle, postDiv) {
  457. this.threadTid = threadTid;
  458. this.threadTitle = threadTitle;
  459. this._post_div = postDiv;
  460. }
  461.  
  462. getPostAuthorName() {
  463. return $(this._post_div).find("div.postinfo > a").first().text();
  464. }
  465.  
  466. getPostAuthorUid() {
  467. return parseInt($(this._post_div).find("div.postinfo > a").first().attr("href").split("uid=")[1]);
  468. }
  469.  
  470. getPostPid() {
  471. return parseInt($(this._post_div).attr("id").split("_")[1]);
  472. }
  473.  
  474. getGotoUrl() {
  475. // return `https://www.hi-pda.com/forum/redirect.php?goto=findpost&ptid=${this.threadTid}&pid=${this.getPostPid()}`;
  476. return `https://www.4d4y.com/forum/redirect.php?goto=findpost&ptid=${this.threadTid}&pid=${this.getPostPid()}`;
  477. }
  478.  
  479. getPostContent() {
  480. // get text without quotes
  481. let t = $(this._post_div).find("td.t_msgfont").first().clone();
  482. t.find('.quote').replaceWith("<p>【引用内容】</p>");
  483. t.find('.t_attach').replaceWith("<p>【附件】</p>");
  484. t.find('img').remove();
  485. let text = t.text().replace(/\n+/g, "\n").trim();
  486. return text;
  487. }
  488.  
  489. getPostBrief(n) {
  490. let content = this.getPostContent();
  491. if (content.length <= n) {
  492. return content;
  493. }
  494. return content.slice(0, n) + "\n\n【以上为截取片段】";
  495. }
  496.  
  497. getOriginalTimestamp(use_string = false) {
  498. let dt = $(this._post_div).find("div.authorinfo > em").text().trim().split(" ").slice(1, 3);
  499. if (use_string) {
  500. return dt.join(" ");
  501. }
  502. return getEpoch(dt[0], dt[1]);
  503. }
  504.  
  505. getLastTimestamp(use_string = false) {
  506. let ele = $(this._post_div).find("i.pstatus").get();
  507. if (ele.length !== 0) {
  508. let dt = $(this._post_div).find("i.pstatus").text().trim().split(" ").slice(3, 5);
  509. if (use_string) {
  510. return dt.join(" ");
  511. }
  512. return getEpoch(dt[0], dt[1]);
  513. }
  514. return null;
  515. }
  516.  
  517. getTimestamp(use_string = false) {
  518. // get last edit time
  519. let lastTimestamp = this.getLastTimestamp(use_string);
  520. return lastTimestamp ? lastTimestamp : this.getOriginalTimestamp(use_string);
  521. }
  522.  
  523. addNoteUI(_notebook) {
  524. let uid = this.getPostAuthorUid();
  525. let index = $(this._post_div).index();
  526. let userName = this.getPostAuthorName();
  527.  
  528. var that = this;
  529. // create an UI element which contains data and hooks
  530. // button
  531. let button = htmlToElement(`
  532. <button id="noteButton_${index}" style="color:grey; margin-left:20px;">
  533. ID笔记
  534. </button>
  535. `);
  536. // note dialog
  537. let dialog = htmlToElement(`
  538. <div id="noteDialog_${index}" style="display: none;">
  539. <textarea rows="10" wrap="hard" placeholder="暂时没有笔记">
  540. </div>
  541. `);
  542. $("body").append(dialog);
  543.  
  544. // add event to button
  545. $(document).ready(function() {
  546. $(document).on("click", `#noteButton_${index}`, async function() {
  547. // try to sync DB
  548. if (!_notebook._synced) {
  549. try {
  550. await _notebook.sync_server(uid);
  551. } catch (err) {
  552. console.log(err);
  553. }
  554. }
  555. console.log("open note for", userName);
  556. // freshly fetched from DB
  557. $(`#noteDialog_${index}`).find('textarea').first().val(_notebook.get(uid));
  558. $(`#noteDialog_${index}`).dialog({
  559. title: `ID笔记:${userName}`,
  560. dialogClass: "no-close",
  561. closeText: "hide",
  562. closeOnEscape: true,
  563. height: Math.max(parseInt($(window).height() * 0.4), 350),
  564. width: Math.max(parseInt($(window).width() * 0.4), 600),
  565. buttons: [{
  566. text: "插入当前楼层",
  567. click: function() {
  568. let txt = $(`#noteDialog_${index}`).find('textarea').first();
  569. var caretPos = txt[0].selectionStart;
  570. var textAreaTxt = txt.val();
  571. var txtToAdd = `\n====\n引用: ${that.getGotoUrl()}\n${that.getTimestamp(true)}】\n${that.getPostAuthorName()} 在《${that.threadTitle}》中说:\n ${that.getPostBrief(200)}\n====\n`;
  572. txt.val(textAreaTxt.substring(0, caretPos) + txtToAdd + textAreaTxt.substring(caretPos));
  573. }
  574. },
  575. {
  576. text: "确认",
  577. click: function() {
  578. // save the new note before close
  579. let newNote = $(`#noteDialog_${index}`).find('textarea').first().val();
  580. if (newNote.length === 0) {
  581. _notebook.delete(uid);
  582. } else {
  583. _notebook.put(uid, userName, newNote);
  584. }
  585. $(this).dialog("close");
  586. }
  587. },
  588. {
  589. text: "取消",
  590. click: function() {
  591. // close without saving
  592. $(this).dialog("close");
  593. }
  594. }
  595. ]
  596. });
  597. });
  598. });
  599.  
  600. // add UI
  601. let d = $(this._post_div).find("td[rowspan='2'].postauthor").first();
  602. d.append(button);
  603. }
  604.  
  605. }
  606.  
  607. class NotebookClient {
  608. // used to connect to the server
  609. constructor(UID, apiKey) {
  610. this.UID = String(UID);
  611. this.apiKey = apiKey;
  612. }
  613.  
  614. get() {
  615. return new Promise((resolve, reject) => {
  616. GM.xmlHttpRequest({
  617. method: "GET",
  618. url: `${this.apiKey}`,
  619. onload: function(response) {
  620. let data = response.responseText;
  621. if (response.status === 200) {
  622. resolve(data);
  623. } else {
  624. reject(data);
  625. }
  626. }
  627. });
  628. });
  629. }
  630.  
  631. put(payload) {
  632. return new Promise((resolve, reject) => {
  633. let d = {
  634. note: payload
  635. };
  636. GM.xmlHttpRequest({
  637. method: "POST",
  638. url: `${this.apiKey}`,
  639. data: JSON.stringify(d),
  640. headers: {
  641. "Content-Type": "application/json"
  642. },
  643. onload: function(response) {
  644. let data = response.responseText;
  645. if (response.status === 200) {
  646. resolve(data);
  647. } else {
  648. reject(data);
  649. }
  650. }
  651. });
  652. });
  653.  
  654. }
  655.  
  656. }
  657.  
  658. class Notebook {
  659. // notebook data structure:
  660. // this._notebook[uid] = {uid, userName, note};
  661. constructor(UID) {
  662. // initialization
  663. this._name = "hipda-notebook";
  664. this._keyname = "hipda-notebook-key";
  665. this._timestamp_name = "hipda-notebook-timestamp";
  666. this._uid = UID;
  667. this._key = null;
  668. this._client = null;
  669. this._notebook = {};
  670. this._synced = false;
  671. return (async () => {
  672. this.loadFromLocalStorage();
  673. this._key = await this.getApiKey();
  674. return this;
  675. })();
  676. }
  677.  
  678. async sync_server() {
  679. showLoader();
  680. await this._sync_server();
  681. hideLoader();
  682. }
  683.  
  684. async _sync_server() {
  685. if (GM.xmlHttpRequest === undefined) {
  686. console.log("浏览器不支持连接服务器");
  687. return;
  688. }
  689. if (this._key === null) {
  690. return;
  691. }
  692. let client = new NotebookClient(this._uid, this._key);
  693. let data;
  694. try {
  695. data = await client.get();
  696. } catch (err) {
  697. console.log(err);
  698. this._synced = true;
  699. }
  700. function isServerDataValid(data) {
  701. if (data === undefined || data === '') {
  702. return false;
  703. }
  704. try {
  705. let serverVal = JSON.parse(JSON.parse(data).note)
  706. if (serverVal.timestamp === undefined) {
  707. return false
  708. }
  709. } catch {
  710. return false;
  711. }
  712. return true;
  713. }
  714. if (!isServerDataValid(data)) {
  715. // initialize in server
  716. let payload = await this.exportNotebook();
  717. let data = await client.put(payload);
  718. console.log("initialize record in server");
  719. console.log("server:", data);
  720. } else {
  721. // check timestamp
  722. let serverVal = JSON.parse(JSON.parse(data).note)
  723. let serverTimestamp = serverVal.timestamp;
  724. let localTimestamp = await this.getTimestamp();
  725. if (localTimestamp === null || localTimestamp < serverTimestamp) {
  726. // import from server
  727. this.importNotebook(serverVal);
  728. console.log("import record from server");
  729. } else if (localTimestamp > serverTimestamp) {
  730. // push to server
  731. let payload = await this.exportNotebook();
  732. let data = await client.put(payload);
  733. console.log("update record in server");
  734. console.log("server:", data);
  735. } else {
  736. console.log("already up-to-date");
  737. }
  738. }
  739. this._synced = true;
  740. }
  741.  
  742. async getTimestamp() {
  743. let data = await GM.getValue(this._timestamp_name, null);
  744. return data;
  745. }
  746.  
  747. async setTimestamp() {
  748. await GM.setValue(this._timestamp_name, +new Date());
  749. }
  750.  
  751. async getApiKey() {
  752. console.log("load ID Notebook API key from Local Storage");
  753. let data = await GM.getValue(this._keyname, null);
  754. return data;
  755. }
  756.  
  757. async setApiKey(apiKey) {
  758. console.log("save ID Notebook API key to Local Storage");
  759. if (apiKey === "") {
  760. await GM.deleteValue(this._keyname);
  761. this._key = null;
  762. } else {
  763. await GM.setValue(this._keyname, apiKey);
  764. this._key = apiKey;
  765. }
  766. }
  767.  
  768. async loadFromLocalStorage() {
  769. console.log("load ID Notebook from Local Storage");
  770. let data = await GM.getValue(this._name, null);
  771. if (data !== null) {
  772. this._notebook = JSON.parse(data);
  773. }
  774. }
  775.  
  776. async saveToLocalStorage() {
  777. console.log("save ID Notebook to Local Storage");
  778. await GM.setValue(this._name, JSON.stringify(this._notebook));
  779. await this.setTimestamp();
  780. await this.sync_server();
  781. }
  782.  
  783. put(uid, userName, note) {
  784. // we need userName here, so user can analyze notes even after export
  785. this._notebook[uid] = {
  786. uid,
  787. userName,
  788. note
  789. };
  790. this.saveToLocalStorage();
  791. }
  792.  
  793. get(uid) {
  794. if (uid in this._notebook) {
  795. return this._notebook[uid].note;
  796. }
  797. return "";
  798. }
  799.  
  800. delete(uid) {
  801. if (uid in this._notebook) {
  802. delete this._notebook[uid];
  803. this.saveToLocalStorage();
  804. }
  805. }
  806.  
  807. getNotesByUsername(userName) {
  808. if (userName.length === 0) {
  809. return [];
  810. }
  811.  
  812. function compareFn(a, b) {
  813. if (a.userName < b.userName) {
  814. return -1;
  815. }
  816. if (a.userName > b.userName) {
  817. return 1;
  818. }
  819. return 0;
  820.  
  821. }
  822. return Object.values(this._notebook).filter(x => x.userName.toLocaleLowerCase().indexOf(userName.toLocaleLowerCase()) !== -1).sort(compareFn);
  823. }
  824.  
  825. getNotesByKeyword(keyword) {
  826. if (keyword.length === 0) {
  827. return [];
  828. }
  829.  
  830. function compareFn(a, b) {
  831. if (a.note < b.userName) {
  832. return -1;
  833. }
  834. if (a.userName > b.userName) {
  835. return 1;
  836. }
  837. return 0;
  838.  
  839. }
  840. return Object.values(this._notebook).filter(x => x.note.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) !== -1).sort(compareFn);
  841. }
  842.  
  843. async exportNotebook() {
  844. // can add meta data here
  845. let timestamp = await this.getTimestamp()
  846. let output = {
  847. notebook: this._notebook,
  848. version: GM_info.script.version,
  849. timestamp: timestamp
  850. };
  851. return JSON.stringify(output);
  852. }
  853.  
  854. importNotebook(input) {
  855. let attrs = ['notebook', 'version', 'timestamp'];
  856. for (let i = 0; i < attrs.length; i++) {
  857. if (!input.hasOwnProperty(attrs[i])) {
  858. throw (`bad format: ${attrs[i]} does not exist`);
  859. }
  860. }
  861. this._notebook = {
  862. ...input.notebook
  863. };
  864. this.saveToLocalStorage();
  865. }
  866.  
  867. resetNotebook() {
  868. this._notebook = {};
  869. this.saveToLocalStorage();
  870. }
  871.  
  872. getNotebookStat() {
  873. return {
  874. 'note_number': Object.keys(this._notebook).length,
  875. 'size_kb': (new TextEncoder().encode(this.exportNotebook())).length / 1024
  876. };
  877. }
  878.  
  879. migrate() {
  880. // update all hi-pda urls to 4d4y urls
  881. Object.keys(this._notebook).forEach(uid => {
  882. let oldVal = this._notebook[uid].note;
  883. let newVal = oldVal.replace('www.hi-pda.com/forum/', 'www.4d4y.com/forum/');
  884. this._notebook[uid].note = newVal;
  885. });
  886. }
  887. }
  888.  
  889. async function main() {
  890.  
  891. // get a thread object
  892. var THIS_THREAD = new HpThread();
  893. var notebook = await new Notebook(THIS_THREAD.getUserUid());
  894.  
  895. // notebook browser
  896. THIS_THREAD.addNoteBrowserUI(notebook);
  897. // management panel
  898. THIS_THREAD.addNoteManagementUI(notebook);
  899.  
  900. // render UI below
  901. // ID notes
  902. var hp_posts = THIS_THREAD.getHpPosts();
  903. for (let i = 0; i < hp_posts.length; i++) {
  904. let hp_post = hp_posts[i];
  905. try {
  906. hp_post.addNoteUI(notebook);
  907. } catch (e) {
  908. // deleted post, simply pass it
  909. console.log("unable to parse the post, pass");
  910. }
  911.  
  912. }
  913.  
  914.  
  915.  
  916. }
  917.  
  918. main();
  919.  
  920.  
  921. })();