Makerworld Points Auto Logger + Viewer (Smart Panel + Toggle)

Log points daily, export only when value changes, show entries in small popup window with minimize/expand toggle

当前为 2025-12-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Makerworld Points Auto Logger + Viewer (Smart Panel + Toggle)
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Log points daily, export only when value changes, show entries in small popup window with minimize/expand toggle
// @match        https://makerworld.com/*
// @grant        GM_download
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    function logPoints() {
        let el = document.querySelector('.mw-css-yyek0l');
        if (!el) {
            console.log("Element not found");
            return;
        }

        let points = el.textContent.trim().replace(/,/g, "");
        let now = new Date();
        let date = now.toISOString().split('T')[0];
        let time = now.toTimeString().split(' ')[0];

        let logs = JSON.parse(localStorage.getItem("pointsLog") || "[]");
        let existingIndex = logs.findIndex(entry => entry.date === date);
        let shouldExport = true;

        if (existingIndex !== -1) {
            if (logs[existingIndex].points === points) {
                shouldExport = false;
            }
            logs[existingIndex] = {date: date, time: time, points: points};
        } else {
            logs.push({date: date, time: time, points: points});
        }

        localStorage.setItem("pointsLog", JSON.stringify(logs));
        console.log("Points logged:", {date, time, points});

        if (shouldExport) {
            exportFullLog(date, time, logs);
        }

        renderPointsLog(logs, date, time);
    }

    function exportFullLog(date, time, logs) {
        if (logs.length === 0) return;

        let header = "DATE,TIME,POINTS\n";
        let lines = logs.map(entry => `${entry.date},${entry.time},${entry.points}`).join("\n");
        let csvContent = header + lines + "\n";

        let filename = `MWPOINT-${date}-${time}.csv`;

        let blob = new Blob([csvContent], {type: "text/csv"});
        let url = URL.createObjectURL(blob);
        GM_download({ url: url, name: filename });
    }

    function renderPointsLog(logs, date, time) {
        let oldPanel = document.getElementById("pointsLogPanel");
        if (oldPanel) oldPanel.remove();

        let container = document.createElement("div");
        container.id = "pointsLogPanel";
        container.style.position = "fixed";
        container.style.bottom = "10px";
        container.style.right = "10px";
        container.style.background = "darkblue";
        container.style.color = "yellow";
        container.style.border = "2px solid yellow";
        container.style.padding = "10px";
        container.style.zIndex = 9999;
        container.style.fontSize = "12px";
        container.style.fontFamily = "monospace";

        // Header with minimize toggle
        let header = document.createElement("div");
        header.style.display = "flex";
        header.style.justifyContent = "space-between";
        header.style.alignItems = "center";
        header.style.marginBottom = "5px";

        let title = document.createElement("span");
        title.textContent = "Points Log";
        title.style.fontWeight = "bold";
        header.appendChild(title);

        let toggleBtn = document.createElement("button");
        toggleBtn.textContent = "Minimize";
        toggleBtn.style.background = "yellow";
        toggleBtn.style.color = "darkblue";
        toggleBtn.style.fontWeight = "bold";
        toggleBtn.style.marginLeft = "10px";
        toggleBtn.onclick = function() {
            let tableWrapper = document.getElementById("pointsLogTableWrapper");
            let stats = document.getElementById("pointsStats");
            let controls = document.getElementById("pointsControls");
            if (tableWrapper.style.display === "none") {
                tableWrapper.style.display = "block";
                if (stats) stats.style.display = "block";
                if (controls) controls.style.display = "block";
                toggleBtn.textContent = "Minimize";
            } else {
                tableWrapper.style.display = "none";
                if (stats) stats.style.display = "none";
                if (controls) controls.style.display = "none";
                toggleBtn.textContent = "Expand";
            }
        };
        header.appendChild(toggleBtn);
        container.appendChild(header);

        // Control buttons
        let controls = document.createElement("div");
        controls.id = "pointsControls";
        controls.style.marginBottom = "5px";
        let clearBtn = document.createElement("button");
        clearBtn.textContent = "Clear Log";
        clearBtn.style.background = "yellow";
        clearBtn.style.color = "darkblue";
        clearBtn.style.fontWeight = "bold";
        clearBtn.style.marginRight = "5px";
        clearBtn.onclick = function() {
            if (confirm("Are you sure you want to clear the points log?")) {
                localStorage.removeItem("pointsLog");
                renderPointsLog([], date, time);
                console.log("Points log cleared.");
            }
        };
        controls.appendChild(clearBtn);

        let exportBtn = document.createElement("button");
        exportBtn.textContent = "Export Log";
        exportBtn.style.background = "yellow";
        exportBtn.style.color = "darkblue";
        exportBtn.style.fontWeight = "bold";
        exportBtn.onclick = function() {
            let logs = JSON.parse(localStorage.getItem("pointsLog") || "[]");
            exportFullLog(date, time, logs);
        };
        controls.appendChild(exportBtn);
        container.appendChild(controls);

        // Inside renderPointsLog, after exportBtn:
        let addBtn = document.createElement("button");
        addBtn.textContent = "Add Entry";
        addBtn.style.background = "yellow";
        addBtn.style.color = "darkblue";
        addBtn.style.fontWeight = "bold";
        addBtn.style.marginLeft = "5px";
        addBtn.onclick = function() {
            // Toggle visibility of the inline form
            let form = document.getElementById("addEntryForm");
            if (form) {
                form.style.display = (form.style.display === "none") ? "block" : "none";
            }
        };
        controls.appendChild(addBtn);

        // Inline form container
        let form = document.createElement("div");
        form.id = "addEntryForm";
        form.style.display = "none";
        form.style.marginTop = "5px";
        form.style.background = "navy";
        form.style.padding = "5px";
        form.style.border = "1px solid yellow";

        // Input fields
        let dateInput = document.createElement("input");
        dateInput.type = "date";
        dateInput.value = new Date().toISOString().split('T')[0];
        form.appendChild(dateInput);

        let timeInput = document.createElement("input");
        timeInput.type = "time";
        timeInput.value = new Date().toTimeString().split(' ')[0].slice(0,5);
        timeInput.style.marginLeft = "5px";
        form.appendChild(timeInput);

        let pointsInput = document.createElement("input");
        pointsInput.type = "number";
        pointsInput.placeholder = "Points";
        pointsInput.style.marginLeft = "5px";
        form.appendChild(pointsInput);

        // Save button
        let saveBtn = document.createElement("button");
        saveBtn.textContent = "Save";
        saveBtn.style.background = "yellow";
        saveBtn.style.color = "darkblue";
        saveBtn.style.fontWeight = "bold";
        saveBtn.style.marginLeft = "5px";
        saveBtn.onclick = function() {
            let date = dateInput.value;
            let time = timeInput.value || new Date().toTimeString().split(' ')[0];
            let points = pointsInput.value.trim();
            if (!date || !points) {
                alert("Date and points are required.");
                return;
            }

            let logs = JSON.parse(localStorage.getItem("pointsLog") || "[]");
            logs.push({date: date, time: time, points: points});
            localStorage.setItem("pointsLog", JSON.stringify(logs));

            console.log("Manual entry added:", {date, time, points});
            renderPointsLog(logs, date, time);
        };
        form.appendChild(saveBtn);

        container.appendChild(form);


        // Table wrapper (scrollable)
        let tableWrapper = document.createElement("div");
        tableWrapper.id = "pointsLogTableWrapper";
        tableWrapper.style.maxHeight = "120px"; // enough for ~5 rows
        tableWrapper.style.overflowY = "auto";
        tableWrapper.style.border = "1px solid yellow";

        let table = document.createElement("table");
        table.id = "pointsLogTable";
        table.style.borderCollapse = "collapse";
        table.style.width = "100%";

        let headerRow = document.createElement("tr");
        ["DATE", "TIME", "POINTS", "GAIN"].forEach(h => {
            let th = document.createElement("th");
            th.textContent = h;
            th.style.border = "1px solid yellow";
            th.style.padding = "2px 5px";
            th.style.background = "navy";
            th.style.color = "yellow";
            headerRow.appendChild(th);
        });
        table.appendChild(headerRow);

        // Sort logs by date descending, then time descending
        let sortedLogs = logs.slice().sort((a, b) => {
            let dateA = new Date(`${a.date}T${a.time}`);
            let dateB = new Date(`${b.date}T${b.time}`);
            return dateB - dateA; // newest first
        });

        // Use sortedLogs instead of lastLogs
        sortedLogs.forEach((entry, idx) => {
            let row = document.createElement("tr");

            // Calculate daily gain (difference from previous entry)
            let gain = "";
            if (idx < sortedLogs.length - 1) {
                let prevPoints = parseFloat(sortedLogs[idx + 1].points);
                let currPoints = parseFloat(entry.points);
                gain = (currPoints - prevPoints >= 0) ? (currPoints - prevPoints) : "";
            }

            [entry.date, entry.time, entry.points, gain].forEach(val => {
                let td = document.createElement("td");
                td.textContent = val;
                td.style.border = "1px solid yellow";
                td.style.padding = "2px 5px";
                td.style.color = "yellow";
                row.appendChild(td);
            });

            // Delete button cell
            let delTd = document.createElement("td");
            let delBtn = document.createElement("button");
            delBtn.textContent = "X";
            delBtn.style.background = "red";
            delBtn.style.color = "white";
            delBtn.style.fontWeight = "bold";
            delBtn.onclick = function() {
                if (!confirm(`Delete entry for ${entry.date} ${entry.time} (${entry.points} points)?`)) {
                    return; // cancel if user clicks "Cancel"
                }
                let logs = JSON.parse(localStorage.getItem("pointsLog") || "[]");

                // Find the matching entry by date+time+points
                let newLogs = logs.filter(l =>
                                          !(l.date === entry.date && l.time === entry.time && l.points === entry.points)
                                         );

                localStorage.setItem("pointsLog", JSON.stringify(newLogs));
                console.log("Deleted entry:", entry);
                renderPointsLog(newLogs, date, time);
            };
            delTd.appendChild(delBtn);
            row.appendChild(delTd);

            table.appendChild(row);
        });


        tableWrapper.appendChild(table);
        container.appendChild(tableWrapper);

        // Helpers: chronological sort and per-day consolidation
        function sortAscByDateTime(logs) {
            return logs.slice().sort((a, b) => {
                const aTime = new Date(`${a.date}T${a.time || '00:00:00'}`).getTime();
                const bTime = new Date(`${b.date}T${b.time || '00:00:00'}`).getTime();
                return aTime - bTime;
            });
        }

        function collapseToDailyLast(logsAsc) {
            // Keep the last entry per date (highest time per day)
            const map = new Map();
            for (const entry of logsAsc) {
                const key = entry.date;
                const prev = map.get(key);
                // Compare times; store the later one
                if (!prev) {
                    map.set(key, entry);
                } else {
                    const prevTime = new Date(`${prev.date}T${prev.time || '00:00:00'}`).getTime();
                    const currTime = new Date(`${entry.date}T${entry.time || '00:00:00'}`).getTime();
                    if (currTime >= prevTime) map.set(key, entry);
                }
            }
            // Return in chronological order by date
            return Array.from(map.values()).sort((a, b) => (new Date(`${a.date}T00:00:00`) - new Date(`${b.date}T00:00:00`)));
        }

        // Daily average + monthly projection (chronological, per-day)
        if (logs.length > 1) {
            const asc = sortAscByDateTime(logs);
            const daily = collapseToDailyLast(asc);

            const diffs = [];
            for (let i = 1; i < daily.length; i++) {
                const prev = parseFloat(daily[i - 1].points);
                const curr = parseFloat(daily[i].points);
                const gain = curr - prev;
                if (!Number.isNaN(gain) && gain >= 0) diffs.push(gain);
            }

            const avg = diffs.length ? (diffs.reduce((a, b) => a + b, 0) / diffs.length) : 0;
            const monthly = (avg * 365) / 12;

            let stats = document.getElementById("pointsStats");
            if (!stats) {
                stats = document.createElement("div");
                stats.id = "pointsStats";
                stats.style.marginTop = "5px";
                stats.style.fontWeight = "bold";
                container.appendChild(stats);
            }
            stats.textContent = `Daily Avg: ${avg.toFixed(2)} | Monthly Projection: ${monthly.toFixed(2)}`;
        }


        document.body.appendChild(container);
    }



    window.addEventListener('load', logPoints);
})();