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

您需要先安装一款用户脚本管理器扩展,例如 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.6
// @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';
    // Currency options: [region, symbol, pointsCost, payoutValue]
    const currencyOptions = [
        {region: "EU", symbol: "€", cost: 524, value: 40},
        {region: "USA", symbol: "$", cost: 490, value: 40},
        {region: "UK", symbol: "£", cost: 535, value: 35},
        {region: "CA", symbol: "C$", cost: 504, value: 55},
        {region: "AU", symbol: "A$", cost: 511, value: 65},
        {region: "JP", symbol: "¥", cost: 502, value: 5300},
        {region: "ASIA", symbol: "$", cost: 490, value: 40},
        {region: "KR", symbol: "₩", cost: 490, value: 58000},
        {region: "CN", symbol: "¥", cost: 490, value: 284}
    ];

    // Helper to get saved currency or default EU
    function getSelectedCurrency() {
        const saved = localStorage.getItem("pointsCurrency");
        return currencyOptions.find(c => c.region === saved) || currencyOptions[0];
    }

    function renderCurrencySelector(container) {
        let selector = document.createElement("select");
        selector.id = "currencySelector";
        selector.style.marginLeft = "5px";
        currencyOptions.forEach(opt => {
            let option = document.createElement("option");
            option.value = opt.region;
            option.textContent = `${opt.region} (${opt.symbol})`;
            selector.appendChild(option);
        });
        selector.value = getSelectedCurrency().region;
        selector.onchange = function() {
            localStorage.setItem("pointsCurrency", selector.value);
            // re-render stats with new currency
            let logs = JSON.parse(localStorage.getItem("pointsLog") || "[]");
            renderPointsLog(logs, new Date().toISOString().split('T')[0], new Date().toTimeString().split(' ')[0]);
        };
        container.appendChild(selector);
    }

    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"; // left group vs right button
        header.style.alignItems = "center";
        header.style.marginBottom = "5px";

        // Left group: title + currency selector
        let leftGroup = document.createElement("div");
        leftGroup.style.display = "flex";
        leftGroup.style.alignItems = "center";

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

        // Insert currency selector next to title
        //renderCurrencySelector(leftGroup);

        header.appendChild(leftGroup);

        // Right side: minimize button
        let toggleBtn = document.createElement("button");
        toggleBtn.textContent = "-";
        toggleBtn.style.background = "yellow";
        toggleBtn.style.color = "darkblue";
        toggleBtn.style.fontWeight = "bold";
        toggleBtn.onclick = function() {
            let tableWrapper = document.getElementById("pointsLogTableWrapper");
            let stats = document.getElementById("pointsStats");
            let giftcards = document.getElementById("pointsGiftcards");
            let totalValue = document.getElementById("pointsTotalValue");
            let controls = document.getElementById("pointsControls");
            let selectorWrapper = document.getElementById("currencySelectorWrapper"); // ⬅️ new line

            if (tableWrapper.style.display === "none") {
                tableWrapper.style.display = "block";
                if (stats) stats.style.display = "block";
                if (giftcards) giftcards.style.display = "block";
                if (totalValue) totalValue.style.display = "block";
                if (controls) controls.style.display = "block";
                if (selectorWrapper) selectorWrapper.style.display = "block"; // ⬅️ show
                toggleBtn.textContent = "-";
            } else {
                tableWrapper.style.display = "none";
                if (stats) stats.style.display = "none";
                if (giftcards) giftcards.style.display = "none";
                if (totalValue) totalValue.style.display = "none";
                if (controls) controls.style.display = "none";
                if (selectorWrapper) selectorWrapper.style.display = "none"; // ⬅️ hide
                toggleBtn.textContent = "+";
            }
        };


        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) — allow negative
            let gain = "";
            if (idx < sortedLogs.length - 1) {
                let prevPoints = parseFloat(sortedLogs[idx + 1].points);
                let currPoints = parseFloat(entry.points);
                gain = currPoints - prevPoints; // can be negative
            }


            [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);

        // Place currency selector under the table, right aligned
        let selectorWrapper = document.createElement("div");
        selectorWrapper.id = "currencySelectorWrapper";
        selectorWrapper.style.marginTop = "5px";
        selectorWrapper.style.textAlign = "right";
        renderCurrencySelector(selectorWrapper);
        container.appendChild(selectorWrapper);


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

            const first = daily[0];
            const last = daily[daily.length - 1];

            const totalGain = parseFloat(last.points) - parseFloat(first.points);

            const firstDate = new Date(`${first.date}T${first.time || '00:00:00'}`);
            const lastDate = new Date(`${last.date}T${last.time || '00:00:00'}`);
            const daySpan = Math.max(1, Math.round((lastDate - firstDate) / (1000 * 60 * 60 * 24)));

            const avg = totalGain / daySpan;
            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)}`;

            // ➕ Add giftcard line (collapsible with stats)
            let giftcards = document.getElementById("pointsGiftcards");
            if (!giftcards) {
                giftcards = document.createElement("div");
                giftcards.id = "pointsGiftcards";
                giftcards.style.marginTop = "3px";
                giftcards.style.fontWeight = "bold";
                container.appendChild(giftcards);
            }
            // Use selected currency for giftcard calculation
            const curr = getSelectedCurrency();
            const X = monthly / curr.cost;
            const Y = X * curr.value;
            giftcards.textContent = `Monthly Giftcards: ${X.toFixed(2)} (${curr.symbol} ${Y.toFixed(2)})`;
            // ➕ Add total value line under giftcards
            let totalValue = document.getElementById("pointsTotalValue");
            if (!totalValue) {
                totalValue = document.createElement("div");
                totalValue.id = "pointsTotalValue";
                totalValue.style.marginTop = "3px";
                totalValue.style.fontWeight = "bold";
                container.appendChild(totalValue);
            }

            // Calculate total value of current points using selected currency
            const currPoints = parseFloat(last.points); // last entry = current total points
            const totalVal = (currPoints / curr.cost) * curr.value;
            totalValue.textContent = `Total value of points: ${curr.symbol} ${totalVal.toFixed(2)}`;

        }



        document.body.appendChild(container);
    }



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