TORN To-Do List

Adds a To-Do list with settings, per-task schedule, drag & drop reordering, and import/export functionality.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
})();