MH - Journal Log Tracker

Tracks when your journal log is going to show up next and shows a button to access your last journal log

当前为 2024-10-08 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MH - Journal Log Tracker
  3. // @version 1.3.0
  4. // @description Tracks when your journal log is going to show up next and shows a button to access your last journal log
  5. // @author hannadDev
  6. // @namespace https://greasyfork.org/en/users/1238393-hannaddev
  7. // @match https://www.mousehuntgame.com/*
  8. // @icon https://www.mousehuntgame.com/images/ui/journal/themes/classic_thumb.gif
  9. // @require https://cdn.jsdelivr.net/npm/mh-assets@1.0.8/scripts/utils.js
  10. // @require https://cdn.jsdelivr.net/npm/mousehunt-utils@1.10.5/mousehunt-utils.js
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // #region Variables
  18. let isDebug = false;
  19.  
  20. const localStorageKey = `mh-journal-log-tracker`;
  21. const PAGE_SIZE = 10;
  22.  
  23. let storedData = {};
  24. //#endregion
  25.  
  26. //#region Loading external assets
  27. const mainStylesheetUrl = "https://cdn.jsdelivr.net/npm/mh-assets@1.0.8/stylesheets/main.css";
  28. const scriptSpecificStylesheetUrl = "https://cdn.jsdelivr.net/npm/mh-assets@1.0.8/stylesheets/journal-log-tracker.css";
  29.  
  30. hd_utils.addStyleElement(mainStylesheetUrl);
  31. hd_utils.addStyleElement(scriptSpecificStylesheetUrl);
  32. //#endregion
  33.  
  34. //#region Initialization
  35. const journalContainerObserver = new MutationObserver(function (mutations) {
  36. if (isDebug) {
  37. console.log('[Journal Container Observer] Mutation');
  38. for (const mutation of mutations) {
  39. console.log({ mutation });
  40. console.log(mutation.target);
  41. }
  42. }
  43.  
  44. // Only save if something was added.
  45. if (mutations.some(v => v.type === 'childList' && v.addedNodes.length > 0 && v.target.className !== 'journaldate')) {
  46. tryToScrapeJournal();
  47. }
  48. });
  49.  
  50. const mousehuntContainerObserver = new MutationObserver(function (mutations) {
  51. if (isDebug) {
  52. console.log('[Mousehunt Container Observer] Mutation');
  53. for (const mutation of mutations) {
  54. console.log({ mutation });
  55. console.log(mutation.target);
  56. }
  57. }
  58.  
  59. // Check if camp or journal pages
  60. const mhContainer = document.getElementById('mousehuntContainer');
  61. if (mhContainer && mhContainer.classList && (mhContainer.classList.contains('PageCamp') || mhContainer.classList.contains('PageJournal'))) {
  62. showButton();
  63. }
  64. });
  65.  
  66. function activateJournalMutationObserver() {
  67. let journalContainerObserverTarget = document.querySelector(`#journalContainer[data-owner="${user.user_id}"] .content`);
  68.  
  69. if (journalContainerObserverTarget) {
  70. journalContainerObserver.observe(journalContainerObserverTarget, {
  71. childList: true,
  72. subtree: true
  73. });
  74. }
  75. }
  76.  
  77. function activateMousehuntContainerMutationObserver() {
  78. let mousehuntContainerObserverTarget = document.getElementById('mousehuntContainer');
  79.  
  80. if (mousehuntContainerObserverTarget) {
  81. mousehuntContainerObserver.observe(mousehuntContainerObserverTarget, {
  82. attributes: true,
  83. attributeFilter: ['class']
  84. });
  85. }
  86. }
  87.  
  88. function Initialize() {
  89. if (isDebug) console.log(`Initializing.`);
  90.  
  91. storedData = getStoredData();
  92.  
  93. activateMousehuntContainerMutationObserver();
  94. activateJournalMutationObserver();
  95. showButton();
  96.  
  97. onRequest(() => { tryToScrapeJournal(); }, 'managers/ajax/turns/activeturn.php');
  98. }
  99.  
  100. Initialize();
  101. //#endregion
  102.  
  103. // #region LocalStorage Methods
  104. function getStoredData() {
  105. const savedData = localStorage.getItem(localStorageKey);
  106.  
  107. if (savedData !== null) {
  108. return JSON.parse(savedData);
  109. }
  110.  
  111. return {
  112. logs: {},
  113. lastSavedEntryId: -1
  114. };
  115. }
  116.  
  117. function setData(stats) {
  118. localStorage.setItem(localStorageKey, JSON.stringify(stats));
  119. }
  120.  
  121. function deleteLog(logId) {
  122. delete storedData.logs[logId];
  123. if (Number.parseInt(storedData.lastSavedEntryId) === Number.parseInt(logId)) {
  124. const keys = Object.keys(storedData.logs);
  125.  
  126. storedData.lastSavedEntryId = -1;
  127. if (keys.length > 0) {
  128. for (let k = 0; k < keys.length; k++) {
  129. if (storedData.lastSavedEntryId < Number.parseInt(keys[k])) {
  130. storedData.lastSavedEntryId = Number.parseInt(keys[k]);
  131. }
  132. }
  133. }
  134. }
  135. }
  136. // #endregion
  137.  
  138. // #region Journal Scraping Methods
  139. function tryToScrapeJournal() {
  140. if (!hd_utils.mh.isOwnJournal()) {
  141. return;
  142. }
  143.  
  144. scrapeJournal();
  145. }
  146.  
  147. function scrapeJournal() {
  148. const entries = document.querySelectorAll('.entry');
  149.  
  150. let addedNewEntries = false;
  151. for (const entry of entries) {
  152. let entryId = entry.dataset.entryId
  153.  
  154. if (!entryId) return;
  155.  
  156. entryId = Number.parseInt(entryId);
  157.  
  158. if (entry.className.search(/(log_summary)/) !== -1) {
  159. if (storedData.logs.hasOwnProperty(entryId)) {
  160. if (isDebug) console.log(`Entry ${entryId} already stored`);
  161. }
  162. else {
  163. if (isDebug) console.log(`New entry ${entryId}`);
  164. const entryInfo = extractInfoFromEntry(entry);
  165.  
  166. if (entryInfo != null) {
  167. storedData.logs[entryId] = entryInfo;
  168.  
  169. if (storedData.lastSavedEntryId < entryId) {
  170. storedData.lastSavedEntryId = entryId;
  171. }
  172.  
  173. addedNewEntries = true;
  174. }
  175. }
  176. }
  177. }
  178.  
  179. if (addedNewEntries) {
  180. setData(storedData);
  181. showButton();
  182. }
  183. }
  184.  
  185. function extractInfoFromEntry(entry) {
  186. const entryInfo = {};
  187.  
  188. const splitStringDate = entry.querySelector(".journaldate").innerHTML.split("-")[0].trim().split(" ");
  189.  
  190. try {
  191. const date = new Date();
  192. date.setMilliseconds(0);
  193. date.setSeconds(0);
  194.  
  195. date.setHours(splitStringDate[0].split(":")[0]);
  196. date.setMinutes(splitStringDate[0].split(":")[1]);
  197.  
  198. if (date.getHours() !== 12 && splitStringDate[1] === "pm") {
  199. date.setHours(date.getHours() + 12);
  200. } else if (date.getHours() === 12 && splitStringDate[1] === "am") {
  201. date.setHours(date.getHours() - 12);
  202. }
  203.  
  204. if (date.getTime() > Date.now()) {
  205. date.setDate(date.getDate() - 1);
  206. }
  207.  
  208. entryInfo.Timestamp = date.getTime();
  209. } catch (e) {
  210. console.log(e);
  211. return null;
  212. }
  213.  
  214. entryInfo.Duration = entry.querySelector(".reportSubtitle").innerHTML.replace("Last ", "");
  215.  
  216. const tableBody = entry.querySelector(".journalbody .journaltext table tbody");
  217. const tdElements = tableBody.querySelectorAll(".leftSide, .rightSide");
  218. for (let i = 0; i < tdElements.length; ++i) {
  219. if (tdElements[i].innerHTML.includes("Catches:")) {
  220. entryInfo.Catches = Number.parseInt(tdElements[i].nextSibling.innerHTML);
  221. } else if (tdElements[i].innerHTML.includes("Misses:")) {
  222. entryInfo.Ftc = Number.parseInt(tdElements[i].nextSibling.innerHTML);
  223. } else if (tdElements[i].innerHTML.includes("Fail to Attract:")) {
  224. entryInfo.Fta = Number.parseInt(tdElements[i].nextSibling.innerHTML);
  225. } else if (tdElements[i].innerHTML.includes("Gained:")) {
  226. // Left is gold. Right is points
  227. if (tdElements[i].classList.contains('leftSide')) {
  228. entryInfo.GoldGained = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
  229. } else {
  230. entryInfo.PointsGained = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
  231. }
  232. } else if (tdElements[i].innerHTML.includes("Lost:")) {
  233. // Left is gold. Right is points
  234. if (tdElements[i].classList.contains('leftSide')) {
  235. entryInfo.GoldLost = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
  236. } else {
  237. entryInfo.PointsLost = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
  238. }
  239. } else if (tdElements[i].innerHTML.includes("Total:")) {
  240. // Left is gold. Right is points
  241. if (tdElements[i].classList.contains('leftSide')) {
  242. entryInfo.GoldTotal = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
  243. } else {
  244. entryInfo.PointsTotal = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
  245. }
  246. }
  247. }
  248.  
  249. const link = tableBody.querySelector("a");
  250.  
  251. entryInfo.OpenSummaryMethod = link.onclick.toString().split("onclick(event) {")[1].split("return false;")[0].trim();
  252.  
  253. return entryInfo;
  254. }
  255. // #endregion
  256.  
  257. // #region UI
  258. function showButton() {
  259. if (!hd_utils.mh.isOwnJournal()) {
  260. return;
  261. }
  262.  
  263. const olderButton = document.querySelector("#journal-log-button");
  264. if (olderButton) {
  265. olderButton.remove();
  266. }
  267.  
  268. const target = document.querySelector("#journalContainer .top");
  269. if (target) {
  270. const link = document.createElement("a");
  271. link.id = "journal-log-button";
  272.  
  273. link.innerText = `Next Log: ${getNextLogTimer()}`;
  274. link.href = "#";
  275. link.classList.add("hd-journal-log-button");
  276. link.addEventListener("click", function () {
  277. showLogs();
  278. });
  279. target.append(link);
  280. }
  281. }
  282.  
  283. function showLogs(page = 1, enableDeleteLogs = false) {
  284. document.querySelectorAll("#journal-logs-popup-div").forEach(el => el.remove());
  285.  
  286. const journalLogsPopup = document.createElement("div");
  287. journalLogsPopup.id = ("journal-logs-popup-div");
  288. journalLogsPopup.classList.add("hd-popup");
  289.  
  290. // Journal Logs Division
  291. const journalLogs = document.createElement("div");
  292.  
  293. // Title
  294. const title = document.createElement("h2");
  295. title.innerText = `Journal Log Tracker`
  296. title.classList.add("hd-bold");
  297. journalLogs.appendChild(title);
  298.  
  299. // Subtitle
  300. let nextLogDateString = "N/A";
  301. if (storedData.lastSavedEntryId !== undefined && storedData.logs[storedData.lastSavedEntryId] !== undefined) {
  302. const logDate = new Date(storedData.logs[storedData.lastSavedEntryId].Timestamp);
  303. logDate.setHours(logDate.getHours() + 36);
  304.  
  305. nextLogDateString = `${logDate.toLocaleString([], { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} - (${getNextLogTimer()})`;
  306. }
  307.  
  308. const subtitle = document.createElement("h4");
  309. subtitle.innerText = `Next Estimated Log: ${nextLogDateString}`;
  310. journalLogs.appendChild(subtitle);
  311.  
  312. journalLogs.appendChild(document.createElement("br"));
  313.  
  314. // Table for journal logs
  315. const journalLogsTable = document.createElement("table");
  316. journalLogsTable.id = "journal-logs-table"
  317. journalLogsTable.classList.add("hd-table");
  318.  
  319. const headings = ["#", "Date & Time", "Duration", "Catches", "FTC", "FTA", "Gold", "Points", "#"];
  320. const keys = ["", "Timestamp", "Duration", "Catches", "Ftc", "Fta", "GoldTotal", "PointsTotal"];
  321.  
  322. // Create headings
  323. for (let i = 0; i < headings.length; ++i) {
  324. if (!enableDeleteLogs && i == headings.length - 1) {
  325. continue;
  326. }
  327.  
  328. const headingElement = document.createElement("th");
  329. headingElement.id = `journal-logs-${headings[i].toLowerCase()}-heading`;
  330. headingElement.innerText = headings[i];
  331. headingElement.classList.add("hd-table-heading");
  332.  
  333. journalLogsTable.appendChild(headingElement);
  334. }
  335.  
  336. // Table Body
  337. const tableBody = document.createElement("tbody");
  338.  
  339. const logIDs = Object.keys(storedData.logs);
  340. logIDs.sort((a, b) => b - a);
  341.  
  342. let j = 0;
  343. for (const logId of logIDs) {
  344. if (j < PAGE_SIZE * (page - 1)) {
  345. ++j;
  346. continue;
  347. }
  348.  
  349. if (j >= PAGE_SIZE * page) {
  350. break;
  351. }
  352.  
  353. const tableRow = document.createElement("tr");
  354. tableRow.id = "journal-logs-table-row-" + j
  355.  
  356. for (let i = 0; i < headings.length; ++i) {
  357. if (!enableDeleteLogs && i == headings.length - 1) {
  358. continue;
  359. }
  360.  
  361. const tdElement = document.createElement("td");
  362. if (i == 0) {
  363. tdElement.innerText = j + 1;
  364. } else if (i == 1) {
  365. // Link element
  366. const link = document.createElement("a");
  367. link.innerText = new Date(storedData.logs[logId][keys[i]]).toLocaleString([], { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
  368. link.href = "#";
  369. link.addEventListener("click", function () {
  370. document.querySelector("#journal-logs-popup-div").remove();
  371. eval(storedData.logs[logId]["OpenSummaryMethod"]);
  372. return false;
  373. });
  374.  
  375. tdElement.append(link);
  376. } else if (i == headings.length - 1) {
  377. // Delete Log element
  378. const link = document.createElement("a");
  379. link.innerText = "X";
  380. link.href = "#";
  381. link.addEventListener("click", function () {
  382. deleteLog(logId);
  383. const pagesCount = Math.ceil(Object.keys(storedData.logs).length / PAGE_SIZE);
  384. showLogs(page > pagesCount ? pagesCount : page, true);
  385. return false;
  386. });
  387.  
  388. tdElement.append(link);
  389. } else {
  390. if (storedData.logs[logId][keys[i]] !== undefined) {
  391. tdElement.innerText = storedData.logs[logId][keys[i]];
  392.  
  393. if ('GoldTotal' === keys[i] || 'PointsTotal' === keys[i]) {
  394. tdElement.innerText = Number.parseInt(tdElement.innerText).toLocaleString();
  395. }
  396. } else {
  397. tdElement.innerText = '-';
  398. }
  399. }
  400.  
  401. tdElement.classList.add("hd-table-td");
  402.  
  403. tableRow.appendChild(tdElement);
  404. }
  405.  
  406. tableBody.appendChild(tableRow);
  407.  
  408. j++;
  409. }
  410.  
  411. // Final append
  412. journalLogsTable.appendChild(tableBody)
  413. journalLogs.appendChild(journalLogsTable);
  414.  
  415. // Pagination links
  416. journalLogs.appendChild(document.createElement("br"));
  417. const pagesCount = Math.ceil(Object.keys(storedData.logs).length / PAGE_SIZE);
  418.  
  419. const paginationDiv = document.createElement("div");
  420. const firstPageLink = document.createElement("a");
  421. firstPageLink.innerText = "<< First";
  422. firstPageLink.classList.add("hd-mx-2");
  423. paginationDiv.appendChild(firstPageLink);
  424.  
  425. const previousPageLink = document.createElement("a");
  426. previousPageLink.innerText = "< Prev";
  427. previousPageLink.classList.add("hd-mx-2");
  428. paginationDiv.appendChild(previousPageLink);
  429.  
  430. if (page > 1) {
  431. firstPageLink.href = "#";
  432.  
  433. firstPageLink.addEventListener("click", function () {
  434. showLogs(1, enableDeleteLogs);
  435. return false;
  436. });
  437.  
  438. previousPageLink.href = "#";
  439.  
  440. previousPageLink.addEventListener("click", function () {
  441. showLogs(page - 1, enableDeleteLogs);
  442. return false;
  443. });
  444. }
  445.  
  446. const currentPageText = document.createElement("span");
  447. currentPageText.innerText = `${page} of ${pagesCount}`;
  448. currentPageText.classList.add("hd-mx-2");
  449. paginationDiv.appendChild(currentPageText);
  450.  
  451. const nextPageLink = document.createElement("a");
  452. nextPageLink.innerText = "Next >";
  453. nextPageLink.classList.add("hd-mx-2");
  454. paginationDiv.appendChild(nextPageLink);
  455.  
  456. const lastPageLink = document.createElement("a");
  457. lastPageLink.innerText = "Last >>";
  458. lastPageLink.classList.add("hd-mx-2");
  459. paginationDiv.appendChild(lastPageLink);
  460.  
  461. if (page < pagesCount) {
  462. nextPageLink.href = "#";
  463. nextPageLink.addEventListener("click", function () {
  464. showLogs(page + 1, enableDeleteLogs);
  465. return false;
  466. });
  467.  
  468. lastPageLink.href = "#";
  469. lastPageLink.addEventListener("click", function () {
  470. showLogs(pagesCount, enableDeleteLogs);
  471. return false;
  472. });
  473. }
  474.  
  475. journalLogs.appendChild(paginationDiv);
  476.  
  477. // Manual fetch link. Remove to other tab later
  478. journalLogs.appendChild(document.createElement("br"));
  479. const manualFetchLink = document.createElement("a");
  480. manualFetchLink.innerText = "Manual Fetch";
  481. manualFetchLink.href = "#";
  482. manualFetchLink.classList.add("hd-button");
  483. manualFetchLink.addEventListener("click", function () {
  484. tryToScrapeJournal();
  485. showLogs();
  486. return false;
  487. });
  488.  
  489. journalLogs.appendChild(manualFetchLink);
  490.  
  491. // Toggle Log Deletion. Remove to other tab later
  492. journalLogs.appendChild(document.createElement("br"));
  493. journalLogs.appendChild(document.createElement("br"));
  494. if (enableDeleteLogs) {
  495. const confirmDeleteLogsLink = document.createElement("a");
  496. confirmDeleteLogsLink.innerText = "Confirm Deletion";
  497. confirmDeleteLogsLink.href = "#";
  498. confirmDeleteLogsLink.classList.add("hd-button");
  499. confirmDeleteLogsLink.addEventListener("click", function () {
  500. setData(storedData);
  501. showLogs(1, !enableDeleteLogs);
  502. return false;
  503. });
  504.  
  505. journalLogs.appendChild(confirmDeleteLogsLink);
  506.  
  507. const discardDeleteLogsLink = document.createElement("a");
  508. discardDeleteLogsLink.innerText = "Discard Deletion";
  509. discardDeleteLogsLink.href = "#";
  510. discardDeleteLogsLink.classList.add("hd-button");
  511. discardDeleteLogsLink.addEventListener("click", function () {
  512. storedData = getStoredData();
  513. showLogs(1, !enableDeleteLogs);
  514. return false;
  515. });
  516.  
  517. journalLogs.appendChild(discardDeleteLogsLink);
  518.  
  519. } else {
  520. const toggleDeleteLogsLink = document.createElement("a");
  521. toggleDeleteLogsLink.innerText = "Toggle Delete Logs";
  522. toggleDeleteLogsLink.href = "#";
  523. toggleDeleteLogsLink.classList.add("hd-button");
  524. toggleDeleteLogsLink.addEventListener("click", function () {
  525. showLogs(page, !enableDeleteLogs);
  526. return false;
  527. });
  528.  
  529. journalLogs.appendChild(toggleDeleteLogsLink);
  530. }
  531.  
  532. journalLogsPopup.appendChild(journalLogs);
  533.  
  534. // Close button
  535. const closeButton = document.createElement("button");
  536. closeButton.id = "close-button";
  537. closeButton.textContent = "Close";
  538. closeButton.classList.add("hd-button");
  539. closeButton.onclick = function () {
  540. document.body.removeChild(journalLogsPopup);
  541. }
  542.  
  543. // Append
  544. journalLogsPopup.appendChild(closeButton);
  545.  
  546. // Final Append
  547. document.body.appendChild(journalLogsPopup);
  548. hd_utils.dragElement(journalLogsPopup, journalLogs);
  549. }
  550. // #endregion
  551.  
  552. // #region Utils
  553. function getNextLogTimer() {
  554. let timerString = "N/A";
  555. if (storedData.lastSavedEntryId !== undefined && storedData.logs[storedData.lastSavedEntryId] !== undefined) {
  556. const lastLogDate = new Date(storedData.logs[storedData.lastSavedEntryId].Timestamp);
  557.  
  558. const nextLogTimestamp = lastLogDate.setHours(lastLogDate.getHours() + 36);
  559. const timestampDifference = Math.abs(nextLogTimestamp - Date.now());
  560. const hasPassed = nextLogTimestamp < Date.now();
  561.  
  562. let hours = Math.floor(timestampDifference / 1000 / 60 / 60);
  563. let minutes = Math.round((timestampDifference - (hours * 1000 * 60 * 60)) / 1000 / 60);
  564.  
  565. if (minutes === 60) {
  566. minutes = 0;
  567. hours += 1;
  568. }
  569.  
  570. timerString = "";
  571.  
  572. if (hours > 0) {
  573. timerString = `${hours}h`;
  574. }
  575.  
  576. if (minutes > 0) {
  577. timerString += `${hours > 0 ? " " : ""}${minutes}m`;
  578. }
  579.  
  580. if (hasPassed) {
  581. timerString += " ago";
  582. }
  583.  
  584. if (hours == 0 && minutes == 0) {
  585. timerString = "Almost ready!"
  586. }
  587. }
  588.  
  589. return timerString;
  590. }
  591. // #endregion
  592. })();