Greasy Fork 支持简体中文。

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

// ==UserScript==
// @name         MH - Journal Log Tracker
// @version      1.4.1
// @description  Tracks when your journal log is going to show up next and shows a button to access your last journal log
// @author       hannadDev
// @namespace    https://greasyfork.org/en/users/1238393-hannaddev
// @match        https://www.mousehuntgame.com/*
// @icon         https://www.mousehuntgame.com/images/ui/journal/themes/classic_thumb.gif
// @require      https://cdn.jsdelivr.net/npm/[email protected]/scripts/utils.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/mousehunt-utils.js
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // #region Variables
    let isDebug = false;

    const localStorageKey = `mh-journal-log-tracker`;
    const PAGE_SIZE = 10;

    let storedData = {};
    //#endregion

    //#region Loading external assets
    const mainStylesheetUrl = "https://cdn.jsdelivr.net/npm/[email protected]/stylesheets/main.css";
    const scriptSpecificStylesheetUrl = "https://cdn.jsdelivr.net/npm/[email protected]/stylesheets/journal-log-tracker.css";

    hd_utils.addStyleElement(mainStylesheetUrl);
    hd_utils.addStyleElement(scriptSpecificStylesheetUrl);
    //#endregion

    //#region Initialization
    const journalContainerObserver = new MutationObserver(function (mutations) {
        if (isDebug) {
            console.log('[Journal Container Observer] Mutation');
            for (const mutation of mutations) {
                console.log({ mutation });
                console.log(mutation.target);
            }
        }

        // Only save if something was added.
        if (mutations.some(v => v.type === 'childList' && v.addedNodes.length > 0 && v.target.className !== 'journaldate')) {
            tryToScrapeJournal();
        }
    });

    const mousehuntContainerObserver = new MutationObserver(function (mutations) {
        if (isDebug) {
            console.log('[Mousehunt Container Observer] Mutation');
            for (const mutation of mutations) {
                console.log({ mutation });
                console.log(mutation.target);
            }
        }

        // Check if camp or journal pages
        const mhContainer = document.getElementById('mousehuntContainer');
        if (mhContainer && mhContainer.classList && (mhContainer.classList.contains('PageCamp') || mhContainer.classList.contains('PageJournal'))) {
            showButton();
        }
    });

    function activateJournalMutationObserver() {
        let journalContainerObserverTarget = document.querySelector(`#journalContainer[data-owner="${user.user_id}"] .content`);

        if (journalContainerObserverTarget) {
            journalContainerObserver.observe(journalContainerObserverTarget, {
                childList: true,
                subtree: true
            });
        }
    }

    function activateMousehuntContainerMutationObserver() {
        let mousehuntContainerObserverTarget = document.getElementById('mousehuntContainer');

        if (mousehuntContainerObserverTarget) {
            mousehuntContainerObserver.observe(mousehuntContainerObserverTarget, {
                attributes: true,
                attributeFilter: ['class']
            });
        }
    }

    function Initialize() {
        if (isDebug) console.log(`Initializing.`);

        storedData = getStoredData();

        activateMousehuntContainerMutationObserver();
        activateJournalMutationObserver();
        showButton();

        onRequest(() => { tryToScrapeJournal(); }, 'managers/ajax/turns/activeturn.php');
    }

    Initialize();
    //#endregion

    // #region LocalStorage Methods
    function getStoredData() {
        const savedData = localStorage.getItem(localStorageKey);

        if (savedData !== null) {
            return JSON.parse(savedData);
        }

        return {
            logs: {},
            lastSavedEntryId: -1
        };
    }

    function setData(stats) {
        localStorage.setItem(localStorageKey, JSON.stringify(stats));
    }

    function deleteLog(logId) {
        delete storedData.logs[logId];
        if (Number.parseInt(storedData.lastSavedEntryId) === Number.parseInt(logId)) {
            const keys = Object.keys(storedData.logs);

            storedData.lastSavedEntryId = -1;
            if (keys.length > 0) {
                for (let k = 0; k < keys.length; k++) {
                    if (storedData.lastSavedEntryId < Number.parseInt(keys[k])) {
                        storedData.lastSavedEntryId = Number.parseInt(keys[k]);
                    }
                }
            }
        }
    }
    // #endregion

    // #region Journal Scraping Methods
    function tryToScrapeJournal() {
        if (!hd_utils.mh.isOwnJournal()) {
            return;
        }

        scrapeJournal();
    }

    function scrapeJournal() {
        const entries = document.querySelectorAll('.entry');

        let addedNewEntries = false;
        for (const entry of entries) {
            let entryId = entry.dataset.entryId

            if (!entryId) return;

            entryId = Number.parseInt(entryId);

            if (entry.className.search(/(log_summary)/) !== -1) {
                if (storedData.logs.hasOwnProperty(entryId)) {
                    if (isDebug) console.log(`Entry ${entryId} already stored`);
                }
                else {
                    if (isDebug) console.log(`New entry ${entryId}`);
                    const entryInfo = extractInfoFromEntry(entry);

                    if (entryInfo != null) {
                        storedData.logs[entryId] = entryInfo;

                        if (storedData.lastSavedEntryId < entryId) {
                            storedData.lastSavedEntryId = entryId;
                        }

                        addedNewEntries = true;
                    }
                }
            }
        }

        if (addedNewEntries) {
            setData(storedData);
            showButton();
        }
    }

    function extractInfoFromEntry(entry) {
        const entryInfo = {};

        const dateString = entry.querySelector(".journaldate").innerHTML.split("-")[0].trim().replaceAll(" ", "");
        const hoursString = dateString.split(':')[0];
        const minutesString = dateString.split(':')[1].replace("am", "").replace("pm", "");
        const timePeriodString = dateString.split(':')[1].includes('am') ? 'am' : 'pm';

        try {
            const date = new Date();
            date.setMilliseconds(0);
            date.setSeconds(0);

            date.setHours(hoursString);
            date.setMinutes(minutesString);

            if (date.getHours() !== 12 && timePeriodString === "pm") {
                date.setHours(date.getHours() + 12);
            } else if (date.getHours() === 12 && timePeriodString === "am") {
                date.setHours(date.getHours() - 12);
            }

            if (date.getTime() > Date.now()) {
                date.setDate(date.getDate() - 1);
            }

            entryInfo.Timestamp = date.getTime();
        } catch (e) {
            console.log(e);
            return null;
        }

        entryInfo.Duration = entry.querySelector(".reportSubtitle").innerHTML.replace("Last ", "");

        const tableBody = entry.querySelector(".journalbody .journaltext table tbody");
        const tdElements = tableBody.querySelectorAll(".leftSide, .rightSide");
        for (let i = 0; i < tdElements.length; ++i) {
            if (tdElements[i].innerHTML.includes("Catches:")) {
                entryInfo.Catches = Number.parseInt(tdElements[i].nextSibling.innerHTML);
            } else if (tdElements[i].innerHTML.includes("Misses:")) {
                entryInfo.Ftc = Number.parseInt(tdElements[i].nextSibling.innerHTML);
            } else if (tdElements[i].innerHTML.includes("Fail to Attract:")) {
                entryInfo.Fta = Number.parseInt(tdElements[i].nextSibling.innerHTML);
            } else if (tdElements[i].innerHTML.includes("Gained:")) {
                // Left is gold. Right is points
                if (tdElements[i].classList.contains('leftSide')) {
                    if (tdElements[i].nextSibling) {
                        entryInfo.GoldGained = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
                    }
                } else {
                    if (tdElements[i].nextSibling) {
                        entryInfo.PointsGained = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
                    }
                }
            } else if (tdElements[i].innerHTML.includes("Lost:")) {
                // Left is gold. Right is points
                if (tdElements[i].classList.contains('leftSide')) {
                    if (tdElements[i].nextSibling) {
                        entryInfo.GoldLost = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
                    }
                } else {
                    if (tdElements[i].nextSibling) {
                        entryInfo.PointsLost = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
                    }
                }
            } else if (tdElements[i].innerHTML.includes("Total:")) {
                // Left is gold. Right is points
                if (tdElements[i].classList.contains('leftSide')) {
                    if (tdElements[i].nextSibling) {
                        entryInfo.GoldTotal = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
                    }
                } else {
                    if (tdElements[i].nextSibling) {
                        entryInfo.PointsTotal = Number.parseInt(tdElements[i].nextSibling.innerHTML.replaceAll(',', ''));
                    }
                }
            }
        }

        const link = tableBody.querySelector("a");

        entryInfo.OpenSummaryMethod = link.onclick.toString().split("onclick(event) {")[1].split("return false;")[0].trim();

        return entryInfo;
    }
    // #endregion

    // #region Export/Import Methods
    function exportData() {
        let filename = `${user.user_id}_${Date.now()}.json`;
        let contentType = "application/json;charset=utf-8;";

        if (window.navigator && window.navigator.msSaveOrOpenBlob) {
            var blob = new Blob([decodeURIComponent(encodeURI(JSON.stringify(storedData)))], { type: contentType });
            navigator.msSaveOrOpenBlob(blob, filename);
        } else {
            var a = document.createElement('a');
            a.download = filename;
            a.href = 'data:' + contentType + ',' + encodeURIComponent(JSON.stringify(storedData));
            a.target = '_blank';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }
    }

    function importData() {
        let input = document.createElement('input');
        input.type = 'file';
        input.onchange = _ => {
            let files = Array.from(input.files);

            if (files.length > 0 && files[0].type === "application/json") {
                let fr = new FileReader();

                fr.onload = function () {
                    const importedData = JSON.parse(this.result);
                    filterAndSaveImportedData(importedData);
                }

                fr.readAsText(files[0]);
            } else {
                console.log("Invalid file imported");
            }
        };
        input.click();
    }

    function filterAndSaveImportedData(importedData) {
        let importedLogCount = 0;
        for (const key in importedData.logs) {
            if (!storedData.logs.hasOwnProperty(key)) {
                storedData.logs[key] = importedData.logs[key];

                if (Number.parseInt(storedData.lastSavedEntryId) < Number.parseInt(key)) {
                    storedData.lastSavedEntryId = Number.parseInt(key);
                }

                importedLogCount++;
            }
        }

        console.log(`Imported ${importedLogCount} logs`);

        if (importedLogCount > 0) {
            setData(storedData);
            showLogs();
            showButton();
        }
    }
    // #endregion

    // #region UI
    function showButton() {
        if (!hd_utils.mh.isOwnJournal()) {
            return;
        }

        const olderButton = document.querySelector("#journal-log-button");
        if (olderButton) {
            olderButton.remove();
        }

        const target = document.querySelector("#journalContainer .top");
        if (target) {
            const link = document.createElement("a");
            link.id = "journal-log-button";

            link.innerText = `Next Log: ${getNextLogTimer()}`;
            link.href = "#";
            link.classList.add("hd-journal-log-button");
            link.addEventListener("click", function () {
                showLogs();
            });
            target.append(link);
        }
    }

    function showLogs(page = 1, enableDeleteLogs = false) {
        document.querySelectorAll("#journal-logs-popup-div").forEach(el => el.remove());

        const journalLogsPopup = document.createElement("div");
        journalLogsPopup.id = ("journal-logs-popup-div");
        journalLogsPopup.classList.add("hd-popup");

        // Journal Logs Division
        const journalLogs = document.createElement("div");

        // Title
        const title = document.createElement("h2");
        title.innerText = `Journal Log Tracker`
        title.classList.add("hd-bold");
        journalLogs.appendChild(title);

        // Subtitle
        let nextLogDateString = "N/A";
        if (storedData.lastSavedEntryId !== undefined && storedData.logs[storedData.lastSavedEntryId] !== undefined) {
            const logDate = new Date(storedData.logs[storedData.lastSavedEntryId].Timestamp);
            do {
                logDate.setHours(logDate.getHours() + 36);
            } while (Date.now() - logDate.getTime() > 4 * (1000 * 60 * 60));

            nextLogDateString = `${logDate.toLocaleString([], { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' })} - (${getNextLogTimer()})`;
        }

        const subtitle = document.createElement("h4");
        subtitle.innerText = `Next Estimated Log: ${nextLogDateString}`;
        journalLogs.appendChild(subtitle);

        journalLogs.appendChild(document.createElement("br"));

        // Table for journal logs
        const journalLogsTable = document.createElement("table");
        journalLogsTable.id = "journal-logs-table"
        journalLogsTable.classList.add("hd-table");

        const headings = ["#", "Date & Time", "Duration", "Catches", "FTC", "FTA", "Gold", "Points", "#"];
        const keys = ["", "Timestamp", "Duration", "Catches", "Ftc", "Fta", "GoldTotal", "PointsTotal"];

        // Create headings
        for (let i = 0; i < headings.length; ++i) {
            if (!enableDeleteLogs && i == headings.length - 1) {
                continue;
            }

            const headingElement = document.createElement("th");
            headingElement.id = `journal-logs-${headings[i].toLowerCase()}-heading`;
            headingElement.innerText = headings[i];
            headingElement.classList.add("hd-table-heading");

            journalLogsTable.appendChild(headingElement);
        }

        // Table Body
        const tableBody = document.createElement("tbody");

        const logIDs = Object.keys(storedData.logs);
        logIDs.sort((a, b) => b - a);

        let j = 0;
        for (const logId of logIDs) {
            if (j < PAGE_SIZE * (page - 1)) {
                ++j;
                continue;
            }

            if (j >= PAGE_SIZE * page) {
                break;
            }

            const tableRow = document.createElement("tr");
            tableRow.id = "journal-logs-table-row-" + j

            for (let i = 0; i < headings.length; ++i) {
                if (!enableDeleteLogs && i == headings.length - 1) {
                    continue;
                }

                const tdElement = document.createElement("td");
                if (i == 0) {
                    tdElement.innerText = j + 1;
                } else if (i == 1) {
                    // Link element
                    const link = document.createElement("a");
                    link.innerText = new Date(storedData.logs[logId][keys[i]]).toLocaleString([], { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
                    link.href = "#";
                    link.addEventListener("click", function () {
                        document.querySelector("#journal-logs-popup-div").remove();
                        eval(storedData.logs[logId]["OpenSummaryMethod"]);
                        return false;
                    });

                    tdElement.append(link);
                } else if (i == headings.length - 1) {
                    // Delete Log element
                    const link = document.createElement("a");
                    link.innerText = "X";
                    link.href = "#";
                    link.addEventListener("click", function () {
                        deleteLog(logId);
                        const pagesCount = Math.ceil(Object.keys(storedData.logs).length / PAGE_SIZE);
                        showLogs(page > pagesCount ? pagesCount : page, true);
                        return false;
                    });

                    tdElement.append(link);
                } else {
                    if (storedData.logs[logId][keys[i]] !== undefined) {
                        tdElement.innerText = storedData.logs[logId][keys[i]];

                        if ('GoldTotal' === keys[i] || 'PointsTotal' === keys[i]) {
                            tdElement.innerText = Number.parseInt(tdElement.innerText).toLocaleString();
                        }
                    } else {
                        tdElement.innerText = '-';
                    }
                }

                tdElement.classList.add("hd-table-td");

                tableRow.appendChild(tdElement);
            }

            tableBody.appendChild(tableRow);

            j++;
        }

        // Final append
        journalLogsTable.appendChild(tableBody)
        journalLogs.appendChild(journalLogsTable);

        // Pagination links
        journalLogs.appendChild(document.createElement("br"));
        const pagesCount = Math.ceil(Object.keys(storedData.logs).length / PAGE_SIZE);

        const paginationDiv = document.createElement("div");
        const firstPageLink = document.createElement("a");
        firstPageLink.innerText = "<< First";
        firstPageLink.classList.add("hd-mx-2");
        paginationDiv.appendChild(firstPageLink);

        const previousPageLink = document.createElement("a");
        previousPageLink.innerText = "< Prev";
        previousPageLink.classList.add("hd-mx-2");
        paginationDiv.appendChild(previousPageLink);

        if (page > 1) {
            firstPageLink.href = "#";

            firstPageLink.addEventListener("click", function () {
                showLogs(1, enableDeleteLogs);
                return false;
            });

            previousPageLink.href = "#";

            previousPageLink.addEventListener("click", function () {
                showLogs(page - 1, enableDeleteLogs);
                return false;
            });
        }

        const currentPageText = document.createElement("span");
        currentPageText.innerText = `${page} of ${pagesCount}`;
        currentPageText.classList.add("hd-mx-2");
        paginationDiv.appendChild(currentPageText);

        const nextPageLink = document.createElement("a");
        nextPageLink.innerText = "Next >";
        nextPageLink.classList.add("hd-mx-2");
        paginationDiv.appendChild(nextPageLink);

        const lastPageLink = document.createElement("a");
        lastPageLink.innerText = "Last >>";
        lastPageLink.classList.add("hd-mx-2");
        paginationDiv.appendChild(lastPageLink);

        if (page < pagesCount) {
            nextPageLink.href = "#";
            nextPageLink.addEventListener("click", function () {
                showLogs(page + 1, enableDeleteLogs);
                return false;
            });

            lastPageLink.href = "#";
            lastPageLink.addEventListener("click", function () {
                showLogs(pagesCount, enableDeleteLogs);
                return false;
            });
        }

        journalLogs.appendChild(paginationDiv);

        // Manual fetch link. Remove to other tab later
        journalLogs.appendChild(document.createElement("br"));
        const manualFetchLink = document.createElement("a");
        manualFetchLink.innerText = "Manual Fetch";
        manualFetchLink.href = "#";
        manualFetchLink.classList.add("hd-button");
        manualFetchLink.addEventListener("click", function () {
            tryToScrapeJournal();
            showLogs();
            return false;
        });

        journalLogs.appendChild(manualFetchLink);

        // Export link. Remove to other tab later
        journalLogs.appendChild(document.createElement("br"));
        journalLogs.appendChild(document.createElement("br"));
        const exportLink = document.createElement("a");
        exportLink.innerText = "Export";
        exportLink.href = "#";
        exportLink.classList.add("hd-button");
        exportLink.addEventListener("click", exportData);

        journalLogs.appendChild(exportLink);

        // Import link. Remove to other tab later
        const importLink = document.createElement("a");
        importLink.innerText = "Import";
        importLink.href = "#";
        importLink.classList.add("hd-button");
        importLink.addEventListener("click", importData);

        journalLogs.appendChild(importLink);

        // Toggle Log Deletion. Remove to other tab later
        journalLogs.appendChild(document.createElement("br"));
        journalLogs.appendChild(document.createElement("br"));
        if (enableDeleteLogs) {
            const confirmDeleteLogsLink = document.createElement("a");
            confirmDeleteLogsLink.innerText = "Confirm Deletion";
            confirmDeleteLogsLink.href = "#";
            confirmDeleteLogsLink.classList.add("hd-button");
            confirmDeleteLogsLink.addEventListener("click", function () {
                setData(storedData);
                showLogs(1, !enableDeleteLogs);
                return false;
            });

            journalLogs.appendChild(confirmDeleteLogsLink);

            const discardDeleteLogsLink = document.createElement("a");
            discardDeleteLogsLink.innerText = "Discard Deletion";
            discardDeleteLogsLink.href = "#";
            discardDeleteLogsLink.classList.add("hd-button");
            discardDeleteLogsLink.addEventListener("click", function () {
                storedData = getStoredData();
                showLogs(1, !enableDeleteLogs);
                return false;
            });

            journalLogs.appendChild(discardDeleteLogsLink);

        } else {
            const toggleDeleteLogsLink = document.createElement("a");
            toggleDeleteLogsLink.innerText = "Toggle Delete Logs";
            toggleDeleteLogsLink.href = "#";
            toggleDeleteLogsLink.classList.add("hd-button");
            toggleDeleteLogsLink.addEventListener("click", function () {
                showLogs(page, !enableDeleteLogs);
                return false;
            });

            journalLogs.appendChild(toggleDeleteLogsLink);
        }

        journalLogsPopup.appendChild(journalLogs);

        // Close button
        const closeButton = document.createElement("button");
        closeButton.id = "close-button";
        closeButton.textContent = "Close";
        closeButton.classList.add("hd-button");
        closeButton.onclick = function () {
            document.body.removeChild(journalLogsPopup);
        }

        // Append
        journalLogsPopup.appendChild(closeButton);

        // Final Append
        document.body.appendChild(journalLogsPopup);
        hd_utils.dragElement(journalLogsPopup, journalLogs);
    }
    // #endregion

    // #region Utils
    function getNextLogTimer() {
        let timerString = "N/A";
        if (storedData.lastSavedEntryId !== undefined && storedData.logs[storedData.lastSavedEntryId] !== undefined) {
            const lastLogDate = new Date(storedData.logs[storedData.lastSavedEntryId].Timestamp);

            const nextLogTimestamp = lastLogDate.setHours(lastLogDate.getHours() + 36);
            const timestampDifference = Math.abs(nextLogTimestamp - Date.now());
            let hasPassed = nextLogTimestamp < Date.now();
            let logsMissed = 0;

            let hours = Math.floor(timestampDifference / 1000 / 60 / 60);
            let minutes = Math.round((timestampDifference - (hours * 1000 * 60 * 60)) / 1000 / 60);

            if (minutes === 60) {
                minutes = 0;
                hours += 1;
            }

            timerString = "";

            if (hours > 0) {
                if (hasPassed && hours >= 8) {
                    logsMissed = Math.floor(hours / 36) + 1;
                    hours = Math.abs(hours - 36 * logsMissed);

                    if (minutes > 0) {
                        hours--;
                        minutes = 60 - minutes;
                    }
                }

                timerString = `${hours}h`;
            }

            if (minutes > 0) {
                timerString += `${hours > 0 ? " " : ""}${minutes}m`;
            }

            if (logsMissed > 0) {
                timerString = `~${timerString}`;
            }

            if (hasPassed && logsMissed === 0) {
                timerString += " ago";
            }

            if (hours == 0 && minutes == 0) {
                timerString = "Almost ready!"
            }
        }

        return timerString;
    }
    // #endregion
})();