Log points daily, export only when value changes, show entries in small popup window with minimize/expand toggle
当前为
// ==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);
})();