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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();