您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a To-Do list with settings, per-task schedule, drag & drop reordering, and import/export functionality.
// ==UserScript== // @name TORN To-Do List // @namespace https://github.com/sternenklinge/TORN-ToDo-List // @author zuko [2620008] // @license MIT // @version 1.0 // @description Adds a To-Do list with settings, per-task schedule, drag & drop reordering, and import/export functionality. // @match https://www.torn.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function () { 'use strict'; let tasks = JSON.parse(GM_getValue("torn_todo_tasks", "[]")); let settings = JSON.parse(GM_getValue("torn_todo_settings", '{"hidden": false}')); let lastCheckedTime = GM_getValue("last_checked_time", "Never"); let draggedTaskIndex = null; let currentModalTaskIndex = null; // stores the task index currently being edited function formatTimestamp(timestamp) { if (timestamp === "Never") return "Never"; const date = new Date(timestamp); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } function addToggleButton() { if (document.getElementById("todo-toggle-button")) return; const button = document.createElement("div"); button.id = "todo-toggle-button"; Object.assign(button.style, { position: "fixed", bottom: "40px", right: "5px", backgroundColor: "#2c2f33", color: "#ffffff", padding: "5px 10px", borderRadius: "5px", cursor: "pointer", zIndex: "1000", }); button.textContent = settings.hidden ? "Show To-Do List" : "Hide To-Do List"; button.onclick = toggleTodoListVisibility; document.body.appendChild(button); } function toggleTodoListVisibility() { settings.hidden = !settings.hidden; GM_setValue("torn_todo_settings", JSON.stringify(settings)); const todoWindow = document.getElementById("todo-list-window"); if (todoWindow) todoWindow.style.display = settings.hidden ? "none" : "block"; // Always close other windows when toggling the todo list const settingsPanel = document.getElementById("todo-settings-panel"); if (settingsPanel) { settingsPanel.style.display = "none"; } const modal = document.getElementById("task-schedule-modal"); if (modal) { modal.remove(); } updateToggleButton(); } function updateToggleButton() { const toggleButton = document.getElementById("todo-toggle-button"); if (toggleButton) toggleButton.textContent = settings.hidden ? "Show To-Do List" : "Hide To-Do List"; } function createTodoListWindow() { if (document.getElementById("todo-list-window")) return; const windowDiv = document.createElement("div"); windowDiv.id = "todo-list-window"; Object.assign(windowDiv.style, { position: "fixed", bottom: "70px", right: "5px", width: "300px", height: "400px", backgroundColor: "#2c2f33", color: "#ffffff", border: "1px solid #444", borderRadius: "8px", boxShadow: "0 2px 5px rgba(0,0,0,0.5)", display: settings.hidden ? "none" : "block", zIndex: "1000", }); windowDiv.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; background-color: #23272a; padding: 5px 10px; border-radius: 8px 8px 0 0; border-bottom: 1px solid #444;"> <span>To-Do List</span> <div style="display: flex; gap: 5px;"> <button id="open-settings" style="background: none; border: none; color: white; cursor: pointer;">⚙️</button> <button id="close-todo-window" style="background: none; border: none; color: white; cursor: pointer;">X</button> </div> </div> <div id="todo-body" style="padding: 10px; overflow-y: auto; height: 300px;"></div> <div style="padding: 10px; border-top: 1px solid #444; display: flex; gap: 5px;"> <input type="text" id="new-task" placeholder="New Task" style="flex: 1; padding: 5px; border: 1px solid #444; border-radius: 4px; background-color: #2c2f33; color: #ffffff;"> <button id="add-task" style="padding: 5px 10px; background-color: #7289da; color: white; border: none; border-radius: 4px; cursor: pointer;">Add</button> </div> `; document.body.appendChild(windowDiv); document.getElementById("add-task").onclick = addTask; // Allow adding a task by pressing Enter in the input field document.getElementById("new-task").addEventListener("keydown", function(e) { if (e.key === "Enter") { addTask(); } }); document.getElementById("close-todo-window").onclick = closeTodoWindow; document.getElementById("open-settings").onclick = openSettingsPanel; renderTasks(); } function closeTodoWindow() { const todoWindow = document.getElementById("todo-list-window"); if (todoWindow) todoWindow.style.display = "none"; settings.hidden = true; GM_setValue("torn_todo_settings", JSON.stringify(settings)); updateToggleButton(); // Also close any open settings panel or task schedule modal const settingsPanel = document.getElementById("todo-settings-panel"); if (settingsPanel) { settingsPanel.style.display = "none"; } const modal = document.getElementById("task-schedule-modal"); if (modal) { modal.remove(); } } function renderTasks() { const taskContainer = document.getElementById("todo-body"); if (!taskContainer) return; taskContainer.innerHTML = ""; tasks.forEach((task, index) => { const taskDiv = document.createElement("div"); taskDiv.style.display = "flex"; taskDiv.style.justifyContent = "space-between"; taskDiv.style.marginBottom = "5px"; taskDiv.setAttribute("draggable", "true"); taskDiv.setAttribute("data-index", index); taskDiv.addEventListener("dragstart", function(e) { draggedTaskIndex = index; e.dataTransfer.effectAllowed = "move"; }); taskDiv.addEventListener("dragover", function(e) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }); taskDiv.addEventListener("drop", function(e) { e.preventDefault(); const targetIndex = parseInt(taskDiv.getAttribute("data-index")); if (draggedTaskIndex === null || draggedTaskIndex === targetIndex) return; const draggedTask = tasks.splice(draggedTaskIndex, 1)[0]; tasks.splice(targetIndex, 0, draggedTask); GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); renderTasks(); }); const label = document.createElement("label"); label.style.flex = "1"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = task.done; checkbox.setAttribute("data-index", index); checkbox.style.marginRight = "5px"; checkbox.onchange = toggleTask; const span = document.createElement("span"); span.textContent = task.text; // Increase clickable area for right-click by adding padding and making the span a block-level element span.style.display = "inline-block"; span.style.padding = "5px"; if (task.done) { span.style.textDecoration = "line-through"; span.style.color = "gray"; } // Right-click on task text opens the edit modal span.addEventListener("contextmenu", function(e) { e.preventDefault(); openTaskScheduleEditor(index); }); label.appendChild(checkbox); label.appendChild(span); const removeBtn = document.createElement("button"); removeBtn.textContent = "X"; removeBtn.style.background = "none"; removeBtn.style.border = "none"; removeBtn.style.color = "#f04747"; removeBtn.style.cursor = "pointer"; removeBtn.setAttribute("data-index", index); removeBtn.onclick = removeTask; taskDiv.appendChild(label); taskDiv.appendChild(removeBtn); taskContainer.appendChild(taskDiv); }); } function addTask() { const input = document.getElementById("new-task"); if (!input || !input.value.trim()) return; tasks.push({ text: input.value.trim(), done: false }); GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); input.value = ""; renderTasks(); } function removeTask(event) { const index = event.target.dataset.index; tasks.splice(index, 1); GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); renderTasks(); } function toggleTask(event) { const index = event.target.dataset.index; tasks[index].done = event.target.checked; GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); renderTasks(); } function openSettingsPanel() { let existingPanel = document.getElementById("todo-settings-panel"); if (existingPanel) { existingPanel.style.display = "block"; // Open settings if it exists updateLastCheckedInfo(); return; } const settingsPanel = document.createElement("div"); settingsPanel.id = "todo-settings-panel"; Object.assign(settingsPanel.style, { position: "fixed", bottom: "70px", right: "310px", width: "300px", height: "400px", backgroundColor: "#2c2f33", color: "#ffffff", border: "1px solid #444", borderRadius: "8px", boxShadow: "0 2px 5px rgba(0,0,0,0.5)", zIndex: "1000", display: "flex", flexDirection: "column" }); settingsPanel.innerHTML = ` <div style="flex: 0 0 auto; display: flex; justify-content: space-between; align-items: center; background-color: #23272a; padding: 5px 10px; border-radius: 8px 8px 0 0; border-bottom: 1px solid #444;"> <span>Settings</span> <button id="close-settings" style="background: none; border: none; color: white; cursor: pointer;">X</button> </div> <div id="settings-body" style="flex: 1 1 auto; overflow-y: auto; padding: 10px;"> <div id="last-checked-info" style="margin-bottom: 15px;"> <p style="margin: 0; font-size: 0.9em;">Last Check: <span id="last-checked-time">${formatTimestamp(lastCheckedTime)}</span></p> </div> <div style="margin-bottom: 15px;"> <p style="font-weight: bold; margin-bottom: 5px;">Import/Export Data</p> <textarea id="import-export-text" style="width: 280px; height: 80px; resize: none; background-color: #2c2f33; color: #ffffff; border: 1px solid #444; border-radius: 4px;"></textarea> <div style="display: flex; gap: 5px; margin-top: 5px;"> <button id="export-data" style="padding: 5px 10px; background-color: #7289da; color: white; border: none; border-radius: 4px; cursor: pointer;">Export</button> <button id="copy-data" style="padding: 5px 10px; background-color: #7289da; color: white; border: none; border-radius: 4px; cursor: pointer;">Copy</button> <button id="import-data" style="padding: 5px 10px; background-color: #7289da; color: white; border: none; border-radius: 4px; cursor: pointer;">Import</button> </div> </div> </div> <div style="flex: 0 0 auto; padding: 10px; border-top: 1px solid #444; display: flex; justify-content: flex-end;"> <button id="save-settings" style="padding: 5px 10px; background-color: #7289da; color: white; border: none; border-radius: 4px; cursor: pointer;">Save</button> </div> `; document.body.appendChild(settingsPanel); document.getElementById("close-settings").onclick = () => settingsPanel.style.display = "none"; document.getElementById("save-settings").onclick = saveSettings; document.getElementById("export-data").onclick = exportData; document.getElementById("copy-data").onclick = copyData; document.getElementById("import-data").onclick = importData; } function saveSettings() { GM_setValue("torn_todo_settings", JSON.stringify(settings)); const settingsPanel = document.getElementById("todo-settings-panel"); if (settingsPanel) settingsPanel.style.display = "none"; } function updateLastCheckedInfo() { const lastCheckedElement = document.getElementById("last-checked-time"); if (lastCheckedElement) lastCheckedElement.textContent = formatTimestamp(lastCheckedTime); } function exportData() { const data = { tasks: tasks, settings: settings, lastCheckedTime: lastCheckedTime, }; document.getElementById("import-export-text").value = JSON.stringify(data, null, 2); } function copyData() { const text = document.getElementById("import-export-text").value; navigator.clipboard.writeText(text); } function importData() { try { const data = JSON.parse(document.getElementById("import-export-text").value); if (data.tasks) tasks = data.tasks; if (data.settings) settings = data.settings; if (data.lastCheckedTime) lastCheckedTime = data.lastCheckedTime; GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); GM_setValue("torn_todo_settings", JSON.stringify(settings)); GM_setValue("last_checked_time", lastCheckedTime); renderTasks(); updateLastCheckedInfo(); alert("Data imported successfully."); } catch (e) { alert("Import failed: Invalid JSON."); } } // --- Modal for editing task details (text and schedule) --- function updateModalSchedule(modal, taskIndex) { const timeInput = modal.querySelector('input[type="time"]'); const daysContainer = modal.querySelector('#schedule-days-container'); const dayButtons = daysContainer.querySelectorAll('button'); const newDayStates = {}; dayButtons.forEach(btn => { newDayStates[btn.dataset.day] = (btn.dataset.active === "true"); }); tasks[taskIndex].schedule = { time: timeInput.value, days: newDayStates }; GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); } function openTaskScheduleEditor(index) { // If a modal is already open, auto-save its changes and remove it const existingModal = document.getElementById("task-schedule-modal"); if (existingModal && currentModalTaskIndex !== null) { updateModalSchedule(existingModal, currentModalTaskIndex); existingModal.remove(); currentModalTaskIndex = null; } currentModalTaskIndex = index; const task = tasks[index]; const modal = document.createElement("div"); modal.id = "task-schedule-modal"; Object.assign(modal.style, { position: "fixed", bottom: "480px", right: "5px", width: "260px", backgroundColor: "#2c2f33", color: "#ffffff", border: "1px solid #444", borderRadius: "8px", padding: "20px", zIndex: "2000" }); // Editable task content with larger input and an edit icon const taskContainer = document.createElement("div"); taskContainer.style.display = "flex"; taskContainer.style.alignItems = "center"; taskContainer.style.marginBottom = "10px"; const taskInput = document.createElement("input"); taskInput.type = "text"; taskInput.value = task.text; Object.assign(taskInput.style, { fontSize: "16px", width: "100%", backgroundColor: "#2c2f33", border: "1px solid #444", color: "#ffffff", borderRadius: "4px", padding: "5px" }); taskInput.onchange = function () { task.text = taskInput.value; GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); renderTasks(); }; const editIcon = document.createElement("button"); editIcon.innerHTML = "✎"; // pencil icon Object.assign(editIcon.style, { background: "none", border: "none", color: "#7289da", cursor: "pointer", fontSize: "18px", marginLeft: "5px" }); editIcon.onclick = function () { taskInput.focus(); }; taskContainer.appendChild(taskInput); taskContainer.appendChild(editIcon); modal.appendChild(taskContainer); // Schedule settings const timeLabel = document.createElement("label"); timeLabel.textContent = "Time (TCT):"; modal.appendChild(timeLabel); const timeInput = document.createElement("input"); timeInput.type = "time"; timeInput.value = task.schedule && task.schedule.time ? task.schedule.time : "00:00"; timeInput.style.marginLeft = "10px"; Object.assign(timeInput.style, { backgroundColor: "#2c2f33", border: "1px solid #444", color: "#ffffff", borderRadius: "4px", padding: "5px" }); timeInput.onchange = function () { updateModalSchedule(modal, currentModalTaskIndex); }; modal.appendChild(timeInput); modal.appendChild(document.createElement("br")); modal.appendChild(document.createElement("br")); const daysContainer = document.createElement("div"); daysContainer.id = "schedule-days-container"; const dayKeys = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]; const dayLabels = ["M", "T", "W", "T", "F", "S", "S"]; dayKeys.forEach((day, i) => { const btn = document.createElement("button"); btn.textContent = dayLabels[i]; btn.style.marginRight = "5px"; btn.style.width = "30px"; const active = task.schedule && task.schedule.days && task.schedule.days[day] ? true : false; btn.style.backgroundColor = active ? "#7289da" : "#444"; btn.style.color = "#fff"; btn.style.border = "none"; btn.style.borderRadius = "4px"; btn.style.cursor = "pointer"; btn.dataset.day = day; btn.dataset.active = active ? "true" : "false"; btn.onclick = function () { const current = btn.dataset.active === "true"; btn.dataset.active = (!current).toString(); btn.style.backgroundColor = (!current) ? "#7289da" : "#444"; updateModalSchedule(modal, currentModalTaskIndex); }; daysContainer.appendChild(btn); }); modal.appendChild(daysContainer); modal.appendChild(document.createElement("br")); modal.appendChild(document.createElement("br")); const btnContainer = document.createElement("div"); btnContainer.style.textAlign = "right"; const removeBtn = document.createElement("button"); removeBtn.textContent = "Remove Schedule"; removeBtn.style.marginRight = "5px"; removeBtn.style.padding = "5px 10px"; removeBtn.style.backgroundColor = "#f04747"; removeBtn.style.border = "none"; removeBtn.style.borderRadius = "4px"; removeBtn.style.cursor = "pointer"; removeBtn.onclick = function () { delete tasks[currentModalTaskIndex].schedule; GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); modal.remove(); currentModalTaskIndex = null; }; btnContainer.appendChild(removeBtn); const closeBtn = document.createElement("button"); closeBtn.textContent = "Close"; closeBtn.style.padding = "5px 10px"; closeBtn.style.backgroundColor = "#7289da"; closeBtn.style.border = "none"; closeBtn.style.borderRadius = "4px"; closeBtn.style.cursor = "pointer"; closeBtn.onclick = function () { updateModalSchedule(modal, currentModalTaskIndex); modal.remove(); currentModalTaskIndex = null; }; btnContainer.appendChild(closeBtn); modal.appendChild(btnContainer); document.body.appendChild(modal); } function checkResetTime() { const now = new Date(); const currentTime = `${now.getUTCHours().toString().padStart(2, "0")}:${now.getUTCMinutes().toString().padStart(2, "0")}`; const dayMapping = {0: "sun", 1: "mon", 2: "tue", 3: "wed", 4: "thu", 5: "fri", 6: "sat"}; const currentDayKey = dayMapping[now.getUTCDay()]; lastCheckedTime = now.toISOString(); GM_setValue("last_checked_time", lastCheckedTime); tasks.forEach(task => { if (task.schedule && task.done) { if (currentTime === task.schedule.time && task.schedule.days && task.schedule.days[currentDayKey]) { task.done = false; } } }); GM_setValue("torn_todo_tasks", JSON.stringify(tasks)); renderTasks(); updateLastCheckedInfo(); } addToggleButton(); createTodoListWindow(); checkResetTime(); setInterval(checkResetTime, 60000); })();