Study Timer

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

// ==UserScript==
// @name         Study Timer
// @namespace    YMHOMER
// @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);
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址