Study Timer

Records webpage study time and displays a floating timer window in the upper right corner, with exportable study logs

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Study Timer
// @namespace    https://github.com/ymhomer/ym_Userscript
// @version      2.0
// @description  Records webpage study time and displays a floating timer window in the upper right corner, with exportable study logs
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // Retrieve the base URL (main page) for logging consistency across multiple tabs of the same site
    const baseUrl = location.origin;

    let timerDisplayed = GM_getValue(baseUrl + "_timerDisplayed", false);
    let timerActive = GM_getValue(baseUrl + "_timerActive", false);
    let startTime = Date.now();
    let elapsed = 0;
    let pauseCount = 0;
    let totalPauseTime = 0;
    let interval;
    let focus = document.hasFocus();
    let pauseStartTime = null;

    // Reset on load to avoid interference from previous records
    GM_setValue(baseUrl + "_startTime", startTime);
    GM_setValue(baseUrl + "_elapsed", elapsed);
    GM_setValue(baseUrl + "_pauseCount", pauseCount);
    GM_setValue(baseUrl + "_totalPauseTime", totalPauseTime);

    // Initialize the timer window
    const timerWindow = document.createElement('div');
    timerWindow.style.position = 'fixed';
    timerWindow.style.top = '10px';
    timerWindow.style.right = '10px';
    timerWindow.style.padding = '10px';
    timerWindow.style.zIndex = '9999';
    timerWindow.style.background = 'rgba(0, 0, 0, 0.8)';
    timerWindow.style.color = '#fff';
    timerWindow.style.borderRadius = '5px';
    timerWindow.style.cursor = 'move';
    timerWindow.style.display = timerDisplayed ? 'block' : 'none';
    timerWindow.innerHTML = `<span id="timeDisplay">00:00:00</span>
                             <button id="toggleTimer" style="margin-left: 5px;">${timerActive ? 'Pause' : 'Start'}</button>
                             <button id="stopTimer" style="margin-left: 5px;">End</button>`;
    document.body.appendChild(timerWindow);

    // Enable dragging of the timer window
    timerWindow.onmousedown = function(e) {
        let shiftX = e.clientX - timerWindow.getBoundingClientRect().left;
        let shiftY = e.clientY - timerWindow.getBoundingClientRect().top;
        document.onmousemove = function(e) {
            timerWindow.style.left = e.pageX - shiftX + 'px';
            timerWindow.style.top = e.pageY - shiftY + 'px';
        };
        document.onmouseup = function() {
            document.onmousemove = null;
            timerWindow.onmouseup = null;
        };
    };

    timerWindow.ondragstart = () => false;

    // Toggle timer state (start/pause)
    function toggleTimer() {
        if (timerActive) {
            // Pause the timer
            clearInterval(interval);
            pauseCount++;
            pauseStartTime = Date.now();
            elapsed += Date.now() - startTime;
            GM_setValue(baseUrl + "_elapsed", elapsed);
            timerWindow.style.background = 'red';
            document.getElementById('toggleTimer').innerText = 'Resume';
        } else {
            // Resume the timer
            if (pauseStartTime) {
                totalPauseTime += Date.now() - pauseStartTime;
                pauseStartTime = null;
            }
            startTime = Date.now();
            timerWindow.style.background = 'rgba(0, 0, 0, 0.8)';
            interval = setInterval(updateTime, 1000);
            document.getElementById('toggleTimer').innerText = 'Pause';
        }
        timerActive = !timerActive;
        GM_setValue(baseUrl + "_timerActive", timerActive);
        GM_setValue(baseUrl + "_pauseCount", pauseCount);
        GM_setValue(baseUrl + "_totalPauseTime", totalPauseTime);
    }

    // Update timer display
    function updateTime() {
        let totalTime = elapsed + (Date.now() - startTime);
        document.getElementById('timeDisplay').innerText = new Date(totalTime).toISOString().substr(11, 8);
    }

    // Stop timer, display dialog with record details, and reset timer to zero
    function stopTimer() {
        clearInterval(interval);
        let currentSessionTime = Date.now() - startTime;
        if (timerActive) elapsed += currentSessionTime;

        // If paused, add the final pause duration
        if (pauseStartTime) {
            totalPauseTime += Date.now() - pauseStartTime;
            pauseStartTime = null;
        }

        let endTime = Date.now();
        let totalElapsed = GM_getValue("total_elapsed", 0) + elapsed;
        GM_setValue("total_elapsed", totalElapsed);

        // Log session by date
        const sessionDate = new Date().toISOString().split("T")[0];
        const history = GM_getValue(baseUrl + "_history", {});
        if (!history[sessionDate]) {
            history[sessionDate] = { sessions: [] };
        }
        history[sessionDate].sessions.push({
            start: new Date(startTime).toLocaleString(),
            end: new Date(endTime).toLocaleString(),
            studyTime: new Date(currentSessionTime).toISOString().substr(11, 8),
            pauseCount,
            totalPauseTime: new Date(totalPauseTime).toISOString().substr(11, 8)
        });
        GM_setValue(baseUrl + "_history", history);

        // Prepare the report with additional details
        const report = `URL: ${baseUrl}\n` +
                       `Start Time: ${new Date(startTime).toLocaleString()}\n` +
                       `End Time: ${new Date(endTime).toLocaleString()}\n` +
                       `Session Study Time: ${new Date(currentSessionTime).toISOString().substr(11, 8)}\n` +
                       `Total Study Time: ${new Date(totalElapsed).toISOString().substr(11, 8)}\n` +
                       `Total Pause Time: ${new Date(totalPauseTime).toISOString().substr(11, 8)}\n` +
                       `Pause Count: ${pauseCount}`;

        // Show the dialog with record details
        showEndDialog(report);

        // Reset timer display to zero and reset variables
        timerActive = false;
        elapsed = 0;
        pauseCount = 0;
        totalPauseTime = 0;
        document.getElementById('timeDisplay').innerText = "00:00:00";
        document.getElementById('toggleTimer').innerText = 'Start';
        timerWindow.style.background = 'rgba(0, 0, 0, 0.8)';
    }

    // Show end dialog with record details
    function showEndDialog(report) {
        const dialog = document.createElement('dialog');
        dialog.style.width = '80%';
        dialog.style.maxWidth = '500px';
        dialog.style.padding = '20px';
        dialog.style.border = '2px solid #888';
        dialog.style.borderRadius = '8px';
        dialog.style.backgroundColor = '#f9f9f9';
        dialog.style.boxShadow = '0px 4px 8px rgba(0, 0, 0, 0.2)';

        let content = `<h2 style="text-align:center; color: #333;">Session Summary</h2><pre style="white-space: pre-wrap; font-size:0.9em; color: #333; padding:10px;">${report}</pre>`;
        content += `<div style="text-align:center; margin-top: 15px;">
                        <button id="exportRecord" style="margin-right:10px; padding:8px 16px; background-color:#333; color:#fff; border:none; border-radius:4px; cursor:pointer;">Export Record</button>
                        <button id="closeDialog" style="padding:8px 16px; background-color:#333; color:#fff; border:none; border-radius:4px; cursor:pointer;">Close</button>
                    </div>`;

        dialog.innerHTML = content;
        document.body.appendChild(dialog);
        dialog.showModal();

        // Export button functionality
        document.getElementById('exportRecord').onclick = () => saveRecord(report);
        // Close button functionality
        document.getElementById('closeDialog').onclick = () => dialog.close();

        dialog.addEventListener('close', () => document.body.removeChild(dialog));
    }

    // Save and export the study record
    function saveRecord(data) {
        const blob = new Blob([data], { type: 'text/plain' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = `study_record_${new Date().toISOString().slice(0, 10)}.txt`;
        link.click();
    }

    // Check if window is in focus
    function focusCheck() {
        focus = document.hasFocus();
        if (focus && timerActive) {
            startTime = Date.now();
            interval = setInterval(updateTime, 1000);
        } else if (!focus && timerActive) {
            clearInterval(interval);
            pauseCount++;
            pauseStartTime = Date.now();
            elapsed += Date.now() - startTime;
        }
    }

    // View study history with a styled calendar dialog
    function viewHistory() {
        const history = GM_getValue(baseUrl + "_history", {});
        const dialog = document.createElement('dialog');
        dialog.style.width = '90%';
        dialog.style.maxWidth = '600px';
        dialog.style.padding = '20px';
        dialog.style.border = '2px solid #888';
        dialog.style.borderRadius = '8px';
        dialog.style.backgroundColor = '#f9f9f9';
        dialog.style.boxShadow = '0px 4px 8px rgba(0, 0, 0, 0.2)';

        let content = `<h2 style="text-align:center; color: #333;">Study History for ${baseUrl}</h2><table border="0" style="width:100%; font-size:0.9em; color: #333;">`;
        for (const date in history) {
            const daySessions = history[date].sessions;
            content += `<tr style="background-color:#eee; color:#555;"><td colspan="3" style="padding: 8px;"><strong>${date}</strong></td></tr>`;
            daySessions.forEach((session, index) => {
                content += `<tr><td style="padding: 6px;">Session ${index + 1}</td><td>Start: ${session.start}</td><td>End: ${session.end}</td></tr>`;
                content += `<tr><td colspan="3" style="padding: 6px 8px;">Study Time: ${session.studyTime}, Pauses: ${session.pauseCount}, Total Pause Time: ${session.totalPauseTime}</td></tr>`;
            });
        }
        content += `</table><br/><button id="closeDialog" style="margin-top: 15px; padding: 8px 16px; background-color: #333; color: #fff; border: none; border-radius: 4px; cursor: pointer;">Close</button>`;
        dialog.innerHTML = content;

        document.body.appendChild(dialog);
        dialog.showModal();

        document.getElementById('closeDialog').onclick = () => dialog.close();
        dialog.addEventListener('close', () => document.body.removeChild(dialog));
    }

    // Bind events
    document.getElementById('toggleTimer').addEventListener('click', toggleTimer);
    document.getElementById('stopTimer').addEventListener('click', stopTimer);
    window.addEventListener('focus', focusCheck);
    window.addEventListener('blur', focusCheck);

    // Add Tampermonkey menu options
    GM_registerMenuCommand("Toggle Study Timer Display", function() {
        timerDisplayed = !timerDisplayed;
        GM_setValue(baseUrl + "_timerDisplayed", timerDisplayed);
        timerWindow.style.display = timerDisplayed ? 'block' : 'none';
    });
    GM_registerMenuCommand("View Study History", viewHistory);

    // Initialize
    if (timerActive) {
        interval = setInterval(updateTime, 1000);
    }
})();