Greasy Fork 支持简体中文。

TORN To-Do List

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 = "&#9998;"; // 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);
})();