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.1
  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. do {
  304. logDate.setHours(logDate.getHours() + 36);
  305. } while (Date.now() - logDate.getTime() > 4 * (1000 * 60 * 60));
  306.  
  307. nextLogDateString = `${logDate.toLocaleString([], { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} - (${getNextLogTimer()})`;
  308. }
  309.  
  310. const subtitle = document.createElement("h4");
  311. subtitle.innerText = `Next Estimated Log: ${nextLogDateString}`;
  312. journalLogs.appendChild(subtitle);
  313.  
  314. journalLogs.appendChild(document.createElement("br"));
  315.  
  316. // Table for journal logs
  317. const journalLogsTable = document.createElement("table");
  318. journalLogsTable.id = "journal-logs-table"
  319. journalLogsTable.classList.add("hd-table");
  320.  
  321. const headings = ["#", "Date & Time", "Duration", "Catches", "FTC", "FTA", "Gold", "Points", "#"];
  322. const keys = ["", "Timestamp", "Duration", "Catches", "Ftc", "Fta", "GoldTotal", "PointsTotal"];
  323.  
  324. // Create headings
  325. for (let i = 0; i < headings.length; ++i) {
  326. if (!enableDeleteLogs && i == headings.length - 1) {
  327. continue;
  328. }
  329.  
  330. const headingElement = document.createElement("th");
  331. headingElement.id = `journal-logs-${headings[i].toLowerCase()}-heading`;
  332. headingElement.innerText = headings[i];
  333. headingElement.classList.add("hd-table-heading");
  334.  
  335. journalLogsTable.appendChild(headingElement);
  336. }
  337.  
  338. // Table Body
  339. const tableBody = document.createElement("tbody");
  340.  
  341. const logIDs = Object.keys(storedData.logs);
  342. logIDs.sort((a, b) => b - a);
  343.  
  344. let j = 0;
  345. for (const logId of logIDs) {
  346. if (j < PAGE_SIZE * (page - 1)) {
  347. ++j;
  348. continue;
  349. }
  350.  
  351. if (j >= PAGE_SIZE * page) {
  352. break;
  353. }
  354.  
  355. const tableRow = document.createElement("tr");
  356. tableRow.id = "journal-logs-table-row-" + j
  357.  
  358. for (let i = 0; i < headings.length; ++i) {
  359. if (!enableDeleteLogs && i == headings.length - 1) {
  360. continue;
  361. }
  362.  
  363. const tdElement = document.createElement("td");
  364. if (i == 0) {
  365. tdElement.innerText = j + 1;
  366. } else if (i == 1) {
  367. // Link element
  368. const link = document.createElement("a");
  369. link.innerText = new Date(storedData.logs[logId][keys[i]]).toLocaleString([], { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
  370. link.href = "#";
  371. link.addEventListener("click", function () {
  372. document.querySelector("#journal-logs-popup-div").remove();
  373. eval(storedData.logs[logId]["OpenSummaryMethod"]);
  374. return false;
  375. });
  376.  
  377. tdElement.append(link);
  378. } else if (i == headings.length - 1) {
  379. // Delete Log element
  380. const link = document.createElement("a");
  381. link.innerText = "X";
  382. link.href = "#";
  383. link.addEventListener("click", function () {
  384. deleteLog(logId);
  385. const pagesCount = Math.ceil(Object.keys(storedData.logs).length / PAGE_SIZE);
  386. showLogs(page > pagesCount ? pagesCount : page, true);
  387. return false;
  388. });
  389.  
  390. tdElement.append(link);
  391. } else {
  392. if (storedData.logs[logId][keys[i]] !== undefined) {
  393. tdElement.innerText = storedData.logs[logId][keys[i]];
  394.  
  395. if ('GoldTotal' === keys[i] || 'PointsTotal' === keys[i]) {
  396. tdElement.innerText = Number.parseInt(tdElement.innerText).toLocaleString();
  397. }
  398. } else {
  399. tdElement.innerText = '-';
  400. }
  401. }
  402.  
  403. tdElement.classList.add("hd-table-td");
  404.  
  405. tableRow.appendChild(tdElement);
  406. }
  407.  
  408. tableBody.appendChild(tableRow);
  409.  
  410. j++;
  411. }
  412.  
  413. // Final append
  414. journalLogsTable.appendChild(tableBody)
  415. journalLogs.appendChild(journalLogsTable);
  416.  
  417. // Pagination links
  418. journalLogs.appendChild(document.createElement("br"));
  419. const pagesCount = Math.ceil(Object.keys(storedData.logs).length / PAGE_SIZE);
  420.  
  421. const paginationDiv = document.createElement("div");
  422. const firstPageLink = document.createElement("a");
  423. firstPageLink.innerText = "<< First";
  424. firstPageLink.classList.add("hd-mx-2");
  425. paginationDiv.appendChild(firstPageLink);
  426.  
  427. const previousPageLink = document.createElement("a");
  428. previousPageLink.innerText = "< Prev";
  429. previousPageLink.classList.add("hd-mx-2");
  430. paginationDiv.appendChild(previousPageLink);
  431.  
  432. if (page > 1) {
  433. firstPageLink.href = "#";
  434.  
  435. firstPageLink.addEventListener("click", function () {
  436. showLogs(1, enableDeleteLogs);
  437. return false;
  438. });
  439.  
  440. previousPageLink.href = "#";
  441.  
  442. previousPageLink.addEventListener("click", function () {
  443. showLogs(page - 1, enableDeleteLogs);
  444. return false;
  445. });
  446. }
  447.  
  448. const currentPageText = document.createElement("span");
  449. currentPageText.innerText = `${page} of ${pagesCount}`;
  450. currentPageText.classList.add("hd-mx-2");
  451. paginationDiv.appendChild(currentPageText);
  452.  
  453. const nextPageLink = document.createElement("a");
  454. nextPageLink.innerText = "Next >";
  455. nextPageLink.classList.add("hd-mx-2");
  456. paginationDiv.appendChild(nextPageLink);
  457.  
  458. const lastPageLink = document.createElement("a");
  459. lastPageLink.innerText = "Last >>";
  460. lastPageLink.classList.add("hd-mx-2");
  461. paginationDiv.appendChild(lastPageLink);
  462.  
  463. if (page < pagesCount) {
  464. nextPageLink.href = "#";
  465. nextPageLink.addEventListener("click", function () {
  466. showLogs(page + 1, enableDeleteLogs);
  467. return false;
  468. });
  469.  
  470. lastPageLink.href = "#";
  471. lastPageLink.addEventListener("click", function () {
  472. showLogs(pagesCount, enableDeleteLogs);
  473. return false;
  474. });
  475. }
  476.  
  477. journalLogs.appendChild(paginationDiv);
  478.  
  479. // Manual fetch link. Remove to other tab later
  480. journalLogs.appendChild(document.createElement("br"));
  481. const manualFetchLink = document.createElement("a");
  482. manualFetchLink.innerText = "Manual Fetch";
  483. manualFetchLink.href = "#";
  484. manualFetchLink.classList.add("hd-button");
  485. manualFetchLink.addEventListener("click", function () {
  486. tryToScrapeJournal();
  487. showLogs();
  488. return false;
  489. });
  490.  
  491. journalLogs.appendChild(manualFetchLink);
  492.  
  493. // Toggle Log Deletion. Remove to other tab later
  494. journalLogs.appendChild(document.createElement("br"));
  495. journalLogs.appendChild(document.createElement("br"));
  496. if (enableDeleteLogs) {
  497. const confirmDeleteLogsLink = document.createElement("a");
  498. confirmDeleteLogsLink.innerText = "Confirm Deletion";
  499. confirmDeleteLogsLink.href = "#";
  500. confirmDeleteLogsLink.classList.add("hd-button");
  501. confirmDeleteLogsLink.addEventListener("click", function () {
  502. setData(storedData);
  503. showLogs(1, !enableDeleteLogs);
  504. return false;
  505. });
  506.  
  507. journalLogs.appendChild(confirmDeleteLogsLink);
  508.  
  509. const discardDeleteLogsLink = document.createElement("a");
  510. discardDeleteLogsLink.innerText = "Discard Deletion";
  511. discardDeleteLogsLink.href = "#";
  512. discardDeleteLogsLink.classList.add("hd-button");
  513. discardDeleteLogsLink.addEventListener("click", function () {
  514. storedData = getStoredData();
  515. showLogs(1, !enableDeleteLogs);
  516. return false;
  517. });
  518.  
  519. journalLogs.appendChild(discardDeleteLogsLink);
  520.  
  521. } else {
  522. const toggleDeleteLogsLink = document.createElement("a");
  523. toggleDeleteLogsLink.innerText = "Toggle Delete Logs";
  524. toggleDeleteLogsLink.href = "#";
  525. toggleDeleteLogsLink.classList.add("hd-button");
  526. toggleDeleteLogsLink.addEventListener("click", function () {
  527. showLogs(page, !enableDeleteLogs);
  528. return false;
  529. });
  530.  
  531. journalLogs.appendChild(toggleDeleteLogsLink);
  532. }
  533.  
  534. journalLogsPopup.appendChild(journalLogs);
  535.  
  536. // Close button
  537. const closeButton = document.createElement("button");
  538. closeButton.id = "close-button";
  539. closeButton.textContent = "Close";
  540. closeButton.classList.add("hd-button");
  541. closeButton.onclick = function () {
  542. document.body.removeChild(journalLogsPopup);
  543. }
  544.  
  545. // Append
  546. journalLogsPopup.appendChild(closeButton);
  547.  
  548. // Final Append
  549. document.body.appendChild(journalLogsPopup);
  550. hd_utils.dragElement(journalLogsPopup, journalLogs);
  551. }
  552. // #endregion
  553.  
  554. // #region Utils
  555. function getNextLogTimer() {
  556. let timerString = "N/A";
  557. if (storedData.lastSavedEntryId !== undefined && storedData.logs[storedData.lastSavedEntryId] !== undefined) {
  558. const lastLogDate = new Date(storedData.logs[storedData.lastSavedEntryId].Timestamp);
  559.  
  560. const nextLogTimestamp = lastLogDate.setHours(lastLogDate.getHours() + 36);
  561. const timestampDifference = Math.abs(nextLogTimestamp - Date.now());
  562. let hasPassed = nextLogTimestamp < Date.now();
  563. let logsMissed = 0;
  564.  
  565. let hours = Math.floor(timestampDifference / 1000 / 60 / 60);
  566. let minutes = Math.round((timestampDifference - (hours * 1000 * 60 * 60)) / 1000 / 60);
  567.  
  568. if (minutes === 60) {
  569. minutes = 0;
  570. hours += 1;
  571. }
  572.  
  573. timerString = "";
  574.  
  575. if (hours > 0) {
  576. if (hasPassed && hours >= 8) {
  577. logsMissed = Math.floor(hours / 36) + 1;
  578. hours = Math.abs(hours - 36 * logsMissed);
  579. }
  580.  
  581. timerString = `${hours}h`;
  582. }
  583.  
  584. if (minutes > 0) {
  585. timerString += `${hours > 0 ? " " : ""}${minutes}m`;
  586. }
  587.  
  588. if(logsMissed > 0) {
  589. timerString = `~${timerString}`;
  590. }
  591.  
  592. if (hasPassed && logsMissed === 0) {
  593. timerString += " ago";
  594. }
  595.  
  596. if (hours == 0 && minutes == 0) {
  597. timerString = "Almost ready!"
  598. }
  599. }
  600.  
  601. return timerString;
  602. }
  603. // #endregion
  604. })();